Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

localinfra: add pdsls

Signed-off-by: dawn <dawn@tangled.org>

authored by

dawn and committed by
Anirudh Oppiliappan
(Jul 3, 2026, 3:49 PM +0530) 0865e39a 6e3fc2d8

+263
+13
docker-compose.yml
··· 357 357 condition: service_completed_successfully 358 358 networks: [tngl] 359 359 360 + pdsls: 361 + build: 362 + context: . 363 + dockerfile: localinfra/pdsls.Dockerfile 364 + restart: unless-stopped 365 + ports: 366 + - "8081:80" 367 + depends_on: 368 + init-accounts: 369 + condition: service_completed_successfully 370 + networks: [tngl] 371 + 360 372 caddy: 361 373 image: caddy:2-alpine 362 374 restart: unless-stopped ··· 380 392 - tngl.boltless.dev 381 393 - mirror.tngl.boltless.dev 382 394 - zoekt.tngl.boltless.dev 395 + - pdsls.tngl.boltless.dev 383 396 384 397 volumes: 385 398 caddy-data:
+6
localinfra/Caddyfile
··· 72 72 tls internal 73 73 reverse_proxy appview:3000 74 74 } 75 + 76 + # pdsls 77 + pdsls.tngl.boltless.dev { 78 + tls internal 79 + reverse_proxy pdsls:80 80 + }
+24
localinfra/pdsls.Dockerfile
··· 1 + # Development only. Not for production use. 2 + 3 + FROM docker.io/oven/bun:alpine AS build 4 + 5 + RUN apk add --no-cache git patch 6 + 7 + WORKDIR /src 8 + RUN git clone https://tangled.org/pds.ls/pdsls.git . && \ 9 + git checkout b52109b6a98953701aa2bf8038568eabc787251a 10 + 11 + COPY localinfra/pdsls.patch ./ 12 + RUN patch -p1 < pdsls.patch 13 + 14 + RUN bun install 15 + RUN bun run build 16 + 17 + FROM docker.io/library/caddy:2-alpine 18 + COPY --from=build /src/dist /usr/share/caddy 19 + RUN echo ":80 {" > /etc/caddy/Caddyfile && \ 20 + echo " root * /usr/share/caddy" >> /etc/caddy/Caddyfile && \ 21 + echo " file_server" >> /etc/caddy/Caddyfile && \ 22 + echo " try_files {path} {path}/ /index.html" >> /etc/caddy/Caddyfile && \ 23 + echo "}" >> /etc/caddy/Caddyfile 24 + EXPOSE 80
+219
localinfra/pdsls.patch
··· 1 + diff --git a/bun.lock b/bun.lock 2 + index 12bc3ba..3d6395a 100644 3 + --- a/bun.lock 4 + +++ b/bun.lock 5 + @@ -37,14 +37,14 @@ 6 + "@takumi-rs/wasm": "^1.8.7", 7 + "@tanstack/solid-virtual": "^3.13.31", 8 + "codemirror": "^6.0.2", 9 + - "solid-js": "^1.9.13", 10 + + "solid-js": "^1.9.14", 11 + "valibot": "^1.4.2", 12 + }, 13 + "devDependencies": { 14 + "@iconify-json/lucide": "^1.2.115", 15 + "@iconify/tailwind4": "^1.2.3", 16 + "@tailwindcss/vite": "^4.3.2", 17 + - "@types/node": "^26.0.1", 18 + + "@types/node": "^26.1.0", 19 + "esbuild": "^0.28.1", 20 + "oxfmt": "^0.57.0", 21 + "tailwindcss": "^4.3.2", 22 + diff --git a/src/auth/account.tsx b/src/auth/account.tsx 23 + index e660c1a..7ae64b1 100644 24 + --- a/src/auth/account.tsx 25 + +++ b/src/auth/account.tsx 26 + @@ -43,6 +43,7 @@ const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => voi 27 + } catch { 28 + deleteStoredSession(did); 29 + } 30 + + localStorage.removeItem(`pass_session:${did}`); 31 + setSessions( 32 + produce((accs) => { 33 + delete accs[did]; 34 + diff --git a/src/auth/session-manager.ts b/src/auth/session-manager.ts 35 + index 6ac8570..bca6de4 100644 36 + --- a/src/auth/session-manager.ts 37 + +++ b/src/auth/session-manager.ts 38 + @@ -42,8 +42,80 @@ export const loadHandleForSession = async (did: Did, storedSessions: Sessions) = 39 + } 40 + }; 41 + 42 + +export class PasswordUserAgent { 43 + + sub: Did; 44 + + service: string; 45 + + accessJwt: string; 46 + + constructor(sub: Did, service: string, accessJwt: string) { 47 + + this.sub = sub; 48 + + this.service = service; 49 + + this.accessJwt = accessJwt; 50 + + } 51 + + async signOut() {} 52 + + async handle(pathname: string, init?: RequestInit): Promise<Response> { 53 + + const url = new URL(pathname, this.service); 54 + + const headers = new Headers(init?.headers); 55 + + headers.set("Authorization", `Bearer ${this.accessJwt}`); 56 + + return await fetch(url.href, { ...init, headers }); 57 + + } 58 + +} 59 + + 60 + +export const autoLoginLocalinfra = async (): Promise<void> => { 61 + + try { 62 + + const pdsUrl = "https://pds.tngl.boltless.dev"; 63 + + 64 + + const healthRes = await fetch(`${pdsUrl}/xrpc/_health`, { signal: AbortSignal.timeout(2000) }); 65 + + if (!healthRes.ok) return; 66 + + 67 + + const resolveAndLogin = async (username: string) => { 68 + + const handle = `${username}.pds.tngl.boltless.dev`; 69 + + const resolveRes = await fetch(`${pdsUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`); 70 + + if (!resolveRes.ok) return null; 71 + + const { did } = await resolveRes.json(); 72 + + 73 + + const loginRes = await fetch(`${pdsUrl}/xrpc/com.atproto.server.createSession`, { 74 + + method: "POST", 75 + + headers: { "Content-Type": "application/json" }, 76 + + body: JSON.stringify({ identifier: handle, password: "password" }), 77 + + }); 78 + + if (!loginRes.ok) return null; 79 + + const { accessJwt } = await loginRes.json(); 80 + + return { did, handle, accessJwt }; 81 + + }; 82 + + 83 + + const alice = await resolveAndLogin("alice"); 84 + + const bob = await resolveAndLogin("bob"); 85 + + 86 + + const sessions = loadSessionsFromStorage() || {}; 87 + + let lastSignedIn = localStorage.getItem("lastSignedIn"); 88 + + 89 + + if (alice) { 90 + + sessions[alice.did] = { signedIn: true, handle: alice.handle, grantedScopes: "atproto,create,update,delete,blob" }; 91 + + localStorage.setItem(`pass_session:${alice.did}`, JSON.stringify({ service: pdsUrl, accessJwt: alice.accessJwt })); 92 + + if (!lastSignedIn) lastSignedIn = alice.did; 93 + + } 94 + + if (bob) { 95 + + sessions[bob.did] = { signedIn: true, handle: bob.handle, grantedScopes: "atproto,create,update,delete,blob" }; 96 + + localStorage.setItem(`pass_session:${bob.did}`, JSON.stringify({ service: pdsUrl, accessJwt: bob.accessJwt })); 97 + + if (!lastSignedIn) lastSignedIn = lastSignedIn || bob.did; 98 + + } 99 + + 100 + + if (alice || bob) { 101 + + saveSessionToStorage(sessions); 102 + + if (lastSignedIn) { 103 + + localStorage.setItem("lastSignedIn", lastSignedIn); 104 + + } 105 + + } 106 + + } catch (e) { 107 + + console.error("Auto login failed:", e); 108 + + } 109 + +}; 110 + + 111 + export const retrieveSession = async (): Promise<void> => { 112 + - const init = async (): Promise<Session | undefined> => { 113 + + await autoLoginLocalinfra(); 114 + + 115 + + const init = async (): Promise<any> => { 116 + const params = new URLSearchParams(decodeURIComponent(location.hash.slice(1))); 117 + 118 + if (params.has("state") && (params.has("code") || params.has("error"))) { 119 + @@ -61,21 +133,34 @@ export const retrieveSession = async (): Promise<void> => { 120 + const newSessions: Sessions = sessions || {}; 121 + newSessions[did] = { signedIn: true, grantedScopes }; 122 + saveSessionToStorage(newSessions); 123 + - return auth.session; 124 + + return new OAuthUserAgent(auth.session); 125 + } else { 126 + const lastSignedIn = localStorage.getItem("lastSignedIn"); 127 + 128 + if (lastSignedIn) { 129 + const sessions = loadSessionsFromStorage(); 130 + const newSessions: Sessions = sessions || {}; 131 + + 132 + + const passSession = localStorage.getItem(`pass_session:${lastSignedIn}`); 133 + + if (passSession) { 134 + + try { 135 + + const { service, accessJwt } = JSON.parse(passSession); 136 + + const agent = new PasswordUserAgent(lastSignedIn as Did, service, accessJwt); 137 + + newSessions[lastSignedIn].signedIn = true; 138 + + saveSessionToStorage(newSessions); 139 + + return agent; 140 + + } catch {} 141 + + } 142 + + 143 + try { 144 + const session = await getSession(lastSignedIn as Did); 145 + - const rpc = new Client({ handler: new OAuthUserAgent(session) }); 146 + + const agent = new OAuthUserAgent(session); 147 + + const rpc = new Client({ handler: agent }); 148 + const res = await rpc.get("com.atproto.server.getSession"); 149 + newSessions[lastSignedIn].signedIn = true; 150 + saveSessionToStorage(newSessions); 151 + if (!res.ok) throw res.data.error; 152 + - return session; 153 + + return agent; 154 + } catch (err) { 155 + newSessions[lastSignedIn].signedIn = false; 156 + saveSessionToStorage(newSessions); 157 + @@ -85,9 +170,9 @@ export const retrieveSession = async (): Promise<void> => { 158 + } 159 + }; 160 + 161 + - const session = await init(); 162 + + const agentInstance = await init(); 163 + 164 + - if (session) setAgent(new OAuthUserAgent(session)); 165 + + if (agentInstance) setAgent(agentInstance); 166 + }; 167 + 168 + export const resumeSession = async (did: Did): Promise<void> => { 169 + diff --git a/src/layout.tsx b/src/layout.tsx 170 + index 51757fa..b76e556 100644 171 + --- a/src/layout.tsx 172 + +++ b/src/layout.tsx 173 + @@ -195,7 +195,7 @@ const Layout = (props: RouteSectionProps<unknown>) => { 174 + </div> 175 + <NotificationContainer /> 176 + <PermissionPromptContainer /> 177 + - <Show when={plcDirectory() !== "https://plc.directory"}> 178 + + <Show when={plcDirectory() !== "https://plc.tngl.boltless.dev"}> 179 + <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"> 180 + <span> 181 + PLC directory: <span class="font-medium">{plcDirectory()}</span> 182 + diff --git a/src/views/settings.tsx b/src/views/settings.tsx 183 + index 5f16783..7621cf8 100644 184 + --- a/src/views/settings.tsx 185 + +++ b/src/views/settings.tsx 186 + @@ -5,7 +5,7 @@ import { ThemeSelection } from "../components/theme.jsx"; 187 + 188 + export const [hideMedia, setHideMedia] = createSignal(localStorage.hideMedia === "true"); 189 + export const [plcDirectory, setPlcDirectory] = createSignal( 190 + - localStorage.plcDirectory || "https://plc.directory", 191 + + localStorage.plcDirectory || "https://plc.tngl.boltless.dev", 192 + ); 193 + 194 + const Settings = () => { 195 + @@ -18,13 +18,13 @@ const Settings = () => { 196 + <label for="plcDirectory" class="font-medium select-none"> 197 + PLC Directory 198 + </label> 199 + - {plcDirectory() !== "https://plc.directory" && ( 200 + + {plcDirectory() !== "https://plc.tngl.boltless.dev" && ( 201 + <button 202 + type="button" 203 + 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" 204 + onClick={() => { 205 + localStorage.removeItem("plcDirectory"); 206 + - setPlcDirectory("https://plc.directory"); 207 + + setPlcDirectory("https://plc.tngl.boltless.dev"); 208 + }} 209 + > 210 + Reset 211 + @@ -41,7 +41,7 @@ const Settings = () => { 212 + setPlcDirectory(value); 213 + } else { 214 + localStorage.removeItem("plcDirectory"); 215 + - setPlcDirectory("https://plc.directory"); 216 + + setPlcDirectory("https://plc.tngl.boltless.dev"); 217 + } 218 + }} 219 + />
+1
localinfra/readme.md
··· 18 18 - knotmirror (<https://knotmirror.tngl.boltless.dev>) 19 19 - appview (<https://tngl.boltless.dev>) (live reloading) 20 20 - [ncps](https://github.com/kalbasit/ncps) nix binary cache (internal, `http://ncps:8501`) 21 + - pdsls (<https://pdsls.tngl.boltless.dev>) 21 22 - caddy reverse proxy 22 23 23 24 ## Setup