A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1/**
2 * Resolve a writer's identity for the read-through renderer (SP4).
3 * Runs server-side (Node/edge): handle → DID → PDS.
4 */
5import { resolvePdsUrl } from '../media/pds';
6import { safeFetch } from '../net/safe-fetch';
7import { isValidHandleOrDid, normalizeHandle } from '../auth/config';
8
9/** Default handle resolver — a `com.atproto.identity.resolveHandle` XRPC endpoint. */
10const FALLBACK_RESOLVER = 'https://bsky.social';
11
12async function resolveViaWellKnown( handle: string ): Promise< string | null > {
13 try {
14 // safeFetch rejects non-public hosts, so an attacker-supplied handle like
15 // `169.254.169.254` or `internal-svc` can't trigger an internal request (SSRF).
16 const res = await safeFetch( `https://${ handle }/.well-known/atproto-did` );
17 if ( ! res.ok ) {
18 return null;
19 }
20 const did = ( await res.text() ).trim();
21 return did.startsWith( 'did:' ) ? did : null;
22 } catch {
23 return null;
24 }
25}
26
27async function resolveViaXrpc( handle: string ): Promise< string | null > {
28 try {
29 const res = await safeFetch(
30 `${ FALLBACK_RESOLVER }/xrpc/com.atproto.identity.resolveHandle?handle=${ encodeURIComponent(
31 handle
32 ) }`
33 );
34 if ( ! res.ok ) {
35 return null;
36 }
37 const data: { did?: string } = await res.json();
38 return data.did ?? null;
39 } catch {
40 return null;
41 }
42}
43
44/**
45 * Resolve a handle (or DID) to a DID. Prefers the handle's own `.well-known`
46 * (no third party), falling back to a public resolver XRPC.
47 */
48export async function resolveHandleToDid( handleOrDid: string ): Promise< string | null > {
49 // Validate before the value is used as a resolver fetch host: the read path takes it
50 // straight from the URL (`/@<author>`), so a non-handle-shaped value could otherwise
51 // smuggle a path/port/query into the outbound request and turn the worker into a
52 // request proxy. safeFetch still guards internal hosts; this rejects bad syntax first.
53 if ( ! isValidHandleOrDid( handleOrDid ) ) {
54 return null;
55 }
56 if ( handleOrDid.startsWith( 'did:' ) ) {
57 return handleOrDid;
58 }
59 const handle = normalizeHandle( handleOrDid );
60 return ( await resolveViaWellKnown( handle ) ) ?? ( await resolveViaXrpc( handle ) );
61}
62
63export interface Author {
64 did: string;
65 pdsUrl: string;
66}
67
68/** Resolve a handle/DID to `{ did, pdsUrl }`, or null if it can't be resolved. */
69export async function resolveAuthor( handleOrDid: string ): Promise< Author | null > {
70 const did = await resolveHandleToDid( handleOrDid );
71 if ( ! did ) {
72 return null;
73 }
74 try {
75 return { did, pdsUrl: await resolvePdsUrl( did ) };
76 } catch {
77 return null;
78 }
79}