forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
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}
18
19async function fileExists(filePath: string): Promise<boolean> {
20 try {
21 await fs.access(filePath);
22 return true;
23 } catch {
24 return false;
25 }
26}
27
28async function loadOAuthStore(): Promise<OAuthStore> {
29 if (!(await fileExists(OAUTH_FILE))) {
30 return { states: {}, sessions: {} };
31 }
32
33 try {
34 const content = await fs.readFile(OAUTH_FILE, "utf-8");
35 return JSON.parse(content) as OAuthStore;
36 } catch {
37 return { states: {}, sessions: {} };
38 }
39}
40
41async function saveOAuthStore(store: OAuthStore): Promise<void> {
42 await fs.mkdir(CONFIG_DIR, { recursive: true });
43 await fs.writeFile(OAUTH_FILE, JSON.stringify(store, null, 2));
44 await fs.chmod(OAUTH_FILE, 0o600);
45}
46
47/**
48 * State store for PKCE flow (temporary, used during auth)
49 */
50export const stateStore: NodeSavedStateStore = {
51 async set(key: string, state: NodeSavedState): Promise<void> {
52 const store = await loadOAuthStore();
53 store.states[key] = state;
54 await saveOAuthStore(store);
55 },
56
57 async get(key: string): Promise<NodeSavedState | undefined> {
58 const store = await loadOAuthStore();
59 return store.states[key];
60 },
61
62 async del(key: string): Promise<void> {
63 const store = await loadOAuthStore();
64 delete store.states[key];
65 await saveOAuthStore(store);
66 },
67};
68
69/**
70 * Session store for OAuth tokens (persistent)
71 */
72export const sessionStore: NodeSavedSessionStore = {
73 async set(sub: string, session: NodeSavedSession): Promise<void> {
74 const store = await loadOAuthStore();
75 store.sessions[sub] = session;
76 await saveOAuthStore(store);
77 },
78
79 async get(sub: string): Promise<NodeSavedSession | undefined> {
80 const store = await loadOAuthStore();
81 return store.sessions[sub];
82 },
83
84 async del(sub: string): Promise<void> {
85 const store = await loadOAuthStore();
86 delete store.sessions[sub];
87 await saveOAuthStore(store);
88 },
89};
90
91/**
92 * List all stored OAuth session DIDs
93 */
94export async function listOAuthSessions(): Promise<string[]> {
95 const store = await loadOAuthStore();
96 return Object.keys(store.sessions);
97}
98
99/**
100 * Get an OAuth session by DID
101 */
102export async function getOAuthSession(
103 did: string,
104): Promise<NodeSavedSession | undefined> {
105 const store = await loadOAuthStore();
106 return store.sessions[did];
107}
108
109/**
110 * Delete an OAuth session by DID
111 */
112export async function deleteOAuthSession(did: string): Promise<boolean> {
113 const store = await loadOAuthStore();
114 if (!store.sessions[did]) {
115 return false;
116 }
117 delete store.sessions[did];
118 await saveOAuthStore(store);
119 return true;
120}
121
122export function getOAuthStorePath(): string {
123 return OAUTH_FILE;
124}