Monorepo for Tangled
Something went wrong. Try again.
8.7 kB · 219 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220diff --git a/bun.lock b/bun.lockindex 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.tsxindex 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.tsindex 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<Response> {+ 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<void> => {+ 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<void> => {- const init = async (): Promise<Session | undefined> => {+ await autoLoginLocalinfra();++ const init = async (): Promise<any> => { 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<void> => { 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<void> => { } }; - 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<void> => {diff --git a/src/layout.tsx b/src/layout.tsxindex 51757fa..b76e556 100644--- a/src/layout.tsx+++ b/src/layout.tsx@@ -195,7 +195,7 @@ const Layout = (props: RouteSectionProps<unknown>) => { </div> <NotificationContainer /> <PermissionPromptContainer />- <Show when={plcDirectory() !== "https://plc.directory"}>+ <Show when={plcDirectory() !== "https://plc.tngl.boltless.dev"}> <div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs"> <span> PLC directory: <span class="font-medium">{plcDirectory()}</span>diff --git a/src/views/settings.tsx b/src/views/settings.tsxindex 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 = () => { <label for="plcDirectory" class="font-medium select-none"> PLC Directory </label>- {plcDirectory() !== "https://plc.directory" && (+ {plcDirectory() !== "https://plc.tngl.boltless.dev" && ( <button type="button" class="rounded px-2 py-1 text-xs text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 active:bg-neutral-300 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-200 dark:active:bg-neutral-600" onClick={() => { localStorage.removeItem("plcDirectory");- setPlcDirectory("https://plc.directory");+ setPlcDirectory("https://plc.tngl.boltless.dev"); }} > Reset@@ -41,7 +41,7 @@ const Settings = () => { setPlcDirectory(value); } else { localStorage.removeItem("plcDirectory");- setPlcDirectory("https://plc.directory");+ setPlcDirectory("https://plc.tngl.boltless.dev"); } }} />