@hasna/connectors 1.3.35 → 1.3.37

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.
Files changed (49) hide show
  1. package/bin/index.js +154 -222
  2. package/bin/mcp.js +191 -268
  3. package/bin/serve.js +265 -342
  4. package/connectors/bluesky/README.md +34 -0
  5. package/connectors/bluesky/package.json +41 -0
  6. package/connectors/bluesky/src/api/client.test.ts +122 -0
  7. package/connectors/bluesky/src/api/client.ts +194 -0
  8. package/connectors/bluesky/src/api/index.ts +108 -0
  9. package/connectors/bluesky/src/index.ts +17 -0
  10. package/connectors/bluesky/src/types/index.ts +46 -0
  11. package/connectors/bluesky/tsconfig.json +16 -0
  12. package/connectors/x/src/api/media.ts +4 -3
  13. package/dashboard/dist/assets/index-ClBXkNbL.css +1 -0
  14. package/dashboard/dist/assets/index-DvfmyAO4.js +284 -0
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/.types/connectors/bluesky/src/api/client.d.ts +73 -0
  17. package/dist/.types/connectors/bluesky/src/api/index.d.ts +76 -0
  18. package/dist/.types/connectors/bluesky/src/index.d.ts +11 -0
  19. package/dist/.types/connectors/bluesky/src/types/index.d.ts +33 -0
  20. package/dist/.types/connectors/x/src/api/client.d.ts +96 -0
  21. package/dist/.types/connectors/x/src/api/index.d.ts +80 -0
  22. package/dist/.types/connectors/x/src/api/media.d.ts +54 -0
  23. package/dist/.types/connectors/x/src/api/oauth.d.ts +82 -0
  24. package/dist/.types/connectors/x/src/api/oauth1.d.ts +68 -0
  25. package/dist/.types/connectors/x/src/api/tweets.d.ts +175 -0
  26. package/dist/.types/connectors/x/src/api/users.d.ts +127 -0
  27. package/dist/.types/connectors/x/src/cli/index.d.ts +2 -0
  28. package/dist/.types/connectors/x/src/index.d.ts +6 -0
  29. package/dist/.types/connectors/x/src/types/index.d.ts +156 -0
  30. package/dist/.types/connectors/x/src/utils/config.d.ts +99 -0
  31. package/dist/.types/connectors/x/src/utils/output.d.ts +8 -0
  32. package/dist/.types/src/social/bluesky.d.ts +65 -0
  33. package/dist/.types/src/social/errors.d.ts +9 -0
  34. package/dist/.types/src/social/index.d.ts +16 -0
  35. package/dist/.types/src/social/mastodon.d.ts +32 -0
  36. package/dist/.types/src/social/types.d.ts +73 -0
  37. package/dist/.types/src/social/util.d.ts +5 -0
  38. package/dist/.types/src/social/x.d.ts +82 -0
  39. package/dist/cli/components/App.d.ts +1 -2
  40. package/dist/cli/components/CategorySelect.d.ts +1 -2
  41. package/dist/cli/components/ConnectorSelect.d.ts +1 -2
  42. package/dist/cli/components/Header.d.ts +1 -2
  43. package/dist/cli/components/InstallProgress.d.ts +1 -2
  44. package/dist/cli/components/SearchView.d.ts +1 -2
  45. package/dist/index.js +123 -110
  46. package/dist/social/index.js +1531 -0
  47. package/package.json +8 -4
  48. package/dashboard/dist/assets/index-DJjxFlTl.css +0 -1
  49. package/dashboard/dist/assets/index-DNPZ58_i.js +0 -284
@@ -0,0 +1,34 @@
1
+ # @hasna/connect-bluesky
2
+
3
+ Bluesky / AT Protocol connector. Stateless, raw-`fetch` transport with **zero runtime
4
+ dependencies**.
5
+
6
+ ## Auth
7
+
8
+ Pass credentials per call — nothing is stored on disk:
9
+
10
+ ```ts
11
+ import { Bluesky } from "@hasna/connect-bluesky";
12
+
13
+ const bsky = new Bluesky({
14
+ identifier: "alice.bsky.social", // handle or DID
15
+ appPassword: "xxxx-xxxx-xxxx-xxxx", // app password, NOT the account password
16
+ pds: "https://bsky.social", // optional, defaults to bsky.social
17
+ });
18
+ ```
19
+
20
+ ## Operations
21
+
22
+ | Method | AT Protocol endpoint |
23
+ |--------|----------------------|
24
+ | `bsky.me()` | `com.atproto.server.createSession` |
25
+ | `bsky.createPost({ text, replyToUri? })` | `com.atproto.repo.createRecord` (`app.bsky.feed.post`) |
26
+ | `bsky.deletePost(uri)` | `com.atproto.repo.deleteRecord` |
27
+ | `bsky.uploadBlob(buffer, mimeType)` | `com.atproto.repo.uploadBlob` |
28
+ | `bsky.listNotifications()` | `app.bsky.notification.listNotifications` |
29
+ | `bsky.getPosts(uris)` | `app.bsky.feed.getPosts` |
30
+
31
+ Threads: `createPost` with `replyToUri` resolves the parent strong-ref and the thread
32
+ root automatically (deep threads stay correctly rooted).
33
+
34
+ This connector is consumed by the stateless `@hasna/connectors/social` transport SDK.
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@hasna/connect-bluesky",
3
+ "version": "0.1.0",
4
+ "description": "Bluesky / AT Protocol connector - posts, threads, mentions",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/index.d.ts"
10
+ }
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "scripts": {
15
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "keywords": [
19
+ "bluesky",
20
+ "atproto",
21
+ "at-protocol",
22
+ "social",
23
+ "connector",
24
+ "typescript",
25
+ "bun",
26
+ "hasna"
27
+ ],
28
+ "author": "Hasna",
29
+ "license": "Apache-2.0",
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "typescript": "^5"
33
+ },
34
+ "engines": {
35
+ "bun": ">=1.0.0"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/hasna/connectors.git"
40
+ }
41
+ }
@@ -0,0 +1,122 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { Bluesky, parseAtUri } from "./index";
3
+
4
+ const realFetch = globalThis.fetch;
5
+
6
+ interface Recorded {
7
+ url: string;
8
+ method: string;
9
+ body?: unknown;
10
+ }
11
+
12
+ function installFetch(handler: (url: string, init: RequestInit | undefined, recorded: Recorded[]) => unknown) {
13
+ const recorded: Recorded[] = [];
14
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
15
+ const url = typeof input === "string" ? input : input.toString();
16
+ recorded.push({ url, method: init?.method ?? "GET", body: init?.body });
17
+ const json = handler(url, init, recorded);
18
+ return {
19
+ ok: true,
20
+ status: 200,
21
+ statusText: "OK",
22
+ async text() {
23
+ return JSON.stringify(json ?? {});
24
+ },
25
+ } as Response;
26
+ }) as typeof fetch;
27
+ return recorded;
28
+ }
29
+
30
+ afterEach(() => {
31
+ globalThis.fetch = realFetch;
32
+ });
33
+
34
+ describe("Bluesky AT Protocol transport", () => {
35
+ test("parseAtUri splits repo/collection/rkey", () => {
36
+ expect(parseAtUri("at://did:plc:abc/app.bsky.feed.post/xyz")).toEqual({
37
+ repo: "did:plc:abc",
38
+ collection: "app.bsky.feed.post",
39
+ rkey: "xyz",
40
+ });
41
+ });
42
+
43
+ test("me() creates a session via com.atproto.server.createSession", async () => {
44
+ const recorded = installFetch((url) => {
45
+ if (url.includes("createSession")) return { accessJwt: "a", refreshJwt: "r", did: "did:plc:me", handle: "me.bsky.social" };
46
+ return {};
47
+ });
48
+ const bsky = new Bluesky({ identifier: "me.bsky.social", appPassword: "pw" });
49
+ const session = await bsky.me();
50
+ expect(session.did).toBe("did:plc:me");
51
+ expect(recorded[0].url).toContain("/xrpc/com.atproto.server.createSession");
52
+ expect(recorded[0].method).toBe("POST");
53
+ });
54
+
55
+ test("createPost (top-level) calls createRecord with app.bsky.feed.post", async () => {
56
+ const recorded = installFetch((url) => {
57
+ if (url.includes("createSession")) return { accessJwt: "a", refreshJwt: "r", did: "did:plc:me", handle: "me.bsky.social" };
58
+ if (url.includes("createRecord")) return { uri: "at://did:plc:me/app.bsky.feed.post/new", cid: "cidnew" };
59
+ return {};
60
+ });
61
+ const bsky = new Bluesky({ identifier: "me.bsky.social", appPassword: "pw" });
62
+ const res = await bsky.createPost({ text: "hello", createdAt: "2026-01-01T00:00:00Z" });
63
+ expect(res).toEqual({ uri: "at://did:plc:me/app.bsky.feed.post/new", cid: "cidnew" });
64
+ const createCall = recorded.find((r) => r.url.includes("createRecord"))!;
65
+ const body = JSON.parse(createCall.body as string);
66
+ expect(body.collection).toBe("app.bsky.feed.post");
67
+ expect(body.repo).toBe("did:plc:me");
68
+ expect(body.record.text).toBe("hello");
69
+ expect(body.record.reply).toBeUndefined();
70
+ });
71
+
72
+ test("createPost reply chains parent + thread root strong-refs", async () => {
73
+ const parentUri = "at://did:plc:other/app.bsky.feed.post/parent";
74
+ const rootRef = { uri: "at://did:plc:other/app.bsky.feed.post/root", cid: "rootcid" };
75
+ const recorded = installFetch((url) => {
76
+ if (url.includes("createSession")) return { accessJwt: "a", refreshJwt: "r", did: "did:plc:me", handle: "me.bsky.social" };
77
+ if (url.includes("getPosts")) {
78
+ return { posts: [{ uri: parentUri, cid: "parentcid", record: { reply: { root: rootRef } } }] };
79
+ }
80
+ if (url.includes("createRecord")) return { uri: "at://did:plc:me/app.bsky.feed.post/reply", cid: "replycid" };
81
+ return {};
82
+ });
83
+ const bsky = new Bluesky({ identifier: "me.bsky.social", appPassword: "pw" });
84
+ await bsky.createPost({ text: "reply", replyToUri: parentUri });
85
+
86
+ const createCall = recorded.find((r) => r.url.includes("createRecord"))!;
87
+ const body = JSON.parse(createCall.body as string);
88
+ expect(body.record.reply.parent).toEqual({ uri: parentUri, cid: "parentcid" });
89
+ expect(body.record.reply.root).toEqual(rootRef);
90
+ });
91
+
92
+ test("createPost reply uses parent as root for a top-level parent", async () => {
93
+ const parentUri = "at://did:plc:other/app.bsky.feed.post/top";
94
+ const recorded = installFetch((url) => {
95
+ if (url.includes("createSession")) return { accessJwt: "a", refreshJwt: "r", did: "did:plc:me", handle: "me.bsky.social" };
96
+ if (url.includes("getPosts")) return { posts: [{ uri: parentUri, cid: "topcid" }] };
97
+ if (url.includes("createRecord")) return { uri: "at://did:plc:me/app.bsky.feed.post/reply", cid: "rc" };
98
+ return {};
99
+ });
100
+ const bsky = new Bluesky({ identifier: "me.bsky.social", appPassword: "pw" });
101
+ await bsky.createPost({ text: "reply", replyToUri: parentUri });
102
+ const body = JSON.parse(recorded.find((r) => r.url.includes("createRecord"))!.body as string);
103
+ expect(body.record.reply.parent).toEqual({ uri: parentUri, cid: "topcid" });
104
+ expect(body.record.reply.root).toEqual({ uri: parentUri, cid: "topcid" });
105
+ });
106
+
107
+ test("deletePost parses the uri and calls deleteRecord", async () => {
108
+ const recorded = installFetch((url) => {
109
+ if (url.includes("createSession")) return { accessJwt: "a", refreshJwt: "r", did: "did:plc:me", handle: "me.bsky.social" };
110
+ return {};
111
+ });
112
+ const bsky = new Bluesky({ identifier: "me.bsky.social", appPassword: "pw" });
113
+ await bsky.deletePost("at://did:plc:me/app.bsky.feed.post/gone");
114
+ const del = recorded.find((r) => r.url.includes("deleteRecord"))!;
115
+ const body = JSON.parse(del.body as string);
116
+ expect(body).toEqual({ repo: "did:plc:me", collection: "app.bsky.feed.post", rkey: "gone" });
117
+ });
118
+
119
+ test("requires identifier + appPassword", () => {
120
+ expect(() => new Bluesky({ identifier: "", appPassword: "" })).toThrow();
121
+ });
122
+ });
@@ -0,0 +1,194 @@
1
+ import {
2
+ BlueskyApiError,
3
+ type AtUriRef,
4
+ type BlueskyConfig,
5
+ type BlueskySession,
6
+ type CreateRecordResult,
7
+ } from "../types/index";
8
+
9
+ const DEFAULT_PDS = "https://bsky.social";
10
+
11
+ /**
12
+ * Minimal AT Protocol (Bluesky) transport — raw fetch, zero dependencies, no fs.
13
+ *
14
+ * Credentials are passed in per construction; the session is created lazily on the
15
+ * first authenticated call (com.atproto.server.createSession). Nothing is persisted.
16
+ */
17
+ export class BlueskyClient {
18
+ private readonly identifier: string;
19
+ private readonly appPassword: string;
20
+ private readonly pds: string;
21
+ private session?: BlueskySession;
22
+
23
+ constructor(config: BlueskyConfig) {
24
+ if (!config?.identifier || !config?.appPassword) {
25
+ throw new Error("bluesky credentials require `identifier` and `appPassword`");
26
+ }
27
+ this.identifier = config.identifier;
28
+ this.appPassword = config.appPassword;
29
+ this.pds = (config.pds || DEFAULT_PDS).replace(/\/+$/, "");
30
+ }
31
+
32
+ private async xrpc<T>(
33
+ nsid: string,
34
+ options: {
35
+ method?: "GET" | "POST";
36
+ params?: Record<string, string | number | undefined>;
37
+ body?: unknown;
38
+ auth?: boolean;
39
+ contentType?: string;
40
+ rawBody?: BodyInit;
41
+ } = {},
42
+ ): Promise<T> {
43
+ const { method = "GET", params, body, auth = true, contentType, rawBody } = options;
44
+
45
+ const url = new URL(`${this.pds}/xrpc/${nsid}`);
46
+ if (params) {
47
+ for (const [k, v] of Object.entries(params)) {
48
+ if (v !== undefined && v !== null && v !== "") url.searchParams.append(k, String(v));
49
+ }
50
+ }
51
+
52
+ const headers: Record<string, string> = { Accept: "application/json" };
53
+ if (auth) {
54
+ const session = await this.ensureSession();
55
+ headers.Authorization = `Bearer ${session.accessJwt}`;
56
+ }
57
+
58
+ const init: RequestInit = { method, headers };
59
+ if (rawBody !== undefined) {
60
+ headers["Content-Type"] = contentType || "application/octet-stream";
61
+ init.body = rawBody;
62
+ } else if (body !== undefined && method !== "GET") {
63
+ headers["Content-Type"] = contentType || "application/json";
64
+ init.body = JSON.stringify(body);
65
+ }
66
+
67
+ const response = await fetch(url.toString(), init);
68
+
69
+ if (response.status === 204) return {} as T;
70
+
71
+ let data: unknown;
72
+ const text = await response.text();
73
+ if (text) {
74
+ try {
75
+ data = JSON.parse(text);
76
+ } catch {
77
+ data = text;
78
+ }
79
+ }
80
+
81
+ if (!response.ok) {
82
+ const errBody = (typeof data === "object" && data !== null ? data : {}) as {
83
+ error?: string;
84
+ message?: string;
85
+ };
86
+ const message = errBody.message || errBody.error || response.statusText || "Request failed";
87
+ throw new BlueskyApiError(message, response.status, errBody.error);
88
+ }
89
+
90
+ return data as T;
91
+ }
92
+
93
+ /** Create (or return cached) a session via com.atproto.server.createSession. */
94
+ async ensureSession(): Promise<BlueskySession> {
95
+ if (this.session) return this.session;
96
+ const session = await this.xrpc<BlueskySession>("com.atproto.server.createSession", {
97
+ method: "POST",
98
+ auth: false,
99
+ body: { identifier: this.identifier, password: this.appPassword },
100
+ });
101
+ this.session = session;
102
+ return session;
103
+ }
104
+
105
+ /** Create a record in the authenticated user's repo. */
106
+ async createRecord(collection: string, record: Record<string, unknown>): Promise<CreateRecordResult> {
107
+ const session = await this.ensureSession();
108
+ return this.xrpc<CreateRecordResult>("com.atproto.repo.createRecord", {
109
+ method: "POST",
110
+ body: { repo: session.did, collection, record },
111
+ });
112
+ }
113
+
114
+ /** Delete a record by its at:// URI (parses repo/collection/rkey). */
115
+ async deleteRecord(uri: string): Promise<void> {
116
+ const session = await this.ensureSession();
117
+ const parsed = parseAtUri(uri);
118
+ await this.xrpc("com.atproto.repo.deleteRecord", {
119
+ method: "POST",
120
+ body: { repo: session.did, collection: parsed.collection, rkey: parsed.rkey },
121
+ });
122
+ }
123
+
124
+ /** Upload a blob (image/video) — returns the blob ref for embedding. */
125
+ async uploadBlob(data: Buffer | Uint8Array, mimeType: string): Promise<{ blob: unknown }> {
126
+ return this.xrpc<{ blob: unknown }>("com.atproto.repo.uploadBlob", {
127
+ method: "POST",
128
+ rawBody: data as unknown as BodyInit,
129
+ contentType: mimeType || "application/octet-stream",
130
+ });
131
+ }
132
+
133
+ /** List notifications (used for mentions). */
134
+ async listNotifications(options?: {
135
+ limit?: number;
136
+ cursor?: string;
137
+ }): Promise<{
138
+ notifications: Array<{
139
+ uri: string;
140
+ cid: string;
141
+ reason: string;
142
+ indexedAt?: string;
143
+ author?: { did: string; handle: string };
144
+ record?: { text?: string; createdAt?: string };
145
+ }>;
146
+ cursor?: string;
147
+ }> {
148
+ return this.xrpc("app.bsky.notification.listNotifications", {
149
+ method: "GET",
150
+ params: { limit: options?.limit, cursor: options?.cursor },
151
+ });
152
+ }
153
+
154
+ /** Fetch hydrated posts by URI (used for analytics/metrics + reply refs). */
155
+ async getPosts(uris: string[]): Promise<{
156
+ posts: Array<{
157
+ uri: string;
158
+ cid?: string;
159
+ likeCount?: number;
160
+ repostCount?: number;
161
+ replyCount?: number;
162
+ quoteCount?: number;
163
+ record?: { reply?: { root?: { uri: string; cid: string } } };
164
+ }>;
165
+ }> {
166
+ const url = new URL(`${this.pds}/xrpc/app.bsky.feed.getPosts`);
167
+ for (const u of uris) url.searchParams.append("uris", u);
168
+ const session = await this.ensureSession();
169
+ const response = await fetch(url.toString(), {
170
+ headers: { Accept: "application/json", Authorization: `Bearer ${session.accessJwt}` },
171
+ });
172
+ const text = await response.text();
173
+ const data = text ? JSON.parse(text) : {};
174
+ if (!response.ok) {
175
+ throw new BlueskyApiError(data?.message || data?.error || response.statusText, response.status, data?.error);
176
+ }
177
+ return data;
178
+ }
179
+
180
+ getSession(): BlueskySession | undefined {
181
+ return this.session;
182
+ }
183
+ }
184
+
185
+ /** Parse an at:// URI into its components. */
186
+ export function parseAtUri(uri: string): { repo: string; collection: string; rkey: string } {
187
+ const match = /^at:\/\/([^/]+)\/([^/]+)\/(.+)$/.exec(uri);
188
+ if (!match) {
189
+ throw new Error(`Invalid at:// URI: ${uri}`);
190
+ }
191
+ return { repo: match[1]!, collection: match[2]!, rkey: match[3]! };
192
+ }
193
+
194
+ export type { AtUriRef };
@@ -0,0 +1,108 @@
1
+ import { BlueskyClient, parseAtUri } from "./client";
2
+ import type { BlueskyConfig, BlueskySession, CreateRecordResult } from "../types/index";
3
+
4
+ const POST_COLLECTION = "app.bsky.feed.post";
5
+
6
+ export interface CreatePostOptions {
7
+ text: string;
8
+ /** at:// URI of the post being replied to (the social SDK passes the parent uri). */
9
+ replyToUri?: string;
10
+ /** Optional embed (e.g. images) built by the caller. */
11
+ embed?: Record<string, unknown>;
12
+ createdAt?: string;
13
+ }
14
+
15
+ /**
16
+ * Main Bluesky (AT Protocol) connector class.
17
+ */
18
+ export class Bluesky {
19
+ private readonly client: BlueskyClient;
20
+
21
+ constructor(config: BlueskyConfig) {
22
+ this.client = new BlueskyClient(config);
23
+ }
24
+
25
+ static fromEnv(): Bluesky {
26
+ const identifier = process.env.BLUESKY_IDENTIFIER;
27
+ const appPassword = process.env.BLUESKY_APP_PASSWORD;
28
+ const pds = process.env.BLUESKY_PDS;
29
+ if (!identifier || !appPassword) {
30
+ throw new Error("BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD environment variables are required");
31
+ }
32
+ return new Bluesky({ identifier, appPassword, pds });
33
+ }
34
+
35
+ /** Resolve the authenticated account (did + handle). */
36
+ async me(): Promise<BlueskySession> {
37
+ return this.client.ensureSession();
38
+ }
39
+
40
+ /**
41
+ * Create a feed post. For replies, we resolve the root of the thread so the
42
+ * reply ref carries both parent and root strong-refs (AT Protocol requirement).
43
+ */
44
+ async createPost(options: CreatePostOptions): Promise<CreateRecordResult> {
45
+ const record: Record<string, unknown> = {
46
+ $type: POST_COLLECTION,
47
+ text: options.text,
48
+ createdAt: options.createdAt || new Date().toISOString(),
49
+ };
50
+
51
+ if (options.embed) {
52
+ record.embed = options.embed;
53
+ }
54
+
55
+ if (options.replyToUri) {
56
+ const { parent, root } = await this.resolveReplyRefs(options.replyToUri);
57
+ record.reply = { root, parent };
58
+ }
59
+
60
+ return this.client.createRecord(POST_COLLECTION, record);
61
+ }
62
+
63
+ async deletePost(uri: string): Promise<void> {
64
+ return this.client.deleteRecord(uri);
65
+ }
66
+
67
+ /** Upload an image/video blob and return a strong blob ref. */
68
+ async uploadBlob(data: Buffer | Uint8Array, mimeType: string): Promise<unknown> {
69
+ const res = await this.client.uploadBlob(data, mimeType);
70
+ return res.blob;
71
+ }
72
+
73
+ async listNotifications(options?: { limit?: number; cursor?: string }) {
74
+ return this.client.listNotifications(options);
75
+ }
76
+
77
+ async getPosts(uris: string[]) {
78
+ return this.client.getPosts(uris);
79
+ }
80
+
81
+ getClient(): BlueskyClient {
82
+ return this.client;
83
+ }
84
+
85
+ /**
86
+ * Resolve the strong-refs needed for a reply: the parent ref ({uri,cid}) and the
87
+ * thread root. If the parent is itself a reply, its record carries the thread
88
+ * root; otherwise the parent IS the root. This keeps deep threads correctly
89
+ * rooted per the AT Protocol app.bsky.feed.post reply schema.
90
+ */
91
+ private async resolveReplyRefs(parentUri: string): Promise<{
92
+ parent: { uri: string; cid: string };
93
+ root: { uri: string; cid: string };
94
+ }> {
95
+ parseAtUri(parentUri);
96
+ const posts = await this.client.getPosts([parentUri]);
97
+ const match = posts.posts.find((p) => p.uri === parentUri);
98
+ if (!match || !match.cid) {
99
+ throw new Error(`Cannot resolve parent post for reply: ${parentUri}`);
100
+ }
101
+ const parent = { uri: match.uri, cid: match.cid };
102
+ const root = match.record?.reply?.root ?? parent;
103
+ return { parent, root };
104
+ }
105
+ }
106
+
107
+ export { BlueskyClient, parseAtUri } from "./client";
108
+ export type { BlueskyConfig, BlueskySession, CreateRecordResult } from "../types/index";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @hasna/connect-bluesky
3
+ *
4
+ * Bluesky / AT Protocol connector. Stateless, raw-fetch transport with zero
5
+ * runtime dependencies — login via com.atproto.server.createSession, post via
6
+ * com.atproto.repo.createRecord (app.bsky.feed.post), delete via
7
+ * com.atproto.repo.deleteRecord.
8
+ */
9
+ export { Bluesky, BlueskyClient, parseAtUri } from "./api/index";
10
+ export {
11
+ BlueskyApiError,
12
+ type BlueskyConfig,
13
+ type BlueskySession,
14
+ type CreateRecordResult,
15
+ type AtUriRef,
16
+ } from "./types/index";
17
+ export type { CreatePostOptions } from "./api/index";
@@ -0,0 +1,46 @@
1
+ // Bluesky / AT Protocol types
2
+
3
+ export interface BlueskyConfig {
4
+ /** Handle or DID, e.g. "alice.bsky.social" */
5
+ identifier: string;
6
+ /** App password (NOT the account password) */
7
+ appPassword: string;
8
+ /** Personal Data Server base URL. Defaults to https://bsky.social */
9
+ pds?: string;
10
+ }
11
+
12
+ export interface BlueskySession {
13
+ accessJwt: string;
14
+ refreshJwt: string;
15
+ handle: string;
16
+ did: string;
17
+ email?: string;
18
+ }
19
+
20
+ /** A strong-ref to an AT Protocol record (used for reply roots/parents). */
21
+ export interface AtUriRef {
22
+ uri: string;
23
+ cid: string;
24
+ }
25
+
26
+ export interface CreateRecordResult {
27
+ uri: string;
28
+ cid: string;
29
+ }
30
+
31
+ export interface BlueskyApiErrorBody {
32
+ error?: string;
33
+ message?: string;
34
+ }
35
+
36
+ export class BlueskyApiError extends Error {
37
+ public readonly statusCode: number;
38
+ public readonly errorCode?: string;
39
+
40
+ constructor(message: string, statusCode: number, errorCode?: string) {
41
+ super(message);
42
+ this.name = "BlueskyApiError";
43
+ this.statusCode = statusCode;
44
+ this.errorCode = errorCode;
45
+ }
46
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "types": ["bun-types"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist", "bin"]
16
+ }
@@ -3,8 +3,6 @@
3
3
  * Uses OAuth 1.0a for authentication
4
4
  */
5
5
 
6
- import { readFileSync, statSync } from 'fs';
7
- import { basename } from 'path';
8
6
  import { OAuth1Client } from './oauth1';
9
7
 
10
8
  const UPLOAD_URL = 'https://upload.twitter.com/1.1/media/upload.json';
@@ -243,8 +241,11 @@ export class MediaApi {
243
241
  filePath: string,
244
242
  options: MediaUploadOptions = {}
245
243
  ): Promise<MediaUploadResult> {
244
+ // Lazy-load node:fs so this module stays bundle-safe (free of static fs
245
+ // imports) for consumers that only ever call uploadBuffer (e.g. the
246
+ // stateless @hasna/connectors/social entry point).
247
+ const { readFileSync } = await import('node:fs');
246
248
  const fileData = readFileSync(filePath);
247
- const fileSize = statSync(filePath).size;
248
249
  const mimeType = getMimeType(filePath);
249
250
 
250
251
  return this.uploadBuffer(fileData, mimeType, {