packages/cli/src/lib/oauth-store.ts 3.0 K raw
1
import * as fs from "node:fs/promises";
2
import * as os from "node:os";
3
import * as path from "node:path";
4
import type {
5
	NodeSavedSession,
6
	NodeSavedSessionStore,
7
	NodeSavedState,
8
	NodeSavedStateStore,
9
} from "@atproto/oauth-client-node";
10
11
const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia");
12
const OAUTH_FILE = path.join(CONFIG_DIR, "oauth.json");
13
14
interface OAuthStore {
15
	states: Record<string, NodeSavedState>;
16
	sessions: Record<string, NodeSavedSession>;
17
}
18
19
async function fileExists(filePath: string): Promise<boolean> {
20
	try {
21
		await fs.access(filePath);
22
		return true;
23
	} catch {
24
		return false;
25
	}
26
}
27
28
async 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
41
async 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
 */
50
export 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
 */
72
export 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
 */
94
export 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
 */
102
export 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
 */
112
export 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
122
export function getOAuthStorePath(): string {
123
	return OAUTH_FILE;
124
}