@msout/microsoft-webauth 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +45 -3
- package/jest.config.js +5 -0
- package/package.json +2 -2
- package/src/auth.js +106 -21
- package/src/config.js +35 -8
- package/src/index.js +13 -5
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# microsoft-webauth
|
|
2
2
|
|
|
3
3
|
A tool to authenticate against microsoft online (live or professionnal tenants)
|
|
4
|
-
|
|
4
|
+
We do not rely on GraphAPI.
|
|
5
|
+
|
|
6
|
+
e.g:
|
|
5
7
|
- https://onenote.cloud.microsoft/notebooks
|
|
6
8
|
- https://outlook.live.com/mail/
|
|
7
9
|
- and by extension https://login.microsoft.com
|
|
8
10
|
|
|
9
|
-
Microsoft web authentication via Playwright — extracted from [MSOneNote Exporter](https://github.com/
|
|
11
|
+
Microsoft web authentication via Playwright — extracted from [MSOneNote Exporter](https://github.com/enoola/Microsoft-OneNote-Exporter).
|
|
12
|
+
I did extract it realising it might be useful out of the box for other projects.
|
|
13
|
+
And it will make it easier to maintain as far as I see it.
|
|
10
14
|
|
|
11
15
|
This is a standalone CLI tool for authenticating with Microsoft accounts using Playwright. It handles:
|
|
16
|
+
- NO GraphAPI
|
|
12
17
|
- Automated login with email/password
|
|
13
18
|
- Manual login in browser
|
|
14
19
|
- MFA/2FA support (OTC codes, number matching)
|
|
15
20
|
- Session persistence
|
|
21
|
+
|
|
22
|
+
## Why this project ?
|
|
23
|
+
|
|
24
|
+
While this let you authenticate this is a part of a bigger purpose,
|
|
25
|
+
primary aim is to offer people a simple way to get out of Microsoft OneNote, because you regardless of what ms documentation states
|
|
26
|
+
=> https://learn.microsoft.com/en-us/answers/questions/2276682/onenote-api-fails-with-large-sharepoint-document-l
|
|
27
|
+
|
|
28
|
+
in essence you want to search for microsoft-onenote-list-notebook, microsoft-onenote-exporter
|
|
29
|
+
|
|
16
30
|
## Available on npmjs
|
|
17
31
|
|
|
18
32
|
You can find this package here: https://www.npmjs.com/package/@msout/microsoft-webauth
|
|
@@ -37,6 +51,12 @@ npm install @msout/microsoft-webauth
|
|
|
37
51
|
microsoft-webauth login --email your@email.com --password yourpassword
|
|
38
52
|
```
|
|
39
53
|
|
|
54
|
+
With custom auth file path:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
microsoft-webauth login --email your@email.com --password yourpassword --auth-file /path/to/authfile.json
|
|
58
|
+
```
|
|
59
|
+
|
|
40
60
|
### Login (Manual/Interactive)
|
|
41
61
|
|
|
42
62
|
```bash
|
|
@@ -45,18 +65,36 @@ microsoft-webauth login
|
|
|
45
65
|
|
|
46
66
|
This will open a browser window. Log in manually, then press Enter when you see the notebooks list.
|
|
47
67
|
|
|
68
|
+
With custom auth file path:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
microsoft-webauth login --auth-file /path/to/authfile.json
|
|
72
|
+
```
|
|
73
|
+
|
|
48
74
|
### Check Authentication Status
|
|
49
75
|
|
|
50
76
|
```bash
|
|
51
77
|
microsoft-webauth check
|
|
52
78
|
```
|
|
53
79
|
|
|
80
|
+
With custom auth file path:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
microsoft-webauth check --auth-file /path/to/authfile.json
|
|
84
|
+
```
|
|
85
|
+
|
|
54
86
|
### Logout
|
|
55
87
|
|
|
56
88
|
```bash
|
|
57
89
|
microsoft-webauth logout
|
|
58
90
|
```
|
|
59
91
|
|
|
92
|
+
With custom auth file path:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
microsoft-webauth logout --auth-file /path/to/authfile.json
|
|
96
|
+
```
|
|
97
|
+
|
|
60
98
|
## Options
|
|
61
99
|
|
|
62
100
|
| Option | Description |
|
|
@@ -65,13 +103,16 @@ microsoft-webauth logout
|
|
|
65
103
|
| `--password <password>` | Microsoft account password (for automated login) |
|
|
66
104
|
| `--notheadless` | Run in visible browser mode (disable headless) |
|
|
67
105
|
| `--dodump` | Dump HTML content to files for debugging |
|
|
106
|
+
| `--auth-file <path>` | Path to auth file (default: ~/.microsoft-webauth/auth-file.json) |
|
|
68
107
|
|
|
69
108
|
## Output
|
|
70
109
|
|
|
71
|
-
Authentication state is saved to
|
|
110
|
+
Authentication state is saved to the specified auth file path. By default, it uses `~/.microsoft-webauth/auth-file.json`. A metadata file `{auth-file-prefix}-meta.json` stores:
|
|
72
111
|
- Email used for login
|
|
73
112
|
- Login timestamp
|
|
74
113
|
|
|
114
|
+
When files already exist, they are automatically backed up with `.old` suffix. If `.old` files already exist, a warning is displayed before overwriting.
|
|
115
|
+
|
|
75
116
|
## Testing
|
|
76
117
|
|
|
77
118
|
```bash
|
|
@@ -102,3 +143,4 @@ microsoft-webauth-playwright/
|
|
|
102
143
|
## License
|
|
103
144
|
|
|
104
145
|
ISC — same as MSOneNote Exporter.
|
|
146
|
+
|
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msout/microsoft-webauth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Microsoft web authentication module, using playwright to automate the login process and retrieve cookies for authenticated sessions.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"javascript"
|
|
23
23
|
],
|
|
24
24
|
"author": "msout@tuta.io",
|
|
25
|
-
"license": "
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "git+https://github.com/Ms-OneNote-Exporter/microsoft-webauth.git"
|
package/src/auth.js
CHANGED
|
@@ -1,27 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview This file handles user authentication logic.
|
|
3
|
+
* @author phptr,enoola,msout
|
|
4
|
+
* @copyright 2026 phptr,enoola,msout
|
|
5
|
+
*/
|
|
1
6
|
const { chromium } = require('playwright');
|
|
2
7
|
const fs = require('fs-extra');
|
|
3
8
|
const logger = require('./utils/logger');
|
|
4
|
-
const {
|
|
9
|
+
const { DEFAULT_AUTH_FILE, getAuthMetaFilePath, ensureAuthDir, ONENOTE_URL } = require('./config');
|
|
5
10
|
const path = require('path');
|
|
6
11
|
const readline = require('readline');
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
/** Returns the auth file path, defaulting to DEFAULT_AUTH_FILE */
|
|
14
|
+
function getAuthFilePath(authFilePath) {
|
|
15
|
+
return authFilePath || DEFAULT_AUTH_FILE;
|
|
16
|
+
}
|
|
10
17
|
|
|
11
18
|
/** Returns { email, loginTime } from auth-meta.json, or null if not found. */
|
|
12
|
-
async function getAuthMeta() {
|
|
19
|
+
async function getAuthMeta(authFilePath) {
|
|
20
|
+
const filePath = getAuthFilePath(authFilePath);
|
|
21
|
+
const metaPath = getAuthMetaFilePath(filePath);
|
|
13
22
|
try {
|
|
14
|
-
if (await fs.pathExists(
|
|
15
|
-
return await fs.readJson(
|
|
23
|
+
if (await fs.pathExists(metaPath)) {
|
|
24
|
+
return await fs.readJson(metaPath);
|
|
16
25
|
}
|
|
17
26
|
} catch (e) { }
|
|
18
27
|
return null;
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
/** Generates backup path by appending .old to file */
|
|
31
|
+
function getBackupPath(filePath) {
|
|
32
|
+
return filePath + '.old';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if files exist and handles backup logic
|
|
37
|
+
* Returns object with { authFileExists, metaFileExists, willOverwriteOld }
|
|
38
|
+
*/
|
|
39
|
+
async function checkAndPrepareFiles(authFilePath) {
|
|
40
|
+
const metaFilePath = getAuthMetaFilePath(authFilePath);
|
|
41
|
+
|
|
42
|
+
const authFileExists = await fs.pathExists(authFilePath);
|
|
43
|
+
const metaFileExists = await fs.pathExists(metaFilePath);
|
|
44
|
+
|
|
45
|
+
// Check if .old versions exist
|
|
46
|
+
const authOldPath = getBackupPath(authFilePath);
|
|
47
|
+
const metaOldPath = getBackupPath(metaFilePath);
|
|
48
|
+
const authOldExists = await fs.pathExists(authOldPath);
|
|
49
|
+
const metaOldExists = await fs.pathExists(metaOldPath);
|
|
50
|
+
|
|
51
|
+
let willOverwriteOld = false;
|
|
52
|
+
|
|
53
|
+
// If current files exist, backup them
|
|
54
|
+
if (authFileExists || metaFileExists) {
|
|
55
|
+
// Warn if .old files already exist (they will be erased)
|
|
56
|
+
if (authOldExists || metaOldExists) {
|
|
57
|
+
logger.warn(`Warning: Backup files (.old) already exist and will be erased:`);
|
|
58
|
+
if (authOldExists) logger.warn(` ${authOldPath}`);
|
|
59
|
+
if (metaOldExists) logger.warn(` ${metaOldPath}`);
|
|
60
|
+
willOverwriteOld = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create backup directory if it doesn't exist
|
|
64
|
+
const dir = path.dirname(authFilePath);
|
|
65
|
+
await ensureAuthDir(authFilePath);
|
|
66
|
+
|
|
67
|
+
// Backup existing files
|
|
68
|
+
if (authFileExists) {
|
|
69
|
+
await fs.move(authFilePath, authOldPath, { overwrite: true });
|
|
70
|
+
logger.info(`Backed up ${authFilePath} to ${authOldPath}`);
|
|
71
|
+
}
|
|
72
|
+
if (metaFileExists) {
|
|
73
|
+
await fs.move(metaFilePath, metaOldPath, { overwrite: true });
|
|
74
|
+
logger.info(`Backed up ${metaFilePath} to ${metaOldPath}`);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Ensure directory exists for new files
|
|
78
|
+
await ensureAuthDir(authFilePath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
authFileExists,
|
|
83
|
+
metaFileExists,
|
|
84
|
+
willOverwriteOld,
|
|
85
|
+
authOldPath,
|
|
86
|
+
metaOldPath
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
21
90
|
/** Deletes auth.json and auth-meta.json (full logout). */
|
|
22
|
-
async function logout() {
|
|
23
|
-
|
|
24
|
-
|
|
91
|
+
async function logout(authFilePath) {
|
|
92
|
+
const filePath = getAuthFilePath(authFilePath);
|
|
93
|
+
const metaPath = getAuthMetaFilePath(filePath);
|
|
94
|
+
|
|
95
|
+
await fs.remove(filePath);
|
|
96
|
+
await fs.remove(metaPath);
|
|
25
97
|
}
|
|
26
98
|
|
|
27
99
|
/**
|
|
@@ -125,15 +197,22 @@ async function dismissFidoPage(page, logger) {
|
|
|
125
197
|
}
|
|
126
198
|
|
|
127
199
|
async function login(credentials = {}) {
|
|
128
|
-
const { email, password, targetUrl } = credentials;
|
|
200
|
+
const { email, password, targetUrl, authFile } = credentials;
|
|
129
201
|
const isAutomated = !!(email && password);
|
|
130
202
|
const headless = !credentials.notheadless && isAutomated;
|
|
131
203
|
// Use targetUrl if provided, otherwise default to ONENOTE_URL for backward compatibility
|
|
132
204
|
const finalTargetUrl = targetUrl || ONENOTE_URL;
|
|
133
205
|
|
|
206
|
+
// Get the auth file path (use provided or default)
|
|
207
|
+
const filePath = getAuthFilePath(authFile);
|
|
208
|
+
const metaPath = getAuthMetaFilePath(filePath);
|
|
209
|
+
|
|
134
210
|
// Added to verify version on user's machine
|
|
135
211
|
logger.debug('Authentication Module: Version 4.4-DEBUG starting...');
|
|
136
212
|
|
|
213
|
+
logger.debug(`Using auth file path: ${filePath}`);
|
|
214
|
+
logger.debug(`Using meta file path: ${metaPath}`);
|
|
215
|
+
|
|
137
216
|
if (isAutomated) {
|
|
138
217
|
logger.info(`Attempting automated login for ${email}...`);
|
|
139
218
|
} else {
|
|
@@ -142,6 +221,9 @@ async function login(credentials = {}) {
|
|
|
142
221
|
const serviceName = finalTargetUrl.includes('outlook') ? 'Outlook' : 'OneNote';
|
|
143
222
|
logger.warn(`The script will wait until you successfully reach the ${serviceName} interface.`);
|
|
144
223
|
}
|
|
224
|
+
|
|
225
|
+
// Prepare files (backup existing if needed, create directory)
|
|
226
|
+
await checkAndPrepareFiles(filePath);
|
|
145
227
|
|
|
146
228
|
const browser = await chromium.launch({ headless: !!headless });
|
|
147
229
|
const context = await browser.newContext({
|
|
@@ -486,7 +568,7 @@ async function login(credentials = {}) {
|
|
|
486
568
|
} catch (e) {
|
|
487
569
|
logger.debug(`Post-password verification handling skipped or failed: ${e.message}`);
|
|
488
570
|
}
|
|
489
|
-
|
|
571
|
+
|
|
490
572
|
// 2.7. Handle "Help protect your account" interrupt screen
|
|
491
573
|
try {
|
|
492
574
|
const interruptPrompt = page.getByText(/Help protect your account/i).first();
|
|
@@ -565,14 +647,14 @@ async function login(credentials = {}) {
|
|
|
565
647
|
}
|
|
566
648
|
|
|
567
649
|
logger.info('Saving authentication state...');
|
|
568
|
-
await context.storageState({ path:
|
|
650
|
+
await context.storageState({ path: filePath });
|
|
569
651
|
|
|
570
|
-
await fs.writeJson(
|
|
652
|
+
await fs.writeJson(metaPath, {
|
|
571
653
|
email: email || 'manual login',
|
|
572
654
|
loginTime: new Date().toISOString()
|
|
573
655
|
});
|
|
574
656
|
|
|
575
|
-
logger.success(`Authentication successful! State saved to ${
|
|
657
|
+
logger.success(`Authentication successful! State saved to ${filePath}`);
|
|
576
658
|
} catch (error) {
|
|
577
659
|
logger.error('Authentication failed or cancelled:', error);
|
|
578
660
|
if (isAutomated) {
|
|
@@ -583,16 +665,19 @@ async function login(credentials = {}) {
|
|
|
583
665
|
}
|
|
584
666
|
}
|
|
585
667
|
|
|
586
|
-
async function getAuthenticatedContext(browser) {
|
|
587
|
-
|
|
588
|
-
|
|
668
|
+
async function getAuthenticatedContext(browser, authFilePath) {
|
|
669
|
+
const filePath = getAuthFilePath(authFilePath);
|
|
670
|
+
if (await fs.pathExists(filePath)) {
|
|
671
|
+
return browser.newContext({ storageState: filePath });
|
|
589
672
|
} else {
|
|
590
673
|
throw new Error('No authentication state found. Please run "login" command first.');
|
|
591
674
|
}
|
|
592
675
|
}
|
|
593
676
|
|
|
594
|
-
async function checkAuth(targetUrl = ONENOTE_URL) {
|
|
595
|
-
|
|
677
|
+
async function checkAuth(targetUrl = ONENOTE_URL, authFilePath) {
|
|
678
|
+
const filePath = getAuthFilePath(authFilePath);
|
|
679
|
+
|
|
680
|
+
if (!(await fs.pathExists(filePath))) {
|
|
596
681
|
return false;
|
|
597
682
|
}
|
|
598
683
|
|
|
@@ -600,7 +685,7 @@ async function checkAuth(targetUrl = ONENOTE_URL) {
|
|
|
600
685
|
try {
|
|
601
686
|
logger.debug('Verifying authentication session...');
|
|
602
687
|
browser = await chromium.launch({ headless: true });
|
|
603
|
-
const context = await browser.newContext({ storageState:
|
|
688
|
+
const context = await browser.newContext({ storageState: filePath });
|
|
604
689
|
const page = await context.newPage();
|
|
605
690
|
|
|
606
691
|
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
@@ -613,7 +698,7 @@ async function checkAuth(targetUrl = ONENOTE_URL) {
|
|
|
613
698
|
|
|
614
699
|
if (isLoginUrl) {
|
|
615
700
|
logger.warn('Authentication session has expired. Deleting stale auth state.');
|
|
616
|
-
await logout();
|
|
701
|
+
await logout(authFilePath);
|
|
617
702
|
return false;
|
|
618
703
|
}
|
|
619
704
|
|
package/src/config.js
CHANGED
|
@@ -1,22 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Returns the directory where auth files (auth.json, auth-meta.json) are stored.
|
|
3
|
+
* @author phptr,enoola,msout
|
|
4
|
+
* @copyright 2026 phptr,enoola,msout
|
|
5
|
+
*/
|
|
1
6
|
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs-extra');
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
|
-
* Returns the
|
|
5
|
-
* For this standalone CLI tool, we use the project root.
|
|
11
|
+
* Returns the default auth file path (~/.microsoft-webauth/auth-file.json)
|
|
6
12
|
*/
|
|
7
|
-
function
|
|
8
|
-
|
|
13
|
+
function getDefaultAuthFilePath() {
|
|
14
|
+
const homeDir = os.homedir();
|
|
15
|
+
const appDir = path.join(homeDir, '.microsoft-webauth');
|
|
16
|
+
return path.join(appDir, 'auth-file.json');
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Generates the meta file path from auth file path
|
|
21
|
+
* e.g., /path/to/auth-file.json -> /path/to/auth-file-meta.json
|
|
22
|
+
*/
|
|
23
|
+
function getAuthMetaFilePath(authFilePath) {
|
|
24
|
+
const dir = path.dirname(authFilePath);
|
|
25
|
+
const name = path.basename(authFilePath);
|
|
26
|
+
// Remove .json extension if present, then add -meta.json
|
|
27
|
+
const baseName = name.replace(/\.json$/, '');
|
|
28
|
+
return path.join(dir, `${baseName}-meta.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ensures the directory for the auth file exists, creates it if needed
|
|
33
|
+
*/
|
|
34
|
+
async function ensureAuthDir(authFilePath) {
|
|
35
|
+
const dir = path.dirname(authFilePath);
|
|
36
|
+
await fs.ensureDir(dir);
|
|
37
|
+
}
|
|
12
38
|
|
|
13
|
-
const
|
|
39
|
+
const DEFAULT_AUTH_FILE = getDefaultAuthFilePath();
|
|
14
40
|
const ONENOTE_URL = 'https://onenote.cloud.microsoft/en-us';
|
|
15
41
|
const OUTLOOK_URL = 'https://outlook.cloud.microsoft/mail/';
|
|
16
42
|
|
|
17
43
|
module.exports = {
|
|
18
|
-
|
|
44
|
+
DEFAULT_AUTH_FILE,
|
|
45
|
+
getAuthMetaFilePath,
|
|
46
|
+
ensureAuthDir,
|
|
19
47
|
ONENOTE_URL,
|
|
20
48
|
OUTLOOK_URL,
|
|
21
|
-
USER_DATA_DIR,
|
|
22
49
|
};
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Main CLI interface to handle authentication commands.
|
|
4
|
+
* @author phptr,enoola,msout
|
|
5
|
+
* @copyright 2026 phptr,enoola,msout
|
|
6
|
+
*/
|
|
2
7
|
const { program } = require('commander');
|
|
3
8
|
const logger = require('./utils/logger');
|
|
4
9
|
const { login, checkAuth, getAuthMeta, logout } = require('./auth');
|
|
5
|
-
const { ONENOTE_URL, OUTLOOK_URL } = require('./config');
|
|
10
|
+
const { DEFAULT_AUTH_FILE, ONENOTE_URL, OUTLOOK_URL } = require('./config');
|
|
6
11
|
|
|
7
12
|
program
|
|
8
13
|
.name('webauth')
|
|
@@ -17,6 +22,7 @@ program
|
|
|
17
22
|
.option('--notheadless', 'Run in visible browser mode for debugging')
|
|
18
23
|
.option('--dodump', 'Dump HTML content to files for debugging')
|
|
19
24
|
.option('--against <target>', 'Target service: onenote (default) or outlook', 'onenote')
|
|
25
|
+
.option('--auth-file <path>', 'Path to auth file (default: ~/.microsoft-webauth/auth-file.json)', DEFAULT_AUTH_FILE)
|
|
20
26
|
.action(async (options) => {
|
|
21
27
|
const targetUrl = options.against === 'outlook' ? OUTLOOK_URL : ONENOTE_URL;
|
|
22
28
|
await login({ ...options, targetUrl });
|
|
@@ -26,12 +32,13 @@ program
|
|
|
26
32
|
.command('check')
|
|
27
33
|
.description('Check if authenticated')
|
|
28
34
|
.option('--against <target>', 'Target service: onenote (default) or outlook', 'onenote')
|
|
35
|
+
.option('--auth-file <path>', 'Path to auth file (default: ~/.microsoft-webauth/auth-file.json)', DEFAULT_AUTH_FILE)
|
|
29
36
|
.action(async (options) => {
|
|
30
37
|
const targetUrl = options.against === 'outlook' ? OUTLOOK_URL : ONENOTE_URL;
|
|
31
|
-
const isAuth = await checkAuth(targetUrl);
|
|
38
|
+
const isAuth = await checkAuth(targetUrl, options.authFile);
|
|
32
39
|
if (isAuth) {
|
|
33
40
|
logger.success('Authentication file found. You are authenticated.');
|
|
34
|
-
const meta = await getAuthMeta();
|
|
41
|
+
const meta = await getAuthMeta(options.authFile);
|
|
35
42
|
if (meta && meta.email) {
|
|
36
43
|
const loginTime = new Date(meta.loginTime).toLocaleString();
|
|
37
44
|
logger.info(`Logged in as: ${meta.email}`);
|
|
@@ -45,8 +52,9 @@ program
|
|
|
45
52
|
program
|
|
46
53
|
.command('logout')
|
|
47
54
|
.description('Clear authentication state')
|
|
48
|
-
.
|
|
49
|
-
|
|
55
|
+
.option('--auth-file <path>', 'Path to auth file (default: ~/.microsoft-webauth/auth-file.json)', DEFAULT_AUTH_FILE)
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
await logout(options.authFile);
|
|
50
58
|
logger.success('Logged out successfully. Authentication state cleared.');
|
|
51
59
|
});
|
|
52
60
|
|