@chronicle.app/bluesky 0.3.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Louis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @chronicle.app/bluesky
2
+
3
+ Read your Bluesky follows, followers, and likes through the atproto API.
4
+
5
+ ```sh
6
+ chronicle extract bluesky --type follows --handle you.bsky.social --password <app-password>
7
+ chronicle extract bluesky --type followers,likes --handle you.bsky.social --access-token <token>
8
+ ```
9
+
10
+ The extractor resolves the handle to a DID, looks up the account's PDS in the PLC directory, and logs in with the password unless `--access-token` is given. `--handle` and `--password` fall back to the `handle`/`clientId` and `password`/`clientSecret` fields of the stored `bluesky` credentials. Use an app password rather than your account password.
11
+
12
+ Each kind is one record type:
13
+
14
+ - `follows` (the default): accounts you follow. Each becomes a `FollowAction` from you to the account.
15
+ - `followers`: accounts that follow you. Each becomes a `FollowAction` from the account to you.
16
+ - `likes`: posts you liked. Each becomes a `LikeAction` on the `Post`, plus a `PublishAction` by the post's author at the time the post was written.
17
+
18
+ Every action is keyed by its atproto record (`did:…/collection/rkey`) and timestamped with the record's `createdAt`, fetched from the repository for each item. Accounts are `Agent`s keyed by their DID, and your own account carries `sameAs: ['@me']`. A post's images become `ImageObject`s in `contains`, keyed by URL, with the image size and alt text when present.
19
+
20
+ Tests run the extractors against a fake Bluesky server on 127.0.0.1, with explicit handle and password or token and an empty credential directory. They never reach the network or read the host's stored credentials.
@@ -0,0 +1,43 @@
1
+ import { Extractor, Record } from '@chronicle.app/etl';
2
+ import { z } from 'zod';
3
+ import BlueskyProxy from '../utils/BlueskyProxy.js';
4
+ import BlueskyTransformer from './BlueskyTransformer.js';
5
+ export default abstract class BlueskyExtractor extends Extractor<typeof BlueskyExtractor> {
6
+ protected proxy: BlueskyProxy;
7
+ protected records: Record[];
8
+ static defaultTransformer: typeof BlueskyTransformer;
9
+ static source: string;
10
+ static delivery: "api";
11
+ static strategy: string;
12
+ static schema: z.ZodObject<{
13
+ since: z.ZodOptional<z.ZodDate>;
14
+ until: z.ZodOptional<z.ZodDate>;
15
+ limit: z.ZodOptional<z.ZodNumber>;
16
+ } & {
17
+ accessToken: z.ZodOptional<z.ZodString>;
18
+ password: z.ZodOptional<z.ZodString>;
19
+ handle: z.ZodOptional<z.ZodString>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ handle?: string | undefined;
22
+ accessToken?: string | undefined;
23
+ password?: string | undefined;
24
+ limit?: number | undefined;
25
+ since?: Date | undefined;
26
+ until?: Date | undefined;
27
+ }, {
28
+ handle?: string | undefined;
29
+ accessToken?: string | undefined;
30
+ password?: string | undefined;
31
+ limit?: number | undefined;
32
+ since?: Date | undefined;
33
+ until?: Date | undefined;
34
+ }>;
35
+ private agentProfile;
36
+ setup(): Promise<void>;
37
+ protected abstract extractRecords(): Promise<Record[]>;
38
+ protected defaultContext(): {
39
+ agent: any;
40
+ };
41
+ extract(): AsyncGenerator<Record>;
42
+ }
43
+ //# sourceMappingURL=BlueskyExtractor.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { Extractor } from '@chronicle.app/etl';
2
+ import { z } from 'zod';
3
+ import BlueskyProxy from '../utils/BlueskyProxy.js';
4
+ import BlueskyTransformer from './BlueskyTransformer.js';
5
+ export default class BlueskyExtractor extends Extractor {
6
+ proxy;
7
+ records;
8
+ static defaultTransformer = BlueskyTransformer;
9
+ static source = 'bluesky';
10
+ static delivery = 'api';
11
+ static strategy = 'api';
12
+ static schema = Extractor.schema.extend({
13
+ accessToken: z.string().describe('Access token for account').optional(),
14
+ password: z
15
+ .string()
16
+ .describe('Password for account (auto-retrieved from stored credentials if not provided)')
17
+ .optional(),
18
+ handle: z
19
+ .string()
20
+ .describe('Bluesky handle (auto-retrieved from stored credentials if not provided)')
21
+ .optional(),
22
+ });
23
+ agentProfile;
24
+ async setup() {
25
+ const proxy = new BlueskyProxy({
26
+ accessToken: this.config.accessToken,
27
+ handle: this.config.handle,
28
+ password: this.config.password,
29
+ });
30
+ await proxy.initialize();
31
+ // Use the resolved handle from proxy after initialization
32
+ this.agentProfile = await proxy.getProfile(proxy.userDid || this.config.handle || '');
33
+ this.proxy = proxy;
34
+ }
35
+ defaultContext() {
36
+ return {
37
+ agent: this.agentProfile,
38
+ };
39
+ }
40
+ async *extract() {
41
+ const records = await this.extractRecords();
42
+ for await (const record of records) {
43
+ yield record;
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,10 @@
1
+ import BlueskyExtractor from './BlueskyExtractor.js';
2
+ import { Record } from '@chronicle.app/etl';
3
+ export default class BlueskyFollowerExtractor extends BlueskyExtractor {
4
+ static description: string;
5
+ static recordTypes: string[];
6
+ /** The follower's follow record AT-URI — atproto's own address for it, stable in their repo. */
7
+ keyOf(record: Record): string | null;
8
+ extractRecords(): Promise<Record[]>;
9
+ }
10
+ //# sourceMappingURL=BlueskyFollowerExtractor.d.ts.map
@@ -0,0 +1,16 @@
1
+ import BlueskyExtractor from './BlueskyExtractor.js';
2
+ export default class BlueskyFollowerExtractor extends BlueskyExtractor {
3
+ static description = 'Accounts following you';
4
+ static recordTypes = ['followers'];
5
+ /** The follower's follow record AT-URI — atproto's own address for it, stable in their repo. */
6
+ keyOf(record) {
7
+ return record.data.viewer?.followedBy ?? null;
8
+ }
9
+ async extractRecords() {
10
+ const followers = await this.proxy.getAllWithRecords('getFollowers', this.config, (follower) => follower.viewer.followedBy);
11
+ return followers.map((follower) => this.createRecord(follower.result, {
12
+ ...this.defaultContext(),
13
+ atprotoRecord: follower.record,
14
+ }));
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ import BlueskyExtractor from './BlueskyExtractor.js';
2
+ import { Record } from '@chronicle.app/etl';
3
+ export default class BlueskyFollowsExtractor extends BlueskyExtractor {
4
+ static description: string;
5
+ static recordTypes: string[];
6
+ static default: boolean;
7
+ /** The follow record's AT-URI — atproto's own address for it, identical through any tool. */
8
+ keyOf(record: Record): string | null;
9
+ extractRecords(): Promise<Record[]>;
10
+ }
11
+ //# sourceMappingURL=BlueskyFollowsExtractor.d.ts.map
@@ -0,0 +1,14 @@
1
+ import BlueskyExtractor from './BlueskyExtractor.js';
2
+ export default class BlueskyFollowsExtractor extends BlueskyExtractor {
3
+ static description = 'Accounts you follow';
4
+ static recordTypes = ['follows'];
5
+ static default = true;
6
+ /** The follow record's AT-URI — atproto's own address for it, identical through any tool. */
7
+ keyOf(record) {
8
+ return record.data.viewer?.following ?? null;
9
+ }
10
+ async extractRecords() {
11
+ const follows = await this.proxy.getAllWithRecords('getFollows', this.config, (follow) => follow.viewer.following);
12
+ return follows.map((follow) => this.createRecord(follow.result, { ...this.defaultContext(), atprotoRecord: follow.record }));
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ import BlueskyExtractor from './BlueskyExtractor.js';
2
+ import { Record } from '@chronicle.app/etl';
3
+ export default class BlueskyLikesExtractor extends BlueskyExtractor {
4
+ static description: string;
5
+ static recordTypes: string[];
6
+ /** The like record's AT-URI — atproto's own address for it, identical through any tool. */
7
+ keyOf(record: Record): string | null;
8
+ extractRecords(): Promise<Record[]>;
9
+ }
10
+ //# sourceMappingURL=BlueskyLikesExtractor.d.ts.map
@@ -0,0 +1,13 @@
1
+ import BlueskyExtractor from './BlueskyExtractor.js';
2
+ export default class BlueskyLikesExtractor extends BlueskyExtractor {
3
+ static description = 'Posts you liked';
4
+ static recordTypes = ['likes'];
5
+ /** The like record's AT-URI — atproto's own address for it, identical through any tool. */
6
+ keyOf(record) {
7
+ return record.data.post?.viewer?.like ?? null;
8
+ }
9
+ async extractRecords() {
10
+ const likes = await this.proxy.getAllWithRecords('getLikes', this.config, (like) => like.post.viewer.like);
11
+ return likes.map((like) => this.createRecord(like.result, { ...this.defaultContext(), atprotoRecord: like.record }));
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ import { ChronicleTransformer, Record } from '@chronicle.app/etl';
2
+ import { ActionAndChildren } from '@chronicle.app/schema';
3
+ export default class BlueskyTransformer extends ChronicleTransformer {
4
+ /** DID of the authenticated account (the "me"), set per record from context. */
5
+ private meDid?;
6
+ transform(record: Record): Promise<ActionAndChildren[]>;
7
+ private buildFollow;
8
+ private buildLike;
9
+ private buildPublish;
10
+ private buildPost;
11
+ private buildUser;
12
+ private uriToId;
13
+ }
14
+ //# sourceMappingURL=BlueskyTransformer.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { ChronicleTransformer, createImageObject } from '@chronicle.app/etl';
2
+ export default class BlueskyTransformer extends ChronicleTransformer {
3
+ /** DID of the authenticated account (the "me"), set per record from context. */
4
+ meDid;
5
+ async transform(record) {
6
+ const type = record.extraction.recordType;
7
+ // record.context.agent is the authenticated account; remember its DID so the
8
+ // shared buildUser tags only the owner, not followed users/authors.
9
+ // Safe as a per-record field: transform builds synchronously below, with no
10
+ // await between this assignment and the buildUser calls.
11
+ this.meDid = record.context.agent?.did;
12
+ this.logger.debug('Transforming Bluesky record', { recordType: type });
13
+ const actions = [];
14
+ switch (type) {
15
+ case 'likes': {
16
+ actions.push(this.buildLike({
17
+ post: record.data.post,
18
+ agent: record.context.agent,
19
+ timestamp: record.context.atprotoRecord.value.createdAt,
20
+ }), this.buildPublish({
21
+ post: record.data.post,
22
+ }));
23
+ break;
24
+ }
25
+ case 'follows': {
26
+ actions.push(this.buildFollow({
27
+ account: record.data,
28
+ agent: record.context.agent,
29
+ timestamp: record.context.atprotoRecord.value.createdAt,
30
+ recordUri: record.data.viewer.following,
31
+ }));
32
+ break;
33
+ }
34
+ case 'followers': {
35
+ actions.push(this.buildFollow({
36
+ account: record.context.agent,
37
+ agent: record.data,
38
+ timestamp: record.context.atprotoRecord.value.createdAt,
39
+ recordUri: record.data.viewer.followedBy,
40
+ }));
41
+ break;
42
+ }
43
+ default: {
44
+ return [];
45
+ }
46
+ }
47
+ this.logger.verboseInfo(`Generated ${actions.length} actions from Bluesky ${type} record`);
48
+ return actions;
49
+ }
50
+ buildFollow({ account, agent, timestamp, recordUri, }) {
51
+ return {
52
+ '@type': 'FollowAction',
53
+ source: 'bluesky',
54
+ sourceId: this.uriToId(recordUri),
55
+ timestamp: new Date(timestamp),
56
+ '@key': ['@type', 'source', 'sourceId'],
57
+ agent: this.buildUser(agent),
58
+ object: this.buildUser(account),
59
+ };
60
+ }
61
+ buildLike({ post, agent, timestamp, }) {
62
+ return {
63
+ '@type': 'LikeAction',
64
+ source: 'bluesky',
65
+ sourceId: this.uriToId(post.viewer.like),
66
+ timestamp: new Date(timestamp),
67
+ '@key': ['@type', 'source', 'sourceId'],
68
+ agent: this.buildUser(agent),
69
+ object: this.buildPost(post),
70
+ };
71
+ }
72
+ buildPublish({ post }) {
73
+ return {
74
+ '@type': 'PublishAction',
75
+ source: 'bluesky',
76
+ sourceId: this.uriToId(post.uri),
77
+ timestamp: new Date(post.record.createdAt),
78
+ '@key': ['@type', 'source', 'sourceId'],
79
+ agent: this.buildUser(post.author),
80
+ object: this.buildPost(post),
81
+ };
82
+ }
83
+ buildPost(post) {
84
+ const posting = {
85
+ '@type': 'Post',
86
+ '@key': ['@type', 'source', 'sourceId'],
87
+ source: 'bluesky',
88
+ sourceId: this.uriToId(post.uri),
89
+ body: post.record.text,
90
+ author: [this.buildUser(post.author)],
91
+ };
92
+ // Extract images from embed data - based on app.bsky.embed.images#view structure
93
+ // Put all images in the contains property instead of the image field
94
+ if (post.embed?.$type === 'app.bsky.embed.images#view' && post.embed.images?.length > 0) {
95
+ posting.contains = post.embed.images.map((img) => {
96
+ const imageOptions = {
97
+ url: img.fullsize,
98
+ width: img.aspectRatio?.width,
99
+ height: img.aspectRatio?.height,
100
+ };
101
+ // Only include description if it's not empty
102
+ if (img.alt && img.alt.trim() !== '') {
103
+ imageOptions.description = img.alt;
104
+ }
105
+ return createImageObject(imageOptions);
106
+ });
107
+ }
108
+ return posting;
109
+ }
110
+ buildUser(agentProfile) {
111
+ const agent = {
112
+ '@type': 'Agent',
113
+ source: 'bluesky',
114
+ sourceId: agentProfile.did,
115
+ '@key': ['@type', 'source', 'sourceId'],
116
+ name: agentProfile.displayName,
117
+ description: agentProfile.description,
118
+ };
119
+ // Tag the owner by identity (DID): this builder also constructs followed
120
+ // accounts and post authors, so match only the authenticated account.
121
+ if (this.meDid && agentProfile.did === this.meDid)
122
+ agent.sameAs = [...(agent.sameAs ?? []), '@me'];
123
+ return agent;
124
+ }
125
+ uriToId(uri) {
126
+ return uri.split('://').pop();
127
+ }
128
+ }
@@ -0,0 +1,6 @@
1
+ export { default as BlueskyExtractor } from './connectors/BlueskyExtractor.js';
2
+ export { default as BlueskyFollowerExtractor } from './connectors/BlueskyFollowerExtractor.js';
3
+ export { default as BlueskyFollowsExtractor } from './connectors/BlueskyFollowsExtractor.js';
4
+ export { default as BlueskyLikesExtractor } from './connectors/BlueskyLikesExtractor.js';
5
+ export { default as BlueskyTransformer } from './connectors/BlueskyTransformer.js';
6
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { default as BlueskyExtractor } from './connectors/BlueskyExtractor.js';
2
+ export { default as BlueskyFollowerExtractor } from './connectors/BlueskyFollowerExtractor.js';
3
+ export { default as BlueskyFollowsExtractor } from './connectors/BlueskyFollowsExtractor.js';
4
+ export { default as BlueskyLikesExtractor } from './connectors/BlueskyLikesExtractor.js';
5
+ export { default as BlueskyTransformer } from './connectors/BlueskyTransformer.js';
@@ -0,0 +1,34 @@
1
+ import { ApiProxy } from '@chronicle.app/etl';
2
+ export default class BlueskyProxy extends ApiProxy {
3
+ private handle;
4
+ userDid?: string;
5
+ private pdsDomain?;
6
+ private password?;
7
+ constructor({ handle, accessToken, password }?: {
8
+ accessToken?: string;
9
+ handle?: string;
10
+ password?: string;
11
+ });
12
+ initialize(): Promise<void>;
13
+ handleToDid(handle: string): Promise<string>;
14
+ static didToPdsDomain(did: string): Promise<string>;
15
+ authenticate(handle: string, password: string): Promise<any>;
16
+ getFollowers({ limit }: {
17
+ limit?: number;
18
+ }): Promise<any[]>;
19
+ getFollows({ limit }: {
20
+ limit?: number;
21
+ }): Promise<any[]>;
22
+ getRecord(repo: string, collection: string, rkey: string): Promise<any>;
23
+ getAllWithRecords(fn: string, parameters: any, recordKeyFn: Function): Promise<any>;
24
+ getLikes({ limit }: {
25
+ limit?: number;
26
+ }): Promise<any[]>;
27
+ getAuthorFeed({ limit }: {
28
+ limit?: number;
29
+ }): Promise<any[]>;
30
+ getProfile(handle: string): Promise<any>;
31
+ private getAllPages;
32
+ private makeRPCRequest;
33
+ }
34
+ //# sourceMappingURL=BlueskyProxy.d.ts.map
@@ -0,0 +1,191 @@
1
+ import { ApiProxy } from '@chronicle.app/etl';
2
+ import { resolveCredentials } from '@chronicle.app/auth';
3
+ export default class BlueskyProxy extends ApiProxy {
4
+ handle;
5
+ userDid;
6
+ pdsDomain;
7
+ password;
8
+ constructor({ handle, accessToken, password } = {}) {
9
+ // No baseURL: every RPC names its PDS domain absolutely.
10
+ super();
11
+ this.handle = handle || '';
12
+ this.setAccessToken(accessToken);
13
+ this.password = password;
14
+ }
15
+ async initialize() {
16
+ // Load credentials if not provided
17
+ const { handle, password } = await resolveCredentials('bluesky', {
18
+ handle: { from: ['handle', 'clientId'], optional: true },
19
+ password: { from: ['password', 'clientSecret'], optional: true },
20
+ }, {
21
+ overrides: { handle: this.handle || undefined, password: this.password },
22
+ });
23
+ this.handle = handle || '';
24
+ this.password = password;
25
+ if (!this.handle) {
26
+ throw new Error('Bluesky handle is required.');
27
+ }
28
+ try {
29
+ this.userDid = await this.handleToDid(this.handle);
30
+ this.pdsDomain = await BlueskyProxy.didToPdsDomain(this.userDid);
31
+ }
32
+ catch {
33
+ throw new Error('Could not resolve handle to DID');
34
+ }
35
+ if (!this.accessToken) {
36
+ if (this.password) {
37
+ await this.authenticate(this.handle, this.password);
38
+ }
39
+ else {
40
+ throw new Error('Password is required.');
41
+ }
42
+ }
43
+ }
44
+ async handleToDid(handle) {
45
+ const response = await this.makeRPCRequest({
46
+ domain: 'https://bsky.social',
47
+ endpoint: 'com.atproto.identity.resolveHandle',
48
+ method: 'GET',
49
+ queryParams: {
50
+ handle,
51
+ },
52
+ });
53
+ const { did } = response;
54
+ if (!did) {
55
+ throw new Error('Could not resolve handle to DID');
56
+ }
57
+ return did;
58
+ }
59
+ static async didToPdsDomain(did) {
60
+ const [, type] = did.split(':');
61
+ try {
62
+ if (type === 'plc') {
63
+ const response = await fetch(`https://plc.directory/${did}`);
64
+ const json = await response.json();
65
+ const { service } = json;
66
+ const pdsService = service.find((service) => service.type === 'AtprotoPersonalDataServer');
67
+ return pdsService.serviceEndpoint;
68
+ }
69
+ // ???
70
+ return '';
71
+ }
72
+ catch {
73
+ throw new Error('Could not resolve DID to PDS domain');
74
+ }
75
+ }
76
+ async authenticate(handle, password) {
77
+ const response = await this.makeRPCRequest({
78
+ domain: 'https://bsky.social',
79
+ endpoint: 'com.atproto.server.createSession',
80
+ method: 'POST',
81
+ params: {
82
+ identifier: handle,
83
+ password,
84
+ },
85
+ });
86
+ this.setAccessToken(response.accessJwt);
87
+ return response;
88
+ }
89
+ async getFollowers({ limit }) {
90
+ const followers = await this.getAllPages({
91
+ endpoint: 'app.bsky.graph.getFollowers',
92
+ resultKey: 'followers',
93
+ queryParams: {
94
+ actor: this.userDid,
95
+ },
96
+ limit,
97
+ });
98
+ return followers;
99
+ }
100
+ async getFollows({ limit }) {
101
+ const followers = await this.getAllPages({
102
+ endpoint: 'app.bsky.graph.getFollows',
103
+ resultKey: 'follows',
104
+ queryParams: {
105
+ actor: this.userDid,
106
+ },
107
+ limit,
108
+ });
109
+ return followers;
110
+ }
111
+ async getRecord(repo, collection, rkey) {
112
+ const response = await this.makeRPCRequest({
113
+ endpoint: 'com.atproto.repo.getRecord',
114
+ method: 'GET',
115
+ queryParams: {
116
+ repo,
117
+ collection,
118
+ rkey,
119
+ },
120
+ });
121
+ return response;
122
+ }
123
+ async getAllWithRecords(fn, parameters, recordKeyFn) {
124
+ // @ts-ignore
125
+ const results = await this[fn](parameters);
126
+ const resultsWithRecords = await Promise.all(results.map(async (result) => {
127
+ const id = recordKeyFn(result).split('://').pop();
128
+ const [repo, collection, rkey] = id.split('/');
129
+ const record = await this.getRecord(repo, collection, rkey);
130
+ return { result, record };
131
+ }));
132
+ return resultsWithRecords;
133
+ }
134
+ async getLikes({ limit }) {
135
+ const likes = await this.getAllPages({
136
+ endpoint: 'app.bsky.feed.getActorLikes',
137
+ resultKey: 'feed',
138
+ queryParams: {
139
+ actor: this.userDid,
140
+ },
141
+ limit,
142
+ });
143
+ return likes;
144
+ }
145
+ async getAuthorFeed({ limit }) {
146
+ const posts = await this.getAllPages({
147
+ endpoint: 'app.bsky.feed.getAuthorFeed',
148
+ resultKey: 'feed',
149
+ queryParams: {
150
+ actor: this.userDid,
151
+ },
152
+ limit,
153
+ });
154
+ return posts;
155
+ }
156
+ async getProfile(handle) {
157
+ const endpoint = 'app.bsky.actor.getProfile';
158
+ const response = await this.makeRPCRequest({
159
+ endpoint,
160
+ method: 'GET',
161
+ queryParams: {
162
+ actor: handle,
163
+ },
164
+ });
165
+ return response;
166
+ }
167
+ async getAllPages({ endpoint, resultKey, queryParams, limit } = {}) {
168
+ return this.paginateCursor({
169
+ limit,
170
+ fetchPage: async (cursor) => {
171
+ const response = await this.makeRPCRequest({
172
+ endpoint,
173
+ method: 'GET',
174
+ queryParams: {
175
+ ...queryParams,
176
+ ...(cursor ? { cursor } : {}),
177
+ },
178
+ });
179
+ return { items: response[resultKey], cursor: response.cursor };
180
+ },
181
+ });
182
+ }
183
+ async makeRPCRequest({ domain = this.pdsDomain, endpoint, method, params, queryParams, }) {
184
+ return this.request({
185
+ url: `${domain}/xrpc/${endpoint}`,
186
+ method,
187
+ data: params,
188
+ params: queryParams,
189
+ });
190
+ }
191
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@chronicle.app/bluesky",
3
+ "version": "0.3.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "files": [
7
+ "dist/**/*.js",
8
+ "dist/**/*.d.ts"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc -b tsconfig.lib.json",
12
+ "typecheck": "tsc -p tsconfig.json",
13
+ "lint": "eslint src --ext .ts,.js",
14
+ "test": "node --test src/*.test.js"
15
+ },
16
+ "keywords": [
17
+ "chronicle"
18
+ ],
19
+ "author": "Andrew Louis",
20
+ "license": "MIT",
21
+ "type": "module",
22
+ "description": "Chronicle plugin for Bluesky follows, followers, and likes",
23
+ "devDependencies": {
24
+ "@chronicle.app/etl": "0.3.0",
25
+ "@chronicle.app/schema": "0.3.0",
26
+ "@chronicle.app/tsconfig": "0.3.0",
27
+ "@types/node": "^22.10.1",
28
+ "typescript": "^5.7.2"
29
+ },
30
+ "homepage": "https://chronicle.app",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/chronicle-app/chronicle.git",
34
+ "directory": "plugins/bluesky"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org/"
39
+ },
40
+ "dependencies": {
41
+ "@chronicle.app/auth": "0.3.0",
42
+ "axios": "^1.7.9",
43
+ "zod": "^3.24.1"
44
+ },
45
+ "peerDependencies": {
46
+ "@chronicle.app/etl": ">=0.1.0 <1.0.0",
47
+ "@chronicle.app/schema": ">=0.1.0 <1.0.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=22.13.0"
51
+ },
52
+ "exports": {
53
+ ".": {
54
+ "types": "./dist/index.d.ts",
55
+ "import": "./dist/index.js"
56
+ }
57
+ },
58
+ "chronicle": {
59
+ "plugin": true,
60
+ "sources": {
61
+ "bluesky": {
62
+ "strategies": {
63
+ "api": {
64
+ "delivery": "api",
65
+ "recordTypes": [
66
+ "followers",
67
+ "follows",
68
+ "likes"
69
+ ],
70
+ "default": true
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }