···1-import type { Did, Handle } from '@atcute/lexicons';
2import { user } from './auth.svelte';
03import {
4 CompositeDidDocumentResolver,
5 CompositeHandleResolver,
···9 WellKnownHandleResolver
10} from '@atcute/identity-resolver';
11import { Client, simpleFetchHandler } from '@atcute/client';
12-import type { AppBskyActorDefs } from '@atcute/bluesky';
13-import { redirect } from '@sveltejs/kit';
1415export type Collection = `${string}.${string}.${string}`;
0160000017export function parseUri(uri: string) {
18- const [did, collection, rkey] = uri.replace('at://', '').split('/');
19- return { did, collection, rkey } as {
20- collection: `${string}.${string}.${string}`;
21- rkey: string;
22- did: Did;
23- };
24}
250000026export async function resolveHandle({ handle }: { handle: Handle }) {
27 const handleResolver = new CompositeHandleResolver({
28 methods: {
···31 }
32 });
3334- try {
35- const data = await handleResolver.resolve(handle);
36- return data;
37- } catch (error) {
38- redirect(307, '/?error=handle_not_found&handle=' + handle);
39- }
40}
4142const didResolver = new CompositeDidDocumentResolver({
···46 }
47});
4800000049export async function getPDS(did: Did) {
50- const doc = await didResolver.resolve(did as `did:plc:${string}` | `did:web:${string}`);
51 if (!doc.service) throw new Error('No PDS found');
52 for (const service of doc.service) {
53 if (service.id === '#atproto_pds') {
···56 }
57}
58000000059export async function getDetailedProfile(data?: { did?: Did; client?: Client }) {
60 data ??= {};
61 data.did ??= user.did;
···75 return response.data;
76}
7778-export async function getAuthorFeed(data?: {
79- did?: Did;
80- client?: Client;
81- filter?: string;
82- limit?: number;
83-}) {
84- data ??= {};
85- data.did ??= user.did;
86-87- if (!data.did) throw new Error('Error getting detailed profile: no did');
88-89- data.client ??= new Client({
90- handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
91- });
92-93- const response = await data.client.get('app.bsky.feed.getAuthorFeed', {
94- params: { actor: data.did, filter: data.filter ?? 'posts_with_media', limit: data.limit || 100 }
95- });
96-97- if (!response.ok) return;
98-99- return response.data;
100-}
101-102export async function getClient({ did }: { did: Did }) {
103 const pds = await getPDS(did);
104 if (!pds) throw new Error('PDS not found');
···110 return client;
111}
112000000000113export async function listRecords({
114 did,
115 collection,
116 cursor,
117- limit = 0,
118 client
119}: {
120 did?: Did;
···141 params: {
142 repo: did,
143 collection,
144- limit: limit || 100,
145 cursor: currentCursor
146 }
147 });
···157 return allRecords;
158}
15900000000160export async function getRecord({
161 did,
162 collection,
163- rkey,
164 client
165}: {
166 did?: Did;
···169 client?: Client;
170}) {
171 did ??= user.did;
172- rkey ??= 'self';
173174 if (!collection) {
175 throw new Error('Missing parameters for getRecord');
···188 }
189 });
190191- if (!record.ok) return;
192-193 return JSON.parse(JSON.stringify(record.data));
194}
195000000000196export async function putRecord({
197 collection,
198- rkey,
199 record
200}: {
201- collection: Collection;
202- rkey: string;
203 record: Record<string, unknown>;
204}) {
205 if (!user.client || !user.did) throw new Error('No rpc or did');
···218 return response;
219}
220221-export async function deleteRecord({ collection, rkey }: { collection: Collection; rkey: string }) {
00000000000000222 if (!user.client || !user.did) throw new Error('No profile or rpc or did');
223224 const response = await user.client.post('com.atproto.repo.deleteRecord', {
···232 return response.ok;
233}
234000000235export async function uploadBlob({ blob }: { blob: Blob }) {
236 if (!user.did || !user.client) throw new Error("Can't upload blob: Not logged in");
237238 const blobResponse = await user.client.post('com.atproto.repo.uploadBlob', {
239- input: blob,
240- data: {
241 repo: user.did
242- }
0243 });
244245- if (!blobResponse?.ok) {
246- return;
247- }
248249 const blobInfo = blobResponse?.data.blob as {
250 $type: 'blob';
···258 return blobInfo;
259}
260000000261export async function describeRepo({ client, did }: { client?: Client; did?: Did }) {
262 did ??= user.did;
263 if (!did) {
···275 return repo.data;
276}
277000000278export async function getBlobURL({
279 did,
280 blob
···291 return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${blob.ref.$link}`;
292}
293294-export function getImageBlobUrl({
000000295 did,
296 blob
297}: {
298- did: string;
299 blob: {
300 $type: 'blob';
301 ref: {
···303 };
304 };
305}) {
306- if (!did || !blob?.ref?.$link) return '';
0307 return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`;
308}
3090000000310export async function searchActorsTypeahead(
311 q: string,
312 limit: number = 10,
···329330 return { actors: response.data.actors, q };
331}
000000000000000000000000000000000
···1+import { parseResourceUri, type Did, type Handle } from '@atcute/lexicons';
2import { user } from './auth.svelte';
3+import type { AllowedCollection } from './settings';
4import {
5 CompositeDidDocumentResolver,
6 CompositeHandleResolver,
···10 WellKnownHandleResolver
11} from '@atcute/identity-resolver';
12import { Client, simpleFetchHandler } from '@atcute/client';
13+import { type AppBskyActorDefs } from '@atcute/bluesky';
01415export type Collection = `${string}.${string}.${string}`;
16+import * as TID from '@atcute/tid';
1718+/**
19+ * Parses an AT Protocol URI into its components.
20+ * @param uri - The AT URI to parse (e.g., "at://did:plc:xyz/app.bsky.feed.post/abc123")
21+ * @returns An object containing the repo, collection, and rkey or undefined if not an AT uri
22+ */
23export function parseUri(uri: string) {
24+ const parts = parseResourceUri(uri);
25+ if (!parts.ok) return;
26+ return parts.value;
00027}
2829+/**
30+ * Resolves a handle to a DID using DNS and HTTP methods.
31+ * @param handle - The handle to resolve (e.g., "alice.bsky.social")
32+ * @returns The DID associated with the handle
33+ */
34export async function resolveHandle({ handle }: { handle: Handle }) {
35 const handleResolver = new CompositeHandleResolver({
36 methods: {
···39 }
40 });
4142+ const data = await handleResolver.resolve(handle);
43+ return data;
000044}
4546const didResolver = new CompositeDidDocumentResolver({
···50 }
51});
5253+/**
54+ * Gets the PDS (Personal Data Server) URL for a given DID.
55+ * @param did - The DID to look up
56+ * @returns The PDS service endpoint URL
57+ * @throws If no PDS is found in the DID document
58+ */
59export async function getPDS(did: Did) {
60+ const doc = await didResolver.resolve(did as Did<'plc'> | Did<'web'>);
61 if (!doc.service) throw new Error('No PDS found');
62 for (const service of doc.service) {
63 if (service.id === '#atproto_pds') {
···66 }
67}
6869+/**
70+ * Fetches a detailed Bluesky profile for a user.
71+ * @param data - Optional object with did and client
72+ * @param data.did - The DID to fetch the profile for (defaults to current user)
73+ * @param data.client - The client to use (defaults to public Bluesky API)
74+ * @returns The profile data or undefined if not found
75+ */
76export async function getDetailedProfile(data?: { did?: Did; client?: Client }) {
77 data ??= {};
78 data.did ??= user.did;
···92 return response.data;
93}
9495+/**
96+ * Creates an AT Protocol client for a user's PDS.
97+ * @param did - The DID of the user
98+ * @returns A client configured for the user's PDS
99+ * @throws If the PDS cannot be found
100+ */
000000000000000000101export async function getClient({ did }: { did: Did }) {
102 const pds = await getPDS(did);
103 if (!pds) throw new Error('PDS not found');
···109 return client;
110}
111112+/**
113+ * Lists records from a repository collection with pagination support.
114+ * @param did - The DID of the repository (defaults to current user)
115+ * @param collection - The collection to list records from
116+ * @param cursor - Pagination cursor for continuing from a previous request
117+ * @param limit - Maximum number of records to return (default 100, set to 0 for all records)
118+ * @param client - The client to use (defaults to user's PDS client)
119+ * @returns An array of records from the collection
120+ */
121export async function listRecords({
122 did,
123 collection,
124 cursor,
125+ limit = 100,
126 client
127}: {
128 did?: Did;
···149 params: {
150 repo: did,
151 collection,
152+ limit: !limit || limit > 100 ? 100 : limit,
153 cursor: currentCursor
154 }
155 });
···165 return allRecords;
166}
167168+/**
169+ * Fetches a single record from a repository.
170+ * @param did - The DID of the repository (defaults to current user)
171+ * @param collection - The collection the record belongs to
172+ * @param rkey - The record key (defaults to "self")
173+ * @param client - The client to use (defaults to user's PDS client)
174+ * @returns The record data
175+ */
176export async function getRecord({
177 did,
178 collection,
179+ rkey = 'self',
180 client
181}: {
182 did?: Did;
···185 client?: Client;
186}) {
187 did ??= user.did;
0188189 if (!collection) {
190 throw new Error('Missing parameters for getRecord');
···203 }
204 });
20500206 return JSON.parse(JSON.stringify(record.data));
207}
208209+/**
210+ * Creates or updates a record in the current user's repository.
211+ * Only accepts collections that are configured in permissions.
212+ * @param collection - The collection to write to (must be in permissions.collections)
213+ * @param rkey - The record key (defaults to "self")
214+ * @param record - The record data to write
215+ * @returns The response from the PDS
216+ * @throws If the user is not logged in
217+ */
218export async function putRecord({
219 collection,
220+ rkey = 'self',
221 record
222}: {
223+ collection: AllowedCollection;
224+ rkey?: string;
225 record: Record<string, unknown>;
226}) {
227 if (!user.client || !user.did) throw new Error('No rpc or did');
···240 return response;
241}
242243+/**
244+ * Deletes a record from the current user's repository.
245+ * Only accepts collections that are configured in permissions.
246+ * @param collection - The collection the record belongs to (must be in permissions.collections)
247+ * @param rkey - The record key (defaults to "self")
248+ * @returns True if the deletion was successful
249+ * @throws If the user is not logged in
250+ */
251+export async function deleteRecord({
252+ collection,
253+ rkey = 'self'
254+}: {
255+ collection: AllowedCollection;
256+ rkey: string;
257+}) {
258 if (!user.client || !user.did) throw new Error('No profile or rpc or did');
259260 const response = await user.client.post('com.atproto.repo.deleteRecord', {
···268 return response.ok;
269}
270271+/**
272+ * Uploads a blob to the current user's PDS.
273+ * @param blob - The blob data to upload
274+ * @returns The blob metadata including ref, mimeType, and size, or undefined on failure
275+ * @throws If the user is not logged in
276+ */
277export async function uploadBlob({ blob }: { blob: Blob }) {
278 if (!user.did || !user.client) throw new Error("Can't upload blob: Not logged in");
279280 const blobResponse = await user.client.post('com.atproto.repo.uploadBlob', {
281+ params: {
0282 repo: user.did
283+ },
284+ input: blob
285 });
286287+ if (!blobResponse?.ok) return;
00288289 const blobInfo = blobResponse?.data.blob as {
290 $type: 'blob';
···298 return blobInfo;
299}
300301+/**
302+ * Gets metadata about a repository.
303+ * @param client - The client to use
304+ * @param did - The DID of the repository (defaults to current user)
305+ * @returns Repository metadata or undefined on failure
306+ */
307export async function describeRepo({ client, did }: { client?: Client; did?: Did }) {
308 did ??= user.did;
309 if (!did) {
···321 return repo.data;
322}
323324+/**
325+ * Constructs a URL to fetch a blob directly from a user's PDS.
326+ * @param did - The DID of the user who owns the blob
327+ * @param blob - The blob reference object
328+ * @returns The URL to fetch the blob
329+ */
330export async function getBlobURL({
331 did,
332 blob
···343 return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${blob.ref.$link}`;
344}
345346+/**
347+ * Constructs a Bluesky CDN URL for an image blob.
348+ * @param did - The DID of the user who owns the blob (defaults to current user)
349+ * @param blob - The blob reference object
350+ * @returns The CDN URL for the image in webp format
351+ */
352+export function getCDNImageBlobUrl({
353 did,
354 blob
355}: {
356+ did?: string;
357 blob: {
358 $type: 'blob';
359 ref: {
···361 };
362 };
363}) {
364+ did ??= user.did;
365+366 return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`;
367}
368369+/**
370+ * Searches for actors with typeahead/autocomplete functionality.
371+ * @param q - The search query
372+ * @param limit - Maximum number of results (default 10)
373+ * @param host - The API host to use (defaults to public Bluesky API)
374+ * @returns An object containing matching actors and the original query
375+ */
376export async function searchActorsTypeahead(
377 q: string,
378 limit: number = 10,
···395396 return { actors: response.data.actors, q };
397}
398+399+/**
400+ * Return a TID based on current time
401+ *
402+ * @returns TID for current time
403+ */
404+export function createTID() {
405+ return TID.now();
406+}
407+408+export async function getAuthorFeed(data?: {
409+ did?: Did;
410+ client?: Client;
411+ filter?: string;
412+ limit?: number;
413+}) {
414+ data ??= {};
415+ data.did ??= user.did;
416+417+ if (!data.did) throw new Error('Error getting detailed profile: no did');
418+419+ data.client ??= new Client({
420+ handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
421+ });
422+423+ const response = await data.client.get('app.bsky.feed.getAuthorFeed', {
424+ params: { actor: data.did, filter: data.filter ?? 'posts_with_media', limit: data.limit || 100 }
425+ });
426+427+ if (!response.ok) return;
428+429+ return response.data;
430+}
···1+export const SITE = 'https://flo-bit.dev';
2+3+type Permissions = {
4+ collections: readonly string[];
5+ rpc: Record<string, string | string[]>;
6+ blobs: readonly string[];
7+};
8+9+export const permissions = {
10+ // collections you can create/delete/update
11+12+ // example: only allow create and delete
13+ // collections: ['xyz.statusphere.status?action=create&action=update'],
14+ collections: [
15+ 'app.blento.card',
16+ 'app.blento.page',
17+ 'app.blento.settings',
18+ 'app.blento.comment',
19+ 'app.blento.guestbook.entry',
20+ 'site.standard.publication',
21+ 'site.standard.document',
22+ 'xyz.statusphere.status'
23+ ],
24+25+ // what types of authenticated proxied requests you can make to services
26+27+ // example: allow authenticated proxying to bsky appview to get a users liked posts
28+ //rpc: {'did:web:api.bsky.app#bsky_appview': ['app.bsky.feed.getActorLikes']}
29+ rpc: {},
30+31+ // what types of blobs you can upload to a users PDS
3233+ // example: allowing video and html uploads
34+ // blobs: ['video/*', 'text/html']
35+ // example: allowing all blob types
36+ // blobs: ['*/*']
37+ blobs: ['*/*']
38+} as const satisfies Permissions;
00003940+// Extract base collection name (before any query params)
41+type ExtractCollectionBase<T extends string> = T extends `${infer Base}?${string}` ? Base : T;
04243+export type AllowedCollection = ExtractCollectionBase<(typeof permissions.collections)[number]>;
4445+// which PDS to use for signup
46+// ATTENTION: pds.rip is only for development, all accounts get deleted automatically after a week
47+export const signUpPDS = 'https://selfhosted.social/';