From 0865e39abb682cad2d95c6141f72cb49e353b652 Mon Sep 17 00:00:00 2001 From: dawn Date: Thu, 2 Jul 2026 17:28:42 +0300 Subject: [PATCH] localinfra: add pdsls Signed-off-by: dawn --- docker-compose.yml | 13 +++ localinfra/Caddyfile | 6 + localinfra/pdsls.Dockerfile | 24 ++++ localinfra/pdsls.patch | 219 ++++++++++++++++++++++++++++++++++++ localinfra/readme.md | 1 + 5 files changed, 263 insertions(+) create mode 100644 localinfra/pdsls.Dockerfile create mode 100644 localinfra/pdsls.patch diff --git a/docker-compose.yml b/docker-compose.yml index dc210d34..4e3b42f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -357,6 +357,18 @@ services: condition: service_completed_successfully networks: [tngl] + pdsls: + build: + context: . + dockerfile: localinfra/pdsls.Dockerfile + restart: unless-stopped + ports: + - "8081:80" + depends_on: + init-accounts: + condition: service_completed_successfully + networks: [tngl] + caddy: image: caddy:2-alpine restart: unless-stopped @@ -380,6 +392,7 @@ services: - tngl.boltless.dev - mirror.tngl.boltless.dev - zoekt.tngl.boltless.dev + - pdsls.tngl.boltless.dev volumes: caddy-data: diff --git a/localinfra/Caddyfile b/localinfra/Caddyfile index 2422b451..ad09cb49 100644 --- a/localinfra/Caddyfile +++ b/localinfra/Caddyfile @@ -72,3 +72,9 @@ tngl.boltless.dev { tls internal reverse_proxy appview:3000 } + +# pdsls +pdsls.tngl.boltless.dev { + tls internal + reverse_proxy pdsls:80 +} diff --git a/localinfra/pdsls.Dockerfile b/localinfra/pdsls.Dockerfile new file mode 100644 index 00000000..ac127aab --- /dev/null +++ b/localinfra/pdsls.Dockerfile @@ -0,0 +1,24 @@ +# Development only. Not for production use. + +FROM docker.io/oven/bun:alpine AS build + +RUN apk add --no-cache git patch + +WORKDIR /src +RUN git clone https://tangled.org/pds.ls/pdsls.git . && \ + git checkout b52109b6a98953701aa2bf8038568eabc787251a + +COPY localinfra/pdsls.patch ./ +RUN patch -p1 < pdsls.patch + +RUN bun install +RUN bun run build + +FROM docker.io/library/caddy:2-alpine +COPY --from=build /src/dist /usr/share/caddy +RUN echo ":80 {" > /etc/caddy/Caddyfile && \ + echo " root * /usr/share/caddy" >> /etc/caddy/Caddyfile && \ + echo " file_server" >> /etc/caddy/Caddyfile && \ + echo " try_files {path} {path}/ /index.html" >> /etc/caddy/Caddyfile && \ + echo "}" >> /etc/caddy/Caddyfile +EXPOSE 80 diff --git a/localinfra/pdsls.patch b/localinfra/pdsls.patch new file mode 100644 index 00000000..97afad72 --- /dev/null +++ b/localinfra/pdsls.patch @@ -0,0 +1,219 @@ +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,80 @@ 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" && ( +