Schedule posts to Bluesky with Cloudflare workers.
skyscheduler.work
cf
tool
bsky-tool
cloudflare
bluesky
schedule
bsky
service
social-media
cloudflare-workers
1import { betterAuth, Session } from "better-auth";
2import { withCloudflare } from "better-auth-cloudflare";
3import { drizzleAdapter } from "better-auth/adapters/drizzle";
4import { username } from "better-auth/plugins";
5import { drizzle, DrizzleD1Database } from "drizzle-orm/d1";
6import { schema } from "../db";
7import { BSKY_MAX_USERNAME_LENGTH, BSKY_MIN_USERNAME_LENGTH } from "../limits";
8import { APP_NAME } from "../siteinfo";
9import { Bindings } from "../types";
10import { lookupBskyHandle } from "../utils/bsky/bskyApi";
11import { createDMWithUser } from "../utils/bsky/bskyMessage";
12import { createPasswordResetMessage } from "../utils/messages/accountReset";
13
14// Single auth configuration that handles both CLI and runtime scenarios
15function createAuth(env?: Bindings, cf?: IncomingRequestCfProperties) {
16 // Use actual DB for runtime, empty object for CLI
17 const db = env ? drizzle(env.DB, { schema, logger: false }) : ({} as any);
18 return betterAuth({
19 disabledPaths: [
20 "/sign-in/email",
21 "/sign-in/social",
22 "/change-email",
23 "/set-password",
24 "/link-social-account",
25 "/unlink-account",
26 "/account-info",
27 "/refresh-token",
28 "/get-access-token",
29 "/verify-email",
30 "/send-verification-email",
31 "/revoke-other-sessions",
32 "/revoke-session",
33 "/link-social",
34 "/list-accounts",
35 "/list-sessions",
36 "/cloudflare/geolocation",
37 "/is-username-available",
38 "/delete-user/callback",
39 "/change-password",
40 "/is-username-available"
41 ],
42 ...withCloudflare(
43 {
44 autoDetectIpAddress: false,
45 geolocationTracking: false,
46 cf: cf || {},
47 d1: env
48 ? {
49 db,
50 options: {
51 usePlural: true,
52 debugLogs: false,
53 },
54 }
55 : undefined,
56 kv: env?.KV,
57 },
58 {
59 emailAndPassword: {
60 enabled: true,
61 requireEmailVerification: false,
62 sendResetPassword: async ({user, url, token}, request) => {
63 const userName = (user as any).username;
64 const bskyUserId = await lookupBskyHandle(userName);
65 if (bskyUserId !== null) {
66 const response = await createDMWithUser(env!, bskyUserId, createPasswordResetMessage(url, token));
67 if (!response)
68 throw new Error("FAILED_MESSAGE");
69 } else {
70 console.error(`Unable to look up bsky username for user ${userName}, got null`);
71 throw new Error("NO_LOOKUP");
72 }
73 },
74 },
75 plugins: [
76 username({
77 // We validate all of our usernames ahead of time
78 // do not use the validator in betterauth but instead our own ZOD system
79 usernameValidator: (username) => {
80 return true;
81 },
82 displayUsernameValidator: (displayUsername) => {
83 return true;
84 },
85 /* we do our own normalization in the zod schemas */
86 usernameNormalization: false,
87 displayUsernameNormalization: false,
88 minUsernameLength: BSKY_MIN_USERNAME_LENGTH,
89 maxUsernameLength: BSKY_MAX_USERNAME_LENGTH
90 })
91 ],
92 rateLimit: {
93 enabled: true,
94 window: 60,
95 max: 100,
96 "*": {
97 window: 60,
98 max: 100,
99 },
100 },
101 }
102 ),
103 appName: APP_NAME,
104 secret: env?.BETTER_AUTH_SECRET,
105 baseURL: (env?.BETTER_AUTH_URL === "*") ? undefined : env?.BETTER_AUTH_URL,
106 user: {
107 additionalFields: {
108 bskyAppPass: {
109 type: "string",
110 required: true
111 },
112 pds: {
113 type: "string",
114 defaultValue: "https://bsky.social",
115 required: true
116 }
117 },
118 changeEmail: {
119 enabled: false
120 },
121 deleteUser: {
122 enabled: false,
123 }
124 },
125 account: {
126 accountLinking: {
127 enabled: false
128 },
129 },
130 telemetry: {
131 enabled: false
132 },
133 logger: {
134 disabled: true
135 },
136 // Only add database adapter for CLI schema generation
137 ...(env ? {} : {
138 database: drizzleAdapter({} as D1Database, {
139 provider: "sqlite",
140 usePlural: true,
141 debugLogs: false,
142 }),
143 }),
144 });
145}
146
147// Export for CLI schema generation
148export const auth = createAuth();
149
150// Export for variable types
151type ContextVariables = {
152 auth: ReturnType<typeof createAuth>;
153 userId: string;
154 isAdmin: boolean;
155 session: Session;
156 db: DrizzleD1Database;
157 pds: string;
158};
159
160// Export for runtime usage
161export { ContextVariables, createAuth };