A CLI for publishing standard.site documents to ATProto
sequoia.pub
standard
site
lexicon
cli
publishing
1import {
2 NodeOAuthClient,
3 type NodeOAuthClientOptions,
4} from "@atproto/oauth-client-node";
5import { sessionStore, stateStore } from "./oauth-store";
6
7const CALLBACK_PORT = 4000;
8const CALLBACK_HOST = "127.0.0.1";
9const CALLBACK_URL = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/oauth/callback`;
10
11// OAuth scope for Sequoia CLI - includes atproto base scope plus our collections
12const OAUTH_SCOPE =
13 "atproto repo:site.standard.document repo:site.standard.publication repo:app.bsky.feed.post blob:*/*";
14
15let oauthClient: NodeOAuthClient | null = null;
16
17// Simple lock implementation for CLI (single process, no contention)
18// This prevents the "No lock mechanism provided" warning
19const locks = new Map<string, Promise<void>>();
20
21async 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 */
48export 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://github.com/stevedylandev/sequoia",
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
84export function getOAuthScope(): string {
85 return OAUTH_SCOPE;
86}
87
88export function getCallbackUrl(): string {
89 return CALLBACK_URL;
90}
91
92export function getCallbackPort(): number {
93 return CALLBACK_PORT;
94}