Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198// app.ziran.space: Ziran in the browser (docs/WEB-APP.md). This entry does// exactly three things — restore or start the OAuth session, install the// web backend under the SAME desk store and components the desktop renders,// and mount either the base window (the app) or a document tab (`#doc=…`).// Everything the person sees past sign-in is shared code.
import { BrowserOAuthClient, type OAuthSession } from '@atproto/oauth-client-browser';import { SpaceClient } from '../sync/spaces/client.ts';import { makeSessionFetch } from '../sync/spaces/session-fetch.ts';import { ZIRAN_SCOPE } from '../sync/spaces/scope.ts';import { resolveIdentity } from '../sync/atproto.ts';import { getProfile } from '../server/atprofiles.ts';import { setBackend } from '../state/backend.ts';import { APP_ORIGIN, clientMetadata } from './client-metadata.ts';import { ensureHomeSpace } from './home.ts';import { WebHost } from './host.ts';import { WebBackend } from './webbackend.ts';
const $ = <T extends HTMLElement = HTMLElement>(id: string): T => document.getElementById(id) as T;
function show(id: string, visible: boolean) { const el = $(id); if (el) el.hidden = !visible;}
function setError(message: string | null, hint?: string) { const box = $('error'); if (!box) return; box.hidden = !message; if (message) { $('error-msg').textContent = message; $('error-hint').textContent = hint ?? ''; }}
/** Space-endpoint refusals, in the desktop panel's honest vocabulary. */function explain(err: unknown): { message: string; hint: string } { const raw = (err as Error)?.message ?? String(err); if (/notimplemented|unknown xrpc|method not implemented/i.test(raw)) { return { message: 'Your server doesn’t support live documents yet.', hint: `They need a server running the AT Proto Spaces protocol. You can create an account at ziran.space/account. (${raw})`, }; } if (/scope|permission|forbidden|missing.*jwt|401|403/i.test(raw)) { return { message: 'This sign-in doesn’t carry permission for your documents.', hint: `Signing out and back in grants it. (${raw})`, }; } return { message: 'Ziran couldn’t reach your server just now.', hint: raw };}
let oauth: BrowserOAuthClient;
async function main() { if (location.origin !== APP_ORIGIN) show('dev-note', true); // Fresh from the account page: `#handle=<handle>` prefills the sign-in // field so the person only confirms what they just created. { const pre = new URLSearchParams(location.hash.slice(1)).get('handle'); if (pre) { $<HTMLInputElement>('handle').value = pre; history.replaceState(null, '', '/'); } } oauth = new BrowserOAuthClient({ clientMetadata: clientMetadata(), handleResolver: 'https://public.api.bsky.app', responseMode: 'fragment', // codes ride the fragment, out of server logs onSessionDeleted: () => { renderSignedOut('Your sign-in expired or was ended elsewhere. Signing in again reconnects everything.'); }, }); $('signin-form')?.addEventListener('submit', onSignIn); try { const restored = await oauth.init(); if (restored?.session) return await boot(restored.session); } catch (err) { const { message, hint } = explain(err); setError(message, hint); } renderSignedOut();}
async function onSignIn(e: Event) { e.preventDefault(); const input = $<HTMLInputElement>('handle'); const handle = input.value.trim().replace(/^@/, ''); if (!handle) return; $<HTMLButtonElement>('signin-go').disabled = true; // A share link's document address rides the fragment, and the OAuth // round-trip overwrites it — stash it so sign-in lands ON the document. try { if (docTarget()) sessionStorage.setItem('ziran-doc-return', location.hash); } catch { /* storage may be unavailable; the person just lands on base */ } try { await oauth.signInRedirect(handle, { scope: ZIRAN_SCOPE }); } catch (err) { $<HTMLButtonElement>('signin-go').disabled = false; setError('Could not start sign-in.', (err as Error).message); }}
function renderSignedOut(note?: string) { show('signin', true); if (note) setError(note);}
/** Doc tabs carry their target in the fragment: `#doc=<rkey>[&ref=<space>]`, or `#new=<modelId>` for a tab that creates its document first (the click gets its tab before the slow work). The OAuth callback also rides the fragment, so only a hash with one of these params is a document address. */function docTarget(): { rkey: string; ref?: string; newModelId?: string } | null { const params = new URLSearchParams(location.hash.slice(1)); const newModelId = params.get('new'); if (newModelId) return { rkey: '', newModelId }; const rkey = params.get('doc'); if (!rkey) return null; return { rkey, ref: params.get('ref') ?? undefined };}
async function boot(session: OAuthSession) { show('signin', false); setError(null); // Back from the OAuth round-trip with a stashed document address: restore // it before deciding what to mount. try { const stash = sessionStorage.getItem('ziran-doc-return'); if (stash) { sessionStorage.removeItem('ziran-doc-return'); if (!docTarget()) history.replaceState(null, '', `/${stash}`); } } catch { /* no storage, no restore */ } const did = session.did; const identity = await resolveIdentity(did).catch(() => null); if (!identity?.pds) { setError('Could not resolve your PDS from your identity.', `DID: ${did}`); renderSignedOut(); return; } const pds = identity.pds.replace(/\/+$/, ''); const fetchImpl = makeSessionFetch(pds, (pathname, init) => session.fetchHandler(pathname, init)); const auth = { did, pds, headers: () => Promise.resolve({}) }; const client = new SpaceClient({ auth, fetch: fetchImpl });
// The shared dataHub (zn-app inits it) is the single tile-message // listener; the host's own hub stays off. const host = new WebHost(auth, fetchImpl, { initHub: false });
// Who this person is, for the identity chip — best-effort, own-PDS only. const profile = await getProfile(did).catch(() => undefined); const backendImpl = new WebBackend(host, client, { did, handle: identity.handle ?? profile?.handle, displayName: profile?.displayName, avatarUrl: profile?.avatar, signOut: async () => { await session.signOut().catch(() => {}); location.href = '/'; }, }, fetchImpl);
try { await ensureHomeSpace(client); } catch (err) { const { message, hint } = explain(err); setError(message, hint); } await host.init(); await backendImpl.start(); setBackend(backendImpl as never);
const target = docTarget(); const mount = $('app-root'); if (target) { const { ZnWebDoc } = await import('./zn-web-doc.ts'); const { dataHub } = await import('../ui/data-protocol.ts'); const { desk } = await import('../state/desk.ts'); dataHub.init(); desk.init(); const el = new ZnWebDoc(); el.docRkey = target.rkey; el.spaceRef = target.ref; el.newModelId = target.newModelId; el.host = host; mount.replaceChildren(el); } else { await import('../components/zn-app.ts'); mount.replaceChildren(document.createElement('zn-app')); }}
main().catch((err) => { setError('Ziran could not start.', (err as Error).message); renderSignedOut();});