diff --git a/bun.lock b/bun.lock index 12bc3ba..3d6395a 100644 --- a/bun.lock +++ b/bun.lock @@ -37,14 +37,14 @@ "@takumi-rs/wasm": "^1.8.7", "@tanstack/solid-virtual": "^3.13.31", "codemirror": "^6.0.2", - "solid-js": "^1.9.13", + "solid-js": "^1.9.14", "valibot": "^1.4.2", }, "devDependencies": { "@iconify-json/lucide": "^1.2.115", "@iconify/tailwind4": "^1.2.3", "@tailwindcss/vite": "^4.3.2", - "@types/node": "^26.0.1", + "@types/node": "^26.1.0", "esbuild": "^0.28.1", "oxfmt": "^0.57.0", "tailwindcss": "^4.3.2", diff --git a/src/auth/account.tsx b/src/auth/account.tsx index e660c1a..7ae64b1 100644 --- a/src/auth/account.tsx +++ b/src/auth/account.tsx @@ -43,6 +43,7 @@ const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => voi } catch { deleteStoredSession(did); } + localStorage.removeItem(`pass_session:${did}`); setSessions( produce((accs) => { delete accs[did]; diff --git a/src/auth/session-manager.ts b/src/auth/session-manager.ts index 6ac8570..bca6de4 100644 --- a/src/auth/session-manager.ts +++ b/src/auth/session-manager.ts @@ -42,8 +42,79 @@ export const loadHandleForSession = async (did: Did, storedSessions: Sessions) = } }; +export class PasswordUserAgent { + sub: Did; + service: string; + accessJwt: string; + constructor(sub: Did, service: string, accessJwt: string) { + this.sub = sub; + this.service = service; + this.accessJwt = accessJwt; + } + async signOut() {} + async handle(pathname: string, init?: RequestInit): Promise { + const url = new URL(pathname, this.service); + const headers = new Headers(init?.headers); + headers.set("Authorization", `Bearer ${this.accessJwt}`); + return await fetch(url.href, { ...init, headers }); + } +} + +export const autoLoginLocalinfra = async (): Promise => { + try { + const pdsUrl = "https://pds.tngl.boltless.dev"; + + const healthRes = await fetch(`${pdsUrl}/xrpc/_health`, { signal: AbortSignal.timeout(2000) }); + if (!healthRes.ok) return; + + const resolveAndLogin = async (username: string) => { + const handle = `${username}.pds.tngl.boltless.dev`; + const resolveRes = await fetch(`${pdsUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`); + if (!resolveRes.ok) return null; + const { did } = await resolveRes.json(); + + const loginRes = await fetch(`${pdsUrl}/xrpc/com.atproto.server.createSession`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ identifier: handle, password: "password" }), + }); + if (!loginRes.ok) return null; + const { accessJwt } = await loginRes.json(); + return { did, handle, accessJwt }; + }; + + const alice = await resolveAndLogin("alice"); + const bob = await resolveAndLogin("bob"); + + const sessions = loadSessionsFromStorage() || {}; + let lastSignedIn = localStorage.getItem("lastSignedIn"); + + if (alice) { + sessions[alice.did] = { signedIn: true, handle: alice.handle, grantedScopes: "atproto,create,update,delete,blob" }; + localStorage.setItem(`pass_session:${alice.did}`, JSON.stringify({ service: pdsUrl, accessJwt: alice.accessJwt })); + if (!lastSignedIn) lastSignedIn = alice.did; + } + if (bob) { + sessions[bob.did] = { signedIn: true, handle: bob.handle, grantedScopes: "atproto,create,update,delete,blob" }; + localStorage.setItem(`pass_session:${bob.did}`, JSON.stringify({ service: pdsUrl, accessJwt: bob.accessJwt })); + if (!lastSignedIn) lastSignedIn = lastSignedIn || bob.did; + } + + if (alice || bob) { + saveSessionToStorage(sessions); + if (lastSignedIn) { + localStorage.setItem("lastSignedIn", lastSignedIn); + } + } + } catch (e) { + console.error("Auto login failed:", e); + } +}; + export const retrieveSession = async (): Promise => { - const init = async (): Promise => { + await autoLoginLocalinfra(); + + const init = async (): Promise => { const params = new URLSearchParams(decodeURIComponent(location.hash.slice(1))); if (params.has("state") && (params.has("code") || params.has("error"))) { @@ -61,21 +133,34 @@ export const retrieveSession = async (): Promise => { const newSessions: Sessions = sessions || {}; newSessions[did] = { signedIn: true, grantedScopes }; saveSessionToStorage(newSessions); - return auth.session; + return new OAuthUserAgent(auth.session); } else { const lastSignedIn = localStorage.getItem("lastSignedIn"); if (lastSignedIn) { const sessions = loadSessionsFromStorage(); const newSessions: Sessions = sessions || {}; + + const passSession = localStorage.getItem(`pass_session:${lastSignedIn}`); + if (passSession) { + try { + const { service, accessJwt } = JSON.parse(passSession); + const agent = new PasswordUserAgent(lastSignedIn as Did, service, accessJwt); + newSessions[lastSignedIn].signedIn = true; + saveSessionToStorage(newSessions); + return agent; + } catch {} + } + try { const session = await getSession(lastSignedIn as Did); - const rpc = new Client({ handler: new OAuthUserAgent(session) }); + const agent = new OAuthUserAgent(session); + const rpc = new Client({ handler: agent }); const res = await rpc.get("com.atproto.server.getSession"); newSessions[lastSignedIn].signedIn = true; saveSessionToStorage(newSessions); if (!res.ok) throw res.data.error; - return session; + return agent; } catch (err) { newSessions[lastSignedIn].signedIn = false; saveSessionToStorage(newSessions); @@ -85,9 +170,9 @@ export const retrieveSession = async (): Promise => { } }; - const session = await init(); + const agentInstance = await init(); - if (session) setAgent(new OAuthUserAgent(session)); + if (agentInstance) setAgent(agentInstance); }; export const resumeSession = async (did: Did): Promise => { diff --git a/src/layout.tsx b/src/layout.tsx index 51757fa..b76e556 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -195,7 +195,7 @@ const Layout = (props: RouteSectionProps) => { - +
PLC directory: {plcDirectory()} diff --git a/src/views/settings.tsx b/src/views/settings.tsx index 5f16783..7621cf8 100644 --- a/src/views/settings.tsx +++ b/src/views/settings.tsx @@ -5,7 +5,7 @@ import { ThemeSelection } from "../components/theme.jsx"; export const [hideMedia, setHideMedia] = createSignal(localStorage.hideMedia === "true"); export const [plcDirectory, setPlcDirectory] = createSignal( - localStorage.plcDirectory || "https://plc.directory", + localStorage.plcDirectory || "https://plc.tngl.boltless.dev", ); const Settings = () => { @@ -18,13 +18,13 @@ const Settings = () => { - {plcDirectory() !== "https://plc.directory" && ( + {plcDirectory() !== "https://plc.tngl.boltless.dev" && (