packages/cli/src/lib/oauth-store.ts 3.8 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
	handles?: Record<string, string>; // DID -> handle mapping (optional for backwards compat)
18
}
19
20
async function fileExists(filePath: string): Promise<boolean> {
21
	try {
22
		await fs.access(filePath);
23
		return true;
24
	} catch {
25
		return false;
26
	}
27
}
28
29
async 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
42
async 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
 */
51
export 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
 */
73
export 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
 */
95
export 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
 */
103
export 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
 */
113
export 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
123
export function getOAuthStorePath(): string {
124
	return OAUTH_FILE;
125
}
126
127
/**
128
 * Store handle for an OAuth session (DID -> handle mapping)
129
 */
130
export 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
 */
145
export 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
 */
153
export 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
}