Commit2cd3bb4
fix(casing): lowercase-on-create + case-insensitive passkey login
fix(casing): lowercase-on-create + case-insensitive passkey login
Safe, contained slice of the lowercase work (no data migration; existing
repos stay put and rely on case-insensitive resolution per owner decision):
- passkeys.tsx: passkey login matched a lowercased input against the
case-sensitive username column, so a mixed-case user ("Ccantynz") could
never sign in with a passkey. Now lower(username)=lower(input).
- import-helper sanitizeRepoName + api.ts repo-create: normalize new repo
names to lowercase so "MyRepo"/"myrepo" can't both exist. Stops the
"Vapron" vs "vapron" confusion from recurring on new repos.
Typecheck clean; suite 3116 pass / 4 pre-existing fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>3 files changed+15−32cd3bb4badd485dda348f8e2129e962e93163a59
3 changed files+15−3
Modifiedsrc/lib/import-helper.ts+6−1View fileUnifiedSplit
@@ -92,7 +92,12 @@ function stripDotGit(name: string): string {
9292 * else with a hyphen so an imported repo is always addressable.
9393 */
9494export function sanitizeRepoName(name: string): string {
95 const cleaned = name.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
95 // Lowercase on create — repo slugs are canonically lowercase to avoid the
96 // "Vapron" vs "vapron" confusion. Imported names are normalized here.
97 const cleaned = name
98 .replace(/[^A-Za-z0-9._-]/g, "-")
99 .replace(/^-+|-+$/g, "")
100 .toLowerCase();
96101 return cleaned || "imported-repo";
97102}
98103
Modifiedsrc/routes/api.ts+4−0View fileUnifiedSplit
@@ -36,6 +36,10 @@ api.post("/repos", async (c) => {
3636 if (!/^[a-zA-Z0-9._-]+$/.test(body.name)) {
3737 return c.json({ error: "Invalid repository name" }, 400);
3838 }
39 // Normalize to lowercase — repo slugs are canonically lowercase so
40 // "MyRepo" and "myrepo" can't both exist and cause "Vapron" vs "vapron"
41 // confusion. Applied before the duplicate checks + on-disk init below.
42 body.name = body.name.toLowerCase();
3943
4044 try {
4145 // Find creator (user who is performing the action)
Modifiedsrc/routes/passkeys.tsx+5−2View fileUnifiedSplit
@@ -24,7 +24,7 @@
2424
2525import { Hono } from "hono";
2626import { setCookie } from "hono/cookie";
27import { eq } from "drizzle-orm";
27import { eq, sql } from "drizzle-orm";
2828import { db } from "../db";
2929import { users, userPasskeys, sessions } from "../db/schema";
3030import type { AuthEnv } from "../middleware/auth";
@@ -1105,7 +1105,10 @@ passkeys.post("/api/passkeys/auth/options", async (c) => {
11051105 const [u] = await db
11061106 .select({ id: users.id })
11071107 .from(users)
1108 .where(eq(users.username, body.username.trim().toLowerCase()))
1108 // Case-insensitive: a user stored with mixed case (e.g. "Ccantynz")
1109 // must still be found by their passkey login. Comparing a lowercased
1110 // input against the case-sensitive column silently failed before.
1111 .where(sql`lower(${users.username}) = lower(${body.username.trim()})`)
11091112 .limit(1);
11101113 if (u) {
11111114 userId = u.id;
11121115