@open-vibe-lab/open-sub-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ import { a as NoCredentialError, l as TokenExpiredError, t as getProvider, u as TokenRefreshError } from "./registry-Cp-_Ipc6.mjs";
3
+ import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from "node:crypto";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { hostname, userInfo } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ //#region src/storage/file-store.ts
8
+ const ALGORITHM = "aes-256-gcm";
9
+ const KEY_LENGTH = 32;
10
+ const IV_LENGTH = 12;
11
+ const SALT_LENGTH = 16;
12
+ const PBKDF2_ITERATIONS = 1e5;
13
+ const AUTH_TAG_LENGTH = 16;
14
+ /** Token store backed by an encrypted JSON file */
15
+ var FileStore = class {
16
+ filePath;
17
+ constructor(filePath) {
18
+ this.filePath = filePath ?? join(process.env.OPEN_SUB_AUTH_HOME ?? join(homedir$1(), ".open-sub-auth"), "credentials.json");
19
+ }
20
+ async get(provider, accountId) {
21
+ const data = this.readFile();
22
+ const key = `${provider}::${accountId}`;
23
+ const entry = data.credentials[key];
24
+ if (!entry) return null;
25
+ try {
26
+ const decrypted = this.decrypt(entry);
27
+ return JSON.parse(decrypted);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ async set(provider, accountId, credential) {
33
+ const data = this.readFile();
34
+ const key = `${provider}::${accountId}`;
35
+ data.credentials[key] = this.encrypt(JSON.stringify(credential));
36
+ this.writeFile(data);
37
+ }
38
+ async delete(provider, accountId) {
39
+ const data = this.readFile();
40
+ const key = `${provider}::${accountId}`;
41
+ delete data.credentials[key];
42
+ this.writeFile(data);
43
+ }
44
+ async list(provider) {
45
+ const data = this.readFile();
46
+ const results = [];
47
+ for (const [key, entry] of Object.entries(data.credentials)) {
48
+ const [keyProvider] = key.split("::");
49
+ if (provider && keyProvider !== provider) continue;
50
+ try {
51
+ const decrypted = this.decrypt(entry);
52
+ results.push(JSON.parse(decrypted));
53
+ } catch {}
54
+ }
55
+ return results;
56
+ }
57
+ deriveKey(salt) {
58
+ return pbkdf2Sync(`${hostname()}:${userInfo().username}`, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
59
+ }
60
+ encrypt(plaintext) {
61
+ const salt = randomBytes(SALT_LENGTH);
62
+ const key = this.deriveKey(salt);
63
+ const iv = randomBytes(IV_LENGTH);
64
+ const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
65
+ return {
66
+ data: Buffer.concat([
67
+ cipher.update(plaintext, "utf8"),
68
+ cipher.final(),
69
+ cipher.getAuthTag()
70
+ ]).toString("base64"),
71
+ iv: iv.toString("base64"),
72
+ salt: salt.toString("base64")
73
+ };
74
+ }
75
+ decrypt(entry) {
76
+ const salt = Buffer.from(entry.salt, "base64");
77
+ const key = this.deriveKey(salt);
78
+ const iv = Buffer.from(entry.iv, "base64");
79
+ const raw = Buffer.from(entry.data, "base64");
80
+ const authTag = raw.subarray(raw.length - AUTH_TAG_LENGTH);
81
+ const ciphertext = raw.subarray(0, raw.length - AUTH_TAG_LENGTH);
82
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
83
+ decipher.setAuthTag(authTag);
84
+ return decipher.update(ciphertext) + decipher.final("utf8");
85
+ }
86
+ readFile() {
87
+ try {
88
+ const content = readFileSync(this.filePath, "utf8");
89
+ return JSON.parse(content);
90
+ } catch {
91
+ return {
92
+ version: 1,
93
+ credentials: {}
94
+ };
95
+ }
96
+ }
97
+ writeFile(data) {
98
+ mkdirSync(dirname(this.filePath), {
99
+ recursive: true,
100
+ mode: 448
101
+ });
102
+ writeFileSync(this.filePath, JSON.stringify(data, null, 2), { mode: 384 });
103
+ }
104
+ };
105
+ function homedir$1() {
106
+ return process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
107
+ }
108
+ //#endregion
109
+ //#region src/storage/keychain-store.ts
110
+ const SERVICE_NAME = "open-sub-auth";
111
+ const INDEX_ACCOUNT = "__index__";
112
+ /** Token store backed by the OS keychain via cross-keychain */
113
+ var KeychainStore = class {
114
+ keychain = null;
115
+ async getKeychain() {
116
+ if (!this.keychain) this.keychain = await import("cross-keychain");
117
+ return this.keychain;
118
+ }
119
+ makeKey(provider, accountId) {
120
+ return `${provider}__${accountId}`;
121
+ }
122
+ async get(provider, accountId) {
123
+ const kc = await this.getKeychain();
124
+ try {
125
+ const value = await kc.getPassword(SERVICE_NAME, this.makeKey(provider, accountId));
126
+ if (!value) return null;
127
+ return JSON.parse(value);
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ async set(provider, accountId, credential) {
133
+ const kc = await this.getKeychain();
134
+ const key = this.makeKey(provider, accountId);
135
+ const value = JSON.stringify(credential);
136
+ try {
137
+ await kc.deletePassword(SERVICE_NAME, key);
138
+ } catch {}
139
+ await kc.setPassword(SERVICE_NAME, key, value);
140
+ await this.addToIndex(key);
141
+ }
142
+ async delete(provider, accountId) {
143
+ const kc = await this.getKeychain();
144
+ const key = this.makeKey(provider, accountId);
145
+ try {
146
+ await kc.deletePassword(SERVICE_NAME, key);
147
+ } catch {}
148
+ await this.removeFromIndex(key);
149
+ }
150
+ async list(provider) {
151
+ const index = await this.getIndex();
152
+ const results = [];
153
+ for (const key of index) {
154
+ const [keyProvider, keyAccountId] = key.split("__");
155
+ if (provider && keyProvider !== provider) continue;
156
+ if (!keyProvider || !keyAccountId) continue;
157
+ const credential = await this.get(keyProvider, keyAccountId);
158
+ if (credential) results.push(credential);
159
+ }
160
+ return results;
161
+ }
162
+ async getIndex() {
163
+ const kc = await this.getKeychain();
164
+ try {
165
+ const value = await kc.getPassword(SERVICE_NAME, INDEX_ACCOUNT);
166
+ if (!value) return [];
167
+ return JSON.parse(value);
168
+ } catch {
169
+ return [];
170
+ }
171
+ }
172
+ async setIndex(index) {
173
+ const kc = await this.getKeychain();
174
+ try {
175
+ await kc.deletePassword(SERVICE_NAME, INDEX_ACCOUNT);
176
+ } catch {}
177
+ await kc.setPassword(SERVICE_NAME, INDEX_ACCOUNT, JSON.stringify(index));
178
+ }
179
+ async addToIndex(key) {
180
+ const index = await this.getIndex();
181
+ if (!index.includes(key)) {
182
+ index.push(key);
183
+ await this.setIndex(index);
184
+ }
185
+ }
186
+ async removeFromIndex(key) {
187
+ const filtered = (await this.getIndex()).filter((k) => k !== key);
188
+ await this.setIndex(filtered);
189
+ }
190
+ };
191
+ //#endregion
192
+ //#region src/storage/store.ts
193
+ /**
194
+ * Create a token store, preferring the OS keychain with encrypted file fallback.
195
+ * @param preferKeychain - If true (default), try keychain first
196
+ */
197
+ async function createTokenStore(preferKeychain = true) {
198
+ if (preferKeychain) try {
199
+ const store = new KeychainStore();
200
+ await store.get("__probe__", "__probe__");
201
+ return store;
202
+ } catch {}
203
+ return new FileStore();
204
+ }
205
+ //#endregion
206
+ //#region src/token/manager.ts
207
+ /** Buffer time before expiry to trigger refresh (5 minutes) */
208
+ const REFRESH_BUFFER_MS = 300 * 1e3;
209
+ /** Central token lifecycle manager */
210
+ var TokenManager = class {
211
+ store;
212
+ /** Per-provider+account mutex to prevent concurrent refreshes */
213
+ refreshLocks = /* @__PURE__ */ new Map();
214
+ constructor(store) {
215
+ this.store = store;
216
+ }
217
+ /** Run the interactive login flow for a provider, store the tokens */
218
+ async login(providerName, options) {
219
+ const provider = getProvider(providerName);
220
+ const tokenSet = await provider.login(options);
221
+ const accountId = provider.getAccountId(tokenSet);
222
+ const accountLabel = provider.getAccountLabel?.(tokenSet);
223
+ const now = Date.now();
224
+ const credential = {
225
+ tokenSet,
226
+ metadata: {
227
+ provider: providerName,
228
+ accountId,
229
+ accountLabel,
230
+ createdAt: now,
231
+ lastRefreshedAt: now
232
+ }
233
+ };
234
+ await this.store.set(providerName, accountId, credential);
235
+ return credential;
236
+ }
237
+ /** Get a valid access token, refreshing if needed */
238
+ async getToken(providerName, accountId) {
239
+ const credential = await this.resolveCredential(providerName, accountId);
240
+ const { tokenSet } = credential;
241
+ if (this.isTokenValid(tokenSet.expiresAt)) return tokenSet.accessToken;
242
+ if (!tokenSet.refreshToken) throw new TokenExpiredError(providerName);
243
+ await this.refreshWithLock(providerName, credential);
244
+ const refreshed = await this.store.get(providerName, credential.metadata.accountId);
245
+ if (!refreshed) throw new TokenExpiredError(providerName);
246
+ return refreshed.tokenSet.accessToken;
247
+ }
248
+ /** Get authenticated headers for API calls */
249
+ async getAuthHeaders(providerName, accountId) {
250
+ const token = await this.getToken(providerName, accountId);
251
+ return getProvider(providerName).getAuthHeaders(token);
252
+ }
253
+ /** Remove stored tokens for a provider */
254
+ async logout(providerName, accountId) {
255
+ if (accountId) await this.store.delete(providerName, accountId);
256
+ else {
257
+ const credentials = await this.store.list(providerName);
258
+ for (const cred of credentials) await this.store.delete(providerName, cred.metadata.accountId);
259
+ }
260
+ }
261
+ /** Get status of all stored credentials */
262
+ async status() {
263
+ return (await this.store.list()).map((cred) => {
264
+ let displayName = cred.metadata.provider;
265
+ try {
266
+ displayName = getProvider(cred.metadata.provider).config.displayName;
267
+ } catch {}
268
+ return {
269
+ provider: cred.metadata.provider,
270
+ displayName,
271
+ accountId: cred.metadata.accountId,
272
+ accountLabel: cred.metadata.accountLabel,
273
+ isExpired: !this.isTokenValid(cred.tokenSet.expiresAt),
274
+ expiresAt: cred.tokenSet.expiresAt,
275
+ hasRefreshToken: cred.tokenSet.refreshToken !== null
276
+ };
277
+ });
278
+ }
279
+ isTokenValid(expiresAt) {
280
+ return Date.now() + REFRESH_BUFFER_MS < expiresAt;
281
+ }
282
+ async resolveCredential(providerName, accountId) {
283
+ if (accountId) {
284
+ const credential = await this.store.get(providerName, accountId);
285
+ if (!credential) throw new NoCredentialError(providerName, accountId);
286
+ return credential;
287
+ }
288
+ const credentials = await this.store.list(providerName);
289
+ if (credentials.length === 0) throw new NoCredentialError(providerName);
290
+ return credentials.sort((a, b) => b.metadata.lastRefreshedAt - a.metadata.lastRefreshedAt)[0];
291
+ }
292
+ async refreshWithLock(providerName, credential) {
293
+ const lockKey = `${providerName}::${credential.metadata.accountId}`;
294
+ const existingLock = this.refreshLocks.get(lockKey);
295
+ if (existingLock) {
296
+ await existingLock;
297
+ return;
298
+ }
299
+ const refreshPromise = this.doRefresh(providerName, credential);
300
+ this.refreshLocks.set(lockKey, refreshPromise);
301
+ try {
302
+ await refreshPromise;
303
+ } finally {
304
+ this.refreshLocks.delete(lockKey);
305
+ }
306
+ }
307
+ async doRefresh(providerName, credential) {
308
+ const provider = getProvider(providerName);
309
+ const { refreshToken } = credential.tokenSet;
310
+ if (!refreshToken) throw new TokenExpiredError(providerName);
311
+ let newTokenSet;
312
+ try {
313
+ newTokenSet = await provider.refresh(refreshToken);
314
+ } catch (err) {
315
+ throw new TokenRefreshError(providerName, err);
316
+ }
317
+ if (!newTokenSet.refreshToken && refreshToken) newTokenSet.refreshToken = refreshToken;
318
+ const updatedCredential = {
319
+ tokenSet: newTokenSet,
320
+ metadata: {
321
+ ...credential.metadata,
322
+ lastRefreshedAt: Date.now()
323
+ }
324
+ };
325
+ await this.store.set(providerName, credential.metadata.accountId, updatedCredential);
326
+ }
327
+ };
328
+ //#endregion
329
+ export { createTokenStore as n, TokenManager as t };
330
+
331
+ //# sourceMappingURL=manager-CKGbp7Yz.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager-CKGbp7Yz.mjs","names":["homedir"],"sources":["../../src/storage/file-store.ts","../../src/storage/keychain-store.ts","../../src/storage/store.ts","../../src/token/manager.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from \"node:crypto\";\nimport { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { hostname, userInfo } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { StoredCredential, TokenStore } from \"@/types.ts\";\n\nconst ALGORITHM = \"aes-256-gcm\";\nconst KEY_LENGTH = 32;\nconst IV_LENGTH = 12;\nconst SALT_LENGTH = 16;\nconst PBKDF2_ITERATIONS = 100_000;\nconst AUTH_TAG_LENGTH = 16;\n\ninterface EncryptedEntry {\n data: string; // base64 encoded ciphertext + auth tag\n iv: string; // base64\n salt: string; // base64\n}\n\ninterface FileStoreData {\n version: 1;\n credentials: Record<string, EncryptedEntry>;\n}\n\n/** Token store backed by an encrypted JSON file */\nexport class FileStore implements TokenStore {\n private readonly filePath: string;\n\n constructor(filePath?: string) {\n this.filePath =\n filePath ??\n join(process.env.OPEN_SUB_AUTH_HOME ?? join(homedir(), \".open-sub-auth\"), \"credentials.json\");\n }\n\n async get(provider: string, accountId: string): Promise<StoredCredential | null> {\n const data = this.readFile();\n const key = `${provider}::${accountId}`;\n const entry = data.credentials[key];\n if (!entry) return null;\n\n try {\n const decrypted = this.decrypt(entry);\n return JSON.parse(decrypted) as StoredCredential;\n } catch {\n return null;\n }\n }\n\n async set(provider: string, accountId: string, credential: StoredCredential): Promise<void> {\n const data = this.readFile();\n const key = `${provider}::${accountId}`;\n data.credentials[key] = this.encrypt(JSON.stringify(credential));\n this.writeFile(data);\n }\n\n async delete(provider: string, accountId: string): Promise<void> {\n const data = this.readFile();\n const key = `${provider}::${accountId}`;\n delete data.credentials[key];\n this.writeFile(data);\n }\n\n async list(provider?: string): Promise<StoredCredential[]> {\n const data = this.readFile();\n const results: StoredCredential[] = [];\n\n for (const [key, entry] of Object.entries(data.credentials)) {\n const [keyProvider] = key.split(\"::\");\n if (provider && keyProvider !== provider) continue;\n\n try {\n const decrypted = this.decrypt(entry);\n results.push(JSON.parse(decrypted) as StoredCredential);\n } catch {\n // Skip corrupted entries\n }\n }\n\n return results;\n }\n\n private deriveKey(salt: Buffer): Buffer {\n const machineId = `${hostname()}:${userInfo().username}`;\n return pbkdf2Sync(machineId, salt, PBKDF2_ITERATIONS, KEY_LENGTH, \"sha512\");\n }\n\n private encrypt(plaintext: string): EncryptedEntry {\n const salt = randomBytes(SALT_LENGTH);\n const key = this.deriveKey(salt);\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH,\n });\n const encrypted = Buffer.concat([\n cipher.update(plaintext, \"utf8\"),\n cipher.final(),\n cipher.getAuthTag(),\n ]);\n\n return {\n data: encrypted.toString(\"base64\"),\n iv: iv.toString(\"base64\"),\n salt: salt.toString(\"base64\"),\n };\n }\n\n private decrypt(entry: EncryptedEntry): string {\n const salt = Buffer.from(entry.salt, \"base64\");\n const key = this.deriveKey(salt);\n const iv = Buffer.from(entry.iv, \"base64\");\n const raw = Buffer.from(entry.data, \"base64\");\n\n const authTag = raw.subarray(raw.length - AUTH_TAG_LENGTH);\n const ciphertext = raw.subarray(0, raw.length - AUTH_TAG_LENGTH);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH,\n });\n decipher.setAuthTag(authTag);\n\n return decipher.update(ciphertext) + decipher.final(\"utf8\");\n }\n\n private readFile(): FileStoreData {\n try {\n const content = readFileSync(this.filePath, \"utf8\");\n return JSON.parse(content) as FileStoreData;\n } catch {\n return { version: 1, credentials: {} };\n }\n }\n\n private writeFile(data: FileStoreData): void {\n mkdirSync(dirname(this.filePath), { recursive: true, mode: 0o700 });\n writeFileSync(this.filePath, JSON.stringify(data, null, 2), { mode: 0o600 });\n }\n}\n\nfunction homedir(): string {\n return process.env.HOME ?? process.env.USERPROFILE ?? \"/tmp\";\n}\n","import type { StoredCredential, TokenStore } from \"@/types.ts\";\n\nconst SERVICE_NAME = \"open-sub-auth\";\nconst INDEX_ACCOUNT = \"__index__\";\n\n/** Token store backed by the OS keychain via cross-keychain */\nexport class KeychainStore implements TokenStore {\n private keychain: typeof import(\"cross-keychain\") | null = null;\n\n private async getKeychain() {\n if (!this.keychain) {\n this.keychain = await import(\"cross-keychain\");\n }\n return this.keychain;\n }\n\n private makeKey(provider: string, accountId: string): string {\n return `${provider}__${accountId}`;\n }\n\n async get(provider: string, accountId: string): Promise<StoredCredential | null> {\n const kc = await this.getKeychain();\n try {\n const value = await kc.getPassword(SERVICE_NAME, this.makeKey(provider, accountId));\n if (!value) return null;\n return JSON.parse(value) as StoredCredential;\n } catch {\n return null;\n }\n }\n\n async set(provider: string, accountId: string, credential: StoredCredential): Promise<void> {\n const kc = await this.getKeychain();\n const key = this.makeKey(provider, accountId);\n const value = JSON.stringify(credential);\n\n try {\n await kc.deletePassword(SERVICE_NAME, key);\n } catch {\n // Ignore if it doesn't exist\n }\n await kc.setPassword(SERVICE_NAME, key, value);\n await this.addToIndex(key);\n }\n\n async delete(provider: string, accountId: string): Promise<void> {\n const kc = await this.getKeychain();\n const key = this.makeKey(provider, accountId);\n try {\n await kc.deletePassword(SERVICE_NAME, key);\n } catch {\n // Ignore if it doesn't exist\n }\n await this.removeFromIndex(key);\n }\n\n async list(provider?: string): Promise<StoredCredential[]> {\n const index = await this.getIndex();\n const results: StoredCredential[] = [];\n\n for (const key of index) {\n const [keyProvider, keyAccountId] = key.split(\"__\");\n if (provider && keyProvider !== provider) continue;\n if (!keyProvider || !keyAccountId) continue;\n\n const credential = await this.get(keyProvider, keyAccountId);\n if (credential) {\n results.push(credential);\n }\n }\n\n return results;\n }\n\n private async getIndex(): Promise<string[]> {\n const kc = await this.getKeychain();\n try {\n const value = await kc.getPassword(SERVICE_NAME, INDEX_ACCOUNT);\n if (!value) return [];\n return JSON.parse(value) as string[];\n } catch {\n return [];\n }\n }\n\n private async setIndex(index: string[]): Promise<void> {\n const kc = await this.getKeychain();\n try {\n await kc.deletePassword(SERVICE_NAME, INDEX_ACCOUNT);\n } catch {\n // Ignore\n }\n await kc.setPassword(SERVICE_NAME, INDEX_ACCOUNT, JSON.stringify(index));\n }\n\n private async addToIndex(key: string): Promise<void> {\n const index = await this.getIndex();\n if (!index.includes(key)) {\n index.push(key);\n await this.setIndex(index);\n }\n }\n\n private async removeFromIndex(key: string): Promise<void> {\n const index = await this.getIndex();\n const filtered = index.filter((k) => k !== key);\n await this.setIndex(filtered);\n }\n}\n","import type { TokenStore } from \"@/types.ts\";\nimport { FileStore } from \"@/storage/file-store.ts\";\nimport { KeychainStore } from \"@/storage/keychain-store.ts\";\n\n/**\n * Create a token store, preferring the OS keychain with encrypted file fallback.\n * @param preferKeychain - If true (default), try keychain first\n */\nexport async function createTokenStore(preferKeychain = true): Promise<TokenStore> {\n if (preferKeychain) {\n try {\n const store = new KeychainStore();\n // Probe if keychain is accessible\n await store.get(\"__probe__\", \"__probe__\");\n return store;\n } catch {\n // Keychain not available, fall through to file store\n }\n }\n return new FileStore();\n}\n\nexport { FileStore, KeychainStore };\n","import { NoCredentialError, TokenExpiredError, TokenRefreshError } from \"@/errors.ts\";\nimport { getProvider } from \"@/providers/registry.ts\";\nimport type {\n AuthHeaders,\n CredentialStatus,\n LoginOptions,\n StoredCredential,\n TokenStore,\n} from \"@/types.ts\";\n\n/** Buffer time before expiry to trigger refresh (5 minutes) */\nconst REFRESH_BUFFER_MS = 5 * 60 * 1000;\n\n/** Central token lifecycle manager */\nexport class TokenManager {\n private readonly store: TokenStore;\n /** Per-provider+account mutex to prevent concurrent refreshes */\n private readonly refreshLocks = new Map<string, Promise<void>>();\n\n constructor(store: TokenStore) {\n this.store = store;\n }\n\n /** Run the interactive login flow for a provider, store the tokens */\n async login(providerName: string, options?: LoginOptions): Promise<StoredCredential> {\n const provider = getProvider(providerName);\n const tokenSet = await provider.login(options);\n\n const accountId = provider.getAccountId(tokenSet);\n const accountLabel = provider.getAccountLabel?.(tokenSet);\n const now = Date.now();\n\n const credential: StoredCredential = {\n tokenSet,\n metadata: {\n provider: providerName,\n accountId,\n accountLabel,\n createdAt: now,\n lastRefreshedAt: now,\n },\n };\n\n await this.store.set(providerName, accountId, credential);\n return credential;\n }\n\n /** Get a valid access token, refreshing if needed */\n async getToken(providerName: string, accountId?: string): Promise<string> {\n const credential = await this.resolveCredential(providerName, accountId);\n const { tokenSet } = credential;\n\n if (this.isTokenValid(tokenSet.expiresAt)) {\n return tokenSet.accessToken;\n }\n\n // Token expired or near-expiry — try to refresh\n if (!tokenSet.refreshToken) {\n throw new TokenExpiredError(providerName);\n }\n\n await this.refreshWithLock(providerName, credential);\n\n // Re-read from store after refresh\n const refreshed = await this.store.get(providerName, credential.metadata.accountId);\n if (!refreshed) {\n throw new TokenExpiredError(providerName);\n }\n return refreshed.tokenSet.accessToken;\n }\n\n /** Get authenticated headers for API calls */\n async getAuthHeaders(providerName: string, accountId?: string): Promise<AuthHeaders> {\n const token = await this.getToken(providerName, accountId);\n const provider = getProvider(providerName);\n return provider.getAuthHeaders(token);\n }\n\n /** Remove stored tokens for a provider */\n async logout(providerName: string, accountId?: string): Promise<void> {\n if (accountId) {\n await this.store.delete(providerName, accountId);\n } else {\n const credentials = await this.store.list(providerName);\n for (const cred of credentials) {\n await this.store.delete(providerName, cred.metadata.accountId);\n }\n }\n }\n\n /** Get status of all stored credentials */\n async status(): Promise<CredentialStatus[]> {\n const credentials = await this.store.list();\n return credentials.map((cred) => {\n let displayName = cred.metadata.provider;\n try {\n const provider = getProvider(cred.metadata.provider);\n displayName = provider.config.displayName;\n } catch {\n // Unknown provider, use raw name\n }\n\n return {\n provider: cred.metadata.provider,\n displayName,\n accountId: cred.metadata.accountId,\n accountLabel: cred.metadata.accountLabel,\n isExpired: !this.isTokenValid(cred.tokenSet.expiresAt),\n expiresAt: cred.tokenSet.expiresAt,\n hasRefreshToken: cred.tokenSet.refreshToken !== null,\n };\n });\n }\n\n private isTokenValid(expiresAt: number): boolean {\n return Date.now() + REFRESH_BUFFER_MS < expiresAt;\n }\n\n private async resolveCredential(\n providerName: string,\n accountId?: string,\n ): Promise<StoredCredential> {\n if (accountId) {\n const credential = await this.store.get(providerName, accountId);\n if (!credential) {\n throw new NoCredentialError(providerName, accountId);\n }\n return credential;\n }\n\n // No account specified — find the most recently refreshed one\n const credentials = await this.store.list(providerName);\n if (credentials.length === 0) {\n throw new NoCredentialError(providerName);\n }\n\n return credentials.sort((a, b) => b.metadata.lastRefreshedAt - a.metadata.lastRefreshedAt)[0]!;\n }\n\n private async refreshWithLock(providerName: string, credential: StoredCredential): Promise<void> {\n const lockKey = `${providerName}::${credential.metadata.accountId}`;\n\n // If a refresh is already in progress for this account, wait for it\n const existingLock = this.refreshLocks.get(lockKey);\n if (existingLock) {\n await existingLock;\n return;\n }\n\n const refreshPromise = this.doRefresh(providerName, credential);\n this.refreshLocks.set(lockKey, refreshPromise);\n\n try {\n await refreshPromise;\n } finally {\n this.refreshLocks.delete(lockKey);\n }\n }\n\n private async doRefresh(providerName: string, credential: StoredCredential): Promise<void> {\n const provider = getProvider(providerName);\n const { refreshToken } = credential.tokenSet;\n\n if (!refreshToken) {\n throw new TokenExpiredError(providerName);\n }\n\n let newTokenSet;\n try {\n newTokenSet = await provider.refresh(refreshToken);\n } catch (err) {\n throw new TokenRefreshError(providerName, err);\n }\n\n // Preserve the refresh token if the new response doesn't include one\n if (!newTokenSet.refreshToken && refreshToken) {\n newTokenSet.refreshToken = refreshToken;\n }\n\n const updatedCredential: StoredCredential = {\n tokenSet: newTokenSet,\n metadata: {\n ...credential.metadata,\n lastRefreshedAt: Date.now(),\n },\n };\n\n await this.store.set(providerName, credential.metadata.accountId, updatedCredential);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ import { c as StateMismatchError, o as OAuthCallbackError, s as OAuthTimeoutError } from "./registry-Cp-_Ipc6.mjs";
3
+ import { createInterface } from "node:readline";
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import { platform } from "node:os";
6
+ import { execFile } from "node:child_process";
7
+ import { createServer } from "node:http";
8
+ //#region src/core/browser.ts
9
+ /** Open a URL in the user's default browser. Does not throw on failure. */
10
+ function openBrowser(url) {
11
+ const os = platform();
12
+ try {
13
+ if (os === "darwin") execFile("open", [url]);
14
+ else if (os === "win32") execFile("cmd", [
15
+ "/c",
16
+ "start",
17
+ "",
18
+ url
19
+ ]);
20
+ else execFile("xdg-open", [url]);
21
+ } catch {}
22
+ }
23
+ //#endregion
24
+ //#region src/core/callback-server.ts
25
+ const SUCCESS_HTML = `<!DOCTYPE html>
26
+ <html><head><meta charset="utf-8"><title>Authorization Successful</title>
27
+ <style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f8f9fa}
28
+ .card{text-align:center;padding:2rem;border-radius:12px;background:white;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
29
+ h1{color:#22c55e;font-size:1.5rem}p{color:#666}</style></head>
30
+ <body><div class="card"><h1>Authorization Successful</h1><p>You can close this window and return to the terminal.</p></div></body></html>`;
31
+ const ERROR_HTML = `<!DOCTYPE html>
32
+ <html><head><meta charset="utf-8"><title>Authorization Failed</title>
33
+ <style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f8f9fa}
34
+ .card{text-align:center;padding:2rem;border-radius:12px;background:white;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
35
+ h1{color:#ef4444;font-size:1.5rem}p{color:#666}</style></head>
36
+ <body><div class="card"><h1>Authorization Failed</h1><p>Something went wrong. Please try again.</p></div></body></html>`;
37
+ /** Start a local HTTP server to receive the OAuth callback redirect */
38
+ function startCallbackServer(options) {
39
+ const { expectedState, timeout = 12e4, path = "/callback" } = options;
40
+ const port = options.port ?? 0;
41
+ return new Promise((resolveSetup, _rejectSetup) => {
42
+ let server;
43
+ let timeoutId;
44
+ const resultPromise = new Promise((resolveResult, rejectResult) => {
45
+ server = createServer((req, res) => {
46
+ const url = new URL(req.url ?? "/", `http://localhost`);
47
+ if (url.pathname !== path) {
48
+ res.writeHead(404);
49
+ res.end("Not found");
50
+ return;
51
+ }
52
+ const code = url.searchParams.get("code");
53
+ const state = url.searchParams.get("state");
54
+ const error = url.searchParams.get("error");
55
+ if (error) {
56
+ const errorDesc = url.searchParams.get("error_description") ?? error;
57
+ res.writeHead(200, { "Content-Type": "text/html" });
58
+ res.end(ERROR_HTML);
59
+ cleanup();
60
+ rejectResult(new OAuthCallbackError(`OAuth error: ${errorDesc}`));
61
+ return;
62
+ }
63
+ if (!code) {
64
+ res.writeHead(200, { "Content-Type": "text/html" });
65
+ res.end(ERROR_HTML);
66
+ cleanup();
67
+ rejectResult(new OAuthCallbackError("No authorization code in callback"));
68
+ return;
69
+ }
70
+ if (state && state !== expectedState) {
71
+ res.writeHead(200, { "Content-Type": "text/html" });
72
+ res.end(ERROR_HTML);
73
+ cleanup();
74
+ rejectResult(new OAuthCallbackError("State mismatch — possible CSRF attack. Please try again."));
75
+ return;
76
+ }
77
+ res.writeHead(200, { "Content-Type": "text/html" });
78
+ res.end(SUCCESS_HTML);
79
+ cleanup();
80
+ resolveResult({
81
+ code,
82
+ state: state ?? expectedState
83
+ });
84
+ });
85
+ const cleanup = () => {
86
+ if (timeoutId) clearTimeout(timeoutId);
87
+ server.close();
88
+ };
89
+ timeoutId = setTimeout(() => {
90
+ cleanup();
91
+ rejectResult(new OAuthTimeoutError(timeout));
92
+ }, timeout);
93
+ server.on("error", (err) => {
94
+ cleanup();
95
+ rejectResult(new OAuthCallbackError(`Callback server error: ${err.message}`));
96
+ });
97
+ server.listen(port, "127.0.0.1", () => {
98
+ const addr = server.address();
99
+ resolveSetup({
100
+ result: resultPromise,
101
+ port: typeof addr === "object" && addr ? addr.port : port,
102
+ close: cleanup
103
+ });
104
+ });
105
+ });
106
+ });
107
+ }
108
+ //#endregion
109
+ //#region src/core/crypto.ts
110
+ /** Generate a cryptographically random PKCE code verifier (43-128 chars, base64url) */
111
+ function generateCodeVerifier() {
112
+ return randomBytes(32).toString("base64url");
113
+ }
114
+ /** Generate a PKCE code challenge from a verifier using S256 method */
115
+ function generateCodeChallenge(verifier) {
116
+ return createHash("sha256").update(verifier).digest("base64url");
117
+ }
118
+ /** Generate both PKCE code verifier and challenge */
119
+ function generatePKCE() {
120
+ const codeVerifier = generateCodeVerifier();
121
+ return {
122
+ codeVerifier,
123
+ codeChallenge: generateCodeChallenge(codeVerifier)
124
+ };
125
+ }
126
+ /** Generate a random state parameter for CSRF protection */
127
+ function generateState() {
128
+ return randomBytes(32).toString("base64url");
129
+ }
130
+ //#endregion
131
+ //#region src/core/manual-code-input.ts
132
+ /**
133
+ * Parse a "code#state" string (Anthropic's manual paste format).
134
+ * Also supports a plain authorization code (state validated separately).
135
+ */
136
+ function parseCodeAndState(input, expectedState) {
137
+ const trimmed = input.trim();
138
+ if (!trimmed) throw new OAuthCallbackError("Empty authorization code input");
139
+ const hashIndex = trimmed.indexOf("#");
140
+ if (hashIndex !== -1) {
141
+ const code = trimmed.slice(0, hashIndex);
142
+ const state = trimmed.slice(hashIndex + 1);
143
+ if (!code) throw new OAuthCallbackError("Empty authorization code in code#state input");
144
+ if (state !== expectedState) throw new StateMismatchError();
145
+ return {
146
+ code,
147
+ state
148
+ };
149
+ }
150
+ return {
151
+ code: trimmed,
152
+ state: expectedState
153
+ };
154
+ }
155
+ /** Prompt the user to paste the authorization code from their browser */
156
+ function promptForCode(expectedState) {
157
+ return new Promise((resolve, reject) => {
158
+ const rl = createInterface({
159
+ input: process.stdin,
160
+ output: process.stderr
161
+ });
162
+ rl.question("Paste the authorization code (or code#state) from your browser: ", (answer) => {
163
+ rl.close();
164
+ try {
165
+ resolve(parseCodeAndState(answer, expectedState));
166
+ } catch (err) {
167
+ reject(err);
168
+ }
169
+ });
170
+ });
171
+ }
172
+ //#endregion
173
+ //#region src/core/oauth-pkce.ts
174
+ /** Build the full OAuth authorization URL with PKCE and state params */
175
+ function buildAuthorizationUrl(config, codeChallenge, state, redirectUri) {
176
+ if (!config.authorizationEndpoint) throw new OAuthCallbackError(`Provider "${config.name}" has no authorization endpoint`);
177
+ const url = new URL(config.authorizationEndpoint);
178
+ url.searchParams.set("response_type", "code");
179
+ url.searchParams.set("client_id", config.clientId);
180
+ url.searchParams.set("redirect_uri", redirectUri);
181
+ url.searchParams.set("code_challenge", codeChallenge);
182
+ url.searchParams.set("code_challenge_method", "S256");
183
+ url.searchParams.set("state", state);
184
+ if (config.scopes?.length) url.searchParams.set("scope", config.scopes.join(" "));
185
+ return url.toString();
186
+ }
187
+ /** Exchange an authorization code for tokens */
188
+ async function exchangeCode(config, code, codeVerifier, redirectUri, state) {
189
+ const useJson = config.tokenBodyFormat === "json";
190
+ const params = {
191
+ grant_type: "authorization_code",
192
+ code,
193
+ code_verifier: codeVerifier,
194
+ client_id: config.clientId,
195
+ redirect_uri: redirectUri
196
+ };
197
+ if (state !== void 0) params.state = state;
198
+ const response = await fetch(config.tokenEndpoint, {
199
+ method: "POST",
200
+ headers: { "Content-Type": useJson ? "application/json" : "application/x-www-form-urlencoded" },
201
+ body: useJson ? JSON.stringify(params) : new URLSearchParams(params).toString()
202
+ });
203
+ if (!response.ok) {
204
+ const errorText = await response.text();
205
+ throw new OAuthCallbackError(`Token exchange failed (${response.status}): ${errorText}`);
206
+ }
207
+ return parseTokenResponse(await response.json());
208
+ }
209
+ /** Refresh an access token using a refresh token */
210
+ async function refreshAccessToken(config, refreshToken) {
211
+ const useJson = config.tokenBodyFormat === "json";
212
+ const params = {
213
+ grant_type: "refresh_token",
214
+ refresh_token: refreshToken,
215
+ client_id: config.clientId
216
+ };
217
+ const response = await fetch(config.tokenEndpoint, {
218
+ method: "POST",
219
+ headers: { "Content-Type": useJson ? "application/json" : "application/x-www-form-urlencoded" },
220
+ body: useJson ? JSON.stringify(params) : new URLSearchParams(params).toString()
221
+ });
222
+ if (!response.ok) {
223
+ const errorText = await response.text();
224
+ throw new OAuthCallbackError(`Token refresh failed (${response.status}): ${errorText}`);
225
+ }
226
+ return parseTokenResponse(await response.json());
227
+ }
228
+ /** Execute the full PKCE flow: generate params, open browser, wait for callback, exchange code */
229
+ async function executePKCEFlow(options) {
230
+ const { config, loginOptions } = options;
231
+ const { codeVerifier, codeChallenge } = generatePKCE();
232
+ const state = config.stateIsVerifier ? codeVerifier : generateState();
233
+ const manual = loginOptions?.manual ?? false;
234
+ let authResult;
235
+ let redirectUri;
236
+ if (manual && options.manualRedirectUri) {
237
+ redirectUri = options.manualRedirectUri;
238
+ const authUrl = buildAuthorizationUrl(config, codeChallenge, state, redirectUri);
239
+ if (loginOptions?.onOpenBrowser) loginOptions.onOpenBrowser(authUrl);
240
+ else openBrowser(authUrl);
241
+ process.stderr.write(`\nOpen this URL in your browser if it didn't open automatically:\n${authUrl}\n\n`);
242
+ authResult = await promptForCode(state);
243
+ } else {
244
+ const server = await startCallbackServer({
245
+ port: loginOptions?.port ?? 0,
246
+ expectedState: state,
247
+ timeout: loginOptions?.timeout ?? 12e4
248
+ });
249
+ redirectUri = `http://127.0.0.1:${server.port}/callback`;
250
+ const authUrl = buildAuthorizationUrl(config, codeChallenge, state, redirectUri);
251
+ if (loginOptions?.onOpenBrowser) loginOptions.onOpenBrowser(authUrl);
252
+ else openBrowser(authUrl);
253
+ process.stderr.write(`\nOpen this URL in your browser if it didn't open automatically:\n${authUrl}\n\nWaiting for authorization...\n`);
254
+ try {
255
+ authResult = await server.result;
256
+ } catch (err) {
257
+ server.close();
258
+ throw err;
259
+ }
260
+ }
261
+ const exchangeState = config.tokenBodyFormat === "json" ? authResult.state : void 0;
262
+ return exchangeCode(config, authResult.code, codeVerifier, redirectUri, exchangeState);
263
+ }
264
+ function parseTokenResponse(data) {
265
+ const accessToken = data.access_token;
266
+ if (!accessToken) throw new OAuthCallbackError("No access_token in token response");
267
+ const expiresIn = data.expires_in ?? 3600;
268
+ const expiresAt = Date.now() + expiresIn * 1e3;
269
+ return {
270
+ accessToken,
271
+ refreshToken: data.refresh_token ?? null,
272
+ expiresAt,
273
+ idToken: data.id_token,
274
+ tokenType: (data.token_type ?? "bearer").toLowerCase(),
275
+ scopes: data.scope ? data.scope.split(" ") : void 0,
276
+ raw: data
277
+ };
278
+ }
279
+ //#endregion
280
+ export { refreshAccessToken as n, executePKCEFlow as t };
281
+
282
+ //# sourceMappingURL=oauth-pkce-Bi02-h23.mjs.map