packages/cli/src/lib/oauth-client.ts 2.4 K raw
1
import {
2
	NodeOAuthClient,
3
	type NodeOAuthClientOptions,
4
} from "@atproto/oauth-client-node";
5
import { sessionStore, stateStore } from "./oauth-store";
6
7
const CALLBACK_PORT = 4000;
8
const CALLBACK_HOST = "127.0.0.1";
9
const CALLBACK_URL = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/oauth/callback`;
10
11
// OAuth scope for Sequoia CLI - includes atproto base scope plus our collections
12
const OAUTH_SCOPE =
13
	"atproto repo:site.standard.document repo:site.standard.publication repo:app.bsky.feed.post blob:*/*";
14
15
let oauthClient: NodeOAuthClient | null = null;
16
17
// Simple lock implementation for CLI (single process, no contention)
18
// This prevents the "No lock mechanism provided" warning
19
const locks = new Map<string, Promise<void>>();
20
21
async function requestLock<T>(
22
	key: string,
23
	fn: () => T | PromiseLike<T>,
24
): Promise<T> {
25
	// Wait for any existing lock on this key
26
	while (locks.has(key)) {
27
		await locks.get(key);
28
	}
29
30
	// Create our lock
31
	let resolve: () => void;
32
	const lockPromise = new Promise<void>((r) => {
33
		resolve = r;
34
	});
35
	locks.set(key, lockPromise);
36
37
	try {
38
		return await fn();
39
	} finally {
40
		locks.delete(key);
41
		resolve!();
42
	}
43
}
44
45
/**
46
 * Get or create the OAuth client singleton
47
 */
48
export async function getOAuthClient(): Promise<NodeOAuthClient> {
49
	if (oauthClient) {
50
		return oauthClient;
51
	}
52
53
	// Build client_id with required parameters
54
	const clientIdParams = new URLSearchParams();
55
	clientIdParams.append("redirect_uri", CALLBACK_URL);
56
	clientIdParams.append("scope", OAUTH_SCOPE);
57
58
	const clientOptions: NodeOAuthClientOptions = {
59
		clientMetadata: {
60
			client_id: `http://localhost?${clientIdParams.toString()}`,
61
			client_name: "Sequoia CLI",
62
			client_uri: "https://sequoia.pub",
63
			redirect_uris: [CALLBACK_URL],
64
			grant_types: ["authorization_code", "refresh_token"],
65
			response_types: ["code"],
66
			token_endpoint_auth_method: "none",
67
			application_type: "web",
68
			scope: OAUTH_SCOPE,
69
			dpop_bound_access_tokens: false,
70
		},
71
		stateStore,
72
		sessionStore,
73
		// Configure identity resolution
74
		plcDirectoryUrl: "https://plc.directory",
75
		// Provide lock mechanism to prevent warning
76
		requestLock,
77
	};
78
79
	oauthClient = new NodeOAuthClient(clientOptions);
80
81
	return oauthClient;
82
}
83
84
export function getOAuthScope(): string {
85
	return OAUTH_SCOPE;
86
}
87
88
export function getCallbackUrl(): string {
89
	return CALLBACK_URL;
90
}
91
92
export function getCallbackPort(): number {
93
	return CALLBACK_PORT;
94
}