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