chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { Heading } from '#/components/atoms/heading.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { Panel } from '#/components/atoms/panel.jsx';
import { Skeleton } from '#/components/atoms/skeleton.jsx';
import { DetailList } from '#/components/molecules/detail-list.jsx';
import { FormShell } from '#/components/molecules/form-shell.jsx';
import { Section } from '#/components/molecules/section.jsx';
import { TextField } from '#/components/molecules/text-field.jsx';
import { get, post } from '#/lib/api.js';
import { queryKeys, useInvalidate } from '#/lib/query.js';
/**
* The domain this PDS hosts, derived from its own endpoint. A handle that is
* this domain or a subdomain of it is verified by the server itself.
* @param {string} endpoint
*/
function pdsDomainOf(endpoint) {
try {
return new URL(endpoint).hostname;
} catch {
return '';
}
}
/**
* The rename control. A username has to be a domain you own, proven with DNS —
* this reads what you type and shows the exact record before you submit. The
* server's own address and its subdomains are special cases it calls out.
* @param {{did: string, pdsDomain: string, onSucceed: () => void}} props
*/
function RenameForm({ did, pdsDomain, onSucceed }) {
const [value, setValue] = useState('');
const typed = value.trim().toLowerCase();
const isSelf = !!pdsDomain && typed === pdsDomain;
const isInternal =
!!pdsDomain && typed !== pdsDomain && typed.endsWith(`.${pdsDomain}`);
const isExternal = typed !== '' && !isSelf && !isInternal;
return (
<FormShell
className="mt-5"
submitLabel="Change username"
onSubmit={({ handle }) => post('/identity/handle', { handle })}
onSucceed={() => {
setValue('');
onSucceed();
}}
>
<TextField
name="handle"
label="New username"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
required
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder="name.example.com"
hint={
pdsDomain
? `A domain you own, verified with DNS. This server's address is ${pdsDomain}.`
: 'A domain you own, e.g. name.example.com.'
}
/>
{isSelf && (
<p className="text-[13px] text-muted-foreground leading-relaxed">
<span className="font-mono text-foreground">{pdsDomain}</span> is this
server's own address — it's already your username.
</p>
)}
{isInternal && (
<div className="space-y-2 rounded-2xl border border-border bg-muted px-4 py-3.5">
<p className="text-[13px] text-muted-foreground leading-relaxed">
<span className="font-mono text-foreground">{typed}</span> is a
subdomain of this server. A single-user PDS only answers at{' '}
<span className="font-mono text-foreground">{pdsDomain}</span>, so a
name like this won't resolve unless you separately host it. To
change your username, use a domain you own below.
</p>
</div>
)}
{isExternal && (
<div className="space-y-3 rounded-2xl border border-border bg-muted px-4 py-3.5">
<p className="text-[13px] text-muted-foreground leading-relaxed">
<span className="font-mono text-foreground">{typed}</span> is a
domain you own. Prove it points here first, with either one of
these, then submit:
</p>
<div className="space-y-1.5">
<div className="text-[11px] text-faint uppercase tracking-wide">
DNS TXT record
</div>
<div className="break-all rounded-xl border border-border bg-background px-3 py-2 font-mono text-[12.5px] text-foreground">
_atproto.{typed} → did={did}
</div>
</div>
<div className="space-y-1.5">
<div className="text-[11px] text-faint uppercase tracking-wide">
or serve this URL
</div>
<div className="break-all rounded-xl border border-border bg-background px-3 py-2 font-mono text-[12.5px] text-foreground">
https://{typed}/.well-known/atproto-did → {did}
</div>
</div>
</div>
)}
</FormShell>
);
}
export function IdentityPage() {
const { data, error } = useQuery({
queryKey: queryKeys.identity,
queryFn: () => get('/identity'),
});
const invalidate = useInvalidate();
if (error) {
return <Notice variant="error">{error.message}</Notice>;
}
if (!data) {
return (
<div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-[1fr_360px]">
<div className="space-y-5">
<Skeleton className="h-40 rounded-3xl" />
<Skeleton className="h-40 rounded-3xl" />
</div>
<div className="space-y-5">
<Skeleton className="h-32 rounded-3xl" />
<Skeleton className="h-32 rounded-3xl" />
</div>
</div>
);
}
return (
<div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-[1fr_360px]">
<div className="space-y-5">
<Panel>
<Section title="Your username">
<div className="rounded-2xl border border-border bg-muted px-4 py-3.5 font-mono text-[15px] text-foreground">
{data.handle ? `@${data.handle}` : 'none'}
</div>
<p className="mt-4 text-[13px] text-muted-foreground leading-relaxed">
This is the name people see in every app. It points at your
permanent ID, so you can change it without losing anything you
have made.
</p>
{!data.readOnly && (
<RenameForm
did={data.did}
pdsDomain={pdsDomainOf(data.pdsEndpoint)}
onSucceed={() =>
invalidate([queryKeys.identity, queryKeys.overview])
}
/>
)}
</Section>
</Panel>
<Panel>
<Section title="Where this account is published">
<DetailList
rows={[
['PDS endpoint', data.pdsEndpoint],
['Directory', data.plcUrl],
['Signing key', data.signingKey || 'none'],
]}
/>
</Section>
</Panel>
</div>
<div className="space-y-5">
<Panel>
<Heading>Your permanent ID</Heading>
<p className="mt-2 text-[13px] text-muted-foreground leading-relaxed">
Never changes, even if you rename or move hosts. Apps follow this,
not your username.
</p>
<div className="mt-4 break-all rounded-2xl border border-border bg-muted px-4 py-3.5 font-mono text-[12.5px] text-muted-foreground">
{data.did}
</div>
</Panel>
<Panel>
<Heading>Change host</Heading>
<p className="mt-2 text-[13px] text-muted-foreground leading-relaxed">
Move your account, everything in it and your followers to another
host. Your username and permanent ID come with you.
</p>
<Button
variant="outline"
size="block"
disabled
className="mt-4 text-[13.5px] text-faint"
>
Not available yet
</Button>
</Panel>
</div>
</div>
);
}