Conversation
NathanTarbert
left a comment
There was a problem hiding this comment.
This is the strongest of the five, and the parts that are easy to get wrong are right.
First, the thing that is our fault rather than yours: CI has never run on any of your PRs. Fork contributions need a maintainer to approve the workflow runs, so every check is parked at action_required. I ran everything locally instead, which is how the blocker below turned up.
One blocker, and it is one word
apps/web/src/app/api/team/invite/resend/route.ts:12 exports INVITE_RESEND_COOLDOWN_MS. Next's App Router only allows HTTP verbs and a fixed set of config fields as route exports, so both pnpm build and pnpm typecheck fail:
Type error: "INVITE_RESEND_COOLDOWN_MS" is not a valid Route export field.
Dropping export fixes both — I verified a clean build and typecheck afterwards, and nothing imports the constant. It is the only non-verb export const in any route file in the repo.
pnpm test passes with the export in place, because vitest imports the module directly and never applies Next's route constraint. So there was no way for you to see this without CI.
Two other PRs of yours have the same shape — #273 exports sanitizeHistory and some constants from its route, and it is the same fix.
Worth fixing before merge
Two of the new tests are load-sensitive. auth-passwords.test.ts:72 and the backward-compat test below it timed out at 5000ms on a full run under load, reproduced 1 of 3 times, and do not fail on main. They add bcrypt cost-12 work, and the pre-existing multi-hash test in that same file already carries { timeout: 30000 } for exactly this reason. Adding the same override to the two new ones should settle it — CI runners are usually slower than a dev machine, so this would surface there.
Smaller
The password policy now exists in four places — validatePassword, the hand-mirrored mocks in the route tests, scripts/create-admin.ts:82, and apps/web/src/app/api/profile/route.ts:64-69. I mutated the real MAX_PASSWORD_BYTES and all 35 web route tests stayed green, because they assert the mirror rather than the policy. The real policy is covered at package level, so the gap is narrow, and you followed the existing convention here — but the "faithful mirror" comment is the only thing keeping them in sync. create-admin.ts and the profile route calling validatePassword would collapse two of the four.
Also: the throttle read-then-write is not atomic, so two concurrent resends can both pass. Admin-gated, so low impact. And invite create has no throttle at all — out of scope here, worth its own issue.
What I verified, so nobody re-checks it
The byte cap is a real byte cap, which is the thing this class of change usually gets wrong. Against the repo's actual bcrypt: a 24-character euro-sign password is 72 bytes and accepted; a 30-character CJK password is 90 bytes and correctly rejected. A character cap would have let the second through. passwordByteLength uses TextEncoder rather than Buffer, so it works in edge runtimes too.
Existing users cannot be locked out. verifyPassword is untouched and the login path never calls validatePassword — a legacy over-length password still verifies. The cap applies at set time only, which is the right call.
The throttle is the good version. Database-backed rather than in-memory, so it survives deploys and works across replicas. Keyed on member id rather than email, so there is no case or whitespace bypass. It fails closed. Retry-After is set.
Both central claims survive mutation. Turning the byte cap into a character cap fails your multibyte test; disabling the throttle fails the 429 test. The multibyte one is doing real work — it is exactly the assertion that separates a correct cap from a plausible-looking wrong one.
Not yours
prettier --check fails on 6 of the 10 files you touched — and all 6 fail on main too. The format check runs over whole changed files, so touching them pulled a pre-existing backlog into scope. We need to decide how to clear that; it is not something to ask you to absorb.
Two auth hardening gaps, one PR:
Changes:
Verification (all real, run locally):