feat(worker): add cloudflare worker oauth relay with turso direct write

husteno-o committed 1 months ago
f781485
1 parent 5470357
commit f781485
4 files changed
+251 -0
▼apps/worker/package.json
+16-0
1
+{
2
+ "name": "@devpulse/worker",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "typecheck": "tsc --noEmit",
9
+ "deploy": "bun x wrangler deploy",
10
+ "dev": "bun x wrangler dev"
11
+ },
12
+ "devDependencies": {
13
+ "@cloudflare/workers-types": "^4.20250224.0",
14
+ "wrangler": "^3.109.2"
15
+ }
16
+}
0
17
▼apps/worker/src/index.ts
+215-0
1
+// Cloudflare Worker for DevPulse GitHub OAuth Relay
2
+// Relays GitHub OAuth callbacks directly to Turso DB with zero open ports on your Docker host.
3
+
4
+export interface Env {
5
+ GITHUB_CLIENT_ID: string;
6
+ GITHUB_CLIENT_SECRET: string;
7
+ TURSO_DATABASE_URL: string;
8
+ TURSO_AUTH_TOKEN?: string;
9
+ ENCRYPTION_KEY?: string; // 32-character key for AES-256-GCM
10
+}
11
+
12
+// Convert string/passphrase into AES-256-GCM CryptoKey
13
+async function getCryptoKey(secretKey: string): Promise<CryptoKey> {
14
+ const enc = new TextEncoder();
15
+ const keyMaterial = await crypto.subtle.importKey(
16
+ "raw",
17
+ enc.encode(secretKey.padEnd(32, "0").slice(0, 32)),
18
+ { name: "AES-GCM" },
19
+ false,
20
+ ["encrypt"],
21
+ );
22
+ return keyMaterial;
23
+}
24
+
25
+// Encrypt token using Web Crypto AES-256-GCM
26
+async function encryptToken(token: string, secretKey: string): Promise<string> {
27
+ const key = await getCryptoKey(secretKey);
28
+ const iv = crypto.getRandomValues(new Uint8Array(12));
29
+ const enc = new TextEncoder();
30
+
31
+ const ciphertextBuffer = await crypto.subtle.encrypt(
32
+ { name: "AES-GCM", iv, tagLength: 128 },
33
+ key,
34
+ enc.encode(token),
35
+ );
36
+
37
+ const ivHex = Array.from(iv)
38
+ .map((b) => b.toString(16).padStart(2, "0"))
39
+ .join("");
40
+ const encryptedHex = Array.from(new Uint8Array(ciphertextBuffer))
41
+ .map((b) => b.toString(16).padStart(2, "0"))
42
+ .join("");
43
+ return `${ivHex}:${encryptedHex}`;
44
+}
45
+
46
+// Execute query against Turso via its HTTP pipeline API
47
+async function executeTurso(
48
+ databaseUrl: string,
49
+ authToken: string | undefined,
50
+ sql: string,
51
+ args: any[],
52
+) {
53
+ const httpUrl = databaseUrl.replace(/^libsql:\/\//, "https://");
54
+ const endpoint = `${httpUrl}/v2/pipeline`;
55
+
56
+ const body = {
57
+ requests: [
58
+ {
59
+ type: "execute",
60
+ stmt: {
61
+ sql,
62
+ args: args.map((arg) => {
63
+ if (arg === null || arg === undefined) return { type: "null" };
64
+ if (typeof arg === "number") return { type: "integer", value: arg.toString() };
65
+ return { type: "text", value: String(arg) };
66
+ }),
67
+ },
68
+ },
69
+ { type: "close" },
70
+ ],
71
+ };
72
+
73
+ const response = await fetch(endpoint, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
78
+ },
79
+ body: JSON.stringify(body),
80
+ });
81
+
82
+ return response.json();
83
+}
84
+
85
+export default {
86
+ async fetch(request: Request, env: Env): Promise<Response> {
87
+ const url = new URL(request.url);
88
+
89
+ // 1. Health check
90
+ if (url.pathname === "/" || url.pathname === "/health") {
91
+ return new Response(
92
+ JSON.stringify({ status: "healthy", service: "DevPulse Cloudflare Auth Relay" }),
93
+ {
94
+ headers: { "Content-Type": "application/json" },
95
+ },
96
+ );
97
+ }
98
+
99
+ // 2. Login Redirect
100
+ if (url.pathname === "/auth/github/login") {
101
+ const userId = url.searchParams.get("userId") || "anonymous";
102
+ const redirectUri = `${url.origin}/auth/github/callback`;
103
+ const githubUrl = `https://github.com/login/oauth/authorize?client_id=${encodeURIComponent(
104
+ env.GITHUB_CLIENT_ID,
105
+ )}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=repo,read:user&state=${encodeURIComponent(userId)}`;
106
+
107
+ return Response.redirect(githubUrl, 302);
108
+ }
109
+
110
+ // 3. OAuth Callback
111
+ if (url.pathname === "/auth/github/callback") {
112
+ const code = url.searchParams.get("code");
113
+ const state = url.searchParams.get("state");
114
+
115
+ if (!code) {
116
+ return new Response("Missing code parameter", { status: 400 });
117
+ }
118
+
119
+ // Exchange code for access token
120
+ const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
121
+ method: "POST",
122
+ headers: {
123
+ Accept: "application/json",
124
+ "Content-Type": "application/json",
125
+ },
126
+ body: JSON.stringify({
127
+ client_id: env.GITHUB_CLIENT_ID,
128
+ client_secret: env.GITHUB_CLIENT_SECRET,
129
+ code,
130
+ }),
131
+ });
132
+
133
+ const tokenData = (await tokenRes.json()) as {
134
+ access_token?: string;
135
+ error?: string;
136
+ error_description?: string;
137
+ };
138
+ if (!tokenData.access_token) {
139
+ return new Response(
140
+ `Authentication error: ${tokenData.error_description || "Failed to acquire token"}`,
141
+ { status: 400 },
142
+ );
143
+ }
144
+
145
+ // Fetch GitHub profile
146
+ const userRes = await fetch("https://api.github.com/user", {
147
+ headers: {
148
+ Authorization: `Bearer ${tokenData.access_token}`,
149
+ "User-Agent": "DevPulse-Cloudflare-Relay",
150
+ },
151
+ });
152
+ const ghUser = (await userRes.json()) as { login: string };
153
+
154
+ // Encrypt token
155
+ const encKey =
156
+ env.ENCRYPTION_KEY || env.GITHUB_CLIENT_SECRET || "devpulse-default-secure-key-32";
157
+ const encrypted = await encryptToken(tokenData.access_token, encKey);
158
+ const discordUserId = state && state !== "anonymous" ? state : null;
159
+
160
+ // Persist directly to shared Turso Database
161
+ if (discordUserId && env.TURSO_DATABASE_URL) {
162
+ const now = Date.now();
163
+ const upsertSql = `
164
+ INSERT INTO users (id, username, github_username, github_token_encrypted, updated_at)
165
+ VALUES (?, ?, ?, ?, ?)
166
+ ON CONFLICT(id) DO UPDATE SET
167
+ github_username = excluded.github_username,
168
+ github_token_encrypted = excluded.github_token_encrypted,
169
+ updated_at = excluded.updated_at
170
+ `;
171
+ await executeTurso(env.TURSO_DATABASE_URL, env.TURSO_AUTH_TOKEN, upsertSql, [
172
+ discordUserId,
173
+ ghUser.login,
174
+ ghUser.login,
175
+ encrypted,
176
+ now,
177
+ ]);
178
+ }
179
+
180
+ // Return success page
181
+ const html = `
182
+ <!DOCTYPE html>
183
+ <html>
184
+ <head>
185
+ <meta charset="UTF-8" />
186
+ <title>DevPulse - Connected</title>
187
+ <style>
188
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0d1117; color: #f0f6fc; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
189
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 40px; text-align: center; max-width: 440px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
190
+ h1 { color: #3fb950; font-size: 1.5rem; margin-bottom: 12px; }
191
+ p { color: #8b949e; line-height: 1.6; margin-bottom: 24px; font-size: 0.95rem; }
192
+ .badge { display: inline-block; background: rgba(56,139,253,0.15); color: #58a6ff; border: 1px solid rgba(56,139,253,0.4); padding: 4px 12px; border-radius: 6px; font-family: monospace; font-size: 0.9rem; margin-bottom: 20px; }
193
+ .footer { font-size: 0.8rem; color: #6e7681; }
194
+ </style>
195
+ </head>
196
+ <body>
197
+ <div class="card">
198
+ <h1>GitHub Connected</h1>
199
+ <div class="badge">@${ghUser.login}</div>
200
+ <p>Your GitHub account has been securely linked and saved to your database. Your Docker bot is now connected without exposing any ports to the internet.</p>
201
+ <p>You can close this tab and return to Discord!</p>
202
+ <div class="footer">DevPulse via Cloudflare Edge Relay</div>
203
+ </div>
204
+ </body>
205
+ </html>
206
+ `;
207
+
208
+ return new Response(html, {
209
+ headers: { "Content-Type": "text/html; charset=utf-8" },
210
+ });
211
+ }
212
+
213
+ return new Response("Not Found", { status: 404 });
214
+ },
215
+};
0
216
▼apps/worker/tsconfig.json
+10-0
1
+{
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist",
6
+ "types": ["bun-types"],
7
+ "lib": ["ESNext", "DOM"]
8
+ },
9
+ "include": ["src/**/*"]
10
+}
0
11
▼apps/worker/wrangler.toml
+10-0
1
+name = "devpulse-auth"
2
+main = "src/index.ts"
3
+compatibility_date = "2024-09-01"
4
+
5
+# Environment variables configured via dashboard or `wrangler secret put`
6
+# Required Secrets:
7
+# - GITHUB_CLIENT_ID
8
+# - GITHUB_CLIENT_SECRET
9
+# - TURSO_DATABASE_URL
10
+# - TURSO_AUTH_TOKEN
0
11