A CLI for publishing standard.site documents to ATProto
sequoia.pub
standard
site
lexicon
cli
publishing
1import * as fs from "node:fs/promises";
2import * as os from "node:os";
3import * as path from "node:path";
4import type {
5 NodeSavedSession,
6 NodeSavedSessionStore,
7 NodeSavedState,
8 NodeSavedStateStore,
9} from "@atproto/oauth-client-node";
10
11const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia");
12const OAUTH_FILE = path.join(CONFIG_DIR, "oauth.json");
13
14interface OAuthStore {
15 states: Record<string, NodeSavedState>;
16 sessions: Record<string, NodeSavedSession>;
17 handles?: Record<string, string>; // DID -> handle mapping (optional for backwards compat)
18}
19
20async function fileExists(filePath: string): Promise<boolean> {
21 try {
22 await fs.access(filePath);
23 return true;
24 } catch {
25 return false;
26 }
27}
28
29async function loadOAuthStore(): Promise<OAuthStore> {
30 if (!(await fileExists(OAUTH_FILE))) {
31 return { states: {}, sessions: {} };
32 }
33
34 try {
35 const content = await fs.readFile(OAUTH_FILE, "utf-8");
36 return JSON.parse(content) as OAuthStore;
37 } catch {
38 return { states: {}, sessions: {} };
39 }
40}
41
42async function saveOAuthStore(store: OAuthStore): Promise<void> {
43 await fs.mkdir(CONFIG_DIR, { recursive: true });
44 await fs.writeFile(OAUTH_FILE, JSON.stringify(store, null, 2));
45 await fs.chmod(OAUTH_FILE, 0o600);
46}
47
48/**
49 * State store for PKCE flow (temporary, used during auth)
50 */
51export const stateStore: NodeSavedStateStore = {
52 async set(key: string, state: NodeSavedState): Promise<void> {
53 const store = await loadOAuthStore();
54 store.states[key] = state;
55 await saveOAuthStore(store);
56 },
57
58 async get(key: string): Promise<NodeSavedState | undefined> {
59 const store = await loadOAuthStore();
60 return store.states[key];
61 },
62
63 async del(key: string): Promise<void> {
64 const store = await loadOAuthStore();
65 delete store.states[key];
66 await saveOAuthStore(store);
67 },
68};
69
70/**
71 * Session store for OAuth tokens (persistent)
72 */
73export const sessionStore: NodeSavedSessionStore = {
74 async set(sub: string, session: NodeSavedSession): Promise<void> {
75 const store = await loadOAuthStore();
76 store.sessions[sub] = session;
77 await saveOAuthStore(store);
78 },
79
80 async get(sub: string): Promise<NodeSavedSession | undefined> {
81 const store = await loadOAuthStore();
82 return store.sessions[sub];
83 },
84
85 async del(sub: string): Promise<void> {
86 const store = await loadOAuthStore();
87 delete store.sessions[sub];
88 await saveOAuthStore(store);
89 },
90};
91
92/**
93 * List all stored OAuth session DIDs
94 */
95export async function listOAuthSessions(): Promise<string[]> {
96 const store = await loadOAuthStore();
97 return Object.keys(store.sessions);
98}
99
100/**
101 * Get an OAuth session by DID
102 */
103export async function getOAuthSession(
104 did: string,
105): Promise<NodeSavedSession | undefined> {
106 const store = await loadOAuthStore();
107 return store.sessions[did];
108}
109
110/**
111 * Delete an OAuth session by DID
112 */
113export async function deleteOAuthSession(did: string): Promise<boolean> {
114 const store = await loadOAuthStore();
115 if (!store.sessions[did]) {
116 return false;
117 }
118 delete store.sessions[did];
119 await saveOAuthStore(store);
120 return true;
121}
122
123export function getOAuthStorePath(): string {
124 return OAUTH_FILE;
125}
126
127/**
128 * Store handle for an OAuth session (DID -> handle mapping)
129 */
130export async function setOAuthHandle(
131 did: string,
132 handle: string,
133): Promise<void> {
134 const store = await loadOAuthStore();
135 if (!store.handles) {
136 store.handles = {};
137 }
138 store.handles[did] = handle;
139 await saveOAuthStore(store);
140}
141
142/**
143 * Get handle for an OAuth session by DID
144 */
145export async function getOAuthHandle(did: string): Promise<string | undefined> {
146 const store = await loadOAuthStore();
147 return store.handles?.[did];
148}
149
150/**
151 * List all stored OAuth sessions with their handles
152 */
153export async function listOAuthSessionsWithHandles(): Promise<
154 Array<{ did: string; handle?: string }>
155> {
156 const store = await loadOAuthStore();
157 return Object.keys(store.sessions).map((did) => ({
158 did,
159 handle: store.handles?.[did],
160 }));
161}