Source specification:
docs/technical_specification.md(25 sections) Last updated: 2026-06-19
This document is Layer 2 of the spec → roadmap → phase-tasks workflow. It decomposes the technical specification into an ordered set of delivery phases with explicit dependencies, a status dashboard, and a definition of done per phase. It does not restate the specification; it sequences the work and defines how progress is tracked. Per-task breakdowns are produced separately in Layer 3.
| Symbol | Meaning |
|---|---|
| 📋 | ToDo |
| 🔄 | In Progress |
| 👀 | Review |
| ✅ | Done |
| ⛔ | Blocked |
| 🟡 | Partial |
- Overall progress: 🔄 12 / 13 phases implemented (92%); the release/publish pipeline is deferred by the user to a future release phase (see
docs/RELEASE.md). The 1.0 is not yet published to crates.io or npm. - Active phase: P12 — its non-publishing scope (docs, the six examples, the extra CI quality/security gates, the dogfood smokes, and the Playwright browser E2E) is ✅ Done (2026-06-22); the registry-publish half (OIDC trusted publishing, the
releaseworkflow, SBOM/attestation publishing, the protected environment, the tag↔version gate) is DEFERRED. - Blocked: —
| ID | Phase | Status | Progress | Size | Last updated |
|---|---|---|---|---|---|
| P0 | Foundation: workspace, toolchain & CI skeleton | ✅ Done | 6/6 | M | 2026-06-17 |
| P1 | bymax-auth-crypto — hashing, constant-time, tokens, MFA-gated AEAD/TOTP |
✅ Done | 6/6 | L | 2026-06-18 |
| P2 | bymax-auth-types + bymax-auth-jwt — model, errors, HS256, ts-rs |
✅ Done | 6/6 | L | 2026-06-19 |
| P3 | bymax-auth-core — engine, builder, config profiles, trait set |
✅ Done | 6/6 | L | 2026-06-19 |
| P4 | Local auth flows + password + brute-force + session-fixation | ✅ Done | 7/7 | L | 2026-06-19 |
| P5 | bymax-auth-redis — stores + Lua + WS ticket (E2E Redis) |
✅ Done | 6/6 | M | 2026-06-19 |
| P6 | Sessions, OTP, password reset, email verification, invitations | ✅ Done | 4/4 | L | 2026-06-19 |
| P7 | MFA (TOTP) end-to-end | ✅ Done | 6/6 | L | 2026-06-19 |
| P8 | OAuth — HttpClient trait + Google + PKCE/state + linking |
✅ Done | 8/8 | L | 2026-06-20 |
| P9 | Platform admin identity domain | ✅ Done | 5/5 | M | 2026-06-20 |
| P10 | bymax-auth-axum — router, extractors, delivery, rate-limit, WS, validation (E2E) |
✅ Done | 7/7 | L | 2026-06-20 |
| P11 | bymax-auth-wasm + @bymax-one/rust-auth (npm) + Rust client |
✅ Done | 6/6 | L | 2026-06-21 |
| P12 | Docs, examples, extra CI gates, dogfood, browser E2E (✅) — release/publish pipeline DEFERRED | 🔄 Partial | impl ✅ / publish deferred | L | 2026-06-22 |
The graph below is a directed acyclic graph: an edge X → Y means phase X must complete before phase Y can begin.
How to read it. Flow runs top to bottom. The drawn lines illustrate the dominant flow; the bracketed list after each node is the authoritative, complete set of that node's direct prerequisites. Any edge not drawn as a line is captured in a bracket.
P0
│
┌───────────┴───────────┐
▼ ▼
P1 P2
│ │
└───────────┬───────────┘
▼
P3 [P1, P2]
│
┌───────────┴───────────┐
▼ ▼
P4 P5
│ │
│ ┌─────┴─────┐
│ ▼ ▼
│ P6 P7
│ [P4, P5] [P1, P4, P5]
│ │
├──▶ P8 [P4] │
│ ▼
│ P9 [P4, P7]
│ │
└───────────┬─────────────────┘
▼
P10 [P4, P6, P7, P8, P9]
│
▼
P11 [P2, P10]
│
▼
P12 [all phases P0–P11]
Edge list (canonical): P0→P1, P0→P2, P1→P3, P2→P3, P3→P4, P3→P5, P4→P6, P5→P6, P4→P7, P5→P7, P1→P7, P4→P8, P4→P9, P7→P9, {P4,P6,P7,P8,P9}→P10, P2→P11, P10→P11, {all}→P12.
- After P0, the two leaf libraries are independent: P1 ∥ P2 can proceed in parallel (crypto vs. types/JWT).
- P3 is the first convergence point — it requires both P1 and P2 — and is the gateway to all flow work.
- After P3, P4 and P5 overlap: P5 (
bymax-auth-redis) depends only on the trait definitions and shared value types surfaced by P3, so it can be built against those contracts while P4 implements the local auth flows. - After P4 + P5, the flow phases P6, P7, and P8 run in parallel (sessions/OTP/reset/verification/invitations; MFA; OAuth) — each consumes the engine, the local flows, and the Redis stores but not one another.
- P9 (platform admin identity) follows P7, since platform authentication reuses the MFA path in addition to the local-auth foundation from P4.
- P10 (
bymax-auth-axum) is the second convergence point: it integrates the flow phases — P4, P6, P7, P8, and P9 — into the HTTP adapter with extractors, delivery, rate-limiting, WebSocket tickets, and validation, validated end-to-end. - P11 is split: the WASM half (
bymax-auth-wasm) needs only P2 and can therefore start early, in parallel with the flow phases; the frontend-client half (@bymax-one/rust-authnpm package and Rust client) needs P10 before it can integrate against the finished HTTP surface. - P12 (release engineering, supply-chain, docs, examples, dogfood) is last — it depends on every prior phase.
Critical path: P0 → P1/P2 → P3 → P4/P5 → P7 → P9 → P10 → P11 → P12 (9 phases). P6 and P8 are depth-5 branches that feed P10 but do not lie on the longest chain; note P9's prerequisites are [P4, P7] (not P6), and P7 depends on P5 — both reflected above.
These rules derive from the specification (notably §3.8, §4, §17, §19, §21, §24) and the Bymax engineering standards. Every phase respects them; per-phase Definitions of Done assume them as a baseline rather than restating them.
| Area | Rule |
|---|---|
| Workspace | Single Cargo workspace; Rust edition 2024; MSRV pinned via rust-toolchain.toml and [workspace.package] rust-version, enforced by a dedicated MSRV build job in CI. |
| Safety | #![forbid(unsafe_code)] on every first-party crate — the sole exception is bymax-auth-wasm, whose wasm-bindgen glue confines generated unsafe to the bindgen boundary under #![deny(unsafe_op_in_unsafe_fn)]. #![deny(missing_docs)] on all public crates. |
| Lint / format | cargo fmt --check and cargo clippy -D warnings are clean and CI-gating across the workspace. |
| Errors | Typed errors only (AuthError / ConfigError / RepositoryError); no stringly-typed errors. No unwrap / expect / panic! on library code paths; fallible items document their error conditions. |
| API design | Follows the Rust API Guidelines: builders for complex construction, strong/named types over boolean traps and magic strings, runnable rustdoc examples (compiled as doctests) on public items. Public surface snapshotted (cargo public-api) and semver-checked (cargo-semver-checks). |
| Tests | 100% line coverage via cargo-llvm-cov as a hard gate; each test documents its scenario; mutation testing (cargo-mutants) near-100% as a pre-release gate; fuzz / property tests for parsers; E2E tests for Axum + Redis using testcontainers; a mandatory wasm32-unknown-unknown build test; feature-combination matrix via cargo-hack; a ts-rs staleness gate; Criterion benchmarks (tracked, non-gating). |
| Dependencies | Minimal-footprint premise: a tiny always-compiled core; every heavy integration is feature-gated or trait-pluggable. Supply chain gated by cargo-deny (ban-list + licenses + advisories + sources), cargo-audit, and cargo-vet; cargo-geiger scans transitive unsafe (non-blocking, reviewed on jumps). Per-feature cargo tree assertions enforce the "pay only for what you use" premise. |
| Features | default = ["scrypt"]; one feature per capability — taxonomy argon2 / sessions / mfa / oauth / oauth-reqwest / platform / invitations / redis / axum / client. No core feature (the core crate is non-optional). At least one hasher feature is required (a compile_error! fires if neither scrypt nor argon2 is enabled). Features are strictly additive. |
| Security | HS256 pinned (no algorithm agility); refresh tokens are opaque; all secret comparisons are constant-time; secrets are never logged (wrapped in secrecy); tokens never travel in a query string. The §24 Security Invariants are CI-enforced. |
| Frontend | TypeScript strict; the @bymax-one/rust-auth package exposes subpaths (./shared, ./nextjs, ./client, ./react); the ./shared types and constants are generated from Rust via ts-rs and are never hand-edited; native fetch with single-flight refresh; React 19 / Next 16 as peer dependencies; dual ESM + CJS output; API docs via TypeDoc. |
| Commits | Conventional Commits, enforced by commitlint + husky; English only; source comments are timeless (no plan/phase/task references). |
| Publishing | Backend to crates.io (the facade bymax-auth plus crates/* only) and frontend to npm (@bymax-one/rust-auth), both via OIDC Trusted Publishing with provenance. Cargo.lock is committed. |
When a phase changes state, apply the following steps so the dashboard, the progress counters, and the per-phase Definitions of Done stay consistent:
- Set the phase row's Status emoji and Last updated date in the phase dashboard.
- Recompute Overall progress as
N / 13phases done and the corresponding percentage. - Update Active phase and Blocked in the Progress section to reflect what is in flight and what is stalled.
- When a phase reaches ✅, confirm that every bullet of its Definition of Done in the "Phase details" section below is fully met.
- Commit the plan update with a
docs(plan):Conventional Commit. - Never mark a phase ✅ while any Definition-of-Done bullet is unmet — use 🟡 Partial until all bullets are satisfied.
This section is the authoritative per-phase contract for the foundation and core-engine
band of the build. Each phase below names the crates, modules, and file paths it owns,
the work it explicitly defers, and the observable conditions under which it is "done".
Workspace-wide conventions (100% line/branch/region coverage as a hard gate, #![forbid(unsafe_code)]
#![deny(missing_docs)]on every own crate, RustCrypto-only crypto, Conventional Commits, timeless comments with no plan/phase references in committed files, and no placeholder.gitkeepfiles) are stated once in the global conventions and are not repeated per phase — each "Rules of phase" entry calls out only the conventions that bite specifically there.
Goal: Stand up the Cargo workspace, every crate skeleton, the pinned toolchain, the CI gate, and supply-chain scaffolding so all later phases build on green, reproducible infrastructure.
Scope — In
- Workspace root
Cargo.toml—[workspace]withresolver = "3", a shared[workspace.dependencies]table, and[workspace.package]carryingedition = "2024"+ a pinnedrust-version(MSRV) inherited by all members;Cargo.lockcommitted. - Library crate skeletons under
crates/:bymax-auth(facade),bymax-auth-types,bymax-auth-crypto,bymax-auth-jwt,bymax-auth-core,bymax-auth-redis,bymax-auth-axum,bymax-auth-client— each a compiling empty stub with its lib header. - WASM binding skeleton
bindings/bymax-auth-wasm(crate-type = ["cdylib"]) and the npm package skeletonpackages/rust-auth(directory +package.jsonexportsmap placeholder for./shared./client./react./nextjs); optionalxtask/dev-automation bin shell. rust-toolchain.tomlpinning the stable channel, components (rustfmt,clippy,llvm-tools-preview), thewasm32-unknown-unknowntarget, andprofile = "minimal".- CI skeleton under
.github/workflows/running fmt, clippy, build, test, andcargo-llvm-cov, plus acargo deny checkstep on the committed lockfile. - Supply-chain config: committed
deny.toml(advisories/licenses/bans/sources),cargo-auditandcargo-vetscaffolding (supply-chain/ledger), and the documented dependency-count cap file alongsidedeny.toml. - Repo files:
LICENSE,SECURITY.md,CONTRIBUTING.md,CHANGELOG.md, and aREADMEstub. - Lib headers on every own crate (
#![forbid(unsafe_code)]+#![deny(missing_docs)]); the WASM binding uses#![deny(unsafe_op_in_unsafe_fn)]instead, sincewasm-bindgenemits generatedunsafeglue it cannot forbid.
Scope — Out
- Any real logic in any crate — all crates are stubs until P1+.
- The
codeql,scorecard,audit, andrelease(OIDC dual-publish) workflows, plus thecargo-mutants, fuzz, and benchmark gates — these belong to a later release-hardening band. - WASM build-integrity, generated-types staleness, and TS typecheck CI steps — wired only once the crates they check have content (P1/P2).
- TypeScript layer implementation and the
ts-rspipeline (P2).
Definition of Done
cargo build --workspace,cargo fmt --all -- --check, andcargo clippy --workspace --all-targets -D warningsare all green on the empty stubs.cargo llvm-cov --workspaceruns and reports cleanly on the stub surface.- The CI workflow executes fmt/clippy/test/llvm-cov on every PR.
cargo deny checkpasses against the committedCargo.lock;deny.toml, the cargo-vet ledger, and the dependency-budget cap file are present.Cargo.lockis committed, the toolchain is pinned, and every own crate carries its lint header.
Context / preconditions: None — this is the first phase.
Rules of phase
- Lib headers are present from the first commit of each crate (not retrofitted later); the WASM binding's
unsafe_op_in_unsafe_fnexception is the only deviation. deny.tomlrestricts sources to crates.io only (no git/unknown registries), encodes the license allow-list, and bansopenssl/openssl-sys(andringon the wasm-targeted crates) and duplicate semver-major versions.- Edition 2024 + MSRV live in
[workspace.package]and are inherited; an MSRV bump is a deliberate, visible change. - Directories materialize only when a real file lands in them — no empty scaffolding, no placeholder files.
References: §3, §4, §19, §21, §23 (the open questions are resolved during this foundation band).
Size: M
Goal: Implement the pure-Rust, wasm-safe cryptographic primitives crate — password hashing, constant-time comparison, CSPRNG token generation, and the MFA-gated AEAD/TOTP set.
Scope — In (crate crates/bymax-auth-crypto)
- Password hashing over RustCrypto:
scrypt(default feature, the always-compiled writer) andargon2/Argon2id (theargon2feature); self-describing PHC strings ($scrypt$…/$argon2id$…) with a 16-byte random salt and 32-byte output; a compatibility parser for the legacyscrypt:hex:hexcorpus that always reports as stale. verifyis total — malformed/unknown hashes returnfalse, never an error (uniform error-path timing); rehash-detection inputs (needs_rehash-style parse of algorithm + parameters) so a caller can compare a stored hash to current params.- Constant-time comparison wrapper over
subtle(the only sanctioned secret comparison), short-circuiting only on length mismatch over fixed-length digests. - CSPRNG secure-token generation (opaque tokens, salts, IVs, OTP digits) via
rand/getrandom(OsRng), withgetrandom'swasm_jsbackend onwasm32(enabled by the WASM leaf binding in P11, not the crypto crate itself). - Mandatory hashing helpers: SHA-256 (high-entropy secret → key suffix) and keyed HMAC-SHA-256 (low-entropy identifier and recovery-code hashing).
- In-memory secret handling:
secrecy/zeroize, with transient key bytes held inZeroizingbuffers. - MFA-gated set (behind this crate's
mfafeature): AES-256-GCM encrypt/decrypt (aes-gcm, wirebase64(iv):base64(tag):base64(ciphertext), fresh 12-byte IV per call, 16-byte tag), TOTP per RFC 6238/4226 overhmac+sha1, and Base32 viadata-encodingforotpauth://URIs (generate_totp_secret,build_totp_uri,verify_totp). - Documented
spawn_blockingguidance: the primitives are synchronous and memory-hard, so callers must dispatch hash/verify to the blocking pool.
Scope — Out
- Any engine wiring or
PasswordServiceconstruction (the service lives inbymax-auth-core; P3/P4). - Startup parameter-floor validation (a
build()/ConfigErrorconcern in core; P3). - HS256 JWT signing/verifying (
bymax-auth-jwt; P2).
Definition of Done
- Builds on native and
wasm32-unknown-unknown. - The argon2-only build (
--no-default-features --features argon2) compiles and drops thescryptcrate entirely; the default build has scrypt; acompile_error!fires when neither hasher feature is enabled. - A no-
mfabuild links none ofaes-gcm/sha1/data-encoding. - 100% line/branch/region coverage.
- Property tests pass for: hash/verify round-trip and wrong-password rejection (both KDFs); AES-GCM
decrypt(encrypt(m,k),k) == mwith tamper/wrong-key failure; TOTP in-window verify / out-of-window reject + Base32 round-trip; constant-time compare matching==; secure-token format/uniqueness.
Context / preconditions: P0 (workspace, pinned toolchain with the wasm target, CI gate).
Rules of phase
- RustCrypto only — no
ring, OpenSSL, or C/C++ bindings on any path; the crate must stay wasm-clean (it is part of the purity tripwire). - No
tokio, no async, no I/O in this crate — primitives are synchronous CPU work passed plain values (timestamps, byte buffers). - A fresh
OsRngIV per AES-GCM encryption is non-negotiable; all decrypt failure modes collapse to one opaque error so the failure type is not an oracle. - Recovery codes are not KDF-hashed here — they are high-entropy and use keyed HMAC-SHA-256.
sha1is used exclusively inside the TOTP HMAC and must never be substituted for general hashing.
References: §17.1, §19.2–§19.3, §20.3.
Size: L
Goal: Define the shared domain/data and error contracts and the pure-Rust HS256 JWT primitive, and stand up the ts-rs generation pipeline that emits the ./shared TypeScript surface.
Scope — In
crates/bymax-auth-types: domain structsAuthUser/SafeAuthUser/AuthPlatformUser/SafeAuthPlatformUserwith their infallible secret-droppingFromprojections; write payloadsCreateUserData/CreateWithOAuthData/UpdateMfaData/UpdatePlatformMfaData; theAuthError/AuthErrorCodecatalog (≈34–40 stableauth.*codes, snake_case wire form) withis_internal_only(); result and claims typesDashboardClaims/PlatformClaims/MfaTempClaimsplusMfaChallengeResult/AuthResult/LoginResult/RotatedTokens; cookie-name and route default constants;RepositoryError.crates/bymax-auth-jwt: hand-rolled HS256sign/verify/decode_unverifiedoverhmac+sha2+base64(URL_SAFE_NO_PAD) +serde_json;HsKey(zeroizing secret wrapper);VerifyOptions(HS256 pinned,leeway_secs,validate_exp/validate_iat); the sealedJwtClaimstrait exposingexp()/iat().- Opaque-refresh helpers:
RawRefreshToken(CSPRNGgenerate+redis_hash=sha256hex), placed inbymax-auth-jwtoverbymax-auth-crypto. ts-rsderivation + generation pipeline + staleness check: thets-exportfeature onbymax-auth-types,#[ts(export, export_to = "shared/")]on every cross-boundary type, the generated./shareddata files (jwt-payload, auth-user, auth-result, auth-error, error-codes, auth-config, cookie-defaults, routes), and thegit diff --exit-codestaleness gate overpackages/rust-auth/src/shared.
Scope — Out
- Engine, services, and HTTP adapter (P3+).
- JTI-blacklist consultation —
verifyperforms signature +alg+ temporal checks only; revocation is a guard/store step (P3/P5). - Field-level DTO validation (an adapter concern, later).
- The hand-written TS surface that
ts-rscannot express (AuthClientError,buildAuthRefreshSkipSuffixes, theAuthResponseCodebrand) — authored on the npm side in a later TS-layer phase.
Definition of Done
- Both crates build native and
wasm32. - HS256 sign/verify round-trips;
verifyrejects non-HS256 algorithms,alg: none, tampered segments, and an expiredexp;decode_unverifiedis display-only and never feeds an authz decision. ts-rsemits the./shareddata types + constants; the staleness gate is green (no working-tree drift).AuthErrorserializes to the exactauth.*strings; internal-only codes are gated byis_internal_only().- 100% coverage, with property tests on the JWT primitive (round-trip, single-bit mutation rejection,
algrejection, expiry).
Context / preconditions: P0 (workspace + toolchain). bymax-auth-jwt depends on bymax-auth-crypto for HMAC/SHA-256/CSPRNG, so P1 should be available or land in parallel; bymax-auth-types has no crypto dependency and can proceed independently.
Rules of phase
bymax-auth-typestakes no async/no I/O dependency (serde + ts-rs only);bymax-auth-jwttakes notokioand is synchronous CPU work — both stay wasm-safe (the WASM build is the enforcing tripwire).- HS256 is pinned at verify time; the inbound
algheader is never read to select an algorithm, and only a symmetric key exists in the system. - Wire parity with nest-auth: camelCase serde renames (
tenantId,mfaEnabled, …), thetypediscriminator via#[serde(rename = "type")], and byte-identical code strings. - The signing secret is a
SecretString/HsKey, zeroized on drop and redacted inDebug/Display. - Generated TS is committed and drift-gated; the hand-written TS set stays hand-written — it is not forced through
ts-rs.
References: §13, §15, §18.3, §19.2.
Size: L
Goal: Assemble the composition root — AuthEngine + AuthEngineBuilder, the resolved AuthConfig, startup validation, and the full plugin trait set with in-memory test doubles — without yet implementing any auth flow.
Scope — In (crate crates/bymax-auth-core)
AuthEnginestruct andAuthEngineBuilderwith a setter per collaborator (user_repository,platform_user_repository,email_provider,hooks, the three individual store setters plus theredis_storesconvenience,oauth_provider,http_client,config,environment) and a single falliblebuild().AuthConfigand its nested groups (JwtConfig,RolesConfig,PasswordConfig,CookieConfig,MfaConfig,SessionConfig,BruteForceConfig,PasswordResetConfig,EmailVerificationConfig,PlatformConfig,InvitationConfig,OAuthConfig,ControllerToggles) with the two named constructorsnest_compat_defaults()andsecure_defaults()(the latter#[cfg(feature = "argon2")]), andDefault≡nest_compat_defaults().ConfigErrorand the full startup-validation rule set (all cross-field invariants), plus the two values derived duringbuild()and stored on the resolved engine — the resolvedsecure_cookiesbool and the identifier-hashing HMAC key (SHA-256("bymax-auth:hmac-key:v1" || jwt.secret)). The resolvedsecure_cookiesand the production-gated OAuth-redirect checks derive from the builder'sEnvironmentinput (defaultProduction; §5.1.4/§5.5).- Resolver traits
TenantIdResolverandMaxSessionsResolver(#[async_trait]) andCookieDomainResolver(a sync, object-safe trait with no macro), plus the framework-neutralRequestPartsview. - The full plugin trait set, all object-safe:
UserRepository,PlatformUserRepository,EmailProvider,AuthHooks,SessionStore,OtpStore,BruteForceStore,OAuthProvider,HttpClient. RequestContext,HookContext, theto_safe_user/to_safe_platform_userprojections, and theNoOpEmailProvider/NoOpAuthHooksdefaults.- In-memory test doubles (HashMap-backed repositories, a recording email provider, a hook spy, and in-memory store fakes that reproduce the atomic semantics) for the hermetic coverage tier.
- Construction-only wiring of internal service collaborators from config (the
PasswordService/TokenManagerService/etc. fields exist and are built), with their flow bodies deferred.
Scope — Out
- The actual auth flows (register/login/logout/refresh/me/verify-email, password reset, sessions, MFA, OAuth, platform, invitations) — P4+.
- Redis store implementations — P5 (this phase ships only the traits + in-memory fakes).
- The HTTP adapter (
bymax-auth-axum) — a later phase.
Definition of Done
- The engine assembles from the builder with test doubles;
build()returnsOkfor a valid config and the matchingConfigErrorfor each invariant violation. - Config validation covers every cross-field rule (secret length/entropy, role referential integrity, platform hierarchy + repository presence, MFA key length/issuer, scrypt/argon2 floors,
SameSite=None⇒ secure, refresh-path coherence, OAuth toggle prerequisites, required stores/repository). default()≡nest_compat_defaults()and never names an uncompiled algorithm;secure_defaults()is absent from the API without theargon2feature.- Every plugin trait is object-safe (
Arc<dyn _>compiles); the NoOp defaults stand in for unset optional collaborators. - 100% coverage; the in-memory doubles implement the trait-level atomic semantics they stand in for.
Context / preconditions: P1 (crypto primitives for the password service and the HMAC-key derivation) and P2 (config value types, claims, error catalog, and HsKey).
Rules of phase
- Object-safety discipline: every host-pluggable trait is
#[async_trait]+Send + Sync; the cookie-domain resolver stays macro-free; internal, statically-dispatched call sites may use nativeasync fn. - The core is transport-agnostic and Redis-free — it depends only on the store traits, never on
axum, a Redis client, orreqwest(the last only underoauth-reqwest). - The resolved
secure_cookiesand the HMAC key are derived inbuild()and stored on the engine, never surfaced onAuthConfig; validation never logs the secret, only its measured properties. - Hook discipline is declared here: only
before_register/on_oauth_loginmay block; allafter_*/on_*hooks are fire-and-forget under the 5 s timeout ceiling (exercised in P4+). - There is no standalone
core/auth/password-resetCargo feature — the engine is always compiled and those flows are runtime toggles.
References: §3, §5, §6, §9, §10.
Size: L
Goal: Implement the always-on local authentication lifecycle — register/login/logout/refresh/me/verify-email/resend — with PasswordService, brute-force throttling, session renewal, and the anti-enumeration guarantees.
Scope — In (in crates/bymax-auth-core, exercised against the in-memory store doubles)
AuthEngineflows:register,login,logout,refresh(atomic rotation + grace window),me,verify_email,resend_verification_email, the privatesend_verification_otp,issue_tokens_for_user_id, and theassert_user_not_blockedstatus helper.PasswordService: scrypt + argon2 via P1, every hash/verify dispatched throughspawn_blocking, PHC storage format, and rehash-on-verify gated byconfig.password.rehash_on_verify; a startup-loaded sentinel PHC hash for the user-not-found path.TokenManagerService(local subset): access-token issuance (issue_access/issue_tokens), opaque-refresh issuance,reissue_tokens(atomic rotation + single-shot grace), and the JTI revocation blacklist on logout via theSessionStoretrait.BruteForceService: fixed-window failure counter keyed on a hashed identifier (hmac_sha256(tenant:email)), non-extending window, reset on success,AccountLockedcarryingretry_after_seconds.- Email-verification OTP path implemented against the
OtpStoretrait (in-memory fake from P3), including the atomic resend cooldown. - Session renewal / fixation resistance (§7.1.9): a fresh opaque refresh token + fresh
jtiminted at every authentication boundary; no pre-auth or client-supplied value is ever adopted as a session key. - Anti-enumeration: the
ANTI_ENUM_MIN_MS = 300timing floor, a genericInvalidCredentialsfor both unknown-email and wrong-password, the always-run sentinel verify, internal-only code collapsing totoken_invalid, and the no-token-in-query-string invariant.
Scope — Out
- Redis store implementations — P5; P4 runs entirely against the in-memory store doubles through the traits.
SessionServiceconcurrent-session tracking / FIFO eviction / list / revoke — P6 (P4 covers token issuance, rotation, brute-force, and the fixation invariants).- The
TokenManagerServiceMFA-temp-token and WebSocket-ticket methods — later phases. - MFA, OAuth, platform login, password-reset service, and the HTTP adapter — later phases.
Definition of Done
- register/login/logout/refresh/me/verify-email/resend all pass integration tests with the in-memory doubles.
- Anti-enumeration is verified: identical timing, status, and body for unknown vs. known email on login and on the anti-enum endpoints; the sentinel verify always runs.
- Refresh rotation is single-use with a working grace window — a concurrent retry succeeds without logout, and a replay past the window returns
RefreshTokenInvalid— against the in-memory atomic fake. - rehash-on-verify upgrades a stale PHC hash fire-and-forget without blocking login; all hashing runs on
spawn_blocking. - Brute-force lockout triggers at
max_attempts, holds a fixed window, resets on success, and surfacesretry_after_seconds. - 100% coverage.
Context / preconditions: P3 (engine, builder, plugin traits, resolved config, in-memory doubles).
Rules of phase
- Never block the async runtime: every hash/verify, including the sentinel and the rehash, goes through
spawn_blocking. - Uniqueness-before-hash on register (no CPU-amplification oracle); the status and email-verification gates precede the KDF on login.
- A generic
InvalidCredentialscovers both unknown-email and wrong-password; the internal-onlytoken_expired/token_revokedcodes collapse totoken_invalidat the boundary. - Operation ordering is security-critical where flows mutate two stores (e.g. credential update before session invalidation).
- No access or refresh token is ever read from or written to a query string.
after_*hooks run fire-and-forget under the 5 s ceiling and never roll back a committed DB/store mutation.
References: §7.1, §7.2, §7.3, §7.7, §15.5, §17.2, §24 (invariants 7, 8, 14).
Size: L
Goal: Provide the canonical Redis-backed implementations of the store traits, with atomic Lua scripts, namespace prefixing, and no-PII keys.
Scope — In (crate crates/bymax-auth-redis)
SessionStore,OtpStore,BruteForceStore, andWsTicketStoreimplementations overredis+deadpool-redis(freddocumented as the alternative single-dependency client), plus the auxiliary single-purpose stores (MfaStore,OAuthStateStore,PasswordResetStore,InvitationStore,UserStatusCache) following the identical pattern.- The
RedisConndriver abstraction and theNamespacedRediswrapper — the single component allowed to build a fully-qualified{namespace}:key (default namespaceauth). - The Lua scripts:
refresh_rotate(rotation + grace pointer holding the newSessionRecord),session_revoke(ownership-checked single revoke),brute_force_incr(fixed-windowINCR+EXPIRE-on-first),otp_verify(verify + attempts + consume, residual-TTL-preserving), the single-use WS ticket (wst:GETDEL), andinvalidate_user_sessions(revoke-all in one transaction). - The
RedisStoreshandle that satisfiesSessionStore + OtpStore + BruteForceStoretogether (backing theredis_storesbuilder convenience). - Namespaced, no-PII keys: SHA-256 of high-entropy secrets and HMAC-SHA-256 of low-entropy identifiers as key suffixes; camelCase JSON value shapes byte-identical to nest-auth; a TTL on every key.
EVALSHA-with-EVALfallback onNOSCRIPT.
Scope — Out
- The services that call the stores (
SessionService,OtpService,PasswordResetService, invitations, MFA) — P6+. - Engine wiring beyond satisfying the traits (the builder already accepts the stores from P3).
- Anything wasm/edge — this crate is never linked into the
wasm32build.
Definition of Done
- Stores pass integration tests against a real Redis via
testcontainers(redis:8). - Lua atomicity is verified: rotation cannot double-spend a refresh token; revoke is ownership-checked (no cross-user revoke); the brute-force window starts at the first failure and does not slide;
otp_verifybumps attempts preserving residual TTL and consumes on success; the WS ticket is single-use viaGETDEL. - Every key is namespaced; no raw secret or PII is ever resident as a key (hashed/HMAC); every key carries a TTL.
- Stored JSON is camelCase and round-trips with the shared DTOs in
bymax-auth-types. - 100% coverage (the in-memory fake serves the hermetic coverage run; the testcontainers tier proves the real Lua/atomicity).
Context / preconditions: P3 (the store trait definitions, SessionKind, and the value DTOs live in bymax-auth-core/bymax-auth-types). Independent of P4 — P5 may proceed in parallel once P3 lands.
Rules of phase
- Parity contract: identical key prefixes, value encodings, TTLs, and Lua semantics to nest-auth so the two backends can share one Redis — the single exception is the additive
wst:(WebSocket) prefix, which is outside the parity surface. - Only
NamespacedRedisconstructs a fully-qualified key;KEYSarrive already namespaced, and the namespace is passed as anARGVelement wherever a script rebuilds member keys from a SET. - The rotation grace pointer stores the new
SessionRecordJSON, never a raw token; no raw refresh token is ever written to Redis. #![forbid(unsafe_code)]; the crate is never compiled into the edge build.- The authoritative constant-time OTP comparison is re-done in Rust via
subtle— the Lua compare only decides the attempts bump.
References: §12, §24 (invariants 9, 15).
Size: M
Goal: Implement the stateful, user-facing services on top of the real stores — session management, OTP, password reset, email verification, and invitations.
Scope — In (in crates/bymax-auth-core, exercised against bymax-auth-redis)
SessionService:create_session, FIFO eviction at the limit (enforce_session_limit, a soft cap by default, with the atomic-Lua hardening path documented for a strictdefault_max_sessions = 1), device/IP metadata (parse_user_agent, IP truncated toMAX_IP_LENGTH),list_sessions, ownership-checkedrevoke_session,revoke_all_except_current, and atomicrotate_session; the new-session and session-evicted hooks.OtpService: CSPRNGgenerate,store, and the atomic, constant-time, timing-normalized (MIN_VERIFY_MS = 100)verifywith a five-attempt ceiling and fail-closed handling of corrupt records.PasswordResetService:initiate_reset(anti-enumeration),reset_password(token | otp | verified_token),verify_otp(returning a short-lived verified token),resend_otp(atomic 60 s cooldown), and the privateapply_password_reset(hash →update_password→revoke_all, in that security-critical order).- The email-verification flow consolidated onto
OtpService/OtpStore(resend cooldown, verify,after_email_verifiedhook). InvitationService:invite(role-authorization on the inviter + a secure single-use token) andaccept_invitation(single-usegetdel, role re-validation against the hierarchy as anti-tamper, duplicate-email guard, user creation + full session issuance).
Scope — Out
- MFA (
MfaService, TOTP challenge, recovery codes), OAuth, platform auth, and the HTTP adapter — later phases. - The Redis store internals themselves (delivered in P5; here they are consumed through the traits).
Definition of Done
- Each flow passes integration tests wired to the engine + a real Redis (testcontainers).
- Session-limit FIFO eviction fires at the configured limit, the new-session alert hook and the session-evicted hook both fire, and eviction excludes the just-created session.
- Password reset works via both the token and OTP methods; the verified-token bridges verify→reset; all sessions are revoked after reset; initiate/resend are uniformly anti-enumerating (≥ 300 ms, identical body).
- OTP verify is single-use, attempt-capped at five, and timing-normalized; corrupt records fail closed.
- Invitation accept is single-use, re-validates the role against the hierarchy, rejects a duplicate email, and issues a full session.
- 100% coverage.
Context / preconditions: P4 (token issuance, PasswordService, brute force, and the engine flow scaffolding) and P5 (the real stores + Lua needed to exercise the atomic session/OTP behavior).
Rules of phase
- Anti-enumeration with normalized timing on initiate/resend; single-use atomic token/OTP consumption via
getdel. apply_password_resetorder is security-critical — the password is updated before sessions are invalidated, so a crash between the two cannot leave the old password able to mint sessions.- Session hashes are validated (64 lowercase hex) before use; full hashes are never logged (truncated to eight chars); revoke is ownership-checked to close IDOR/BOLA.
- The stored invitation payload is trusted on accept, so the role is re-validated; deployments that do not fully trust Redis should HMAC-sign the stored record.
sessionsandinvitationsare gated by both a Cargo feature and a runtime toggle, auto-promoted to active when their config is enabled.
References: §7.4, §7.6, §7.8, §7.10, §10.
Size: L
These detail entries continue the roadmap's per-phase layer; P1–P6 precede them. The global conventions — 100% line/branch/region coverage as a hard gate, RustCrypto-only with #![forbid(unsafe_code)], timeless self-explanatory comments, Conventional Commits, and OIDC-only publishing — are defined once in the roadmap's conventions section and are referenced, not restated, below.
Goal Deliver the complete TOTP MFA lifecycle as an engine service — encrypted-at-rest secrets, hashed show-once recovery codes, and anti-replay on every path — so that local-login MFA works end to end.
Scope — In
MfaServiceincrates/bymax-auth-coreimplementing the §7.5 operations:setup(idempotent; secret +otpauth://URI + recovery codes),verify_and_enable,challenge(TOTP or recovery code),disable, andregenerate_recovery_codes.regenerate_recovery_codesas an official v1 capability: strong TOTP re-auth gate, atomic wholesale invalidation of the prior set, and one-time display of the new set.- MFA-gated crypto primitives added to
crates/bymax-auth-cryptounder itsmfafeature: AES-256-GCM (Aes256Gcm) secret encryption, RFC 6238 TOTP overhmac+sha1, Base32 viadata-encoding, keyed HMAC-SHA-256 recovery-code hashing, constant-time compare. - AES-256-GCM-protected pending-setup record (
MfaSetupData, 600 s TTL) andMfaContext { Dashboard, Platform }routing to the correct repository. - Anti-replay:
verify_totp_with_anti_replayplus the fused challenge Lua (set thetu:replay marker and consume themfa:temp token in one atomic step) over the Redis store layer; recovery-code single-use removal. - Short-lived MFA temp-token issue/verify/consume on
TokenManagerService(split verify/consume, §7.3.5) feeding the dashboardMfaChallengeResult. - Namespaced brute-force counters (
challenge:isolated fromdisable:) overBruteForceStore. mfafacade feature wiring so MFA crypto and code are absent from a no-MFA build.
Scope — Out
- OAuth MFA-branch wiring (P8 consumes the temp-token path) and platform MFA challenge/controller wiring (P9/P10 consume
MfaContext::Platform). - MFA HTTP controllers and extractors (P10).
Definition of Done
- The full lifecycle (setup → verify-and-enable → challenge via TOTP and via recovery code → disable) passes integration tests for local login against both the in-memory and the
testcontainersRedis tiers. - Recovery-code regeneration is atomic (old set invalidated wholesale) and returns the new plaintext set exactly once; no endpoint returns the TOTP secret after
verify_and_enable. - Anti-replay rejects a replayed TOTP on every path (enable, challenge, disable, regenerate); the fused challenge Lua proves single-consume under concurrent same-code submissions.
- MFA crypto (
aes-gcm,sha1, Base32) compiles only under themfafeature; a no-MFA build pulls none of it into its tree. - 100% coverage on the new surface.
Context / preconditions
- P4 — core
AuthEngineand always-on flows (TokenManagerService, repository/email/hooks contracts,SafeAuthUserprojection). - P5 — the Redis store layer (
BruteForceStore,SessionStore, atomic Lua harness) for setup records, replay markers, and temp tokens. - P1 — the
bymax-auth-cryptocrate (always-onhmac/sha2/subtle/rand/secrecy); this phase adds itsmfa-gated primitives.
Rules of phase
- Uphold the §24 MFA invariants: the secret is returned only by
setup; recovery codes are stored only as keyed HMAC-SHA-256 digests; the AES-GCM IV is unique per encryption; all secret/code comparisons are constant-time. - Every racing transition (setup
SET NX, completionGETDEL, replay-mark + consume) is a single atomic Lua step — no read-then-write. - Decrypt failures collapse to one opaque
AuthError(no oracle); platform misconfiguration fails fast. disableandregenerateaccept TOTP only (never a recovery code) and honor the documented session-invalidation divergence between the two.- Follow the intentional nest-auth divergence (keyed-HMAC recovery codes, not scrypt) exactly.
References §7.5 (incl. §7.5.1–§7.5.6), §17.1 (MFA secret encryption, TOTP, HMAC split, constant-time, secure-token generation), §7.3.5, §24 invariants 5–6, 13, 16, 18.
Size L
Goal
Ship the provider-agnostic OAuth authorize→callback flow with a pluggable HTTP transport, a built-in Google provider, single-use state + PKCE, and the create/link/reject decision — all in the engine, with no hard-wired HTTP client.
Scope — In
OAuthProvidertrait and registry (OAuthProviders) incrates/bymax-auth-core, withOAuthTokens/OAuthProfile/OAuthProviderErrorsupporting types.- The object-safe
HttpClienttrait and its core-ownedHttpRequest/HttpResponse/HttpError(nohttp/reqwesttypes in the contract), plusReqwestHttpClientbehind theoauth-reqwestfacade feature. - Built-in Google provider (§11.2) implemented over the injected
HttpClient. - Engine flow:
oauth_initiate(resolve provider before any Redis write; 64-hexstate; PKCEcode_verifier→ S256code_challenge;os:{sha256(state)}at 600 s TTL) andoauth_callback(atomicGETDELstate check, code exchange, profile fetch, OAuth-identity lookup). on_oauth_logincreate/link/reject decisioning with tenant-membership enforcement, andOAuthOutcome { Authenticated, MfaChallenge }including the MFA-temp-token branch for MFA-enabled users (no MfaService dependency).- Callback success/error/MFA branch separation and the three operator-configured redirect URLs with §11.4 hardening: no insecure (non-
https) redirect in production, host allow-list (oauth.redirect_allowlist), re-serialization on?error=append. - Unverified-provider-email handling collapsed to
OAuthFailed. oauthfacade feature (orchestration + traits, zero transport deps) kept distinct fromoauth-reqwest.
Scope — Out
- Any provider beyond Google.
- The two Axum handlers, their query DTOs, and redirect-vs-JSON response shaping (P10; §8.2.7, §11.3.3).
Definition of Done
- The full authorize→callback flow is tested end to end against a mock
HttpClient, covering the create, link, and reject paths and the MFA-challenge branch. reqwestis absent from the graph unlessoauth-reqwestis enabled; the baseoauthfeature adds no HTTP/TLS crate.- A missing/forged/replayed
stateis rejected; the PKCEcode_verifieris forwarded on exchange; the NoOp hook default rejects sign-in and triggers a startup warning. - Production startup rejects a non-
httpsor off-allow-list redirect/callback URL. - 100% coverage on the OAuth surface.
Context / preconditions
- P4 — core
AuthEngine, the hooks contract (AuthHooks::on_oauth_login), the repository OAuth methods (find_by_oauth_id,create_with_oauth,link_oauth), token issuance, and the engine-minted MFA-temp-token path.
Rules of phase
- Security-critical logic (state, PKCE, exchange orchestration, hook invocation, issuance) lives in core; no HTTP handler logic in this phase.
- Every network call goes through
HttpClient; a provider never embeds a client. - Redirect targets are never request-derived; the
stateGETDELis the single-use CSRF + consume step. - Provider internals never reach the client — all provider errors map to
OAuthFailed; onlyOAuthFailed-family errors become an error-redirect, while transport/programmer errors propagate as 500. - OAuth is disabled by default until
on_oauth_loginis implemented (§24 invariant 12).
References §11 (incl. §11.1, §11.1.1, §11.2, §11.3.1–§11.3.3, §11.4), §19.2 (oauth / oauth-reqwest), §24 invariant 12.
Size L
Goal Provide the platform-administrator authentication domain — login (with MFA challenge), me, logout, refresh, and revoke-all — as a separate identity surface with its own role hierarchy and none of the tenant, email-verification, or OAuth machinery.
Scope — In
PlatformAuthServiceincrates/bymax-auth-core(§7.9):login(PlatformLoginResult = Success | MfaChallenge),logout,refresh,me,revoke_all_platform_sessions.- Platform JWT issuance/rotation over
TokenManagerService(platform claims, notenantId); platform session sets (psess:/prt:/psd:/prp:) and bearer-mode delivery. - A platform role hierarchy (
roles.platform_hierarchy) kept isolated from the tenant/dashboard hierarchy. - Anti-enumeration parity: sentinel-hash verify on an unknown admin, HMAC-keyed brute-force identifier (
platform:{email}), genericInvalidCredentials. - MFA-challenge integration for platform admins via
MfaContext::Platform— the temp token carries thecontext: platformdiscriminant so persistence routes through the platform user store. PlatformUserRepositoryconsumption;platformfacade-feature gating; no tenant scoping, no email-verification flow, no OAuth on this surface.
Scope — Out
- All HTTP wiring — the
PlatformAuthController/PlatformMfaControllerroutes and thePlatformUser/RequirePlatformRoleextractors (P10; §8.2.5–§8.2.6).
Definition of Done
- Platform login, refresh, me, logout, and revoke-all pass integration tests, including the login → MFA-challenge → full-token exchange for an MFA-enabled admin.
- The platform and tenant role hierarchies are provably isolated (a tenant role cannot satisfy a platform-role check, and vice versa).
- Logout blacklists the access
jtiand cleans both the primary and grace refresh keys; revoke-all atomically invalidates every platform session. - No tenant, email-verification, or OAuth path is reachable for platform admins.
- 100% coverage on the platform surface.
Context / preconditions
- P4 — core engine,
TokenManagerService, theSessionService/store contracts, brute force, and the sentinel-hash anti-enumeration helper. - P7 —
MfaServiceand theMfaContext::Platformchallenge path (platform login routes into it; the platform MFA management endpoints reuse it in P10).
Rules of phase
- Treat the platform identity as a distinct domain: never reuse the tenant role hierarchy, never attach a
tenantIdclaim, never expose verification or OAuth. - HMAC-keyed identifiers only (no PII in Redis); generic credential errors; uniform login latency via the sentinel hash.
- The service is only constructed when
config.platform.enabled, which itself requiresroles.platform_hierarchy.
References §5.1.6 (PlatformConfig), §7.9, §8.2.5–§8.2.6 (the routes this service backs, wired in P10), §13.3 (platform claims / MfaChallengeResult).
Size M
Goal Build the Axum adapter that exposes every engine capability over HTTP — the complete route table, all extractors/guards, validated DTOs, token delivery, per-route rate limiting, and the WebSocket upgrade-ticket flow — so the backend is reachable end to end.
Scope — In
- Router factory
auth_routerplusAxumAuthConfig/AuthState/RouteGroupsincrates/bymax-auth-axum; group mounting feature- and toggle-gated and derived from the engine's resolvedControllerToggles. - The COMPLETE route table (§8.2.1–§8.2.8):
auth,mfa,password_reset,sessions,platform,platform_mfa,oauth,invitations— every endpoint across all controllers. - Extractors via
FromRequestParts:AuthUser,OptionalAuthUser,RequireRole<R>,PlatformUser,RequirePlatformRole<R>,CurrentUser,SelfOrAdmin,UserStatus,MfaSatisfied. garde-backedValidatedJson/ValidatedQuerywithdeny_unknown_fields.AuthError→IntoResponseproducing the canonical JSON envelope and status.- The token-delivery helper (
cookie/bearer/both) with secure cookies (HttpOnly, Secure-by-default, refresh path-scoped +SameSite=Strict, the non-HttpOnlyhas_sessionsignal cookie) per §14. tower-governorper-route rate limiting (RateLimitConfigdefaults mirroringAUTH_THROTTLE_CONFIGS) normalized into theauth.too_many_requestsenvelope withRetry-After.- The WebSocket upgrade-ticket endpoint (
POST /auth/ws-ticket, composingAuthUser+UserStatus+MfaSatisfied) and theWsAuthUser/WsAuthUserFromHeaderextractors behind thewebsocketfeature. - The ordered tower middleware stack (trace, request-body limit, sensitive-header redaction, optional CORS, cookie manager).
Scope — Out
- All frontend/npm artefacts (P11) and release automation (P12).
Definition of Done
- Every endpoint in the route table works end to end under E2E tests (Axum router + real Redis via
testcontainers) across register/login/refresh/logout, MFA, sessions, the OAuth callback, platform, and invitations. - Per-route rate limiting returns 429 +
Retry-Afteras the typedTooManyRequestsenvelope; the WS ticket is single-use (GETDEL) and refuses replay. - Token delivery is verified in all three modes with correct cookie attributes;
SameSite=NonewithoutSecureis rejected at resolution. - An unconfigured group contributes zero routes (both the compile-time feature off and the runtime toggle off remove it).
- 100% coverage on the adapter.
Context / preconditions
- P4 — core
AuthEngineand always-on flows. - P6 — the remaining engine services the routes expose (sessions, token delivery, password-reset/OTP, invitations).
- P7 —
MfaService(themfaandplatform_mfaroutes). - P8 — the OAuth engine flow (the
oauthroutes/handlers). - P9 —
PlatformAuthService(theplatformandplatform_mfaroutes).
Rules of phase
- No HTTP guard ever sources a token from the query string — cookie or
Authorizationheader only; the WS upgrade ticket is the sole, single-use, URL-borne exception (§24 invariant 4). - Routing is derived from the engine, never configured independently, so the router cannot disagree with what the engine wired.
- Rate-limit layers attach per route group, never as one global layer; the adapter emits tracing spans but installs no subscriber.
- The wire contract (paths, status codes, cookie names, error envelope) matches nest-auth byte-for-byte.
References §8 (incl. §8.1–§8.8), §14 (cookie catalog, delivery modes, security constraints), §16 (rate limiting), §7.3.6 (WS ticket), §24 invariants 4, 11.
Size L
Goal Publish the edge/frontend surface — the WASM edge JWT verifier, the four-subpath npm package, and the native Rust client — and prove a React app and a Next.js middleware authenticate against the running Rust backend with zero type drift.
Scope — In
bindings/bymax-auth-wasm(cdylib): the wasm-bindgen edge surface (decode_jwt,verify_jwt_hs256with secret zeroization,extract_claims) over the wasm-safe subset ofbymax-auth-jwt, built viawasm-pack --target bundler(npm-only, never crates.io); HS256 pinned,getrandomwasm_jsbackend (features = ["wasm_js"]).- The npm package
@bymax-one/rust-auth(packages/rust-auth) with./shared(ts-rs-generated types + constants, plus the hand-writtenAuthClientErrorandbuildAuthRefreshSkipSuffixes),./client(native-fetchsingle-flight client),./react(AuthProvider/useAuth/useSession/useAuthStatus), and./nextjs(createAuthProxy+ route handlers + the WASM-backedverifyJwtToken). - Dual ESM+CJS build per subpath with the scoped
sideEffectsarray and the bundledwasm/asset; no.root export. - The Rust
crates/bymax-auth-clientcrate (reqwest,clientfeature) — the native typed auth client for Rust consumers. - The
ts-rsgeneration pipeline (ts-exportonbymax-auth-types) feeding./shared, with the staleness-gate wiring.
Scope — Out
- Final release/publish automation, SBOM, attestations, and registry publishing (P12).
Definition of Done
- The npm package builds dual ESM+CJS with a
.d.tsper subpath and the WASM asset bundled;tsc --noEmittype-checks the kept layers against freshly generated./shared. - A React app and a Next.js middleware authenticate against the running Rust backend; the Next.js edge verifies (via WASM) a token signed by the backend, demonstrating server/edge parity.
- Zero type drift: regenerating
ts-rsoutput leaves the committed./sharedunchanged (staleness gate green). - The Rust client and the WASM tests pass (
wasm-pack test, native client integration); 100% Rust coverage plus thevitestfrontend suite.
Context / preconditions
- P2 —
bymax-auth-jwt(pinned HS256 sign/verify) exists and is wasm-clean enough to compile into the edge binding. - P10 — the running Axum backend the React/Next.js consumers and the Rust client authenticate against.
Rules of phase
- The WASM module and
verifyJwtTokenare server/edge-only and must never be imported into a client bundle (the HS256 secret must not reach the browser); an accidental client import is a security defect. ./shareddata types and constants are generated, never hand-authored; onlyAuthClientErrorand the refresh-skip builder are hand-written.- The npm public API stays byte-for-byte compatible with nest-auth (consumers change only the import specifier); the proxy
{ proxy, config }shape is preserved. bymax-auth-wasmnever becomes a facade dependency or a crates.io crate.
References §18 (incl. §18.1–§18.5), §13.2 (one HS256 implementation, server + edge), §13.7 (no token in URL), §19.2 (client / wasm features), §20.8–§20.9 (frontend + type-gen tests).
Size L
Goal Stand up the full CI/release pipeline, the supply-chain and provenance controls, the documentation, the official examples, and the production dogfood, so a tagged build publishes the 1.0 to both registries with provenance, SBOM, and attestations.
Scope — In
- The full
ciworkflow:fmt,clippy -D warnings, build,llvm-cov100% (lines/regions) across thecargo-hackfeature matrix, doctests,cargo-deny,cargo-vet, the dependency-budget gate, a short time-boxedcargo-fuzzsmoke (§20.10),cargo public-api+cargo-semver-checksover the public surface (success criterion 10), WASM build-integrity + size budget, thets-rsstaleness gate, andtsc/ESLint over the npm package. (cargo-geigerruns at release, not per-PR — see the release workflow;criterionbenchmarks are tracked observationally and stay non-gating, §20.11.) - The
codeql,scorecard, and scheduledaudit(RustSec) workflows. cargo-mutantsnear-100% mutation testing as a pre-release (not per-PR) gate.- The
releaseworkflow: tag↔version match; crates.io OIDC Trusted Publishing in leaf-first DAG order ofcrates/*only (facade last;bymax-auth-wasmexcluded); npm OIDC--provenancepublish;wasm-packbuild into the npmwasm/; a CycloneDX SBOM per shipped artefact; a non-blockingcargo-geigertransitive-unsafescan (§19.6/§21.10); GitHub Artifact Attestations for crate/npm/WASM/SBOM; fail-fast on any gate; and the WASM-only security-patch release policy. - docs.rs config (feature
full,--cfg docsrs) with#![deny(missing_docs)]; TypeDoc for the npm surface; the README (two-package map, feature matrix, the two default profiles, condensed threat model, badge row) and the required repo files (SECURITY.md,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,deny.toml,supply-chain/, committedCargo.lock,.github/workflows +dependabot.yml). - The official examples under
examples/:axum-minimal,axum-mfa,axum-oauth-google,react-vite,nextjs, andbymax-live-auth— built and linted in CI so they cannot rot. - The crate and npm dogfood smoke tests before tagging, and CI enforcement of the §24 Security Invariants as a blocking gate.
Scope — Out
- Nothing — this is the publishable 1.0.
Definition of Done
- A tagged dry-run publishes to BOTH registries via OIDC (no long-lived tokens), emitting provenance, a CycloneDX SBOM, and Artifact Attestations for the crate tarball(s), the npm tarball, the
*_bg.wasm, and the SBOM itself (verifiable withgh attestation verify). - Every CI gate is green: 100% coverage across the feature matrix, doctests, WASM build-integrity + size budget,
cargo-deny/cargo-vet/ dependency-budget,ts-rsstaleness, andtsc/ESLint. - All six examples build and lint; both dogfood smokes (the crate Axum app and the npm Next.js app) pass against the to-be-published artefacts.
- The mutation score meets the agreed near-100% floor, and the §24 invariants are wired as a blocking review/CI contract.
Context / preconditions
- All prior phases (P1–P11) — the complete crate set, the npm package, the WASM binding, and the examples must exist and pass their own gates.
Rules of phase
- OIDC Trusted Publishing only — no
CARGO_REGISTRY_TOKEN/NPM_TOKENsecrets;releaseruns one-at-a-time (cancel-in-progress: false) behind a protected, manually-approved environment. - Fail-fast: a failure in type generation, the WASM build, the SBOM, the advisory audit, or any attestation aborts the release with nothing published.
- Publish
crates/*only;bymax-auth-wasmships solely inside the npm artefact; the tag↔version gate keeps the crate and npm versions in lockstep. - Coverage stays a hard PR gate, mutation testing stays the pre-release gate, and any PR weakening a §24 invariant is blocked.
References §19 (dependencies, feature matrix, supply-chain posture), §20 (testing & quality gates), §21 (incl. §21.1–§21.10), §24 (Security Invariants), §25 (examples + the Bymax Live dogfood).
Size L