diff --git a/src/__tests__/auth/login-page.test.tsx b/src/__tests__/auth/login-page.test.tsx
index 09c569e..5d44255 100644
--- a/src/__tests__/auth/login-page.test.tsx
+++ b/src/__tests__/auth/login-page.test.tsx
@@ -123,6 +123,72 @@ describe('LoginPage', () => {
expect(mockLogin).toHaveBeenCalledWith('test.bsky.social')
})
+ it('strips at:// prefix from AT-URI with handle', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.type(screen.getByLabelText(/handle/i), 'at://ngerakines.me')
+ await user.click(screen.getByRole('button', { name: /continue/i }))
+
+ expect(mockLogin).toHaveBeenCalledWith('ngerakines.me')
+ })
+
+ it('strips at:// prefix from AT-URI with DID', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.type(screen.getByLabelText(/handle/i), 'at://did:plc:cbkjy5n7bk3ax2wplmtjofq2')
+ await user.click(screen.getByRole('button', { name: /continue/i }))
+
+ expect(mockLogin).toHaveBeenCalledWith('did:plc:cbkjy5n7bk3ax2wplmtjofq2')
+ })
+
+ it('passes plain DID through without lowercasing', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.type(screen.getByLabelText(/handle/i), 'did:plc:CbKjY5N7Bk3Ax2WplmTjOfQ2')
+ await user.click(screen.getByRole('button', { name: /continue/i }))
+
+ expect(mockLogin).toHaveBeenCalledWith('did:plc:CbKjY5N7Bk3Ax2WplmTjOfQ2')
+ })
+
+ it('passes did:web identifier through without lowercasing', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.type(screen.getByLabelText(/handle/i), 'did:web:Example.Com')
+ await user.click(screen.getByRole('button', { name: /continue/i }))
+
+ expect(mockLogin).toHaveBeenCalledWith('did:web:Example.Com')
+ })
+
+ it('extracts DID from bsky.app profile URL', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.type(
+ screen.getByLabelText(/handle/i),
+ 'https://bsky.app/profile/did:plc:cbkjy5n7bk3ax2wplmtjofq2'
+ )
+ await user.click(screen.getByRole('button', { name: /continue/i }))
+
+ expect(mockLogin).toHaveBeenCalledWith('did:plc:cbkjy5n7bk3ax2wplmtjofq2')
+ })
+
+ it('strips AT-URI path segments after authority', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.type(
+ screen.getByLabelText(/handle/i),
+ 'at://ngerakines.me/app.bsky.feed.post/abc123'
+ )
+ await user.click(screen.getByRole('button', { name: /continue/i }))
+
+ expect(mockLogin).toHaveBeenCalledWith('ngerakines.me')
+ })
+
it('shows user-friendly error for unknown handle (502)', async () => {
const { ApiError } = await import('@/lib/api/client')
mockLogin.mockRejectedValueOnce(new ApiError(502, 'API 502: Bad Gateway'))
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 4840a78..42e6c0e 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -38,13 +38,21 @@ function LoginContent() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- const trimmed = handle
+ let identifier = handle
.trim()
.replace(/^https?:\/\/bsky\.app\/profile\//i, '')
+ .replace(/^at:\/\//i, '')
.replace(/^@/, '')
- .replace(/\.$/, '')
- .toLowerCase()
- if (!trimmed) {
+ // Strip AT-URI path segments (e.g. "handle/app.bsky.feed.post/abc" → "handle")
+ if (!identifier.startsWith('did:')) {
+ identifier = identifier.split('/')[0] ?? identifier
+ }
+ identifier = identifier.replace(/\.$/, '')
+ // Only lowercase handles -- DIDs are case-sensitive
+ if (!identifier.startsWith('did:')) {
+ identifier = identifier.toLowerCase()
+ }
+ if (!identifier) {
setError('Please enter your handle')
return
}
@@ -55,11 +63,11 @@ function LoginContent() {
try {
// Store returnTo so callback can redirect back
sessionStorage.setItem('auth_returnTo', returnTo)
- await login(trimmed)
+ await login(identifier)
} catch (err) {
if (err instanceof ApiError && err.status === 502) {
setError(
- `We couldn't find an account for "${trimmed}". Please check for typos and try again.`
+ `We couldn't find an account for "${identifier}". Please check for typos and try again.`
)
} else {
setError(err instanceof Error ? err.message : 'Failed to start login')
@@ -112,7 +120,7 @@ function LoginContent() {