@mcp-z/oauth-google 1.0.0 → 1.0.2

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.
Files changed (48) hide show
  1. package/README.md +8 -0
  2. package/dist/cjs/index.d.cts +2 -1
  3. package/dist/cjs/index.d.ts +2 -1
  4. package/dist/cjs/index.js +4 -0
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/lib/dcr-router.js.map +1 -1
  7. package/dist/cjs/lib/dcr-utils.js.map +1 -1
  8. package/dist/cjs/lib/dcr-verify.js.map +1 -1
  9. package/dist/cjs/lib/fetch-with-timeout.js.map +1 -1
  10. package/dist/cjs/lib/loopback-router.d.cts +8 -0
  11. package/dist/cjs/lib/loopback-router.d.ts +8 -0
  12. package/dist/cjs/lib/loopback-router.js +219 -0
  13. package/dist/cjs/lib/loopback-router.js.map +1 -0
  14. package/dist/cjs/lib/token-verifier.js.map +1 -1
  15. package/dist/cjs/providers/dcr.js.map +1 -1
  16. package/dist/cjs/providers/loopback-oauth.d.cts +94 -27
  17. package/dist/cjs/providers/loopback-oauth.d.ts +94 -27
  18. package/dist/cjs/providers/loopback-oauth.js +868 -498
  19. package/dist/cjs/providers/loopback-oauth.js.map +1 -1
  20. package/dist/cjs/providers/service-account.js.map +1 -1
  21. package/dist/cjs/schemas/index.js.map +1 -1
  22. package/dist/cjs/setup/config.d.cts +6 -1
  23. package/dist/cjs/setup/config.d.ts +6 -1
  24. package/dist/cjs/setup/config.js +6 -3
  25. package/dist/cjs/setup/config.js.map +1 -1
  26. package/dist/cjs/types.js.map +1 -1
  27. package/dist/esm/index.d.ts +2 -1
  28. package/dist/esm/index.js +1 -0
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/lib/dcr-router.js.map +1 -1
  31. package/dist/esm/lib/dcr-utils.js.map +1 -1
  32. package/dist/esm/lib/dcr-verify.js.map +1 -1
  33. package/dist/esm/lib/fetch-with-timeout.js.map +1 -1
  34. package/dist/esm/lib/loopback-router.d.ts +8 -0
  35. package/dist/esm/lib/loopback-router.js +32 -0
  36. package/dist/esm/lib/loopback-router.js.map +1 -0
  37. package/dist/esm/lib/token-verifier.js.map +1 -1
  38. package/dist/esm/providers/dcr.js.map +1 -1
  39. package/dist/esm/providers/loopback-oauth.d.ts +94 -27
  40. package/dist/esm/providers/loopback-oauth.js +461 -296
  41. package/dist/esm/providers/loopback-oauth.js.map +1 -1
  42. package/dist/esm/providers/service-account.js.map +1 -1
  43. package/dist/esm/schemas/index.js.map +1 -1
  44. package/dist/esm/setup/config.d.ts +6 -1
  45. package/dist/esm/setup/config.js +7 -3
  46. package/dist/esm/setup/config.js.map +1 -1
  47. package/dist/esm/types.js.map +1 -1
  48. package/package.json +1 -1
@@ -3,6 +3,16 @@
3
3
  *
4
4
  * Implements OAuth 2.0 Authorization Code Flow with PKCE using loopback interface redirection.
5
5
  * Uses ephemeral local server with OS-assigned port (RFC 8252 Section 8.3).
6
+ *
7
+ * CHANGE (2026-01-03):
8
+ * - Non-headless mode now opens the auth URL AND blocks (polls) until tokens are available,
9
+ * for BOTH redirectUri (persistent) and ephemeral (loopback) modes.
10
+ * - Ephemeral flow no longer calls `open()` itself. Instead it:
11
+ * 1) starts the loopback callback server
12
+ * 2) throws AuthRequiredError(auth_url)
13
+ * - Middleware catches AuthRequiredError(auth_url):
14
+ * - if not headless: open(url) once + poll pending state until callback completes (or timeout)
15
+ * - then retries token acquisition and injects authContext in the SAME tool call.
6
16
  */ "use strict";
7
17
  Object.defineProperty(exports, "__esModule", {
8
18
  value: true
@@ -22,6 +32,7 @@ _export(exports, {
22
32
  }
23
33
  });
24
34
  var _oauth = require("@mcp-z/oauth");
35
+ var _crypto = require("crypto");
25
36
  var _googleauthlibrary = require("google-auth-library");
26
37
  var _http = /*#__PURE__*/ _interop_require_wildcard(require("http"));
27
38
  var _open = /*#__PURE__*/ _interop_require_default(require("open"));
@@ -289,10 +300,14 @@ function _ts_generator(thisArg, body) {
289
300
  };
290
301
  }
291
302
  }
303
+ var OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
304
+ var OAUTH_POLL_MS = 500;
292
305
  var LoopbackOAuthProvider = /*#__PURE__*/ function() {
293
306
  "use strict";
294
307
  function LoopbackOAuthProvider(config) {
295
308
  _class_call_check(this, LoopbackOAuthProvider);
309
+ // Track URLs we've already opened for a given state within this process (prevents tab spam).
310
+ this.openedStates = new Set();
296
311
  this.config = config;
297
312
  }
298
313
  var _proto = LoopbackOAuthProvider.prototype;
@@ -303,7 +318,7 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
303
318
  * @returns Access token for API requests
304
319
  */ _proto.getAccessToken = function getAccessToken(accountId) {
305
320
  return _async_to_generator(function() {
306
- var _this_config, logger, service, tokenStore, effectiveAccountId, _tmp, storedToken, refreshedToken, error, headless, _this_config1, clientId, scope, existingAccounts, hasOtherAccounts, authUrl, hint, baseDescriptor, descriptor, _ref, token, email;
321
+ var _this_config, logger, service, tokenStore, effectiveAccountId, _tmp, storedToken, refreshedToken, error, _this_config1, clientId, scope, redirectUri, _generatePKCE, codeVerifier, codeChallenge, stateId, authUrl, descriptor;
307
322
  return _ts_generator(this, function(_state) {
308
323
  switch(_state.label){
309
324
  case 0:
@@ -400,110 +415,56 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
400
415
  9
401
416
  ];
402
417
  case 9:
403
- // No valid token or no account - check if we can start OAuth flow
404
- headless = this.config.headless;
405
- if (!headless) return [
418
+ // No valid token or no account - need OAuth authentication
419
+ _this_config1 = this.config, clientId = _this_config1.clientId, scope = _this_config1.scope, redirectUri = _this_config1.redirectUri;
420
+ if (!redirectUri) return [
406
421
  3,
407
422
  11
408
423
  ];
409
- // In headless mode (production), cannot start OAuth flow
410
- // Throw AuthRequiredError with auth_url descriptor for MCP tool response
411
- _this_config1 = this.config, clientId = _this_config1.clientId, scope = _this_config1.scope;
424
+ // Persistent callback mode (cloud deployment with configured redirect_uri)
425
+ _generatePKCE = (0, _oauth.generatePKCE)(), codeVerifier = _generatePKCE.verifier, codeChallenge = _generatePKCE.challenge;
426
+ stateId = (0, _crypto.randomUUID)();
427
+ // Store PKCE verifier for callback (5 minute TTL)
412
428
  return [
413
429
  4,
414
- this.getExistingAccounts()
430
+ this.createPendingAuth({
431
+ state: stateId,
432
+ codeVerifier: codeVerifier
433
+ })
415
434
  ];
416
435
  case 10:
417
- existingAccounts = _state.sent();
418
- hasOtherAccounts = effectiveAccountId ? existingAccounts.length > 0 && !existingAccounts.includes(effectiveAccountId) : existingAccounts.length > 0;
419
- // Build informational OAuth URL for headless mode
420
- // Note: No redirect_uri included - user must use account-add tool which starts proper ephemeral server
421
- authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
422
- authUrl.searchParams.set('client_id', clientId);
423
- authUrl.searchParams.set('response_type', 'code');
424
- authUrl.searchParams.set('scope', scope);
425
- authUrl.searchParams.set('access_type', 'offline');
426
- authUrl.searchParams.set('prompt', 'consent');
427
- if (hasOtherAccounts) {
428
- hint = "Existing ".concat(service, " accounts found. Use account-list to view, account-switch to change account, or account-add to add new account");
429
- } else if (effectiveAccountId) {
430
- hint = "Use account-add to authenticate ".concat(effectiveAccountId);
431
- } else {
432
- hint = 'Use account-add to authenticate interactively';
433
- }
434
- baseDescriptor = {
436
+ _state.sent();
437
+ // Build auth URL with configured redirect_uri
438
+ authUrl = this.buildAuthUrl({
439
+ redirectUri: redirectUri,
440
+ codeChallenge: codeChallenge,
441
+ state: stateId
442
+ });
443
+ logger.info('OAuth required - persistent callback mode', {
444
+ service: service,
445
+ redirectUri: redirectUri,
446
+ clientId: clientId,
447
+ scope: scope
448
+ });
449
+ throw new _typests.AuthRequiredError({
435
450
  kind: 'auth_url',
436
- provider: 'google',
437
- url: authUrl.toString(),
438
- hint: hint
439
- };
440
- descriptor = effectiveAccountId ? _object_spread_props(_object_spread({}, baseDescriptor), {
441
- accountId: effectiveAccountId
442
- }) : baseDescriptor;
443
- throw new _typests.AuthRequiredError(descriptor);
451
+ provider: service,
452
+ url: authUrl
453
+ });
444
454
  case 11:
445
- // Interactive mode - start ephemeral OAuth flow
455
+ // Ephemeral callback mode (local development)
456
+ // IMPORTANT: do NOT open here anymore; we throw auth_url and the middleware will open+poll.
446
457
  logger.info('Starting ephemeral OAuth flow', {
447
458
  service: service,
448
- headless: headless
459
+ headless: this.config.headless
449
460
  });
450
461
  return [
451
462
  4,
452
- this.performEphemeralOAuthFlow()
463
+ this.startEphemeralOAuthFlow()
453
464
  ];
454
465
  case 12:
455
- _ref = _state.sent(), token = _ref.token, email = _ref.email;
456
- // Store token with email as accountId
457
- return [
458
- 4,
459
- (0, _oauth.setToken)(tokenStore, {
460
- accountId: email,
461
- service: service
462
- }, token)
463
- ];
464
- case 13:
465
- _state.sent();
466
- // Register account in account management system
467
- return [
468
- 4,
469
- (0, _oauth.addAccount)(tokenStore, {
470
- service: service,
471
- accountId: email
472
- })
473
- ];
474
- case 14:
475
- _state.sent();
476
- // Set as active account so subsequent getAccessToken() calls find it
477
- return [
478
- 4,
479
- (0, _oauth.setActiveAccount)(tokenStore, {
480
- service: service,
481
- accountId: email
482
- })
483
- ];
484
- case 15:
485
- _state.sent();
486
- // Store account metadata (email, added timestamp)
487
- return [
488
- 4,
489
- (0, _oauth.setAccountInfo)(tokenStore, {
490
- service: service,
491
- accountId: email
492
- }, {
493
- email: email,
494
- addedAt: new Date().toISOString()
495
- })
496
- ];
497
- case 16:
498
- _state.sent();
499
- logger.info('OAuth flow completed', {
500
- service: service,
501
- accountId: email
502
- });
503
- return [
504
- 2,
505
- token.accessToken
506
- ];
466
+ descriptor = _state.sent();
467
+ throw new _typests.AuthRequiredError(descriptor);
507
468
  }
508
469
  });
509
470
  }).call(this);
@@ -555,86 +516,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
555
516
  return client;
556
517
  };
557
518
  /**
558
- * Authenticate new account with OAuth flow
559
- * Triggers account selection, stores token, registers account
560
- *
561
- * @returns Email address of newly authenticated account
562
- * @throws Error in headless mode (cannot open browser for OAuth)
563
- */ _proto.authenticateNewAccount = function authenticateNewAccount() {
564
- return _async_to_generator(function() {
565
- var _this_config, logger, headless, service, tokenStore, _ref, token, email;
566
- return _ts_generator(this, function(_state) {
567
- switch(_state.label){
568
- case 0:
569
- _this_config = this.config, logger = _this_config.logger, headless = _this_config.headless, service = _this_config.service, tokenStore = _this_config.tokenStore;
570
- if (headless) {
571
- throw new Error('Cannot authenticate new account in headless mode - interactive OAuth required');
572
- }
573
- logger.info('Starting new account authentication', {
574
- service: service
575
- });
576
- return [
577
- 4,
578
- this.performEphemeralOAuthFlow()
579
- ];
580
- case 1:
581
- _ref = _state.sent(), token = _ref.token, email = _ref.email;
582
- // Store token
583
- return [
584
- 4,
585
- (0, _oauth.setToken)(tokenStore, {
586
- accountId: email,
587
- service: service
588
- }, token)
589
- ];
590
- case 2:
591
- _state.sent();
592
- // Register account
593
- return [
594
- 4,
595
- (0, _oauth.addAccount)(tokenStore, {
596
- service: service,
597
- accountId: email
598
- })
599
- ];
600
- case 3:
601
- _state.sent();
602
- // Set as active account
603
- return [
604
- 4,
605
- (0, _oauth.setActiveAccount)(tokenStore, {
606
- service: service,
607
- accountId: email
608
- })
609
- ];
610
- case 4:
611
- _state.sent();
612
- // Store account metadata
613
- return [
614
- 4,
615
- (0, _oauth.setAccountInfo)(tokenStore, {
616
- service: service,
617
- accountId: email
618
- }, {
619
- email: email,
620
- addedAt: new Date().toISOString()
621
- })
622
- ];
623
- case 5:
624
- _state.sent();
625
- logger.info('New account authenticated', {
626
- service: service,
627
- email: email
628
- });
629
- return [
630
- 2,
631
- email
632
- ];
633
- }
634
- });
635
- }).call(this);
636
- };
637
- /**
638
519
  * Get user email from Google's userinfo endpoint (pure query)
639
520
  * Used to query email for existing authenticated account
640
521
  *
@@ -694,24 +575,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
694
575
  });
695
576
  }).call(this);
696
577
  };
697
- /**
698
- * Check for existing accounts in token storage (incremental OAuth detection)
699
- *
700
- * Uses key-utils helper for forward compatibility with key format changes.
701
- *
702
- * @returns Array of account IDs that have tokens for this service
703
- */ _proto.getExistingAccounts = function getExistingAccounts() {
704
- return _async_to_generator(function() {
705
- var _this_config, service, tokenStore;
706
- return _ts_generator(this, function(_state) {
707
- _this_config = this.config, service = _this_config.service, tokenStore = _this_config.tokenStore;
708
- return [
709
- 2,
710
- (0, _oauth.listAccountIds)(tokenStore, service)
711
- ];
712
- });
713
- }).call(this);
714
- };
715
578
  _proto.isTokenValid = function isTokenValid(token) {
716
579
  if (!token.expiresAt) return true; // No expiry = assume valid
717
580
  return Date.now() < token.expiresAt - 60000; // 1 minute buffer
@@ -769,240 +632,643 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
769
632
  });
770
633
  }).call(this);
771
634
  };
772
- _proto.performEphemeralOAuthFlow = function performEphemeralOAuthFlow() {
635
+ // ---------------------------------------------------------------------------
636
+ // Shared OAuth helpers
637
+ // ---------------------------------------------------------------------------
638
+ /**
639
+ * Build OAuth authorization URL with the "most parameters" baseline.
640
+ * This is shared by BOTH persistent (redirectUri) and ephemeral (loopback) modes.
641
+ */ _proto.buildAuthUrl = function buildAuthUrl(args) {
642
+ var _this_config = this.config, clientId = _this_config.clientId, scope = _this_config.scope;
643
+ var authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
644
+ authUrl.searchParams.set('client_id', clientId);
645
+ authUrl.searchParams.set('redirect_uri', args.redirectUri);
646
+ authUrl.searchParams.set('response_type', 'code');
647
+ authUrl.searchParams.set('scope', scope);
648
+ authUrl.searchParams.set('access_type', 'offline'); // always
649
+ authUrl.searchParams.set('prompt', 'consent'); // always
650
+ authUrl.searchParams.set('code_challenge', args.codeChallenge);
651
+ authUrl.searchParams.set('code_challenge_method', 'S256');
652
+ authUrl.searchParams.set('state', args.state);
653
+ return authUrl.toString();
654
+ };
655
+ /**
656
+ * Create a cached token + email from an authorization code.
657
+ * This is the shared callback handler for BOTH persistent and ephemeral modes.
658
+ */ _proto.handleAuthorizationCode = function handleAuthorizationCode(args) {
773
659
  return _async_to_generator(function() {
774
- var _this, _this_config, clientId, scope, headless, logger, configRedirectUri, targetHost, targetPort, targetProtocol, callbackPath, useConfiguredUri, parsed;
660
+ var tokenResponse, cachedToken, email;
775
661
  return _ts_generator(this, function(_state) {
776
- _this = this;
777
- _this_config = this.config, clientId = _this_config.clientId, scope = _this_config.scope, headless = _this_config.headless, logger = _this_config.logger, configRedirectUri = _this_config.redirectUri;
778
- // Parse redirectUri if provided to extract host, protocol, port, and path
779
- targetHost = 'localhost'; // Default: localhost (match registered redirect URI)
780
- targetPort = 0; // Default: OS-assigned ephemeral port
781
- targetProtocol = 'http:'; // Default: http
782
- callbackPath = '/callback'; // Default callback path
783
- useConfiguredUri = false;
784
- if (configRedirectUri) {
785
- try {
786
- parsed = new URL(configRedirectUri);
787
- // Use configured redirect URI as-is for production deployments
788
- targetHost = parsed.hostname;
789
- targetProtocol = parsed.protocol;
790
- // Extract port from URL (use default ports if not specified)
791
- if (parsed.port) {
792
- targetPort = Number.parseInt(parsed.port, 10);
793
- } else {
794
- targetPort = parsed.protocol === 'https:' ? 443 : 80;
662
+ switch(_state.label){
663
+ case 0:
664
+ return [
665
+ 4,
666
+ this.exchangeCodeForToken(args.code, args.codeVerifier, args.redirectUri)
667
+ ];
668
+ case 1:
669
+ tokenResponse = _state.sent();
670
+ // Build cached token
671
+ cachedToken = _object_spread({
672
+ accessToken: tokenResponse.access_token
673
+ }, tokenResponse.refresh_token !== undefined && {
674
+ refreshToken: tokenResponse.refresh_token
675
+ }, tokenResponse.expires_in !== undefined && {
676
+ expiresAt: Date.now() + tokenResponse.expires_in * 1000
677
+ }, tokenResponse.scope !== undefined && {
678
+ scope: tokenResponse.scope
679
+ });
680
+ return [
681
+ 4,
682
+ this.fetchUserEmailFromToken(tokenResponse.access_token)
683
+ ];
684
+ case 2:
685
+ email = _state.sent();
686
+ return [
687
+ 2,
688
+ {
689
+ email: email,
690
+ token: cachedToken
691
+ }
692
+ ];
693
+ }
694
+ });
695
+ }).call(this);
696
+ };
697
+ /**
698
+ * Store token + account metadata. Shared by BOTH persistent and ephemeral modes.
699
+ */ _proto.persistAuthResult = function persistAuthResult(args) {
700
+ return _async_to_generator(function() {
701
+ var _this_config, tokenStore, service;
702
+ return _ts_generator(this, function(_state) {
703
+ switch(_state.label){
704
+ case 0:
705
+ _this_config = this.config, tokenStore = _this_config.tokenStore, service = _this_config.service;
706
+ return [
707
+ 4,
708
+ (0, _oauth.setToken)(tokenStore, {
709
+ accountId: args.email,
710
+ service: service
711
+ }, args.token)
712
+ ];
713
+ case 1:
714
+ _state.sent();
715
+ return [
716
+ 4,
717
+ (0, _oauth.addAccount)(tokenStore, {
718
+ service: service,
719
+ accountId: args.email
720
+ })
721
+ ];
722
+ case 2:
723
+ _state.sent();
724
+ return [
725
+ 4,
726
+ (0, _oauth.setActiveAccount)(tokenStore, {
727
+ service: service,
728
+ accountId: args.email
729
+ })
730
+ ];
731
+ case 3:
732
+ _state.sent();
733
+ return [
734
+ 4,
735
+ (0, _oauth.setAccountInfo)(tokenStore, {
736
+ service: service,
737
+ accountId: args.email
738
+ }, {
739
+ email: args.email,
740
+ addedAt: new Date().toISOString()
741
+ })
742
+ ];
743
+ case 4:
744
+ _state.sent();
745
+ return [
746
+ 2
747
+ ];
748
+ }
749
+ });
750
+ }).call(this);
751
+ };
752
+ /**
753
+ * Pending auth (PKCE verifier) key format.
754
+ * Keep as-is (confirmed), even though it's not a public contract.
755
+ */ _proto.pendingKey = function pendingKey(state) {
756
+ return "".concat(this.config.service, ":pending:").concat(state);
757
+ };
758
+ /**
759
+ * Store PKCE verifier for callback (5 minute TTL).
760
+ * Shared by BOTH persistent and ephemeral modes.
761
+ */ _proto.createPendingAuth = function createPendingAuth(args) {
762
+ return _async_to_generator(function() {
763
+ var tokenStore, record;
764
+ return _ts_generator(this, function(_state) {
765
+ switch(_state.label){
766
+ case 0:
767
+ tokenStore = this.config.tokenStore;
768
+ record = {
769
+ codeVerifier: args.codeVerifier,
770
+ createdAt: Date.now()
771
+ };
772
+ return [
773
+ 4,
774
+ tokenStore.set(this.pendingKey(args.state), record, OAUTH_TIMEOUT_MS)
775
+ ];
776
+ case 1:
777
+ _state.sent();
778
+ return [
779
+ 2
780
+ ];
781
+ }
782
+ });
783
+ }).call(this);
784
+ };
785
+ /**
786
+ * Load and validate pending auth state (5 minute TTL).
787
+ * Shared by BOTH persistent and ephemeral modes.
788
+ */ _proto.readAndValidatePendingAuth = function readAndValidatePendingAuth(state) {
789
+ return _async_to_generator(function() {
790
+ var tokenStore, pendingAuth;
791
+ return _ts_generator(this, function(_state) {
792
+ switch(_state.label){
793
+ case 0:
794
+ tokenStore = this.config.tokenStore;
795
+ return [
796
+ 4,
797
+ tokenStore.get(this.pendingKey(state))
798
+ ];
799
+ case 1:
800
+ pendingAuth = _state.sent();
801
+ if (!pendingAuth) {
802
+ throw new Error('Invalid or expired OAuth state. Please try again.');
795
803
  }
796
- // Extract path (default to /callback if URL has no path or just '/')
797
- if (parsed.pathname && parsed.pathname !== '/') {
798
- callbackPath = parsed.pathname;
804
+ if (!(Date.now() - pendingAuth.createdAt > OAUTH_TIMEOUT_MS)) return [
805
+ 3,
806
+ 3
807
+ ];
808
+ return [
809
+ 4,
810
+ tokenStore.delete(this.pendingKey(state))
811
+ ];
812
+ case 2:
813
+ _state.sent();
814
+ throw new Error('OAuth state expired. Please try again.');
815
+ case 3:
816
+ return [
817
+ 2,
818
+ pendingAuth
819
+ ];
820
+ }
821
+ });
822
+ }).call(this);
823
+ };
824
+ /**
825
+ * Mark pending auth as completed (used by middleware polling).
826
+ */ _proto.markPendingComplete = function markPendingComplete(args) {
827
+ return _async_to_generator(function() {
828
+ var tokenStore, updated;
829
+ return _ts_generator(this, function(_state) {
830
+ switch(_state.label){
831
+ case 0:
832
+ tokenStore = this.config.tokenStore;
833
+ updated = _object_spread_props(_object_spread({}, args.pending), {
834
+ completedAt: Date.now(),
835
+ email: args.email
836
+ });
837
+ return [
838
+ 4,
839
+ tokenStore.set(this.pendingKey(args.state), updated, OAUTH_TIMEOUT_MS)
840
+ ];
841
+ case 1:
842
+ _state.sent();
843
+ return [
844
+ 2
845
+ ];
846
+ }
847
+ });
848
+ }).call(this);
849
+ };
850
+ /**
851
+ * Clean up pending auth state.
852
+ */ _proto.deletePendingAuth = function deletePendingAuth(state) {
853
+ return _async_to_generator(function() {
854
+ var tokenStore;
855
+ return _ts_generator(this, function(_state) {
856
+ switch(_state.label){
857
+ case 0:
858
+ tokenStore = this.config.tokenStore;
859
+ return [
860
+ 4,
861
+ tokenStore.delete(this.pendingKey(state))
862
+ ];
863
+ case 1:
864
+ _state.sent();
865
+ return [
866
+ 2
867
+ ];
868
+ }
869
+ });
870
+ }).call(this);
871
+ };
872
+ /**
873
+ * Wait until pending auth is marked completed (or timeout).
874
+ * Used by middleware after opening auth URL in non-headless mode.
875
+ */ _proto.waitForOAuthCompletion = function waitForOAuthCompletion(state) {
876
+ return _async_to_generator(function() {
877
+ var tokenStore, key, start, pending;
878
+ return _ts_generator(this, function(_state) {
879
+ switch(_state.label){
880
+ case 0:
881
+ tokenStore = this.config.tokenStore;
882
+ key = this.pendingKey(state);
883
+ start = Date.now();
884
+ _state.label = 1;
885
+ case 1:
886
+ if (!(Date.now() - start < OAUTH_TIMEOUT_MS)) return [
887
+ 3,
888
+ 4
889
+ ];
890
+ return [
891
+ 4,
892
+ tokenStore.get(key)
893
+ ];
894
+ case 2:
895
+ pending = _state.sent();
896
+ if (pending === null || pending === void 0 ? void 0 : pending.completedAt) {
897
+ return [
898
+ 2,
899
+ {
900
+ email: pending.email
901
+ }
902
+ ];
799
903
  }
800
- useConfiguredUri = true;
801
- logger.debug('Using configured redirect URI', {
802
- host: targetHost,
803
- protocol: targetProtocol,
804
- port: targetPort,
805
- path: callbackPath,
806
- redirectUri: configRedirectUri
904
+ return [
905
+ 4,
906
+ new Promise(function(r) {
907
+ return setTimeout(r, OAUTH_POLL_MS);
908
+ })
909
+ ];
910
+ case 3:
911
+ _state.sent();
912
+ return [
913
+ 3,
914
+ 1
915
+ ];
916
+ case 4:
917
+ throw new Error('OAuth flow timed out after 5 minutes');
918
+ }
919
+ });
920
+ }).call(this);
921
+ };
922
+ /**
923
+ * Process an OAuth callback using shared state validation + token exchange + persistence.
924
+ * Used by BOTH:
925
+ * - ephemeral loopback server callback handler
926
+ * - persistent redirectUri callback handler
927
+ *
928
+ * IMPORTANT CHANGE:
929
+ * - We do NOT delete pending state here anymore.
930
+ * - We mark it completed so middleware can poll and then clean it up.
931
+ */ _proto.processOAuthCallback = function processOAuthCallback(args) {
932
+ return _async_to_generator(function() {
933
+ var _this_config, logger, service, pending, result;
934
+ return _ts_generator(this, function(_state) {
935
+ switch(_state.label){
936
+ case 0:
937
+ _this_config = this.config, logger = _this_config.logger, service = _this_config.service;
938
+ return [
939
+ 4,
940
+ this.readAndValidatePendingAuth(args.state)
941
+ ];
942
+ case 1:
943
+ pending = _state.sent();
944
+ logger.info('Processing OAuth callback', {
945
+ service: service,
946
+ state: args.state
807
947
  });
808
- } catch (error) {
809
- logger.warn('Failed to parse redirectUri, using ephemeral defaults', {
810
- redirectUri: configRedirectUri,
811
- error: _instanceof(error, Error) ? error.message : String(error)
948
+ return [
949
+ 4,
950
+ this.handleAuthorizationCode({
951
+ code: args.code,
952
+ codeVerifier: pending.codeVerifier,
953
+ redirectUri: args.redirectUri
954
+ })
955
+ ];
956
+ case 2:
957
+ result = _state.sent();
958
+ return [
959
+ 4,
960
+ this.persistAuthResult(result)
961
+ ];
962
+ case 3:
963
+ _state.sent();
964
+ return [
965
+ 4,
966
+ this.markPendingComplete({
967
+ state: args.state,
968
+ email: result.email,
969
+ pending: pending
970
+ })
971
+ ];
972
+ case 4:
973
+ _state.sent();
974
+ logger.info('OAuth callback completed', {
975
+ service: service,
976
+ email: result.email
812
977
  });
813
- // Continue with defaults (127.0.0.1, port 0, http, /callback)
814
- }
978
+ return [
979
+ 2,
980
+ result
981
+ ];
815
982
  }
816
- return [
817
- 2,
818
- new Promise(function(resolve, reject) {
819
- // Generate PKCE challenge
820
- var _generatePKCE = (0, _oauth.generatePKCE)(), codeVerifier = _generatePKCE.verifier, codeChallenge = _generatePKCE.challenge;
821
- var server = null;
822
- var serverPort;
823
- var finalRedirectUri; // Will be set in server.listen callback
824
- // Create ephemeral server with OS-assigned port (RFC 8252)
825
- server = _http.createServer(function(req, res) {
826
- return _async_to_generator(function() {
827
- var url, code, _$error, tokenResponse, cachedToken, email, exchangeError;
828
- return _ts_generator(this, function(_state) {
829
- switch(_state.label){
830
- case 0:
831
- if (!req.url) {
832
- res.writeHead(400, {
833
- 'Content-Type': 'text/html'
834
- });
835
- res.end((0, _oauth.getErrorTemplate)('Invalid request'));
836
- server === null || server === void 0 ? void 0 : server.close();
837
- reject(new Error('Invalid request: missing URL'));
838
- return [
839
- 2
840
- ];
841
- }
842
- url = new URL(req.url, "http://127.0.0.1:".concat(serverPort));
843
- if (!(url.pathname === callbackPath)) return [
844
- 3,
845
- 6
846
- ];
847
- code = url.searchParams.get('code');
848
- _$error = url.searchParams.get('error');
849
- if (_$error) {
850
- res.writeHead(400, {
851
- 'Content-Type': 'text/html'
852
- });
853
- res.end((0, _oauth.getErrorTemplate)(_$error));
854
- server === null || server === void 0 ? void 0 : server.close();
855
- reject(new Error("OAuth error: ".concat(_$error)));
856
- return [
857
- 2
858
- ];
859
- }
860
- if (!code) {
861
- res.writeHead(400, {
862
- 'Content-Type': 'text/html'
863
- });
864
- res.end((0, _oauth.getErrorTemplate)('No authorization code received'));
865
- server === null || server === void 0 ? void 0 : server.close();
866
- reject(new Error('No authorization code received'));
867
- return [
868
- 2
869
- ];
870
- }
871
- _state.label = 1;
872
- case 1:
873
- _state.trys.push([
874
- 1,
875
- 4,
876
- ,
877
- 5
878
- ]);
879
- return [
880
- 4,
881
- this.exchangeCodeForToken(code, codeVerifier, finalRedirectUri)
882
- ];
883
- case 2:
884
- tokenResponse = _state.sent();
885
- // Build cached token
886
- cachedToken = _object_spread({
887
- accessToken: tokenResponse.access_token
888
- }, tokenResponse.refresh_token !== undefined && {
889
- refreshToken: tokenResponse.refresh_token
890
- }, tokenResponse.expires_in !== undefined && {
891
- expiresAt: Date.now() + tokenResponse.expires_in * 1000
892
- }, tokenResponse.scope !== undefined && {
893
- scope: tokenResponse.scope
894
- });
895
- return [
896
- 4,
897
- this.fetchUserEmailFromToken(tokenResponse.access_token)
898
- ];
899
- case 3:
900
- email = _state.sent();
901
- res.writeHead(200, {
902
- 'Content-Type': 'text/html'
903
- });
904
- res.end((0, _oauth.getSuccessTemplate)());
905
- server === null || server === void 0 ? void 0 : server.close();
906
- resolve({
907
- token: cachedToken,
908
- email: email
909
- });
910
- return [
911
- 3,
912
- 5
913
- ];
914
- case 4:
915
- exchangeError = _state.sent();
916
- logger.error('Token exchange failed', {
917
- error: _instanceof(exchangeError, Error) ? exchangeError.message : String(exchangeError)
918
- });
919
- res.writeHead(500, {
920
- 'Content-Type': 'text/html'
921
- });
922
- res.end((0, _oauth.getErrorTemplate)('Token exchange failed'));
923
- server === null || server === void 0 ? void 0 : server.close();
924
- reject(exchangeError);
925
- return [
926
- 3,
927
- 5
928
- ];
929
- case 5:
930
- return [
931
- 3,
932
- 7
933
- ];
934
- case 6:
935
- res.writeHead(404, {
936
- 'Content-Type': 'text/plain'
937
- });
938
- res.end('Not Found');
939
- _state.label = 7;
940
- case 7:
941
- return [
942
- 2
943
- ];
944
- }
983
+ });
984
+ }).call(this);
985
+ };
986
+ // ---------------------------------------------------------------------------
987
+ // Ephemeral loopback server + flow
988
+ // ---------------------------------------------------------------------------
989
+ /**
990
+ * Loopback OAuth server helper (RFC 8252 Section 7.3)
991
+ *
992
+ * Implements ephemeral local server with OS-assigned port (RFC 8252 Section 8.3).
993
+ * Shared callback handling uses:
994
+ * - the same authUrl builder as redirectUri mode
995
+ * - the same pending PKCE verifier storage as redirectUri mode
996
+ * - the same callback processor as redirectUri mode
997
+ */ _proto.createOAuthCallbackServer = function createOAuthCallbackServer(args) {
998
+ var _this = this;
999
+ var logger = this.config.logger;
1000
+ // Create ephemeral server with OS-assigned port (RFC 8252)
1001
+ return _http.createServer(function(req, res) {
1002
+ return _async_to_generator(function() {
1003
+ var url, code, error, state, exchangeError, outerError;
1004
+ return _ts_generator(this, function(_state) {
1005
+ switch(_state.label){
1006
+ case 0:
1007
+ _state.trys.push([
1008
+ 0,
1009
+ 5,
1010
+ ,
1011
+ 6
1012
+ ]);
1013
+ if (!req.url) {
1014
+ res.writeHead(400, {
1015
+ 'Content-Type': 'text/html'
945
1016
  });
946
- }).call(_this);
947
- });
948
- // Listen on targetPort (0 for OS assignment, or custom port from redirectUri)
949
- server.listen(targetPort, targetHost, function() {
950
- var address = server === null || server === void 0 ? void 0 : server.address();
951
- if (!address || typeof address === 'string') {
952
- server === null || server === void 0 ? void 0 : server.close();
953
- reject(new Error('Failed to start ephemeral server'));
954
- return;
1017
+ res.end((0, _oauth.getErrorTemplate)('Invalid request'));
1018
+ args.onError(new Error('Invalid request: missing URL'));
1019
+ return [
1020
+ 2
1021
+ ];
1022
+ }
1023
+ // Use loopback base for URL parsing (port is not important for parsing path/query)
1024
+ url = new URL(req.url, 'http://127.0.0.1');
1025
+ if (url.pathname !== args.callbackPath) {
1026
+ res.writeHead(404, {
1027
+ 'Content-Type': 'text/plain'
1028
+ });
1029
+ res.end('Not Found');
1030
+ return [
1031
+ 2
1032
+ ];
1033
+ }
1034
+ code = url.searchParams.get('code');
1035
+ error = url.searchParams.get('error');
1036
+ state = url.searchParams.get('state');
1037
+ if (error) {
1038
+ res.writeHead(400, {
1039
+ 'Content-Type': 'text/html'
1040
+ });
1041
+ res.end((0, _oauth.getErrorTemplate)(error));
1042
+ args.onError(new Error("OAuth error: ".concat(error)));
1043
+ return [
1044
+ 2
1045
+ ];
955
1046
  }
956
- serverPort = address.port;
957
- // Construct final redirect URI
958
- if (useConfiguredUri && configRedirectUri) {
959
- // Use configured redirect URI as-is for production
960
- finalRedirectUri = configRedirectUri;
961
- } else {
962
- // Construct ephemeral redirect URI with actual server port
963
- finalRedirectUri = "".concat(targetProtocol, "//").concat(targetHost, ":").concat(serverPort).concat(callbackPath);
1047
+ if (!code) {
1048
+ res.writeHead(400, {
1049
+ 'Content-Type': 'text/html'
1050
+ });
1051
+ res.end((0, _oauth.getErrorTemplate)('No authorization code received'));
1052
+ args.onError(new Error('No authorization code received'));
1053
+ return [
1054
+ 2
1055
+ ];
964
1056
  }
965
- // Build auth URL
966
- var authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
967
- authUrl.searchParams.set('client_id', clientId);
968
- authUrl.searchParams.set('redirect_uri', finalRedirectUri);
969
- authUrl.searchParams.set('response_type', 'code');
970
- authUrl.searchParams.set('scope', scope);
971
- authUrl.searchParams.set('access_type', 'offline');
972
- authUrl.searchParams.set('prompt', 'consent');
973
- authUrl.searchParams.set('code_challenge', codeChallenge);
974
- authUrl.searchParams.set('code_challenge_method', 'S256');
975
- logger.info('Ephemeral OAuth server started', {
976
- port: serverPort,
977
- headless: headless
1057
+ if (!state) {
1058
+ res.writeHead(400, {
1059
+ 'Content-Type': 'text/html'
1060
+ });
1061
+ res.end((0, _oauth.getErrorTemplate)('Missing state parameter in OAuth callback'));
1062
+ args.onError(new Error('Missing state parameter in OAuth callback'));
1063
+ return [
1064
+ 2
1065
+ ];
1066
+ }
1067
+ _state.label = 1;
1068
+ case 1:
1069
+ _state.trys.push([
1070
+ 1,
1071
+ 3,
1072
+ ,
1073
+ 4
1074
+ ]);
1075
+ return [
1076
+ 4,
1077
+ this.processOAuthCallback({
1078
+ code: code,
1079
+ state: state,
1080
+ redirectUri: args.finalRedirectUri()
1081
+ })
1082
+ ];
1083
+ case 2:
1084
+ _state.sent();
1085
+ res.writeHead(200, {
1086
+ 'Content-Type': 'text/html'
978
1087
  });
979
- if (headless) {
980
- // Headless mode: Print auth URL to stderr (stdout is MCP protocol)
981
- console.error('\n🔐 OAuth Authorization Required');
982
- console.error('📋 Please visit this URL in your browser:\n');
983
- console.error(" ".concat(authUrl.toString(), "\n"));
984
- console.error('⏳ Waiting for authorization...\n');
985
- } else {
986
- // Interactive mode: Open browser automatically
987
- logger.info('Opening browser for OAuth authorization');
988
- (0, _open.default)(authUrl.toString()).catch(function(error) {
989
- logger.info('Failed to open browser automatically', {
990
- error: error.message
991
- });
992
- console.error('\n🔐 OAuth Authorization Required');
993
- console.error(" ".concat(authUrl.toString(), "\n"));
1088
+ res.end((0, _oauth.getSuccessTemplate)());
1089
+ args.onDone();
1090
+ return [
1091
+ 3,
1092
+ 4
1093
+ ];
1094
+ case 3:
1095
+ exchangeError = _state.sent();
1096
+ logger.error('Token exchange failed', {
1097
+ error: _instanceof(exchangeError, Error) ? exchangeError.message : String(exchangeError)
1098
+ });
1099
+ res.writeHead(500, {
1100
+ 'Content-Type': 'text/html'
1101
+ });
1102
+ res.end((0, _oauth.getErrorTemplate)('Token exchange failed'));
1103
+ args.onError(exchangeError);
1104
+ return [
1105
+ 3,
1106
+ 4
1107
+ ];
1108
+ case 4:
1109
+ return [
1110
+ 3,
1111
+ 6
1112
+ ];
1113
+ case 5:
1114
+ outerError = _state.sent();
1115
+ logger.error('OAuth callback server error', {
1116
+ error: _instanceof(outerError, Error) ? outerError.message : String(outerError)
1117
+ });
1118
+ res.writeHead(500, {
1119
+ 'Content-Type': 'text/html'
1120
+ });
1121
+ res.end((0, _oauth.getErrorTemplate)('Internal server error'));
1122
+ args.onError(outerError);
1123
+ return [
1124
+ 3,
1125
+ 6
1126
+ ];
1127
+ case 6:
1128
+ return [
1129
+ 2
1130
+ ];
1131
+ }
1132
+ });
1133
+ }).call(_this);
1134
+ });
1135
+ };
1136
+ /**
1137
+ * Starts the ephemeral loopback server and returns an AuthRequiredError(auth_url).
1138
+ * Middleware will open+poll and then retry in the same call.
1139
+ */ _proto.startEphemeralOAuthFlow = function startEphemeralOAuthFlow() {
1140
+ return _async_to_generator(function() {
1141
+ var _this, _this_config, headless, logger, configRedirectUri, service, tokenStore, listenHost, listenPort, callbackPath, useConfiguredUri, parsed, isLoopback, envPort, _generatePKCE, codeVerifier, codeChallenge, stateId, server, serverPort, finalRedirectUri, authUrl;
1142
+ return _ts_generator(this, function(_state) {
1143
+ switch(_state.label){
1144
+ case 0:
1145
+ _this = this;
1146
+ _this_config = this.config, headless = _this_config.headless, logger = _this_config.logger, configRedirectUri = _this_config.redirectUri, service = _this_config.service, tokenStore = _this_config.tokenStore;
1147
+ // Server listen configuration (where ephemeral server binds)
1148
+ listenHost = 'localhost'; // Default: localhost for ephemeral loopback
1149
+ listenPort = 0; // Default: OS-assigned ephemeral port
1150
+ // Redirect URI configuration (what goes in auth URL and token exchange)
1151
+ callbackPath = '/callback'; // Default callback path
1152
+ useConfiguredUri = false;
1153
+ if (configRedirectUri) {
1154
+ try {
1155
+ parsed = new URL(configRedirectUri);
1156
+ isLoopback = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
1157
+ if (isLoopback) {
1158
+ // Local development: Listen on specific loopback address/port
1159
+ listenHost = parsed.hostname;
1160
+ listenPort = parsed.port ? Number.parseInt(parsed.port, 10) : 0;
1161
+ } else {
1162
+ // Cloud deployment: Listen on 0.0.0.0 with PORT from environment
1163
+ // The redirectUri is the PUBLIC URL (e.g., https://example.com/oauth/callback)
1164
+ // The server listens on 0.0.0.0:PORT and the load balancer routes to it
1165
+ listenHost = '0.0.0.0';
1166
+ envPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : undefined;
1167
+ listenPort = envPort && Number.isFinite(envPort) ? envPort : 8080;
1168
+ }
1169
+ // Extract callback path from URL
1170
+ if (parsed.pathname && parsed.pathname !== '/') {
1171
+ callbackPath = parsed.pathname;
1172
+ }
1173
+ useConfiguredUri = true;
1174
+ logger.debug('Using configured redirect URI', {
1175
+ listenHost: listenHost,
1176
+ listenPort: listenPort,
1177
+ callbackPath: callbackPath,
1178
+ redirectUri: configRedirectUri,
1179
+ isLoopback: isLoopback
1180
+ });
1181
+ } catch (error) {
1182
+ logger.warn('Failed to parse redirectUri, using ephemeral defaults', {
1183
+ redirectUri: configRedirectUri,
1184
+ error: _instanceof(error, Error) ? error.message : String(error)
994
1185
  });
1186
+ // Continue with defaults (localhost, port 0, http, /callback)
1187
+ }
1188
+ }
1189
+ // Generate PKCE challenge + state
1190
+ _generatePKCE = (0, _oauth.generatePKCE)(), codeVerifier = _generatePKCE.verifier, codeChallenge = _generatePKCE.challenge;
1191
+ stateId = (0, _crypto.randomUUID)();
1192
+ // Store PKCE verifier for callback (5 minute TTL)
1193
+ return [
1194
+ 4,
1195
+ this.createPendingAuth({
1196
+ state: stateId,
1197
+ codeVerifier: codeVerifier
1198
+ })
1199
+ ];
1200
+ case 1:
1201
+ _state.sent();
1202
+ server = null;
1203
+ // Create ephemeral server with OS-assigned port (RFC 8252)
1204
+ server = this.createOAuthCallbackServer({
1205
+ callbackPath: callbackPath,
1206
+ finalRedirectUri: function() {
1207
+ return finalRedirectUri;
1208
+ },
1209
+ onDone: function() {
1210
+ server === null || server === void 0 ? void 0 : server.close();
1211
+ },
1212
+ onError: function(err) {
1213
+ logger.error('Ephemeral OAuth server error', {
1214
+ error: _instanceof(err, Error) ? err.message : String(err)
1215
+ });
1216
+ server === null || server === void 0 ? void 0 : server.close();
995
1217
  }
996
1218
  });
997
- // Timeout after 5 minutes
1219
+ // Start listening
1220
+ return [
1221
+ 4,
1222
+ new Promise(function(resolve, reject) {
1223
+ server === null || server === void 0 ? void 0 : server.listen(listenPort, listenHost, function() {
1224
+ var address = server === null || server === void 0 ? void 0 : server.address();
1225
+ if (!address || typeof address === 'string') {
1226
+ server === null || server === void 0 ? void 0 : server.close();
1227
+ reject(new Error('Failed to start ephemeral server'));
1228
+ return;
1229
+ }
1230
+ serverPort = address.port;
1231
+ // Construct final redirect URI
1232
+ if (useConfiguredUri && configRedirectUri) {
1233
+ finalRedirectUri = configRedirectUri;
1234
+ } else {
1235
+ finalRedirectUri = "http://localhost:".concat(serverPort).concat(callbackPath);
1236
+ }
1237
+ logger.info('Ephemeral OAuth server started', {
1238
+ port: serverPort,
1239
+ headless: headless,
1240
+ service: service
1241
+ });
1242
+ resolve();
1243
+ });
1244
+ })
1245
+ ];
1246
+ case 2:
1247
+ _state.sent();
1248
+ // Timeout after 5 minutes (match middleware polling timeout)
998
1249
  setTimeout(function() {
999
1250
  if (server) {
1000
1251
  server.close();
1001
- reject(new Error('OAuth flow timed out after 5 minutes'));
1252
+ // Best-effort cleanup if user never completes flow:
1253
+ // delete pending so a future attempt can restart cleanly.
1254
+ void tokenStore.delete(_this.pendingKey(stateId));
1002
1255
  }
1003
- }, 5 * 60 * 1000);
1004
- })
1005
- ];
1256
+ }, OAUTH_TIMEOUT_MS);
1257
+ // Build auth URL - SAME helper as persistent mode
1258
+ authUrl = this.buildAuthUrl({
1259
+ redirectUri: finalRedirectUri,
1260
+ codeChallenge: codeChallenge,
1261
+ state: stateId
1262
+ });
1263
+ return [
1264
+ 2,
1265
+ {
1266
+ kind: 'auth_url',
1267
+ provider: service,
1268
+ url: authUrl
1269
+ }
1270
+ ];
1271
+ }
1006
1272
  });
1007
1273
  }).call(this);
1008
1274
  };
@@ -1125,6 +1391,44 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1125
1391
  }).call(this);
1126
1392
  };
1127
1393
  /**
1394
+ * Handle OAuth callback from persistent endpoint.
1395
+ * Used by HTTP servers with configured redirectUri.
1396
+ *
1397
+ * @param params - OAuth callback parameters
1398
+ * @returns Email and cached token
1399
+ */ _proto.handleOAuthCallback = function handleOAuthCallback(params) {
1400
+ return _async_to_generator(function() {
1401
+ var code, state, redirectUri;
1402
+ return _ts_generator(this, function(_state) {
1403
+ switch(_state.label){
1404
+ case 0:
1405
+ code = params.code, state = params.state;
1406
+ redirectUri = this.config.redirectUri;
1407
+ if (!state) {
1408
+ throw new Error('Missing state parameter in OAuth callback');
1409
+ }
1410
+ if (!redirectUri) {
1411
+ throw new Error('handleOAuthCallback requires configured redirectUri');
1412
+ }
1413
+ return [
1414
+ 4,
1415
+ this.processOAuthCallback({
1416
+ code: code,
1417
+ state: state,
1418
+ redirectUri: redirectUri
1419
+ })
1420
+ ];
1421
+ case 1:
1422
+ // Shared callback processor (same code path as ephemeral)
1423
+ return [
1424
+ 2,
1425
+ _state.sent()
1426
+ ];
1427
+ }
1428
+ });
1429
+ }).call(this);
1430
+ };
1431
+ /**
1128
1432
  * Create authentication middleware for MCP tools, resources, and prompts
1129
1433
  *
1130
1434
  * Returns position-aware middleware wrappers that enrich handlers with authentication context.
@@ -1138,15 +1442,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1138
1442
  * All requests use token lookups based on the active account or account override.
1139
1443
  *
1140
1444
  * @returns Object with withToolAuth, withResourceAuth, withPromptAuth methods
1141
- *
1142
- * @example
1143
- * ```typescript
1144
- * const loopback = new LoopbackOAuthProvider({ service: 'gmail', ... });
1145
- * const authMiddleware = loopback.authMiddleware();
1146
- * const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);
1147
- * const resources = resourceFactories.map(f => f()).map(authMiddleware.withResourceAuth);
1148
- * const prompts = promptFactories.map(f => f()).map(authMiddleware.withPromptAuth);
1149
- * ```
1150
1445
  */ _proto.authMiddleware = function authMiddleware() {
1151
1446
  var _this = this;
1152
1447
  var _this_config = this.config, service = _this_config.service, tokenStore = _this_config.tokenStore, logger = _this_config.logger;
@@ -1161,96 +1456,173 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1161
1456
  allArgs[_key] = arguments[_key];
1162
1457
  }
1163
1458
  return _async_to_generator(function() {
1164
- var extra, accountId, _ref, _extra__meta, _tmp, error, effectiveAccountId, _tmp1, auth, error1, authRequiredResponse;
1459
+ var _this, extra, ensureAuthenticatedOrThrow, effectiveAccountId, auth, error, authRequiredResponse;
1165
1460
  return _ts_generator(this, function(_state) {
1166
1461
  switch(_state.label){
1167
1462
  case 0:
1463
+ _this = this;
1168
1464
  // Extract extra from the correct position
1169
1465
  extra = allArgs[extraPosition];
1466
+ // Helper: retry once after open+poll completes
1467
+ ensureAuthenticatedOrThrow = function() {
1468
+ return _async_to_generator(function() {
1469
+ var accountId, _ref, _extra__meta, _tmp, error, effectiveAccountId, _tmp1, error1, authUrl, state;
1470
+ return _ts_generator(this, function(_state) {
1471
+ switch(_state.label){
1472
+ case 0:
1473
+ _state.trys.push([
1474
+ 0,
1475
+ 11,
1476
+ ,
1477
+ 16
1478
+ ]);
1479
+ _state.label = 1;
1480
+ case 1:
1481
+ _state.trys.push([
1482
+ 1,
1483
+ 5,
1484
+ ,
1485
+ 6
1486
+ ]);
1487
+ if (!((_ref = (_extra__meta = extra._meta) === null || _extra__meta === void 0 ? void 0 : _extra__meta.accountId) !== null && _ref !== void 0)) return [
1488
+ 3,
1489
+ 2
1490
+ ];
1491
+ _tmp = _ref;
1492
+ return [
1493
+ 3,
1494
+ 4
1495
+ ];
1496
+ case 2:
1497
+ return [
1498
+ 4,
1499
+ (0, _oauth.getActiveAccount)(tokenStore, {
1500
+ service: service
1501
+ })
1502
+ ];
1503
+ case 3:
1504
+ _tmp = _state.sent();
1505
+ _state.label = 4;
1506
+ case 4:
1507
+ accountId = _tmp;
1508
+ return [
1509
+ 3,
1510
+ 6
1511
+ ];
1512
+ case 5:
1513
+ error = _state.sent();
1514
+ if (_instanceof(error, Error) && (error.code === 'REQUIRES_AUTHENTICATION' || error.name === 'AccountManagerError')) {
1515
+ accountId = undefined;
1516
+ } else {
1517
+ throw error;
1518
+ }
1519
+ return [
1520
+ 3,
1521
+ 6
1522
+ ];
1523
+ case 6:
1524
+ return [
1525
+ 4,
1526
+ this.getAccessToken(accountId)
1527
+ ];
1528
+ case 7:
1529
+ _state.sent();
1530
+ if (!(accountId !== null && accountId !== void 0)) return [
1531
+ 3,
1532
+ 8
1533
+ ];
1534
+ _tmp1 = accountId;
1535
+ return [
1536
+ 3,
1537
+ 10
1538
+ ];
1539
+ case 8:
1540
+ return [
1541
+ 4,
1542
+ (0, _oauth.getActiveAccount)(tokenStore, {
1543
+ service: service
1544
+ })
1545
+ ];
1546
+ case 9:
1547
+ _tmp1 = _state.sent();
1548
+ _state.label = 10;
1549
+ case 10:
1550
+ effectiveAccountId = _tmp1;
1551
+ if (!effectiveAccountId) {
1552
+ throw new Error("No account found after OAuth flow for service ".concat(service));
1553
+ }
1554
+ return [
1555
+ 2,
1556
+ effectiveAccountId
1557
+ ];
1558
+ case 11:
1559
+ error1 = _state.sent();
1560
+ if (!(_instanceof(error1, _typests.AuthRequiredError) && error1.descriptor.kind === 'auth_url')) return [
1561
+ 3,
1562
+ 15
1563
+ ];
1564
+ // Headless: don't open/poll; just propagate to outer handler to return auth_required.
1565
+ if (this.config.headless) throw error1;
1566
+ // Non-headless: open once + poll until callback completes, then retry token acquisition.
1567
+ authUrl = new URL(error1.descriptor.url);
1568
+ state = authUrl.searchParams.get('state');
1569
+ if (!state) throw new Error('Auth URL missing state parameter');
1570
+ if (!this.openedStates.has(state)) {
1571
+ this.openedStates.add(state);
1572
+ (0, _open.default)(error1.descriptor.url).catch(function(e) {
1573
+ logger.info('Failed to open browser automatically', {
1574
+ error: _instanceof(e, Error) ? e.message : String(e)
1575
+ });
1576
+ });
1577
+ }
1578
+ // Block until callback completes (or timeout)
1579
+ return [
1580
+ 4,
1581
+ this.waitForOAuthCompletion(state)
1582
+ ];
1583
+ case 12:
1584
+ _state.sent();
1585
+ // Cleanup pending state after we observe completion
1586
+ return [
1587
+ 4,
1588
+ this.deletePendingAuth(state)
1589
+ ];
1590
+ case 13:
1591
+ _state.sent();
1592
+ return [
1593
+ 4,
1594
+ ensureAuthenticatedOrThrow()
1595
+ ];
1596
+ case 14:
1597
+ // Retry after completion
1598
+ return [
1599
+ 2,
1600
+ _state.sent()
1601
+ ];
1602
+ case 15:
1603
+ throw error1;
1604
+ case 16:
1605
+ return [
1606
+ 2
1607
+ ];
1608
+ }
1609
+ });
1610
+ }).call(_this);
1611
+ };
1170
1612
  _state.label = 1;
1171
1613
  case 1:
1172
1614
  _state.trys.push([
1173
1615
  1,
1174
- 13,
1175
- ,
1176
- 14
1177
- ]);
1178
- _state.label = 2;
1179
- case 2:
1180
- _state.trys.push([
1181
- 2,
1182
- 6,
1616
+ 4,
1183
1617
  ,
1184
- 7
1185
- ]);
1186
- if (!((_ref = (_extra__meta = extra._meta) === null || _extra__meta === void 0 ? void 0 : _extra__meta.accountId) !== null && _ref !== void 0)) return [
1187
- 3,
1188
- 3
1189
- ];
1190
- _tmp = _ref;
1191
- return [
1192
- 3,
1193
1618
  5
1194
- ];
1195
- case 3:
1196
- return [
1197
- 4,
1198
- (0, _oauth.getActiveAccount)(tokenStore, {
1199
- service: service
1200
- })
1201
- ];
1202
- case 4:
1203
- _tmp = _state.sent();
1204
- _state.label = 5;
1205
- case 5:
1206
- accountId = _tmp;
1207
- return [
1208
- 3,
1209
- 7
1210
- ];
1211
- case 6:
1212
- error = _state.sent();
1213
- if (_instanceof(error, Error) && (error.code === 'REQUIRES_AUTHENTICATION' || error.name === 'AccountManagerError')) {
1214
- accountId = undefined;
1215
- } else {
1216
- throw error;
1217
- }
1218
- return [
1219
- 3,
1220
- 7
1221
- ];
1222
- case 7:
1223
- // Eagerly validate token exists or trigger OAuth flow
1619
+ ]);
1224
1620
  return [
1225
1621
  4,
1226
- this.getAccessToken(accountId)
1227
- ];
1228
- case 8:
1229
- _state.sent();
1230
- if (!(accountId !== null && accountId !== void 0)) return [
1231
- 3,
1232
- 9
1233
- ];
1234
- _tmp1 = accountId;
1235
- return [
1236
- 3,
1237
- 11
1622
+ ensureAuthenticatedOrThrow()
1238
1623
  ];
1239
- case 9:
1240
- return [
1241
- 4,
1242
- (0, _oauth.getActiveAccount)(tokenStore, {
1243
- service: service
1244
- })
1245
- ];
1246
- case 10:
1247
- _tmp1 = _state.sent();
1248
- _state.label = 11;
1249
- case 11:
1250
- effectiveAccountId = _tmp1;
1251
- if (!effectiveAccountId) {
1252
- throw new Error("No account found after OAuth flow for service ".concat(service));
1253
- }
1624
+ case 2:
1625
+ effectiveAccountId = _state.sent();
1254
1626
  auth = this.toAuth(effectiveAccountId);
1255
1627
  // Inject authContext and logger into extra
1256
1628
  extra.authContext = {
@@ -1262,27 +1634,25 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1262
1634
  4,
1263
1635
  originalHandler.apply(void 0, _to_consumable_array(allArgs))
1264
1636
  ];
1265
- case 12:
1637
+ case 3:
1266
1638
  // Call original handler with all args
1267
1639
  return [
1268
1640
  2,
1269
1641
  _state.sent()
1270
1642
  ];
1271
- case 13:
1272
- error1 = _state.sent();
1273
- if (_instanceof(error1, _typests.AuthRequiredError)) {
1643
+ case 4:
1644
+ error = _state.sent();
1645
+ if (_instanceof(error, _typests.AuthRequiredError)) {
1274
1646
  logger.info('Authentication required', {
1275
1647
  service: service,
1276
1648
  tool: operation,
1277
- descriptor: error1.descriptor
1649
+ descriptor: error.descriptor
1278
1650
  });
1279
- // Return auth_required response wrapped in { result } to match tool outputSchema pattern
1280
- // Tools define outputSchema: z.object({ result: discriminatedUnion(...) }) where auth_required is a branch
1281
1651
  authRequiredResponse = {
1282
1652
  type: 'auth_required',
1283
1653
  provider: service,
1284
1654
  message: "Authentication required for ".concat(operation, ". Please authenticate with ").concat(service, "."),
1285
- url: error1.descriptor.kind === 'auth_url' ? error1.descriptor.url : undefined
1655
+ url: error.descriptor.kind === 'auth_url' ? error.descriptor.url : undefined
1286
1656
  };
1287
1657
  return [
1288
1658
  2,
@@ -1301,8 +1671,8 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1301
1671
  }
1302
1672
  ];
1303
1673
  }
1304
- throw error1;
1305
- case 14:
1674
+ throw error;
1675
+ case 5:
1306
1676
  return [
1307
1677
  2
1308
1678
  ];