ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
1#!/usr/bin/env tsx
2/**
3 * Test Login Helper
4 *
5 * This script helps you obtain a real Bluesky session for integration testing.
6 * It guides you through the OAuth flow and saves the session ID for use in tests.
7 *
8 * Usage:
9 * pnpm test:login
10 *
11 * After running, set the TEST_SESSION environment variable:
12 * PowerShell: $env:TEST_SESSION="<session-id>"; pnpm test
13 * CMD: set TEST_SESSION=<session-id> && pnpm test
14 * Bash: TEST_SESSION=<session-id> pnpm test
15 */
16
17import "dotenv/config";
18import * as readline from "readline";
19import { db } from "../src/db/client";
20import app from "../src/server";
21
22const rl = readline.createInterface({
23 input: process.stdin,
24 output: process.stdout,
25});
26
27function ask(question: string): Promise<string> {
28 return new Promise((resolve) => {
29 rl.question(question, (answer) => {
30 resolve(answer.trim());
31 });
32 });
33}
34
35async function main() {
36 console.log("\n🔐 ATlast Test Login Helper\n");
37 console.log("This will help you get a real Bluesky session for testing.");
38 console.log("You'll need to authorize ATlast with your Bluesky account.\n");
39 console.log("─".repeat(60));
40
41 // Step 1: Get user's handle
42 const handle = await ask(
43 "\n📝 Enter your Bluesky handle (e.g., yourname.bsky.social): "
44 );
45
46 if (!handle) {
47 console.error("❌ Handle is required");
48 process.exit(1);
49 }
50
51 console.log(`\n⏳ Starting OAuth flow for: ${handle}`);
52
53 try {
54 // Step 2: Call our OAuth start endpoint
55 const startRes = await app.request("/api/auth/oauth-start", {
56 method: "POST",
57 headers: { "Content-Type": "application/json" },
58 body: JSON.stringify({ login_hint: handle }),
59 });
60
61 if (!startRes.ok) {
62 const error = await startRes.json();
63 throw new Error(error.error || "Failed to start OAuth");
64 }
65
66 const { data } = await startRes.json();
67 const authUrl = data.url;
68
69 console.log("\n" + "─".repeat(60));
70 console.log("\n🌐 Step 1: Open this URL in your browser:\n");
71 console.log(` ${authUrl}`);
72 console.log(
73 "\n📋 (Copy the URL above - it's also copied to clipboard if supported)"
74 );
75
76 // Try to copy to clipboard (Windows)
77 try {
78 const { exec } = await import("child_process");
79 exec(`echo ${authUrl} | clip`, (err) => {
80 if (!err) console.log(" ✅ URL copied to clipboard!");
81 });
82 } catch {
83 // Clipboard copy failed, that's ok
84 }
85
86 console.log("\n" + "─".repeat(60));
87 console.log("\n🔑 Step 2: After authorizing, you'll be redirected to a URL.");
88 console.log(" Copy the FULL URL from your browser's address bar.");
89 console.log(
90 " It will look like: http://127.0.0.1:3000/api/auth/oauth-callback?code=...&state=...\n"
91 );
92
93 const callbackUrl = await ask("📋 Paste the full callback URL here: ");
94
95 if (!callbackUrl) {
96 console.error("❌ Callback URL is required");
97 process.exit(1);
98 }
99
100 // Step 3: Parse the callback URL and extract session
101 const url = new URL(callbackUrl);
102 let sessionId: string | null = null;
103
104 // Check if this is the final redirect (has session param)
105 if (url.searchParams.has("session")) {
106 sessionId = url.searchParams.get("session");
107 }
108 // Check if this is the OAuth callback (has code and state)
109 else if (url.searchParams.has("code") && url.searchParams.has("state")) {
110 console.log("\n⏳ Processing OAuth callback...");
111
112 // Call our callback endpoint to exchange the code
113 const callbackPath = url.pathname + url.search;
114 const callbackRes = await app.request(callbackPath, {
115 method: "GET",
116 headers: {
117 // Simulate the host header for proper OAuth config
118 Host: url.host,
119 },
120 });
121
122 // The callback returns a redirect with the session in the URL
123 if (callbackRes.status === 302 || callbackRes.status === 301) {
124 const redirectUrl = callbackRes.headers.get("Location");
125 if (redirectUrl) {
126 const redirectParsed = new URL(redirectUrl, url.origin);
127 sessionId = redirectParsed.searchParams.get("session");
128
129 if (!sessionId && redirectParsed.searchParams.has("error")) {
130 throw new Error(
131 `OAuth failed: ${redirectParsed.searchParams.get("error")}`
132 );
133 }
134 }
135 } else {
136 // Try to get error from response body
137 const body = await callbackRes.text();
138 throw new Error(`Callback failed (${callbackRes.status}): ${body}`);
139 }
140 }
141 // Maybe they pasted just the session ID
142 else if (callbackUrl.match(/^[0-9a-f-]{36}$/i)) {
143 sessionId = callbackUrl;
144 }
145
146 if (!sessionId) {
147 console.error("\n❌ Could not extract session from URL");
148 console.error(" Expected either:");
149 console.error(
150 " - Callback URL: http://127.0.0.1:3000/api/auth/oauth-callback?code=...&state=..."
151 );
152 console.error(" - Final URL: http://127.0.0.1:3000/?session=<uuid>");
153 console.error(" - Just the session ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
154 process.exit(1);
155 }
156
157 // Step 4: Verify the session works
158 console.log("\n⏳ Verifying session...");
159
160 const verifyRes = await app.request(`/api/auth/session?session=${sessionId}`);
161 const verifyData = await verifyRes.json();
162
163 if (!verifyData.success) {
164 console.error("❌ Session verification failed:", verifyData.error);
165 process.exit(1);
166 }
167
168 console.log("\n" + "─".repeat(60));
169 console.log("\n✅ Session verified successfully!\n");
170 console.log(` DID: ${verifyData.data.did}`);
171 console.log(` Session ID: ${sessionId}`);
172
173 // Step 5: Show how to use it
174 console.log("\n" + "─".repeat(60));
175 console.log("\n📋 To use this session in tests:\n");
176
177 console.log(" PowerShell:");
178 console.log(` $env:TEST_SESSION="${sessionId}"; pnpm test\n`);
179
180 console.log(" CMD:");
181 console.log(` set TEST_SESSION=${sessionId} && pnpm test\n`);
182
183 console.log(" Bash/Zsh:");
184 console.log(` TEST_SESSION=${sessionId} pnpm test\n`);
185
186 console.log(" Or add to your .env file:");
187 console.log(` TEST_SESSION=${sessionId}\n`);
188
189 console.log("─".repeat(60));
190 console.log(
191 "\n💡 Tip: The session lasts 7 days. Run this script again to get a new one.\n"
192 );
193
194 // Offer to save to .env.test
195 const saveToFile = await ask("Save session to .env.test file? (y/N): ");
196
197 if (saveToFile.toLowerCase() === "y") {
198 const fs = await import("fs");
199 const path = await import("path");
200 const envTestPath = path.join(process.cwd(), ".env.test");
201
202 let content = "";
203 try {
204 content = fs.readFileSync(envTestPath, "utf-8");
205 } catch {
206 // File doesn't exist yet
207 }
208
209 // Update or add TEST_SESSION
210 if (content.includes("TEST_SESSION=")) {
211 content = content.replace(/TEST_SESSION=.*/g, `TEST_SESSION=${sessionId}`);
212 } else {
213 content += `\nTEST_SESSION=${sessionId}\n`;
214 }
215
216 fs.writeFileSync(envTestPath, content.trim() + "\n");
217 console.log(`\n✅ Saved to ${envTestPath}`);
218 console.log(
219 ' Load it with: source .env.test (Bash) or Get-Content .env.test | ForEach-Object { $_ -replace "^", "`$env:" } | Invoke-Expression (PowerShell)\n'
220 );
221 }
222 } catch (error) {
223 console.error("\n❌ Error:", error instanceof Error ? error.message : error);
224 process.exit(1);
225 } finally {
226 rl.close();
227 // Close database connection
228 await db.destroy();
229 }
230}
231
232main().catch(console.error);