@mcp-z/oauth-google 1.0.1 → 1.0.3

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.
@@ -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
@@ -290,10 +300,14 @@ function _ts_generator(thisArg, body) {
290
300
  };
291
301
  }
292
302
  }
303
+ var OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
304
+ var OAUTH_POLL_MS = 500;
293
305
  var LoopbackOAuthProvider = /*#__PURE__*/ function() {
294
306
  "use strict";
295
307
  function LoopbackOAuthProvider(config) {
296
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();
297
311
  this.config = config;
298
312
  }
299
313
  var _proto = LoopbackOAuthProvider.prototype;
@@ -304,7 +318,7 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
304
318
  * @returns Access token for API requests
305
319
  */ _proto.getAccessToken = function getAccessToken(accountId) {
306
320
  return _async_to_generator(function() {
307
- var _this_config, logger, service, tokenStore, effectiveAccountId, _tmp, storedToken, refreshedToken, error, _this_config1, clientId, scope, redirectUri, _generatePKCE, codeVerifier, codeChallenge, stateId, authUrl, _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;
308
322
  return _ts_generator(this, function(_state) {
309
323
  switch(_state.label){
310
324
  case 0:
@@ -413,92 +427,44 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
413
427
  // Store PKCE verifier for callback (5 minute TTL)
414
428
  return [
415
429
  4,
416
- tokenStore.set("".concat(service, ":pending:").concat(stateId), {
417
- codeVerifier: codeVerifier,
418
- createdAt: Date.now()
419
- }, 5 * 60 * 1000)
430
+ this.createPendingAuth({
431
+ state: stateId,
432
+ codeVerifier: codeVerifier
433
+ })
420
434
  ];
421
435
  case 10:
422
436
  _state.sent();
423
437
  // Build auth URL with configured redirect_uri
424
- authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
425
- authUrl.searchParams.set('client_id', clientId);
426
- authUrl.searchParams.set('redirect_uri', redirectUri);
427
- authUrl.searchParams.set('response_type', 'code');
428
- authUrl.searchParams.set('scope', scope);
429
- authUrl.searchParams.set('access_type', 'offline');
430
- authUrl.searchParams.set('code_challenge', codeChallenge);
431
- authUrl.searchParams.set('code_challenge_method', 'S256');
432
- authUrl.searchParams.set('state', stateId);
433
- authUrl.searchParams.set('prompt', 'consent');
438
+ authUrl = this.buildAuthUrl({
439
+ redirectUri: redirectUri,
440
+ codeChallenge: codeChallenge,
441
+ state: stateId
442
+ });
434
443
  logger.info('OAuth required - persistent callback mode', {
435
444
  service: service,
436
- redirectUri: redirectUri
445
+ redirectUri: redirectUri,
446
+ clientId: clientId,
447
+ scope: scope
437
448
  });
438
449
  throw new _typests.AuthRequiredError({
439
450
  kind: 'auth_url',
440
451
  provider: service,
441
- url: authUrl.toString()
452
+ url: authUrl
442
453
  });
443
454
  case 11:
444
455
  // Ephemeral callback mode (local development)
456
+ // IMPORTANT: do NOT open here anymore; we throw auth_url and the middleware will open+poll.
445
457
  logger.info('Starting ephemeral OAuth flow', {
446
458
  service: service,
447
459
  headless: this.config.headless
448
460
  });
449
461
  return [
450
462
  4,
451
- this.performEphemeralOAuthFlow()
463
+ this.startEphemeralOAuthFlow()
452
464
  ];
453
465
  case 12:
454
- _ref = _state.sent(), token = _ref.token, email = _ref.email;
455
- return [
456
- 4,
457
- (0, _oauth.setToken)(tokenStore, {
458
- accountId: email,
459
- service: service
460
- }, token)
461
- ];
462
- case 13:
463
- _state.sent();
464
- return [
465
- 4,
466
- (0, _oauth.addAccount)(tokenStore, {
467
- service: service,
468
- accountId: email
469
- })
470
- ];
471
- case 14:
472
- _state.sent();
473
- return [
474
- 4,
475
- (0, _oauth.setActiveAccount)(tokenStore, {
476
- service: service,
477
- accountId: email
478
- })
479
- ];
480
- case 15:
481
- _state.sent();
482
- return [
483
- 4,
484
- (0, _oauth.setAccountInfo)(tokenStore, {
485
- service: service,
486
- accountId: email
487
- }, {
488
- email: email,
489
- addedAt: new Date().toISOString()
490
- })
491
- ];
492
- case 16:
493
- _state.sent();
494
- logger.info('OAuth flow completed', {
495
- service: service,
496
- accountId: email
497
- });
498
- return [
499
- 2,
500
- token.accessToken
501
- ];
466
+ descriptor = _state.sent();
467
+ throw new _typests.AuthRequiredError(descriptor);
502
468
  }
503
469
  });
504
470
  }).call(this);
@@ -666,531 +632,802 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
666
632
  });
667
633
  }).call(this);
668
634
  };
669
- _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) {
670
659
  return _async_to_generator(function() {
671
- var _this, _this_config, clientId, scope, headless, logger, configRedirectUri, listenHost, listenPort, callbackPath, useConfiguredUri, parsed, isLoopback, envPort;
660
+ var tokenResponse, cachedToken, email;
672
661
  return _ts_generator(this, function(_state) {
673
- _this = this;
674
- _this_config = this.config, clientId = _this_config.clientId, scope = _this_config.scope, headless = _this_config.headless, logger = _this_config.logger, configRedirectUri = _this_config.redirectUri;
675
- // Server listen configuration (where ephemeral server binds)
676
- listenHost = 'localhost'; // Default: localhost for ephemeral loopback
677
- listenPort = 0; // Default: OS-assigned ephemeral port
678
- // Redirect URI configuration (what goes in auth URL and token exchange)
679
- callbackPath = '/callback'; // Default callback path
680
- useConfiguredUri = false;
681
- if (configRedirectUri) {
682
- try {
683
- parsed = new URL(configRedirectUri);
684
- isLoopback = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
685
- if (isLoopback) {
686
- // Local development: Listen on specific loopback address/port
687
- listenHost = parsed.hostname;
688
- listenPort = parsed.port ? Number.parseInt(parsed.port, 10) : 0;
689
- } else {
690
- // Cloud deployment: Listen on 0.0.0.0 with PORT from environment
691
- // The redirectUri is the PUBLIC URL (e.g., https://example.com/oauth/callback)
692
- // The server listens on 0.0.0.0:PORT and the load balancer routes to it
693
- listenHost = '0.0.0.0';
694
- envPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : undefined;
695
- listenPort = envPort && Number.isFinite(envPort) ? envPort : 8080;
696
- }
697
- // Extract callback path from URL
698
- if (parsed.pathname && parsed.pathname !== '/') {
699
- callbackPath = parsed.pathname;
700
- }
701
- useConfiguredUri = true;
702
- logger.debug('Using configured redirect URI', {
703
- listenHost: listenHost,
704
- listenPort: listenPort,
705
- callbackPath: callbackPath,
706
- redirectUri: configRedirectUri,
707
- isLoopback: isLoopback
708
- });
709
- } catch (error) {
710
- logger.warn('Failed to parse redirectUri, using ephemeral defaults', {
711
- redirectUri: configRedirectUri,
712
- error: _instanceof(error, Error) ? error.message : String(error)
713
- });
714
- // Continue with defaults (localhost, port 0, http, /callback)
715
- }
716
- }
717
- return [
718
- 2,
719
- new Promise(function(resolve, reject) {
720
- // Generate PKCE challenge
721
- var _generatePKCE = (0, _oauth.generatePKCE)(), codeVerifier = _generatePKCE.verifier, codeChallenge = _generatePKCE.challenge;
722
- var server = null;
723
- var serverPort;
724
- var finalRedirectUri; // Will be set in server.listen callback
725
- // Create ephemeral server with OS-assigned port (RFC 8252)
726
- server = _http.createServer(function(req, res) {
727
- return _async_to_generator(function() {
728
- var url, code, _$error, tokenResponse, cachedToken, email, exchangeError;
729
- return _ts_generator(this, function(_state) {
730
- switch(_state.label){
731
- case 0:
732
- if (!req.url) {
733
- res.writeHead(400, {
734
- 'Content-Type': 'text/html'
735
- });
736
- res.end((0, _oauth.getErrorTemplate)('Invalid request'));
737
- server === null || server === void 0 ? void 0 : server.close();
738
- reject(new Error('Invalid request: missing URL'));
739
- return [
740
- 2
741
- ];
742
- }
743
- url = new URL(req.url, "http://127.0.0.1:".concat(serverPort));
744
- if (!(url.pathname === callbackPath)) return [
745
- 3,
746
- 6
747
- ];
748
- code = url.searchParams.get('code');
749
- _$error = url.searchParams.get('error');
750
- if (_$error) {
751
- res.writeHead(400, {
752
- 'Content-Type': 'text/html'
753
- });
754
- res.end((0, _oauth.getErrorTemplate)(_$error));
755
- server === null || server === void 0 ? void 0 : server.close();
756
- reject(new Error("OAuth error: ".concat(_$error)));
757
- return [
758
- 2
759
- ];
760
- }
761
- if (!code) {
762
- res.writeHead(400, {
763
- 'Content-Type': 'text/html'
764
- });
765
- res.end((0, _oauth.getErrorTemplate)('No authorization code received'));
766
- server === null || server === void 0 ? void 0 : server.close();
767
- reject(new Error('No authorization code received'));
768
- return [
769
- 2
770
- ];
771
- }
772
- _state.label = 1;
773
- case 1:
774
- _state.trys.push([
775
- 1,
776
- 4,
777
- ,
778
- 5
779
- ]);
780
- return [
781
- 4,
782
- this.exchangeCodeForToken(code, codeVerifier, finalRedirectUri)
783
- ];
784
- case 2:
785
- tokenResponse = _state.sent();
786
- // Build cached token
787
- cachedToken = _object_spread({
788
- accessToken: tokenResponse.access_token
789
- }, tokenResponse.refresh_token !== undefined && {
790
- refreshToken: tokenResponse.refresh_token
791
- }, tokenResponse.expires_in !== undefined && {
792
- expiresAt: Date.now() + tokenResponse.expires_in * 1000
793
- }, tokenResponse.scope !== undefined && {
794
- scope: tokenResponse.scope
795
- });
796
- return [
797
- 4,
798
- this.fetchUserEmailFromToken(tokenResponse.access_token)
799
- ];
800
- case 3:
801
- email = _state.sent();
802
- res.writeHead(200, {
803
- 'Content-Type': 'text/html'
804
- });
805
- res.end((0, _oauth.getSuccessTemplate)());
806
- server === null || server === void 0 ? void 0 : server.close();
807
- resolve({
808
- token: cachedToken,
809
- email: email
810
- });
811
- return [
812
- 3,
813
- 5
814
- ];
815
- case 4:
816
- exchangeError = _state.sent();
817
- logger.error('Token exchange failed', {
818
- error: _instanceof(exchangeError, Error) ? exchangeError.message : String(exchangeError)
819
- });
820
- res.writeHead(500, {
821
- 'Content-Type': 'text/html'
822
- });
823
- res.end((0, _oauth.getErrorTemplate)('Token exchange failed'));
824
- server === null || server === void 0 ? void 0 : server.close();
825
- reject(exchangeError);
826
- return [
827
- 3,
828
- 5
829
- ];
830
- case 5:
831
- return [
832
- 3,
833
- 7
834
- ];
835
- case 6:
836
- res.writeHead(404, {
837
- 'Content-Type': 'text/plain'
838
- });
839
- res.end('Not Found');
840
- _state.label = 7;
841
- case 7:
842
- return [
843
- 2
844
- ];
845
- }
846
- });
847
- }).call(_this);
848
- });
849
- // Listen on configured host/port
850
- server.listen(listenPort, listenHost, function() {
851
- var address = server === null || server === void 0 ? void 0 : server.address();
852
- if (!address || typeof address === 'string') {
853
- server === null || server === void 0 ? void 0 : server.close();
854
- reject(new Error('Failed to start ephemeral server'));
855
- return;
856
- }
857
- serverPort = address.port;
858
- // Construct final redirect URI
859
- if (useConfiguredUri && configRedirectUri) {
860
- // Use configured redirect URI as-is (public URL for cloud, or specific local URL)
861
- finalRedirectUri = configRedirectUri;
862
- } else {
863
- // Construct ephemeral redirect URI with actual server port (default local behavior)
864
- finalRedirectUri = "http://localhost:".concat(serverPort).concat(callbackPath);
865
- }
866
- // Build auth URL
867
- var authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
868
- authUrl.searchParams.set('client_id', clientId);
869
- authUrl.searchParams.set('redirect_uri', finalRedirectUri);
870
- authUrl.searchParams.set('response_type', 'code');
871
- authUrl.searchParams.set('scope', scope);
872
- authUrl.searchParams.set('access_type', 'offline');
873
- authUrl.searchParams.set('prompt', 'consent');
874
- authUrl.searchParams.set('code_challenge', codeChallenge);
875
- authUrl.searchParams.set('code_challenge_method', 'S256');
876
- logger.info('Ephemeral OAuth server started', {
877
- port: serverPort,
878
- headless: headless
879
- });
880
- if (headless) {
881
- // Headless mode: Print auth URL to stderr (stdout is MCP protocol)
882
- console.error('\n🔐 OAuth Authorization Required');
883
- console.error('📋 Please visit this URL in your browser:\n');
884
- console.error(" ".concat(authUrl.toString(), "\n"));
885
- console.error('⏳ Waiting for authorization...\n');
886
- } else {
887
- // Interactive mode: Open browser automatically
888
- logger.info('Opening browser for OAuth authorization');
889
- (0, _open.default)(authUrl.toString()).catch(function(error) {
890
- logger.info('Failed to open browser automatically', {
891
- error: error.message
892
- });
893
- console.error('\n🔐 OAuth Authorization Required');
894
- console.error(" ".concat(authUrl.toString(), "\n"));
895
- });
896
- }
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
897
679
  });
898
- // Timeout after 5 minutes
899
- setTimeout(function() {
900
- if (server) {
901
- server.close();
902
- reject(new Error('OAuth flow timed out after 5 minutes'));
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
903
691
  }
904
- }, 5 * 60 * 1000);
905
- })
906
- ];
692
+ ];
693
+ }
907
694
  });
908
695
  }).call(this);
909
696
  };
910
- _proto.exchangeCodeForToken = function exchangeCodeForToken(code, codeVerifier, redirectUri) {
697
+ /**
698
+ * Store token + account metadata. Shared by BOTH persistent and ephemeral modes.
699
+ */ _proto.persistAuthResult = function persistAuthResult(args) {
911
700
  return _async_to_generator(function() {
912
- var _this_config, clientId, clientSecret, tokenUrl, params, body, response, errorText;
701
+ var _this_config, tokenStore, service;
913
702
  return _ts_generator(this, function(_state) {
914
703
  switch(_state.label){
915
704
  case 0:
916
- _this_config = this.config, clientId = _this_config.clientId, clientSecret = _this_config.clientSecret;
917
- tokenUrl = 'https://oauth2.googleapis.com/token';
918
- params = {
919
- code: code,
920
- client_id: clientId,
921
- redirect_uri: redirectUri,
922
- grant_type: 'authorization_code',
923
- code_verifier: codeVerifier
924
- };
925
- if (clientSecret) {
926
- params.client_secret = clientSecret;
927
- }
928
- body = new URLSearchParams(params);
705
+ _this_config = this.config, tokenStore = _this_config.tokenStore, service = _this_config.service;
929
706
  return [
930
707
  4,
931
- fetch(tokenUrl, {
932
- method: 'POST',
933
- headers: {
934
- 'Content-Type': 'application/x-www-form-urlencoded'
935
- },
936
- body: body.toString()
937
- })
708
+ (0, _oauth.setToken)(tokenStore, {
709
+ accountId: args.email,
710
+ service: service
711
+ }, args.token)
938
712
  ];
939
713
  case 1:
940
- response = _state.sent();
941
- if (!!response.ok) return [
942
- 3,
943
- 3
944
- ];
714
+ _state.sent();
945
715
  return [
946
716
  4,
947
- response.text()
717
+ (0, _oauth.addAccount)(tokenStore, {
718
+ service: service,
719
+ accountId: args.email
720
+ })
948
721
  ];
949
722
  case 2:
950
- errorText = _state.sent();
951
- throw new Error("Token exchange failed: ".concat(response.status, " ").concat(errorText));
723
+ _state.sent();
724
+ return [
725
+ 4,
726
+ (0, _oauth.setActiveAccount)(tokenStore, {
727
+ service: service,
728
+ accountId: args.email
729
+ })
730
+ ];
952
731
  case 3:
732
+ _state.sent();
953
733
  return [
954
734
  4,
955
- response.json()
735
+ (0, _oauth.setAccountInfo)(tokenStore, {
736
+ service: service,
737
+ accountId: args.email
738
+ }, {
739
+ email: args.email,
740
+ addedAt: new Date().toISOString()
741
+ })
956
742
  ];
957
743
  case 4:
744
+ _state.sent();
958
745
  return [
959
- 2,
960
- _state.sent()
746
+ 2
961
747
  ];
962
748
  }
963
749
  });
964
750
  }).call(this);
965
751
  };
966
- _proto.refreshAccessToken = function refreshAccessToken(refreshToken) {
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) {
967
762
  return _async_to_generator(function() {
968
- var _this_config, clientId, clientSecret, tokenUrl, params, body, response, errorText, tokenResponse;
763
+ var tokenStore, record;
969
764
  return _ts_generator(this, function(_state) {
970
765
  switch(_state.label){
971
766
  case 0:
972
- _this_config = this.config, clientId = _this_config.clientId, clientSecret = _this_config.clientSecret;
973
- tokenUrl = 'https://oauth2.googleapis.com/token';
974
- params = {
975
- refresh_token: refreshToken,
976
- client_id: clientId,
977
- grant_type: 'refresh_token'
767
+ tokenStore = this.config.tokenStore;
768
+ record = {
769
+ codeVerifier: args.codeVerifier,
770
+ createdAt: Date.now()
978
771
  };
979
- if (clientSecret) {
980
- params.client_secret = clientSecret;
981
- }
982
- body = new URLSearchParams(params);
983
772
  return [
984
773
  4,
985
- fetch(tokenUrl, {
986
- method: 'POST',
987
- headers: {
988
- 'Content-Type': 'application/x-www-form-urlencoded'
989
- },
990
- body: body.toString()
991
- })
774
+ tokenStore.set(this.pendingKey(args.state), record, OAUTH_TIMEOUT_MS)
992
775
  ];
993
776
  case 1:
994
- response = _state.sent();
995
- if (!!response.ok) return [
996
- 3,
997
- 3
998
- ];
999
- return [
1000
- 4,
1001
- response.text()
1002
- ];
1003
- case 2:
1004
- errorText = _state.sent();
1005
- throw new Error("Token refresh failed: ".concat(response.status, " ").concat(errorText));
1006
- case 3:
1007
- return [
1008
- 4,
1009
- response.json()
1010
- ];
1011
- case 4:
1012
- tokenResponse = _state.sent();
777
+ _state.sent();
1013
778
  return [
1014
- 2,
1015
- _object_spread({
1016
- accessToken: tokenResponse.access_token,
1017
- refreshToken: refreshToken
1018
- }, tokenResponse.expires_in !== undefined && {
1019
- expiresAt: Date.now() + tokenResponse.expires_in * 1000
1020
- }, tokenResponse.scope !== undefined && {
1021
- scope: tokenResponse.scope
1022
- })
779
+ 2
1023
780
  ];
1024
781
  }
1025
782
  });
1026
783
  }).call(this);
1027
784
  };
1028
785
  /**
1029
- * Handle OAuth callback from persistent endpoint.
1030
- * Used by HTTP servers with configured redirectUri.
1031
- *
1032
- * @param params - OAuth callback parameters
1033
- * @returns Email and cached token
1034
- */ _proto.handleOAuthCallback = function handleOAuthCallback(params) {
786
+ * Load and validate pending auth state (5 minute TTL).
787
+ * Shared by BOTH persistent and ephemeral modes.
788
+ */ _proto.readAndValidatePendingAuth = function readAndValidatePendingAuth(state) {
1035
789
  return _async_to_generator(function() {
1036
- var code, state, _this_config, logger, service, tokenStore, clientId, clientSecret, redirectUri, pendingKey, pendingAuth, body, response, errorText, tokenResponse, email, cachedToken;
790
+ var tokenStore, pendingAuth;
1037
791
  return _ts_generator(this, function(_state) {
1038
792
  switch(_state.label){
1039
793
  case 0:
1040
- code = params.code, state = params.state;
1041
- _this_config = this.config, logger = _this_config.logger, service = _this_config.service, tokenStore = _this_config.tokenStore, clientId = _this_config.clientId, clientSecret = _this_config.clientSecret, redirectUri = _this_config.redirectUri;
1042
- if (!state) {
1043
- throw new Error('Missing state parameter in OAuth callback');
1044
- }
1045
- if (!redirectUri) {
1046
- throw new Error('handleOAuthCallback requires configured redirectUri');
1047
- }
1048
- // Load pending auth (includes PKCE verifier)
1049
- pendingKey = "".concat(service, ":pending:").concat(state);
794
+ tokenStore = this.config.tokenStore;
1050
795
  return [
1051
796
  4,
1052
- tokenStore.get(pendingKey)
797
+ tokenStore.get(this.pendingKey(state))
1053
798
  ];
1054
799
  case 1:
1055
800
  pendingAuth = _state.sent();
1056
801
  if (!pendingAuth) {
1057
802
  throw new Error('Invalid or expired OAuth state. Please try again.');
1058
803
  }
1059
- if (!(Date.now() - pendingAuth.createdAt > 5 * 60 * 1000)) return [
804
+ if (!(Date.now() - pendingAuth.createdAt > OAUTH_TIMEOUT_MS)) return [
1060
805
  3,
1061
806
  3
1062
807
  ];
1063
808
  return [
1064
809
  4,
1065
- tokenStore.delete(pendingKey)
810
+ tokenStore.delete(this.pendingKey(state))
1066
811
  ];
1067
812
  case 2:
1068
813
  _state.sent();
1069
814
  throw new Error('OAuth state expired. Please try again.');
1070
815
  case 3:
1071
- logger.info('Processing OAuth callback', {
1072
- service: service,
1073
- state: state
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
1074
836
  });
1075
- // Exchange code for token
1076
- body = new URLSearchParams(_object_spread_props(_object_spread({
1077
- code: code,
1078
- client_id: clientId
1079
- }, clientSecret && {
1080
- client_secret: clientSecret
1081
- }), {
1082
- redirect_uri: redirectUri,
1083
- grant_type: 'authorization_code',
1084
- code_verifier: pendingAuth.codeVerifier
1085
- }));
1086
837
  return [
1087
838
  4,
1088
- fetch('https://oauth2.googleapis.com/token', {
1089
- method: 'POST',
1090
- headers: {
1091
- 'Content-Type': 'application/x-www-form-urlencoded'
1092
- },
1093
- body: body.toString()
1094
- })
839
+ tokenStore.set(this.pendingKey(args.state), updated, OAUTH_TIMEOUT_MS)
1095
840
  ];
1096
- case 4:
1097
- response = _state.sent();
1098
- if (!!response.ok) return [
1099
- 3,
1100
- 6
841
+ case 1:
842
+ _state.sent();
843
+ return [
844
+ 2
1101
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;
1102
859
  return [
1103
860
  4,
1104
- response.text()
861
+ tokenStore.delete(this.pendingKey(state))
1105
862
  ];
1106
- case 5:
1107
- errorText = _state.sent();
1108
- throw new Error("Token exchange failed: ".concat(response.status, " ").concat(errorText));
1109
- case 6:
863
+ case 1:
864
+ _state.sent();
1110
865
  return [
1111
- 4,
1112
- response.json()
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
1113
889
  ];
1114
- case 7:
1115
- tokenResponse = _state.sent();
1116
890
  return [
1117
891
  4,
1118
- this.fetchUserEmailFromToken(tokenResponse.access_token)
892
+ tokenStore.get(key)
1119
893
  ];
1120
- case 8:
1121
- email = _state.sent();
1122
- // Create cached token
1123
- cachedToken = _object_spread({
1124
- accessToken: tokenResponse.access_token,
1125
- refreshToken: tokenResponse.refresh_token,
1126
- expiresAt: tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1000 : undefined
1127
- }, tokenResponse.scope !== undefined && {
1128
- scope: tokenResponse.scope
1129
- });
1130
- // Store token
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
+ ];
903
+ }
1131
904
  return [
1132
905
  4,
1133
- (0, _oauth.setToken)(tokenStore, {
1134
- accountId: email,
1135
- service: service
1136
- }, cachedToken)
906
+ new Promise(function(r) {
907
+ return setTimeout(r, OAUTH_POLL_MS);
908
+ })
1137
909
  ];
1138
- case 9:
910
+ case 3:
1139
911
  _state.sent();
1140
- // Add account and set as active
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;
1141
938
  return [
1142
939
  4,
1143
- (0, _oauth.addAccount)(tokenStore, {
1144
- service: service,
1145
- accountId: email
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
947
+ });
948
+ return [
949
+ 4,
950
+ this.handleAuthorizationCode({
951
+ code: args.code,
952
+ codeVerifier: pending.codeVerifier,
953
+ redirectUri: args.redirectUri
1146
954
  })
1147
955
  ];
1148
- case 10:
956
+ case 2:
957
+ result = _state.sent();
958
+ return [
959
+ 4,
960
+ this.persistAuthResult(result)
961
+ ];
962
+ case 3:
1149
963
  _state.sent();
1150
964
  return [
1151
965
  4,
1152
- (0, _oauth.setActiveAccount)(tokenStore, {
1153
- service: service,
1154
- accountId: email
966
+ this.markPendingComplete({
967
+ state: args.state,
968
+ email: result.email,
969
+ pending: pending
1155
970
  })
1156
971
  ];
1157
- case 11:
972
+ case 4:
1158
973
  _state.sent();
1159
- // Store account metadata
974
+ logger.info('OAuth callback completed', {
975
+ service: service,
976
+ email: result.email
977
+ });
978
+ return [
979
+ 2,
980
+ result
981
+ ];
982
+ }
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'
1016
+ });
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
+ ];
1046
+ }
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
+ ];
1056
+ }
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'
1087
+ });
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)
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)
1160
1193
  return [
1161
1194
  4,
1162
- (0, _oauth.setAccountInfo)(tokenStore, {
1163
- service: service,
1164
- accountId: email
1165
- }, {
1166
- email: email,
1167
- addedAt: new Date().toISOString()
1195
+ this.createPendingAuth({
1196
+ state: stateId,
1197
+ codeVerifier: codeVerifier
1168
1198
  })
1169
1199
  ];
1170
- case 12:
1200
+ case 1:
1171
1201
  _state.sent();
1172
- // Clean up pending auth
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();
1217
+ }
1218
+ });
1219
+ // Start listening
1173
1220
  return [
1174
1221
  4,
1175
- tokenStore.delete(pendingKey)
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
+ })
1176
1245
  ];
1177
- case 13:
1246
+ case 2:
1178
1247
  _state.sent();
1179
- logger.info('OAuth callback completed', {
1180
- service: service,
1181
- email: email
1248
+ // Timeout after 5 minutes (match middleware polling timeout)
1249
+ setTimeout(function() {
1250
+ if (server) {
1251
+ server.close();
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));
1255
+ }
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
1182
1262
  });
1183
1263
  return [
1184
1264
  2,
1185
1265
  {
1186
- email: email,
1187
- token: cachedToken
1266
+ kind: 'auth_url',
1267
+ provider: service,
1268
+ url: authUrl
1188
1269
  }
1189
1270
  ];
1190
1271
  }
1191
1272
  });
1192
1273
  }).call(this);
1193
1274
  };
1275
+ _proto.exchangeCodeForToken = function exchangeCodeForToken(code, codeVerifier, redirectUri) {
1276
+ return _async_to_generator(function() {
1277
+ var _this_config, clientId, clientSecret, tokenUrl, params, body, response, errorText;
1278
+ return _ts_generator(this, function(_state) {
1279
+ switch(_state.label){
1280
+ case 0:
1281
+ _this_config = this.config, clientId = _this_config.clientId, clientSecret = _this_config.clientSecret;
1282
+ tokenUrl = 'https://oauth2.googleapis.com/token';
1283
+ params = {
1284
+ code: code,
1285
+ client_id: clientId,
1286
+ redirect_uri: redirectUri,
1287
+ grant_type: 'authorization_code',
1288
+ code_verifier: codeVerifier
1289
+ };
1290
+ if (clientSecret) {
1291
+ params.client_secret = clientSecret;
1292
+ }
1293
+ body = new URLSearchParams(params);
1294
+ return [
1295
+ 4,
1296
+ fetch(tokenUrl, {
1297
+ method: 'POST',
1298
+ headers: {
1299
+ 'Content-Type': 'application/x-www-form-urlencoded'
1300
+ },
1301
+ body: body.toString()
1302
+ })
1303
+ ];
1304
+ case 1:
1305
+ response = _state.sent();
1306
+ if (!!response.ok) return [
1307
+ 3,
1308
+ 3
1309
+ ];
1310
+ return [
1311
+ 4,
1312
+ response.text()
1313
+ ];
1314
+ case 2:
1315
+ errorText = _state.sent();
1316
+ throw new Error("Token exchange failed: ".concat(response.status, " ").concat(errorText));
1317
+ case 3:
1318
+ return [
1319
+ 4,
1320
+ response.json()
1321
+ ];
1322
+ case 4:
1323
+ return [
1324
+ 2,
1325
+ _state.sent()
1326
+ ];
1327
+ }
1328
+ });
1329
+ }).call(this);
1330
+ };
1331
+ _proto.refreshAccessToken = function refreshAccessToken(refreshToken) {
1332
+ return _async_to_generator(function() {
1333
+ var _this_config, clientId, clientSecret, tokenUrl, params, body, response, errorText, tokenResponse;
1334
+ return _ts_generator(this, function(_state) {
1335
+ switch(_state.label){
1336
+ case 0:
1337
+ _this_config = this.config, clientId = _this_config.clientId, clientSecret = _this_config.clientSecret;
1338
+ tokenUrl = 'https://oauth2.googleapis.com/token';
1339
+ params = {
1340
+ refresh_token: refreshToken,
1341
+ client_id: clientId,
1342
+ grant_type: 'refresh_token'
1343
+ };
1344
+ if (clientSecret) {
1345
+ params.client_secret = clientSecret;
1346
+ }
1347
+ body = new URLSearchParams(params);
1348
+ return [
1349
+ 4,
1350
+ fetch(tokenUrl, {
1351
+ method: 'POST',
1352
+ headers: {
1353
+ 'Content-Type': 'application/x-www-form-urlencoded'
1354
+ },
1355
+ body: body.toString()
1356
+ })
1357
+ ];
1358
+ case 1:
1359
+ response = _state.sent();
1360
+ if (!!response.ok) return [
1361
+ 3,
1362
+ 3
1363
+ ];
1364
+ return [
1365
+ 4,
1366
+ response.text()
1367
+ ];
1368
+ case 2:
1369
+ errorText = _state.sent();
1370
+ throw new Error("Token refresh failed: ".concat(response.status, " ").concat(errorText));
1371
+ case 3:
1372
+ return [
1373
+ 4,
1374
+ response.json()
1375
+ ];
1376
+ case 4:
1377
+ tokenResponse = _state.sent();
1378
+ return [
1379
+ 2,
1380
+ _object_spread({
1381
+ accessToken: tokenResponse.access_token,
1382
+ refreshToken: refreshToken
1383
+ }, tokenResponse.expires_in !== undefined && {
1384
+ expiresAt: Date.now() + tokenResponse.expires_in * 1000
1385
+ }, tokenResponse.scope !== undefined && {
1386
+ scope: tokenResponse.scope
1387
+ })
1388
+ ];
1389
+ }
1390
+ });
1391
+ }).call(this);
1392
+ };
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
+ };
1194
1431
  /**
1195
1432
  * Create authentication middleware for MCP tools, resources, and prompts
1196
1433
  *
@@ -1205,15 +1442,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1205
1442
  * All requests use token lookups based on the active account or account override.
1206
1443
  *
1207
1444
  * @returns Object with withToolAuth, withResourceAuth, withPromptAuth methods
1208
- *
1209
- * @example
1210
- * ```typescript
1211
- * const loopback = new LoopbackOAuthProvider({ service: 'gmail', ... });
1212
- * const authMiddleware = loopback.authMiddleware();
1213
- * const tools = toolFactories.map(f => f()).map(authMiddleware.withToolAuth);
1214
- * const resources = resourceFactories.map(f => f()).map(authMiddleware.withResourceAuth);
1215
- * const prompts = promptFactories.map(f => f()).map(authMiddleware.withPromptAuth);
1216
- * ```
1217
1445
  */ _proto.authMiddleware = function authMiddleware() {
1218
1446
  var _this = this;
1219
1447
  var _this_config = this.config, service = _this_config.service, tokenStore = _this_config.tokenStore, logger = _this_config.logger;
@@ -1228,96 +1456,173 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1228
1456
  allArgs[_key] = arguments[_key];
1229
1457
  }
1230
1458
  return _async_to_generator(function() {
1231
- var extra, accountId, _ref, _extra__meta, _tmp, error, effectiveAccountId, _tmp1, auth, error1, authRequiredResponse;
1459
+ var _this, extra, ensureAuthenticatedOrThrow, effectiveAccountId, auth, error, authRequiredResponse;
1232
1460
  return _ts_generator(this, function(_state) {
1233
1461
  switch(_state.label){
1234
1462
  case 0:
1463
+ _this = this;
1235
1464
  // Extract extra from the correct position
1236
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
+ };
1237
1612
  _state.label = 1;
1238
1613
  case 1:
1239
1614
  _state.trys.push([
1240
1615
  1,
1241
- 13,
1242
- ,
1243
- 14
1244
- ]);
1245
- _state.label = 2;
1246
- case 2:
1247
- _state.trys.push([
1248
- 2,
1249
- 6,
1616
+ 4,
1250
1617
  ,
1251
- 7
1252
- ]);
1253
- if (!((_ref = (_extra__meta = extra._meta) === null || _extra__meta === void 0 ? void 0 : _extra__meta.accountId) !== null && _ref !== void 0)) return [
1254
- 3,
1255
- 3
1256
- ];
1257
- _tmp = _ref;
1258
- return [
1259
- 3,
1260
1618
  5
1261
- ];
1262
- case 3:
1263
- return [
1264
- 4,
1265
- (0, _oauth.getActiveAccount)(tokenStore, {
1266
- service: service
1267
- })
1268
- ];
1269
- case 4:
1270
- _tmp = _state.sent();
1271
- _state.label = 5;
1272
- case 5:
1273
- accountId = _tmp;
1274
- return [
1275
- 3,
1276
- 7
1277
- ];
1278
- case 6:
1279
- error = _state.sent();
1280
- if (_instanceof(error, Error) && (error.code === 'REQUIRES_AUTHENTICATION' || error.name === 'AccountManagerError')) {
1281
- accountId = undefined;
1282
- } else {
1283
- throw error;
1284
- }
1285
- return [
1286
- 3,
1287
- 7
1288
- ];
1289
- case 7:
1290
- // Eagerly validate token exists or trigger OAuth flow
1291
- return [
1292
- 4,
1293
- this.getAccessToken(accountId)
1294
- ];
1295
- case 8:
1296
- _state.sent();
1297
- if (!(accountId !== null && accountId !== void 0)) return [
1298
- 3,
1299
- 9
1300
- ];
1301
- _tmp1 = accountId;
1302
- return [
1303
- 3,
1304
- 11
1305
- ];
1306
- case 9:
1619
+ ]);
1307
1620
  return [
1308
1621
  4,
1309
- (0, _oauth.getActiveAccount)(tokenStore, {
1310
- service: service
1311
- })
1622
+ ensureAuthenticatedOrThrow()
1312
1623
  ];
1313
- case 10:
1314
- _tmp1 = _state.sent();
1315
- _state.label = 11;
1316
- case 11:
1317
- effectiveAccountId = _tmp1;
1318
- if (!effectiveAccountId) {
1319
- throw new Error("No account found after OAuth flow for service ".concat(service));
1320
- }
1624
+ case 2:
1625
+ effectiveAccountId = _state.sent();
1321
1626
  auth = this.toAuth(effectiveAccountId);
1322
1627
  // Inject authContext and logger into extra
1323
1628
  extra.authContext = {
@@ -1329,27 +1634,25 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1329
1634
  4,
1330
1635
  originalHandler.apply(void 0, _to_consumable_array(allArgs))
1331
1636
  ];
1332
- case 12:
1637
+ case 3:
1333
1638
  // Call original handler with all args
1334
1639
  return [
1335
1640
  2,
1336
1641
  _state.sent()
1337
1642
  ];
1338
- case 13:
1339
- error1 = _state.sent();
1340
- if (_instanceof(error1, _typests.AuthRequiredError)) {
1643
+ case 4:
1644
+ error = _state.sent();
1645
+ if (_instanceof(error, _typests.AuthRequiredError)) {
1341
1646
  logger.info('Authentication required', {
1342
1647
  service: service,
1343
1648
  tool: operation,
1344
- descriptor: error1.descriptor
1649
+ descriptor: error.descriptor
1345
1650
  });
1346
- // Return auth_required response wrapped in { result } to match tool outputSchema pattern
1347
- // Tools define outputSchema: z.object({ result: discriminatedUnion(...) }) where auth_required is a branch
1348
1651
  authRequiredResponse = {
1349
1652
  type: 'auth_required',
1350
1653
  provider: service,
1351
1654
  message: "Authentication required for ".concat(operation, ". Please authenticate with ").concat(service, "."),
1352
- url: error1.descriptor.kind === 'auth_url' ? error1.descriptor.url : undefined
1655
+ url: error.descriptor.kind === 'auth_url' ? error.descriptor.url : undefined
1353
1656
  };
1354
1657
  return [
1355
1658
  2,
@@ -1368,8 +1671,8 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1368
1671
  }
1369
1672
  ];
1370
1673
  }
1371
- throw error1;
1372
- case 14:
1674
+ throw error;
1675
+ case 5:
1373
1676
  return [
1374
1677
  2
1375
1678
  ];