@mcp-z/oauth-microsoft 1.0.0 → 1.0.1

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 (49) 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 +15 -17
  18. package/dist/cjs/providers/loopback-oauth.d.ts +15 -17
  19. package/dist/cjs/providers/loopback-oauth.js +190 -156
  20. package/dist/cjs/providers/loopback-oauth.js.map +1 -1
  21. package/dist/cjs/schemas/index.js.map +1 -1
  22. package/dist/cjs/setup/config.d.cts +4 -1
  23. package/dist/cjs/setup/config.d.ts +4 -1
  24. package/dist/cjs/setup/config.js +3 -0
  25. package/dist/cjs/setup/config.js.map +1 -1
  26. package/dist/cjs/types.js.map +1 -1
  27. package/dist/esm/index.d.ts +2 -1
  28. package/dist/esm/index.js +1 -0
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/lib/dcr-router.js.map +1 -1
  31. package/dist/esm/lib/dcr-utils.js.map +1 -1
  32. package/dist/esm/lib/dcr-verify.js.map +1 -1
  33. package/dist/esm/lib/fetch-with-timeout.js.map +1 -1
  34. package/dist/esm/lib/loopback-router.d.ts +8 -0
  35. package/dist/esm/lib/loopback-router.js +32 -0
  36. package/dist/esm/lib/loopback-router.js.map +1 -0
  37. package/dist/esm/lib/token-verifier.js.map +1 -1
  38. package/dist/esm/providers/dcr.js.map +1 -1
  39. package/dist/esm/providers/device-code.js +2 -2
  40. package/dist/esm/providers/device-code.js.map +1 -1
  41. package/dist/esm/providers/loopback-oauth.d.ts +15 -17
  42. package/dist/esm/providers/loopback-oauth.js +133 -115
  43. package/dist/esm/providers/loopback-oauth.js.map +1 -1
  44. package/dist/esm/schemas/index.js.map +1 -1
  45. package/dist/esm/setup/config.d.ts +4 -1
  46. package/dist/esm/setup/config.js +3 -0
  47. package/dist/esm/setup/config.js.map +1 -1
  48. package/dist/esm/types.js.map +1 -1
  49. package/package.json +1 -1
@@ -32,6 +32,7 @@ _export(exports, {
32
32
  }
33
33
  });
34
34
  var _oauth = require("@mcp-z/oauth");
35
+ var _crypto = require("crypto");
35
36
  var _http = /*#__PURE__*/ _interop_require_wildcard(require("http"));
36
37
  var _open = /*#__PURE__*/ _interop_require_default(require("open"));
37
38
  var _fetchwithtimeoutts = require("../lib/fetch-with-timeout.js");
@@ -317,7 +318,7 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
317
318
  * @returns Access token for API requests
318
319
  */ _proto.getAccessToken = function getAccessToken(accountId) {
319
320
  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;
321
+ var _this_config, logger, service, tokenStore, effectiveAccountId, _tmp, storedToken, refreshedToken, error, _this_config1, clientId, tenantId, scope, redirectUri, _generatePKCE, codeVerifier, codeChallenge, stateId, authUrl, _ref, token, email;
321
322
  return _ts_generator(this, function(_state) {
322
323
  switch(_state.label){
323
324
  case 0:
@@ -414,52 +415,49 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
414
415
  9
415
416
  ];
416
417
  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 [
418
+ _this_config1 = this.config, clientId = _this_config1.clientId, tenantId = _this_config1.tenantId, scope = _this_config1.scope, redirectUri = _this_config1.redirectUri;
419
+ if (!redirectUri) return [
420
420
  3,
421
421
  11
422
422
  ];
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;
423
+ // Persistent callback mode (cloud deployment with configured redirect_uri)
424
+ _generatePKCE = (0, _oauth.generatePKCE)(), codeVerifier = _generatePKCE.verifier, codeChallenge = _generatePKCE.challenge;
425
+ stateId = (0, _crypto.randomUUID)();
426
+ // Store PKCE verifier for callback (5 minute TTL)
426
427
  return [
427
428
  4,
428
- this.getExistingAccounts()
429
+ tokenStore.set("".concat(service, ":pending:").concat(stateId), {
430
+ codeVerifier: codeVerifier,
431
+ createdAt: Date.now()
432
+ }, 5 * 60 * 1000)
429
433
  ];
430
434
  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
+ _state.sent();
436
+ // Build auth URL with configured redirect_uri
435
437
  authUrl = new URL("https://login.microsoftonline.com/".concat(tenantId, "/oauth2/v2.0/authorize"));
436
438
  authUrl.searchParams.set('client_id', clientId);
439
+ authUrl.searchParams.set('redirect_uri', redirectUri);
437
440
  authUrl.searchParams.set('response_type', 'code');
438
441
  authUrl.searchParams.set('scope', scope);
439
442
  authUrl.searchParams.set('response_mode', 'query');
443
+ authUrl.searchParams.set('code_challenge', codeChallenge);
444
+ authUrl.searchParams.set('code_challenge_method', 'S256');
445
+ authUrl.searchParams.set('state', stateId);
440
446
  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 = {
447
+ logger.info('OAuth required - persistent callback mode', {
448
+ service: service,
449
+ redirectUri: redirectUri
450
+ });
451
+ throw new _typests.AuthRequiredError({
449
452
  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);
453
+ provider: service,
454
+ url: authUrl.toString()
455
+ });
458
456
  case 11:
459
- // Interactive mode - start ephemeral OAuth flow
457
+ // Ephemeral callback mode (local development)
460
458
  logger.info('Starting ephemeral OAuth flow', {
461
459
  service: service,
462
- headless: headless
460
+ headless: this.config.headless
463
461
  });
464
462
  return [
465
463
  4,
@@ -467,7 +465,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
467
465
  ];
468
466
  case 12:
469
467
  _ref = _state.sent(), token = _ref.token, email = _ref.email;
470
- // Store token with email as accountId
471
468
  return [
472
469
  4,
473
470
  (0, _oauth.setToken)(tokenStore, {
@@ -477,7 +474,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
477
474
  ];
478
475
  case 13:
479
476
  _state.sent();
480
- // Register account in account management system
481
477
  return [
482
478
  4,
483
479
  (0, _oauth.addAccount)(tokenStore, {
@@ -487,7 +483,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
487
483
  ];
488
484
  case 14:
489
485
  _state.sent();
490
- // Set as active account so subsequent getAccessToken() calls find it
491
486
  return [
492
487
  4,
493
488
  (0, _oauth.setActiveAccount)(tokenStore, {
@@ -497,7 +492,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
497
492
  ];
498
493
  case 15:
499
494
  _state.sent();
500
- // Store account metadata (email, added timestamp)
501
495
  return [
502
496
  4,
503
497
  (0, _oauth.setAccountInfo)(tokenStore, {
@@ -538,86 +532,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
538
532
  };
539
533
  };
540
534
  /**
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
535
  * Get user email from Microsoft Graph API (pure query)
622
536
  * Used to query email for existing authenticated account
623
537
  *
@@ -677,24 +591,6 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
677
591
  });
678
592
  }).call(this);
679
593
  };
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
594
  _proto.isTokenValid = function isTokenValid(token) {
699
595
  if (!token.expiresAt) return true; // No expiry = assume valid
700
596
  return Date.now() < token.expiresAt - 60000; // 1 minute buffer
@@ -754,46 +650,50 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
754
650
  };
755
651
  _proto.performEphemeralOAuthFlow = function performEphemeralOAuthFlow() {
756
652
  return _async_to_generator(function() {
757
- var _this, _this_config, clientId, tenantId, scope, headless, logger, configRedirectUri, targetHost, targetPort, targetProtocol, callbackPath, useConfiguredUri, parsed;
653
+ var _this, _this_config, clientId, tenantId, scope, headless, logger, configRedirectUri, listenHost, listenPort, callbackPath, useConfiguredUri, parsed, isLoopback, envPort;
758
654
  return _ts_generator(this, function(_state) {
759
655
  _this = this;
760
656
  _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
657
+ // Server listen configuration (where ephemeral server binds)
658
+ listenHost = 'localhost'; // Default: localhost for ephemeral loopback
659
+ listenPort = 0; // Default: OS-assigned ephemeral port
660
+ // Redirect URI configuration (what goes in auth URL and token exchange)
765
661
  callbackPath = '/callback'; // Default callback path
766
662
  useConfiguredUri = false;
767
663
  if (configRedirectUri) {
768
664
  try {
769
665
  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);
666
+ isLoopback = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
667
+ if (isLoopback) {
668
+ // Local development: Listen on specific loopback address/port
669
+ listenHost = parsed.hostname;
670
+ listenPort = parsed.port ? Number.parseInt(parsed.port, 10) : 0;
776
671
  } else {
777
- targetPort = parsed.protocol === 'https:' ? 443 : 80;
672
+ // Cloud deployment: Listen on 0.0.0.0 with PORT from environment
673
+ // The redirectUri is the PUBLIC URL (e.g., https://example.com/oauth/callback)
674
+ // The server listens on 0.0.0.0:PORT and the load balancer routes to it
675
+ listenHost = '0.0.0.0';
676
+ envPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : undefined;
677
+ listenPort = envPort && Number.isFinite(envPort) ? envPort : 8080;
778
678
  }
779
- // Extract path (default to /callback if URL has no path or just '/')
679
+ // Extract callback path from URL
780
680
  if (parsed.pathname && parsed.pathname !== '/') {
781
681
  callbackPath = parsed.pathname;
782
682
  }
783
683
  useConfiguredUri = true;
784
684
  logger.debug('Using configured redirect URI', {
785
- host: targetHost,
786
- protocol: targetProtocol,
787
- port: targetPort,
788
- path: callbackPath,
789
- redirectUri: configRedirectUri
685
+ listenHost: listenHost,
686
+ listenPort: listenPort,
687
+ callbackPath: callbackPath,
688
+ redirectUri: configRedirectUri,
689
+ isLoopback: isLoopback
790
690
  });
791
691
  } catch (error) {
792
692
  logger.warn('Failed to parse redirectUri, using ephemeral defaults', {
793
693
  redirectUri: configRedirectUri,
794
694
  error: _instanceof(error, Error) ? error.message : String(error)
795
695
  });
796
- // Continue with defaults (127.0.0.1, port 0, http, /callback)
696
+ // Continue with defaults (localhost, port 0, http, /callback)
797
697
  }
798
698
  }
799
699
  return [
@@ -928,8 +828,11 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
928
828
  });
929
829
  }).call(_this);
930
830
  });
931
- // Listen on targetPort (0 for OS assignment, or custom port from redirectUri)
932
- server.listen(targetPort, targetHost, function() {
831
+ // Listen on configured host/port
832
+ // - For loopback (default): localhost with OS-assigned port
833
+ // - For configured loopback: specific localhost port from redirectUri
834
+ // - For cloud deployment: 0.0.0.0:${PORT} from environment
835
+ server.listen(listenPort, listenHost, function() {
933
836
  var address = server === null || server === void 0 ? void 0 : server.address();
934
837
  if (!address || typeof address === 'string') {
935
838
  server === null || server === void 0 ? void 0 : server.close();
@@ -939,11 +842,11 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
939
842
  serverPort = address.port;
940
843
  // Construct final redirect URI
941
844
  if (useConfiguredUri && configRedirectUri) {
942
- // Use configured redirect URI as-is for production
845
+ // Use configured redirect URI as-is (public URL for cloud, or specific local URL)
943
846
  finalRedirectUri = configRedirectUri;
944
847
  } else {
945
- // Construct ephemeral redirect URI with actual server port
946
- finalRedirectUri = "".concat(targetProtocol, "//").concat(targetHost, ":").concat(serverPort).concat(callbackPath);
848
+ // Construct ephemeral redirect URI with actual server port (default local behavior)
849
+ finalRedirectUri = "http://localhost:".concat(serverPort).concat(callbackPath);
947
850
  }
948
851
  // Build Microsoft auth URL
949
852
  var authUrl = new URL("https://login.microsoftonline.com/".concat(tenantId, "/oauth2/v2.0/authorize"));
@@ -1111,6 +1014,137 @@ var LoopbackOAuthProvider = /*#__PURE__*/ function() {
1111
1014
  }).call(this);
1112
1015
  };
1113
1016
  /**
1017
+ * Handle OAuth callback from persistent endpoint.
1018
+ * Used by HTTP servers with configured redirectUri.
1019
+ *
1020
+ * @param params - OAuth callback parameters
1021
+ * @returns Email and cached token
1022
+ */ _proto.handleOAuthCallback = function handleOAuthCallback(params) {
1023
+ return _async_to_generator(function() {
1024
+ var code, state, _this_config, logger, service, tokenStore, redirectUri, pendingKey, pendingAuth, tokenResponse, cachedToken, email;
1025
+ return _ts_generator(this, function(_state) {
1026
+ switch(_state.label){
1027
+ case 0:
1028
+ code = params.code, state = params.state;
1029
+ _this_config = this.config, logger = _this_config.logger, service = _this_config.service, tokenStore = _this_config.tokenStore, redirectUri = _this_config.redirectUri;
1030
+ if (!state) {
1031
+ throw new Error('Missing state parameter in OAuth callback');
1032
+ }
1033
+ if (!redirectUri) {
1034
+ throw new Error('handleOAuthCallback requires configured redirectUri');
1035
+ }
1036
+ // Load pending auth (includes PKCE verifier)
1037
+ pendingKey = "".concat(service, ":pending:").concat(state);
1038
+ return [
1039
+ 4,
1040
+ tokenStore.get(pendingKey)
1041
+ ];
1042
+ case 1:
1043
+ pendingAuth = _state.sent();
1044
+ if (!pendingAuth) {
1045
+ throw new Error('Invalid or expired OAuth state. Please try again.');
1046
+ }
1047
+ if (!(Date.now() - pendingAuth.createdAt > 5 * 60 * 1000)) return [
1048
+ 3,
1049
+ 3
1050
+ ];
1051
+ return [
1052
+ 4,
1053
+ tokenStore.delete(pendingKey)
1054
+ ];
1055
+ case 2:
1056
+ _state.sent();
1057
+ throw new Error('OAuth state expired. Please try again.');
1058
+ case 3:
1059
+ logger.info('Processing OAuth callback', {
1060
+ service: service,
1061
+ state: state
1062
+ });
1063
+ return [
1064
+ 4,
1065
+ this.exchangeCodeForToken(code, pendingAuth.codeVerifier, redirectUri)
1066
+ ];
1067
+ case 4:
1068
+ tokenResponse = _state.sent();
1069
+ // Create cached token
1070
+ cachedToken = _object_spread({
1071
+ accessToken: tokenResponse.access_token,
1072
+ refreshToken: tokenResponse.refresh_token,
1073
+ expiresAt: tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1000 : undefined
1074
+ }, tokenResponse.scope !== undefined && {
1075
+ scope: tokenResponse.scope
1076
+ });
1077
+ return [
1078
+ 4,
1079
+ this.fetchUserEmailFromToken(tokenResponse.access_token)
1080
+ ];
1081
+ case 5:
1082
+ email = _state.sent();
1083
+ // Store token
1084
+ return [
1085
+ 4,
1086
+ (0, _oauth.setToken)(tokenStore, {
1087
+ accountId: email,
1088
+ service: service
1089
+ }, cachedToken)
1090
+ ];
1091
+ case 6:
1092
+ _state.sent();
1093
+ // Add account and set as active
1094
+ return [
1095
+ 4,
1096
+ (0, _oauth.addAccount)(tokenStore, {
1097
+ service: service,
1098
+ accountId: email
1099
+ })
1100
+ ];
1101
+ case 7:
1102
+ _state.sent();
1103
+ return [
1104
+ 4,
1105
+ (0, _oauth.setActiveAccount)(tokenStore, {
1106
+ service: service,
1107
+ accountId: email
1108
+ })
1109
+ ];
1110
+ case 8:
1111
+ _state.sent();
1112
+ // Store account metadata
1113
+ return [
1114
+ 4,
1115
+ (0, _oauth.setAccountInfo)(tokenStore, {
1116
+ service: service,
1117
+ accountId: email
1118
+ }, {
1119
+ email: email,
1120
+ addedAt: new Date().toISOString()
1121
+ })
1122
+ ];
1123
+ case 9:
1124
+ _state.sent();
1125
+ // Clean up pending auth
1126
+ return [
1127
+ 4,
1128
+ tokenStore.delete(pendingKey)
1129
+ ];
1130
+ case 10:
1131
+ _state.sent();
1132
+ logger.info('OAuth callback completed', {
1133
+ service: service,
1134
+ email: email
1135
+ });
1136
+ return [
1137
+ 2,
1138
+ {
1139
+ email: email,
1140
+ token: cachedToken
1141
+ }
1142
+ ];
1143
+ }
1144
+ });
1145
+ }).call(this);
1146
+ };
1147
+ /**
1114
1148
  * Create auth middleware for single-user context (single active account per service)
1115
1149
  *
1116
1150
  * Single-user mode: