@ewanc26/atproto 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agents.js ADDED
@@ -0,0 +1,102 @@
1
+ import { AtpAgent } from '@atproto/api';
2
+ import { cache } from './cache.js';
3
+ export function createAgent(service, fetchFn) {
4
+ const wrappedFetch = fetchFn
5
+ ? async (url, init) => {
6
+ const urlStr = url instanceof URL ? url.toString() : url;
7
+ const response = await fetchFn(urlStr, init);
8
+ const headers = new Headers(response.headers);
9
+ if (!headers.has('content-type')) {
10
+ headers.set('content-type', 'application/json');
11
+ }
12
+ return new Response(response.body, {
13
+ status: response.status,
14
+ statusText: response.statusText,
15
+ headers
16
+ });
17
+ }
18
+ : undefined;
19
+ return new AtpAgent({
20
+ service,
21
+ ...(wrappedFetch && { fetch: wrappedFetch })
22
+ });
23
+ }
24
+ export const constellationAgent = createAgent('https://constellation.microcosm.blue');
25
+ export const defaultAgent = createAgent('https://public.api.bsky.app');
26
+ let resolvedAgent = null;
27
+ let pdsAgent = null;
28
+ export async function resolveIdentity(did, fetchFn) {
29
+ const cacheKey = `identity:${did}`;
30
+ const cached = cache.get(cacheKey);
31
+ if (cached)
32
+ return cached;
33
+ const _fetch = fetchFn ?? globalThis.fetch;
34
+ const response = await _fetch(`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}`);
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to resolve identifier via Slingshot: ${response.status} ${response.statusText}`);
37
+ }
38
+ const rawText = await response.text();
39
+ let data;
40
+ try {
41
+ data = JSON.parse(rawText);
42
+ }
43
+ catch (err) {
44
+ throw new Error('Failed to parse identity resolver response');
45
+ }
46
+ if (!data.did || !data.pds) {
47
+ throw new Error('Invalid response from identity resolver');
48
+ }
49
+ cache.set(cacheKey, data);
50
+ return data;
51
+ }
52
+ export async function getPublicAgent(did, fetchFn) {
53
+ if (resolvedAgent)
54
+ return resolvedAgent;
55
+ try {
56
+ try {
57
+ const response = await constellationAgent.getProfile({ actor: did });
58
+ if (response.success) {
59
+ resolvedAgent = constellationAgent;
60
+ return resolvedAgent;
61
+ }
62
+ }
63
+ catch {
64
+ // fall through
65
+ }
66
+ const resolved = await resolveIdentity(did, fetchFn);
67
+ resolvedAgent = createAgent(resolved.pds, fetchFn);
68
+ return resolvedAgent;
69
+ }
70
+ catch {
71
+ resolvedAgent = defaultAgent;
72
+ return resolvedAgent;
73
+ }
74
+ }
75
+ export async function getPDSAgent(did, fetchFn) {
76
+ if (pdsAgent)
77
+ return pdsAgent;
78
+ const resolved = await resolveIdentity(did, fetchFn);
79
+ pdsAgent = createAgent(resolved.pds, fetchFn);
80
+ return pdsAgent;
81
+ }
82
+ export async function withFallback(did, operation, usePDSFirst = false, fetchFn) {
83
+ const defaultAgentFn = () => fetchFn ? createAgent('https://public.api.bsky.app', fetchFn) : Promise.resolve(defaultAgent);
84
+ const agents = usePDSFirst
85
+ ? [() => getPDSAgent(did, fetchFn), defaultAgentFn]
86
+ : [defaultAgentFn, () => getPDSAgent(did, fetchFn)];
87
+ let lastError;
88
+ for (const getAgent of agents) {
89
+ try {
90
+ const agent = await getAgent();
91
+ return await operation(agent);
92
+ }
93
+ catch (error) {
94
+ lastError = error;
95
+ }
96
+ }
97
+ throw lastError;
98
+ }
99
+ export function resetAgents() {
100
+ resolvedAgent = null;
101
+ pdsAgent = null;
102
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Cache TTL values in milliseconds (production defaults).
3
+ * All functions that previously read PUBLIC_ATPROTO_DID from the environment
4
+ * now accept `did: string` as their first argument.
5
+ */
6
+ export declare const CACHE_TTL: {
7
+ readonly PROFILE: number;
8
+ readonly SITE_INFO: number;
9
+ readonly LINKS: number;
10
+ readonly MUSIC_STATUS: number;
11
+ readonly KIBUN_STATUS: number;
12
+ readonly TANGLED_REPOS: number;
13
+ readonly BLOG_POSTS: number;
14
+ readonly PUBLICATIONS: number;
15
+ readonly INDIVIDUAL_POST: number;
16
+ readonly IDENTITY: number;
17
+ };
18
+ export declare class ATProtoCache {
19
+ private cache;
20
+ private getTTL;
21
+ get<T>(key: string): T | null;
22
+ set<T>(key: string, data: T): void;
23
+ delete(key: string): void;
24
+ clear(): void;
25
+ }
26
+ export declare const cache: ATProtoCache;
27
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;CAWZ,CAAC;AAEX,qBAAa,YAAY;IACxB,OAAO,CAAC,KAAK,CAAsC;IAEnD,OAAO,CAAC,MAAM;IAed,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI;IAW7B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI;IAIlC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,KAAK,IAAI,IAAI;CAGb;AAED,eAAO,MAAM,KAAK,cAAqB,CAAC"}
package/dist/cache.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Cache TTL values in milliseconds (production defaults).
3
+ * All functions that previously read PUBLIC_ATPROTO_DID from the environment
4
+ * now accept `did: string` as their first argument.
5
+ */
6
+ export const CACHE_TTL = {
7
+ PROFILE: 60 * 60 * 1000,
8
+ SITE_INFO: 120 * 60 * 1000,
9
+ LINKS: 60 * 60 * 1000,
10
+ MUSIC_STATUS: 10 * 60 * 1000,
11
+ KIBUN_STATUS: 15 * 60 * 1000,
12
+ TANGLED_REPOS: 60 * 60 * 1000,
13
+ BLOG_POSTS: 30 * 60 * 1000,
14
+ PUBLICATIONS: 60 * 60 * 1000,
15
+ INDIVIDUAL_POST: 60 * 60 * 1000,
16
+ IDENTITY: 24 * 60 * 60 * 1000
17
+ };
18
+ export class ATProtoCache {
19
+ cache = new Map();
20
+ getTTL(key) {
21
+ if (key.startsWith('profile:'))
22
+ return CACHE_TTL.PROFILE;
23
+ if (key.startsWith('siteinfo:'))
24
+ return CACHE_TTL.SITE_INFO;
25
+ if (key.startsWith('links:'))
26
+ return CACHE_TTL.LINKS;
27
+ if (key.startsWith('music-status:'))
28
+ return CACHE_TTL.MUSIC_STATUS;
29
+ if (key.startsWith('kibun-status:'))
30
+ return CACHE_TTL.KIBUN_STATUS;
31
+ if (key.startsWith('tangled:'))
32
+ return CACHE_TTL.TANGLED_REPOS;
33
+ if (key.startsWith('blog-posts:') || key.startsWith('blogposts:'))
34
+ return CACHE_TTL.BLOG_POSTS;
35
+ if (key.startsWith('publications:') || key.startsWith('standard-site:publications:'))
36
+ return CACHE_TTL.PUBLICATIONS;
37
+ if (key.startsWith('post:') || key.startsWith('blueskypost:'))
38
+ return CACHE_TTL.INDIVIDUAL_POST;
39
+ if (key.startsWith('identity:'))
40
+ return CACHE_TTL.IDENTITY;
41
+ return 30 * 60 * 1000;
42
+ }
43
+ get(key) {
44
+ const entry = this.cache.get(key);
45
+ if (!entry)
46
+ return null;
47
+ const ttl = this.getTTL(key);
48
+ if (Date.now() - entry.timestamp > ttl) {
49
+ this.cache.delete(key);
50
+ return null;
51
+ }
52
+ return entry.data;
53
+ }
54
+ set(key, data) {
55
+ this.cache.set(key, { data, timestamp: Date.now() });
56
+ }
57
+ delete(key) {
58
+ this.cache.delete(key);
59
+ }
60
+ clear() {
61
+ this.cache.clear();
62
+ }
63
+ }
64
+ export const cache = new ATProtoCache();
@@ -0,0 +1,8 @@
1
+ import type { StandardSitePublicationsData, StandardSiteDocument, StandardSiteDocumentsData, BlogPost } from './types.js';
2
+ export declare function fetchPublications(did: string, fetchFn?: typeof fetch): Promise<StandardSitePublicationsData>;
3
+ export declare function fetchDocuments(did: string, fetchFn?: typeof fetch): Promise<StandardSiteDocumentsData>;
4
+ export declare function fetchRecentDocuments(did: string, limit?: number, fetchFn?: typeof fetch): Promise<StandardSiteDocument[]>;
5
+ export declare function fetchBlogPosts(did: string, fetchFn?: typeof fetch): Promise<{
6
+ posts: BlogPost[];
7
+ }>;
8
+ //# sourceMappingURL=documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../src/documents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEX,4BAA4B,EAC5B,oBAAoB,EACpB,yBAAyB,EAIzB,QAAQ,EACR,MAAM,YAAY,CAAC;AAiEpB,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,4BAA4B,CAAC,CAgEvC;AAED,wBAAsB,cAAc,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,yBAAyB,CAAC,CA+EpC;AAED,wBAAsB,oBAAoB,CACzC,GAAG,EAAE,MAAM,EACX,KAAK,SAAI,EACT,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAGjC;AAED,wBAAsB,cAAc,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC;IAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,CAAC,CAiBhC"}
@@ -0,0 +1,174 @@
1
+ import { cache } from './cache.js';
2
+ import { withFallback, resolveIdentity } from './agents.js';
3
+ import { buildPdsBlobUrl } from './media.js';
4
+ async function getBlobUrl(did, blob, fetchFn) {
5
+ try {
6
+ const cid = blob.ref?.$link || blob.cid;
7
+ if (!cid)
8
+ return undefined;
9
+ const resolved = await resolveIdentity(did, fetchFn);
10
+ return buildPdsBlobUrl(resolved.pds, did, cid);
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ function resolveViewUrl(site, path, publicationUrl, rkey) {
17
+ const docPath = path || `/${rkey}`;
18
+ const normalizedPath = docPath.startsWith('/') ? docPath : `/${docPath}`;
19
+ if (site.startsWith('at://')) {
20
+ if (!publicationUrl)
21
+ return `${site}${normalizedPath}`;
22
+ const baseUrl = publicationUrl.startsWith('http') ? publicationUrl : `https://${publicationUrl}`;
23
+ return `${baseUrl.replace(/\/$/, '')}${normalizedPath}`;
24
+ }
25
+ else {
26
+ const baseUrl = site.startsWith('http') ? site : `https://${site}`;
27
+ return `${baseUrl.replace(/\/$/, '')}${normalizedPath}`;
28
+ }
29
+ }
30
+ export async function fetchPublications(did, fetchFn) {
31
+ const cacheKey = `standard-site:publications:${did}`;
32
+ const cached = cache.get(cacheKey);
33
+ if (cached)
34
+ return cached;
35
+ const publications = [];
36
+ try {
37
+ const publicationsRecords = await withFallback(did, async (agent) => {
38
+ const response = await agent.com.atproto.repo.listRecords({
39
+ repo: did,
40
+ collection: 'site.standard.publication',
41
+ limit: 100
42
+ });
43
+ return response.data.records;
44
+ }, true, fetchFn);
45
+ for (const pubRecord of publicationsRecords) {
46
+ const pubValue = pubRecord.value;
47
+ const rkey = pubRecord.uri.split('/').pop() || '';
48
+ let basicTheme;
49
+ if (pubValue.basicTheme) {
50
+ basicTheme = {
51
+ background: pubValue.basicTheme.background,
52
+ foreground: pubValue.basicTheme.foreground,
53
+ accent: pubValue.basicTheme.accent,
54
+ accentForeground: pubValue.basicTheme.accentForeground
55
+ };
56
+ }
57
+ const preferences = pubValue.preferences
58
+ ? {
59
+ showInDiscover: pubValue.preferences.showInDiscover,
60
+ showComments: pubValue.preferences.showComments,
61
+ showMentions: pubValue.preferences.showMentions,
62
+ showPrevNext: pubValue.preferences.showPrevNext,
63
+ showRecommends: pubValue.preferences.showRecommends
64
+ }
65
+ : undefined;
66
+ publications.push({
67
+ name: pubValue.name,
68
+ rkey,
69
+ uri: pubRecord.uri,
70
+ url: pubValue.url,
71
+ description: pubValue.description,
72
+ icon: pubValue.icon ? await getBlobUrl(did, pubValue.icon, fetchFn) : undefined,
73
+ basicTheme,
74
+ preferences
75
+ });
76
+ }
77
+ const data = { publications };
78
+ cache.set(cacheKey, data);
79
+ return data;
80
+ }
81
+ catch {
82
+ return { publications: [] };
83
+ }
84
+ }
85
+ export async function fetchDocuments(did, fetchFn) {
86
+ const cacheKey = `standard-site:documents:${did}`;
87
+ const cached = cache.get(cacheKey);
88
+ if (cached)
89
+ return cached;
90
+ const documents = [];
91
+ try {
92
+ const publicationsData = await fetchPublications(did, fetchFn);
93
+ const publicationsMap = new Map();
94
+ for (const pub of publicationsData.publications) {
95
+ publicationsMap.set(pub.uri, pub);
96
+ }
97
+ const documentsRecords = await withFallback(did, async (agent) => {
98
+ const response = await agent.com.atproto.repo.listRecords({
99
+ repo: did,
100
+ collection: 'site.standard.document',
101
+ limit: 100
102
+ });
103
+ return response.data.records;
104
+ }, true, fetchFn);
105
+ for (const docRecord of documentsRecords) {
106
+ const docValue = docRecord.value;
107
+ const rkey = docRecord.uri.split('/').pop() || '';
108
+ const site = docValue.site;
109
+ let publication;
110
+ let publicationRkey;
111
+ let pubUrl;
112
+ if (site.startsWith('at://')) {
113
+ publication = publicationsMap.get(site);
114
+ publicationRkey = site.split('/').pop();
115
+ pubUrl = publication?.url;
116
+ }
117
+ else {
118
+ pubUrl = site;
119
+ }
120
+ const url = resolveViewUrl(site, docValue.path, pubUrl, rkey);
121
+ const coverImage = docValue.coverImage
122
+ ? await getBlobUrl(did, docValue.coverImage, fetchFn)
123
+ : undefined;
124
+ documents.push({
125
+ title: docValue.title,
126
+ rkey,
127
+ uri: docRecord.uri,
128
+ url,
129
+ site,
130
+ path: docValue.path,
131
+ description: docValue.description,
132
+ coverImage,
133
+ content: docValue.content,
134
+ textContent: docValue.textContent,
135
+ bskyPostRef: docValue.bskyPostRef,
136
+ tags: docValue.tags,
137
+ publishedAt: docValue.publishedAt,
138
+ updatedAt: docValue.updatedAt,
139
+ publicationName: publication?.name,
140
+ publicationRkey,
141
+ preferences: docValue.preferences
142
+ });
143
+ }
144
+ documents.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
145
+ const data = { documents };
146
+ cache.set(cacheKey, data);
147
+ return data;
148
+ }
149
+ catch {
150
+ return { documents: [] };
151
+ }
152
+ }
153
+ export async function fetchRecentDocuments(did, limit = 5, fetchFn) {
154
+ const { documents } = await fetchDocuments(did, fetchFn);
155
+ return documents.slice(0, limit);
156
+ }
157
+ export async function fetchBlogPosts(did, fetchFn) {
158
+ const { documents } = await fetchDocuments(did, fetchFn);
159
+ const posts = documents.map((doc) => ({
160
+ title: doc.title,
161
+ url: doc.url,
162
+ createdAt: doc.publishedAt,
163
+ platform: 'standard.site',
164
+ description: doc.description,
165
+ rkey: doc.rkey,
166
+ publicationName: doc.publicationName,
167
+ publicationRkey: doc.publicationRkey,
168
+ tags: doc.tags,
169
+ coverImage: doc.coverImage,
170
+ textContent: doc.textContent,
171
+ updatedAt: doc.updatedAt
172
+ }));
173
+ return { posts };
174
+ }
@@ -0,0 +1,9 @@
1
+ export type EngagementType = 'app.bsky.feed.like' | 'app.bsky.feed.repost';
2
+ interface EngagementResponse {
3
+ dids: string[];
4
+ cursor?: string;
5
+ }
6
+ export declare function fetchEngagementFromConstellation(uri: string, type: EngagementType, cursor?: string): Promise<EngagementResponse>;
7
+ export declare function fetchAllEngagement(uri: string, type: EngagementType): Promise<string[]>;
8
+ export {};
9
+ //# sourceMappingURL=engagement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG,oBAAoB,GAAG,sBAAsB,CAAC;AAE3E,UAAU,kBAAkB;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,gCAAgC,CACrD,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,kBAAkB,CAAC,CAyB7B;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAe7F"}
@@ -0,0 +1,40 @@
1
+ import { cache } from './cache.js';
2
+ export async function fetchEngagementFromConstellation(uri, type, cursor) {
3
+ const cacheKey = `engagement:${type}:${uri}:${cursor || 'initial'}`;
4
+ const cached = cache.get(cacheKey);
5
+ if (cached)
6
+ return cached;
7
+ const url = new URL('https://constellation.microcosm.blue/links/distinct-dids');
8
+ url.searchParams.append('target', uri);
9
+ url.searchParams.append('collection', type);
10
+ url.searchParams.append('path', '');
11
+ url.searchParams.append('limit', '100');
12
+ if (cursor)
13
+ url.searchParams.append('cursor', cursor);
14
+ const response = await fetch(url);
15
+ if (!response.ok) {
16
+ throw new Error(`Constellation HTTP error! Status: ${response.status}`);
17
+ }
18
+ const data = await response.json();
19
+ const result = {
20
+ dids: data.dids || [],
21
+ cursor: data.cursor
22
+ };
23
+ cache.set(cacheKey, result);
24
+ return result;
25
+ }
26
+ export async function fetchAllEngagement(uri, type) {
27
+ const allDids = new Set();
28
+ let cursor = undefined;
29
+ try {
30
+ do {
31
+ const response = await fetchEngagementFromConstellation(uri, type, cursor);
32
+ response.dids.forEach((did) => allDids.add(did));
33
+ cursor = response.cursor;
34
+ } while (cursor);
35
+ }
36
+ catch {
37
+ // return what we have
38
+ }
39
+ return Array.from(allDids);
40
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProfileData, SiteInfoData, LinkData, MusicStatusData, KibunStatusData, TangledReposData } from './types.js';
2
+ export declare function fetchProfile(did: string, fetchFn?: typeof fetch): Promise<ProfileData>;
3
+ export declare function fetchSiteInfo(did: string, fetchFn?: typeof fetch): Promise<SiteInfoData | null>;
4
+ export declare function fetchLinks(did: string, fetchFn?: typeof fetch): Promise<LinkData | null>;
5
+ export declare function fetchMusicStatus(did: string, fetchFn?: typeof fetch): Promise<MusicStatusData | null>;
6
+ export declare function fetchKibunStatus(did: string, fetchFn?: typeof fetch): Promise<KibunStatusData | null>;
7
+ export declare function fetchTangledRepos(did: string, fetchFn?: typeof fetch): Promise<TangledReposData | null>;
8
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,eAAe,EACf,eAAe,EAEf,gBAAgB,EAChB,MAAM,YAAY,CAAC;AAEpB,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,CAgD5F;AAED,wBAAsB,aAAa,CAClC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAgC9B;AAED,wBAAsB,UAAU,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA2B1B;AAED,wBAAsB,gBAAgB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAkIjC;AAED,wBAAsB,gBAAgB,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAmCjC;AAED,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA2ClC"}