Universal links for the ATmosphere. Share ATProto content with anyone, let them choose where to view it.
1export type ParsedURI = {
2 type: 'post' | 'profile' | 'list' | 'record' | 'unknown';
3 uri: string;
4 handle: string;
5 did?: string;
6 collection?: string;
7 rkey?: string;
8 error?: string;
9};
10
11/**
12 * Parse URL path segments into structured AT URI data
13 * Examples:
14 * - /alice.bsky.social -> profile
15 * - /alice.bsky.social/app.bsky.feed.post/3k7qw... -> post
16 * - /did:plc:xxx/app.bsky.graph.list/abc -> list
17 */
18export function parseURI(handle: string, collection?: string, rkey?: string): ParsedURI {
19 // Handle is required
20 if (!handle) {
21 return {
22 type: 'unknown',
23 uri: '',
24 handle: '',
25 error: 'Handle or DID is required',
26 };
27 }
28
29 // Profile case (no collection/rkey)
30 if (!collection && !rkey) {
31 return {
32 type: 'profile',
33 uri: `at://${handle}`,
34 handle,
35 did: handle.startsWith('did:') ? handle : undefined,
36 };
37 }
38
39 // Record case (has collection and rkey)
40 if (collection && rkey) {
41 let type: 'post' | 'list' | 'record' = 'record';
42
43 if (collection === 'app.bsky.feed.post') {
44 type = 'post';
45 } else if (collection === 'app.bsky.graph.list') {
46 type = 'list';
47 }
48 // All other collections are treated as generic records
49
50 return {
51 type,
52 uri: `at://${handle}/${collection}/${rkey}`,
53 handle,
54 did: handle.startsWith('did:') ? handle : undefined,
55 collection,
56 rkey,
57 };
58 }
59
60 // Invalid case
61 return {
62 type: 'unknown',
63 uri: '',
64 handle,
65 error: 'Invalid URI structure',
66 };
67}
68
69/**
70 * Resolve a handle to a DID using the Bluesky API
71 */
72export async function resolveHandle(handle: string): Promise<string | null> {
73 if (handle.startsWith('did:')) {
74 return handle;
75 }
76
77 try {
78 const apiUrl = process.env.NEXT_PUBLIC_BSKY_API_URL || 'https://public.api.bsky.app';
79 const response = await fetch(
80 `${apiUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`
81 );
82
83 if (!response.ok) {
84 return null;
85 }
86
87 const data = await response.json();
88 return data.did || null;
89 } catch (error) {
90 console.error('Error resolving handle:', error);
91 return null;
92 }
93}
94
95/**
96 * Get display name from handle or DID
97 */
98export function getDisplayName(handle: string, did?: string): string {
99 if (handle.startsWith('did:')) {
100 return did ? `@${did.slice(0, 16)}...` : 'Unknown';
101 }
102 return `@${handle}`;
103}
104
105