oauth.do 0.1.12 → 0.1.14

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.
package/dist/index.d.ts CHANGED
@@ -20,6 +20,12 @@ interface OAuthConfig {
20
20
  * Custom fetch implementation
21
21
  */
22
22
  fetch?: typeof fetch;
23
+ /**
24
+ * Custom path for token storage
25
+ * Supports ~ for home directory (e.g., '~/.studio/tokens.json')
26
+ * @default '~/.oauth.do/token'
27
+ */
28
+ storagePath?: string;
23
29
  }
24
30
  /**
25
31
  * User information returned from auth endpoints
@@ -163,15 +169,25 @@ declare function configure(config: OAuthConfig): void;
163
169
  /**
164
170
  * Get current configuration
165
171
  */
166
- declare function getConfig(): Required<OAuthConfig>;
172
+ declare function getConfig(): Omit<Required<OAuthConfig>, 'storagePath'> & Pick<OAuthConfig, 'storagePath'>;
167
173
 
174
+ /**
175
+ * OAuth provider options for direct provider login
176
+ * Bypasses AuthKit login screen and goes directly to the provider
177
+ */
178
+ type OAuthProvider = 'GitHubOAuth' | 'GoogleOAuth' | 'MicrosoftOAuth' | 'AppleOAuth';
179
+ interface DeviceAuthOptions {
180
+ /** OAuth provider to use directly (bypasses AuthKit login screen) */
181
+ provider?: OAuthProvider;
182
+ }
168
183
  /**
169
184
  * Initiate device authorization flow
170
185
  * Following OAuth 2.0 Device Authorization Grant (RFC 8628)
171
186
  *
187
+ * @param options - Optional settings including provider for direct OAuth
172
188
  * @returns Device authorization response with codes and URIs
173
189
  */
174
- declare function authorizeDevice(): Promise<DeviceAuthorizationResponse>;
190
+ declare function authorizeDevice(options?: DeviceAuthOptions): Promise<DeviceAuthorizationResponse>;
175
191
  /**
176
192
  * Poll for tokens after device authorization
177
193
  *
@@ -182,6 +198,115 @@ declare function authorizeDevice(): Promise<DeviceAuthorizationResponse>;
182
198
  */
183
199
  declare function pollForTokens(deviceCode: string, interval?: number, expiresIn?: number): Promise<TokenResponse>;
184
200
 
201
+ /**
202
+ * GitHub Device Flow implementation
203
+ * Following OAuth 2.0 Device Authorization Grant (RFC 8628)
204
+ * https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
205
+ */
206
+ interface GitHubDeviceFlowOptions {
207
+ /** GitHub OAuth App client ID */
208
+ clientId: string;
209
+ /** OAuth scopes (default: 'user:email read:user') */
210
+ scope?: string;
211
+ /** Custom fetch implementation */
212
+ fetch?: typeof fetch;
213
+ }
214
+ interface GitHubDeviceAuthResponse {
215
+ /** Device verification code */
216
+ deviceCode: string;
217
+ /** User verification code to display */
218
+ userCode: string;
219
+ /** Verification URI for user to visit */
220
+ verificationUri: string;
221
+ /** Expiration time in seconds */
222
+ expiresIn: number;
223
+ /** Polling interval in seconds */
224
+ interval: number;
225
+ }
226
+ interface GitHubTokenResponse {
227
+ /** Access token for GitHub API */
228
+ accessToken: string;
229
+ /** Token type (typically 'bearer') */
230
+ tokenType: string;
231
+ /** Granted scopes */
232
+ scope: string;
233
+ }
234
+ interface GitHubUser {
235
+ /** Numeric GitHub user ID (critical for sqid generation) */
236
+ id: number;
237
+ /** GitHub username */
238
+ login: string;
239
+ /** User's email (may be null if not public) */
240
+ email: string | null;
241
+ /** User's display name */
242
+ name: string | null;
243
+ /** Avatar image URL */
244
+ avatarUrl: string;
245
+ }
246
+ /**
247
+ * Start GitHub Device Flow
248
+ *
249
+ * Initiates device authorization flow by requesting device and user codes.
250
+ *
251
+ * @param options - Client ID, scope, and optional custom fetch
252
+ * @returns Device authorization response with codes and URIs
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * const auth = await startGitHubDeviceFlow({
257
+ * clientId: 'Ov23liABCDEFGHIJKLMN',
258
+ * scope: 'user:email read:user'
259
+ * })
260
+ *
261
+ * console.log(`Visit ${auth.verificationUri} and enter code: ${auth.userCode}`)
262
+ * ```
263
+ */
264
+ declare function startGitHubDeviceFlow(options: GitHubDeviceFlowOptions): Promise<GitHubDeviceAuthResponse>;
265
+ /**
266
+ * Poll GitHub Device Flow for access token
267
+ *
268
+ * Polls GitHub's token endpoint until user completes authorization.
269
+ * Handles all error states including authorization_pending, slow_down, etc.
270
+ *
271
+ * @param deviceCode - Device code from startGitHubDeviceFlow
272
+ * @param options - Client ID and optional custom fetch
273
+ * @returns Token response with access token
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * const auth = await startGitHubDeviceFlow({ clientId: '...' })
278
+ * // User completes authorization...
279
+ * const token = await pollGitHubDeviceFlow(auth.deviceCode, {
280
+ * clientId: '...',
281
+ * interval: auth.interval,
282
+ * expiresIn: auth.expiresIn
283
+ * })
284
+ * console.log('Access token:', token.accessToken)
285
+ * ```
286
+ */
287
+ declare function pollGitHubDeviceFlow(deviceCode: string, options: GitHubDeviceFlowOptions & {
288
+ interval?: number;
289
+ expiresIn?: number;
290
+ }): Promise<GitHubTokenResponse>;
291
+ /**
292
+ * Get GitHub user information
293
+ *
294
+ * Fetches authenticated user's profile from GitHub API.
295
+ *
296
+ * @param accessToken - GitHub access token
297
+ * @param options - Optional custom fetch implementation
298
+ * @returns GitHub user profile
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * const user = await getGitHubUser(token.accessToken)
303
+ * console.log(`Logged in as ${user.login} (ID: ${user.id})`)
304
+ * ```
305
+ */
306
+ declare function getGitHubUser(accessToken: string, options?: {
307
+ fetch?: typeof fetch;
308
+ }): Promise<GitHubUser>;
309
+
185
310
  /**
186
311
  * Keychain-based token storage using OS credential manager
187
312
  * - macOS: Keychain
@@ -218,6 +343,8 @@ declare class SecureFileTokenStorage implements TokenStorage {
218
343
  private tokenPath;
219
344
  private configDir;
220
345
  private initialized;
346
+ private customPath?;
347
+ constructor(customPath?: string);
221
348
  private init;
222
349
  getToken(): Promise<string | null>;
223
350
  setToken(token: string): Promise<void>;
@@ -299,8 +426,10 @@ declare class CompositeTokenStorage implements TokenStorage {
299
426
  *
300
427
  * Note: We use file storage by default because keychain storage on macOS
301
428
  * requires GUI authorization popups, which breaks automation and agent workflows.
429
+ *
430
+ * @param storagePath - Optional custom path for token storage (e.g., '~/.studio/tokens.json')
302
431
  */
303
- declare function createSecureStorage(): TokenStorage;
432
+ declare function createSecureStorage(storagePath?: string): TokenStorage;
304
433
 
305
434
  /**
306
435
  * CLI-centric login utilities
@@ -312,6 +441,8 @@ interface LoginOptions {
312
441
  openBrowser?: boolean;
313
442
  /** Custom print function for output */
314
443
  print?: (message: string) => void;
444
+ /** OAuth provider to use directly (bypasses AuthKit login screen) */
445
+ provider?: OAuthProvider;
315
446
  /** Storage to use (default: createSecureStorage()) */
316
447
  storage?: {
317
448
  getToken: () => Promise<string | null>;
@@ -340,4 +471,4 @@ declare function forceLogin(options?: LoginOptions): Promise<LoginResult>;
340
471
  */
341
472
  declare function ensureLoggedOut(options?: LoginOptions): Promise<void>;
342
473
 
343
- export { type AuthProvider, type AuthResult, CompositeTokenStorage, type DeviceAuthorizationResponse, FileTokenStorage, KeychainTokenStorage, LocalStorageTokenStorage, type LoginOptions, type LoginResult, MemoryTokenStorage, type OAuthConfig, SecureFileTokenStorage, type TokenError, type TokenResponse, type TokenStorage, type User, ensureLoggedOut as a, auth, authorizeDevice, buildAuthUrl, configure, createSecureStorage, ensureLoggedIn as e, forceLogin as f, getConfig, getToken, getUser, isAuthenticated, login, logout, pollForTokens };
474
+ export { type AuthProvider, type AuthResult, CompositeTokenStorage, type DeviceAuthorizationResponse, FileTokenStorage, type GitHubDeviceAuthResponse, type GitHubDeviceFlowOptions, type GitHubTokenResponse, type GitHubUser, KeychainTokenStorage, LocalStorageTokenStorage, type LoginOptions, type LoginResult, MemoryTokenStorage, type OAuthConfig, type OAuthProvider, SecureFileTokenStorage, type TokenError, type TokenResponse, type TokenStorage, type User, ensureLoggedOut as a, auth, authorizeDevice, buildAuthUrl, configure, createSecureStorage, ensureLoggedIn as e, forceLogin as f, getConfig, getGitHubUser, getToken, getUser, isAuthenticated, login, logout, pollForTokens, pollGitHubDeviceFlow, startGitHubDeviceFlow };
package/dist/index.js CHANGED
@@ -26,9 +26,9 @@ function getEnv2(key) {
26
26
  if (typeof process !== "undefined" && process.env?.[key]) return process.env[key];
27
27
  return void 0;
28
28
  }
29
- function createSecureStorage() {
29
+ function createSecureStorage(storagePath) {
30
30
  if (isNode()) {
31
- return new SecureFileTokenStorage();
31
+ return new SecureFileTokenStorage(storagePath);
32
32
  }
33
33
  if (typeof localStorage !== "undefined") {
34
34
  return new LocalStorageTokenStorage();
@@ -133,6 +133,10 @@ var init_storage = __esm({
133
133
  tokenPath = null;
134
134
  configDir = null;
135
135
  initialized = false;
136
+ customPath;
137
+ constructor(customPath) {
138
+ this.customPath = customPath;
139
+ }
136
140
  async init() {
137
141
  if (this.initialized) return this.tokenPath !== null;
138
142
  this.initialized = true;
@@ -140,8 +144,14 @@ var init_storage = __esm({
140
144
  try {
141
145
  const os = await import('os');
142
146
  const path = await import('path');
143
- this.configDir = path.join(os.homedir(), ".oauth.do");
144
- this.tokenPath = path.join(this.configDir, "token");
147
+ if (this.customPath) {
148
+ const expandedPath = this.customPath.startsWith("~/") ? path.join(os.homedir(), this.customPath.slice(2)) : this.customPath;
149
+ this.tokenPath = expandedPath;
150
+ this.configDir = path.dirname(expandedPath);
151
+ } else {
152
+ this.configDir = path.join(os.homedir(), ".oauth.do");
153
+ this.tokenPath = path.join(this.configDir, "token");
154
+ }
145
155
  return true;
146
156
  } catch {
147
157
  return false;
@@ -376,7 +386,8 @@ var globalConfig = {
376
386
  apiUrl: getEnv("OAUTH_API_URL") || getEnv("API_URL") || "https://apis.do",
377
387
  clientId: getEnv("OAUTH_CLIENT_ID") || "client_01JQYTRXK9ZPD8JPJTKDCRB656",
378
388
  authKitDomain: getEnv("OAUTH_AUTHKIT_DOMAIN") || "login.oauth.do",
379
- fetch: globalThis.fetch
389
+ fetch: globalThis.fetch,
390
+ storagePath: getEnv("OAUTH_STORAGE_PATH")
380
391
  };
381
392
  function configure(config) {
382
393
  globalConfig = {
@@ -485,7 +496,8 @@ async function getToken() {
485
496
  }
486
497
  try {
487
498
  const { createSecureStorage: createSecureStorage2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
488
- const storage = createSecureStorage2();
499
+ const config = getConfig();
500
+ const storage = createSecureStorage2(config.storagePath);
489
501
  return await storage.getToken();
490
502
  } catch {
491
503
  return null;
@@ -515,7 +527,7 @@ function buildAuthUrl(options) {
515
527
  }
516
528
 
517
529
  // src/device.ts
518
- async function authorizeDevice() {
530
+ async function authorizeDevice(options = {}) {
519
531
  const config = getConfig();
520
532
  if (!config.clientId) {
521
533
  throw new Error('Client ID is required for device authorization. Set OAUTH_CLIENT_ID or configure({ clientId: "..." })');
@@ -526,12 +538,15 @@ async function authorizeDevice() {
526
538
  client_id: config.clientId,
527
539
  scope: "openid profile email"
528
540
  });
541
+ if (options.provider) {
542
+ body.set("provider", options.provider);
543
+ }
529
544
  const response = await config.fetch(url, {
530
545
  method: "POST",
531
546
  headers: {
532
547
  "Content-Type": "application/x-www-form-urlencoded"
533
548
  },
534
- body
549
+ body: body.toString()
535
550
  });
536
551
  if (!response.ok) {
537
552
  const errorText = await response.text();
@@ -567,7 +582,7 @@ async function pollForTokens(deviceCode, interval = 5, expiresIn = 600) {
567
582
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
568
583
  device_code: deviceCode,
569
584
  client_id: config.clientId
570
- })
585
+ }).toString()
571
586
  });
572
587
  if (response.ok) {
573
588
  const data = await response.json();
@@ -597,9 +612,138 @@ async function pollForTokens(deviceCode, interval = 5, expiresIn = 600) {
597
612
  }
598
613
  }
599
614
 
615
+ // src/github-device.ts
616
+ async function startGitHubDeviceFlow(options) {
617
+ const { clientId, scope = "user:email read:user" } = options;
618
+ const fetchImpl = options.fetch || globalThis.fetch;
619
+ if (!clientId) {
620
+ throw new Error("GitHub client ID is required for device authorization");
621
+ }
622
+ try {
623
+ const url = "https://github.com/login/device/code";
624
+ const body = new URLSearchParams({
625
+ client_id: clientId,
626
+ scope
627
+ });
628
+ const response = await fetchImpl(url, {
629
+ method: "POST",
630
+ headers: {
631
+ "Content-Type": "application/x-www-form-urlencoded",
632
+ "Accept": "application/json"
633
+ },
634
+ body
635
+ });
636
+ if (!response.ok) {
637
+ const errorText = await response.text();
638
+ throw new Error(`GitHub device authorization failed: ${response.statusText} - ${errorText}`);
639
+ }
640
+ const data = await response.json();
641
+ return {
642
+ deviceCode: data.device_code,
643
+ userCode: data.user_code,
644
+ verificationUri: data.verification_uri,
645
+ expiresIn: data.expires_in,
646
+ interval: data.interval
647
+ };
648
+ } catch (error) {
649
+ console.error("GitHub device authorization error:", error);
650
+ throw error;
651
+ }
652
+ }
653
+ async function pollGitHubDeviceFlow(deviceCode, options) {
654
+ const { clientId, interval = 5, expiresIn = 900 } = options;
655
+ const fetchImpl = options.fetch || globalThis.fetch;
656
+ if (!clientId) {
657
+ throw new Error("GitHub client ID is required for token polling");
658
+ }
659
+ const startTime = Date.now();
660
+ const timeout = expiresIn * 1e3;
661
+ let currentInterval = interval * 1e3;
662
+ while (true) {
663
+ if (Date.now() - startTime > timeout) {
664
+ throw new Error("GitHub device authorization expired. Please try again.");
665
+ }
666
+ await new Promise((resolve) => setTimeout(resolve, currentInterval));
667
+ try {
668
+ const url = "https://github.com/login/oauth/access_token";
669
+ const body = new URLSearchParams({
670
+ client_id: clientId,
671
+ device_code: deviceCode,
672
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
673
+ });
674
+ const response = await fetchImpl(url, {
675
+ method: "POST",
676
+ headers: {
677
+ "Content-Type": "application/x-www-form-urlencoded",
678
+ "Accept": "application/json"
679
+ },
680
+ body
681
+ });
682
+ const data = await response.json();
683
+ if ("access_token" in data) {
684
+ return {
685
+ accessToken: data.access_token,
686
+ tokenType: data.token_type,
687
+ scope: data.scope
688
+ };
689
+ }
690
+ const error = data.error || "unknown";
691
+ switch (error) {
692
+ case "authorization_pending":
693
+ continue;
694
+ case "slow_down":
695
+ currentInterval += 5e3;
696
+ continue;
697
+ case "access_denied":
698
+ throw new Error("Access denied by user");
699
+ case "expired_token":
700
+ throw new Error("Device code expired");
701
+ default:
702
+ throw new Error(`GitHub token polling failed: ${error}`);
703
+ }
704
+ } catch (error) {
705
+ if (error instanceof Error) {
706
+ throw error;
707
+ }
708
+ continue;
709
+ }
710
+ }
711
+ }
712
+ async function getGitHubUser(accessToken, options = {}) {
713
+ const fetchImpl = options.fetch || globalThis.fetch;
714
+ if (!accessToken) {
715
+ throw new Error("GitHub access token is required");
716
+ }
717
+ try {
718
+ const response = await fetchImpl("https://api.github.com/user", {
719
+ method: "GET",
720
+ headers: {
721
+ "Authorization": `Bearer ${accessToken}`,
722
+ "Accept": "application/vnd.github+json",
723
+ "X-GitHub-Api-Version": "2022-11-28"
724
+ }
725
+ });
726
+ if (!response.ok) {
727
+ const errorText = await response.text();
728
+ throw new Error(`GitHub user fetch failed: ${response.statusText} - ${errorText}`);
729
+ }
730
+ const data = await response.json();
731
+ return {
732
+ id: data.id,
733
+ login: data.login,
734
+ email: data.email,
735
+ name: data.name,
736
+ avatarUrl: data.avatar_url
737
+ };
738
+ } catch (error) {
739
+ console.error("GitHub user fetch error:", error);
740
+ throw error;
741
+ }
742
+ }
743
+
600
744
  // src/index.ts
601
745
  init_storage();
602
746
 
603
- export { CompositeTokenStorage, FileTokenStorage, KeychainTokenStorage, LocalStorageTokenStorage, MemoryTokenStorage, SecureFileTokenStorage, auth, authorizeDevice, buildAuthUrl, configure, createSecureStorage, getConfig, getToken, getUser, isAuthenticated, login, logout, pollForTokens };
747
+ export { CompositeTokenStorage, FileTokenStorage, KeychainTokenStorage, LocalStorageTokenStorage, MemoryTokenStorage, SecureFileTokenStorage, auth, authorizeDevice, buildAuthUrl, configure, createSecureStorage, getConfig, getGitHubUser, getToken, getUser, isAuthenticated, login, logout, pollForTokens, pollGitHubDeviceFlow, startGitHubDeviceFlow };
604
748
  //# sourceMappingURL=index.js.map
605
749
  //# sourceMappingURL=index.js.map