Something went wrong. Try again.
Migrating content using the Ghost API to Offprint / Standard Site on atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121import { createServer } from 'node:http';import { readFile, writeFile } from 'node:fs/promises';import type { AddressInfo } from 'node:net';import { createInterface } from 'node:readline/promises';import type { NodeSavedSession, NodeSavedSessionStore, NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node';
async function readJsonFile<T>(path: string): Promise<T> { try { const raw = await readFile(path, 'utf-8'); return JSON.parse(raw) as T; } catch { return {} as T; }}
async function writeJsonFile(path: string, data: unknown, mode?: number): Promise<void> { await writeFile(path, JSON.stringify(data, null, 2), { mode });}
export function createFileStateStore(filePath: string): NodeSavedStateStore { return { async set(key: string, value: NodeSavedState) { const data = await readJsonFile<Record<string, NodeSavedState>>(filePath); data[key] = value; await writeJsonFile(filePath, data, 0o600); }, async get(key: string): Promise<NodeSavedState | undefined> { const data = await readJsonFile<Record<string, NodeSavedState>>(filePath); return data[key]; }, async del(key: string) { const data = await readJsonFile<Record<string, NodeSavedState>>(filePath); delete data[key]; await writeJsonFile(filePath, data, 0o600); }, };}
export function createFileSessionStore(filePath: string): NodeSavedSessionStore { return { async set(sub: string, value: NodeSavedSession) { const data = await readJsonFile<Record<string, NodeSavedSession>>(filePath); data[sub] = value; await writeJsonFile(filePath, data, 0o600); }, async get(sub: string): Promise<NodeSavedSession | undefined> { const data = await readJsonFile<Record<string, NodeSavedSession>>(filePath); return data[sub]; }, async del(sub: string) { const data = await readJsonFile<Record<string, NodeSavedSession>>(filePath); delete data[sub]; await writeJsonFile(filePath, data, 0o600); }, };}
export function startLoopbackCallbackServer(port = 0): Promise<{ port: number; getParams: () => Promise<URLSearchParams> }> { return new Promise((resolve, reject) => { let gotParams: ((params: URLSearchParams) => void) | undefined; let rejectParams: ((err: Error) => void) | undefined;
const paramsPromise = new Promise<URLSearchParams>((res, rej) => { gotParams = res; rejectParams = rej; });
const server = createServer((req, res) => { const reqUrl = new URL(req.url || '/', 'http://127.0.0.1'); if (reqUrl.pathname !== '/callback') { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('not found'); return; }
const params = reqUrl.searchParams; res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('<html><body><p>Authentication successful. You can close this tab and return to the terminal.</p></body></html>');
server.close((closeErr) => { if (closeErr) { rejectParams?.(closeErr); } else { gotParams?.(params); } }); });
server.on('error', reject);
server.listen({ host: '127.0.0.1', port }, () => { const address = server.address() as AddressInfo; resolve({ port: address.port, getParams: () => paramsPromise, }); }); });}
export async function promptForHandle(): Promise<string> { const rl = createInterface({ input: process.stdin, output: process.stdout }); try { const answer = await rl.question('Enter your atproto handle (e.g. handle.example.com): '); return answer.trim(); } finally { rl.close(); }}
export async function confirmOAuth(): Promise<boolean> { const rl = createInterface({ input: process.stdin, output: process.stdout }); try { const answer = await rl.question('No app password found in .env or --atproto-app-password. Would you like to authenticate via OAuth [Y/n]? '); const normalized = answer.trim().toLowerCase(); return normalized === '' || normalized === 'y' || normalized === 'yes'; } finally { rl.close(); }}