m365-cli 0.1.0

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/bin/m365.js ADDED
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { readFileSync } from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+ import { login, logout } from '../src/auth/token-manager.js';
8
+ import mailCommands from '../src/commands/mail.js';
9
+ import calendarCommands from '../src/commands/calendar.js';
10
+ import onedriveCommands from '../src/commands/onedrive.js';
11
+ import sharepointCommands from '../src/commands/sharepoint.js';
12
+ import { handleError } from '../src/utils/error.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ // Load package.json for version
18
+ const packageJson = JSON.parse(
19
+ readFileSync(join(__dirname, '../package.json'), 'utf-8')
20
+ );
21
+
22
+ const program = new Command();
23
+
24
+ program
25
+ .name('m365')
26
+ .description('Microsoft 365 CLI - Manage Mail, Calendar, and OneDrive')
27
+ .version(packageJson.version);
28
+
29
+ // Login command
30
+ program
31
+ .command('login')
32
+ .description('Authenticate with Microsoft 365')
33
+ .action(async () => {
34
+ try {
35
+ await login();
36
+ } catch (error) {
37
+ handleError(error);
38
+ }
39
+ });
40
+
41
+ // Logout command
42
+ program
43
+ .command('logout')
44
+ .description('Clear stored credentials')
45
+ .action(async () => {
46
+ try {
47
+ await logout();
48
+ } catch (error) {
49
+ handleError(error);
50
+ }
51
+ });
52
+
53
+ // Mail commands
54
+ const mailCommand = program
55
+ .command('mail')
56
+ .description('Manage emails');
57
+
58
+ mailCommand
59
+ .command('list')
60
+ .description('List emails')
61
+ .option('-t, --top <number>', 'Number of emails to list', '10')
62
+ .option('-f, --folder <name>', 'Folder name (inbox, sent, drafts)', 'inbox')
63
+ .option('--json', 'Output as JSON')
64
+ .action(async (options) => {
65
+ await mailCommands.list({
66
+ top: parseInt(options.top),
67
+ folder: options.folder,
68
+ json: options.json,
69
+ });
70
+ });
71
+
72
+ mailCommand
73
+ .command('read')
74
+ .description('Read email by ID')
75
+ .argument('<id>', 'Email ID')
76
+ .option('--force', 'Skip whitelist check and show full content')
77
+ .option('--json', 'Output as JSON')
78
+ .action(async (id, options) => {
79
+ await mailCommands.read(id, {
80
+ force: options.force,
81
+ json: options.json,
82
+ });
83
+ });
84
+
85
+ mailCommand
86
+ .command('send')
87
+ .description('Send an email')
88
+ .argument('<to>', 'Recipient email address(es) (comma-separated)')
89
+ .argument('<subject>', 'Email subject')
90
+ .argument('<body>', 'Email body (HTML supported)')
91
+ .option('-a, --attach <files...>', 'Attach files')
92
+ .option('--cc <emails>', 'CC recipients (comma-separated)')
93
+ .option('--bcc <emails>', 'BCC recipients (comma-separated)')
94
+ .option('--json', 'Output as JSON')
95
+ .action(async (to, subject, body, options) => {
96
+ await mailCommands.send(to, subject, body, {
97
+ attach: options.attach || [],
98
+ cc: options.cc,
99
+ bcc: options.bcc,
100
+ json: options.json,
101
+ });
102
+ });
103
+
104
+ mailCommand
105
+ .command('search')
106
+ .description('Search emails')
107
+ .argument('<query>', 'Search query')
108
+ .option('-t, --top <number>', 'Number of results', '10')
109
+ .option('--json', 'Output as JSON')
110
+ .action(async (query, options) => {
111
+ await mailCommands.search(query, {
112
+ top: parseInt(options.top),
113
+ json: options.json,
114
+ });
115
+ });
116
+
117
+ mailCommand
118
+ .command('attachments')
119
+ .description('List email attachments')
120
+ .argument('<id>', 'Email ID')
121
+ .option('--json', 'Output as JSON')
122
+ .action(async (id, options) => {
123
+ await mailCommands.attachments(id, {
124
+ json: options.json,
125
+ });
126
+ });
127
+
128
+ mailCommand
129
+ .command('download-attachment')
130
+ .description('Download email attachment')
131
+ .argument('<message-id>', 'Email ID')
132
+ .argument('<attachment-id>', 'Attachment ID')
133
+ .argument('[local-path]', 'Local file path (default: attachment name)')
134
+ .option('--json', 'Output as JSON')
135
+ .action(async (messageId, attachmentId, localPath, options) => {
136
+ await mailCommands.downloadAttachment(messageId, attachmentId, localPath, {
137
+ json: options.json,
138
+ });
139
+ });
140
+
141
+ mailCommand
142
+ .command('trust')
143
+ .description('Add email or domain to whitelist')
144
+ .argument('<email>', 'Email address or domain (e.g., user@example.com or @example.com)')
145
+ .option('--json', 'Output as JSON')
146
+ .action(async (email, options) => {
147
+ await mailCommands.trust(email, {
148
+ json: options.json,
149
+ });
150
+ });
151
+
152
+ mailCommand
153
+ .command('untrust')
154
+ .description('Remove email or domain from whitelist')
155
+ .argument('<email>', 'Email address or domain to remove')
156
+ .option('--json', 'Output as JSON')
157
+ .action(async (email, options) => {
158
+ await mailCommands.untrust(email, {
159
+ json: options.json,
160
+ });
161
+ });
162
+
163
+ mailCommand
164
+ .command('trusted')
165
+ .description('List trusted senders whitelist')
166
+ .option('--json', 'Output as JSON')
167
+ .action(async (options) => {
168
+ await mailCommands.trusted({
169
+ json: options.json,
170
+ });
171
+ });
172
+
173
+ // Calendar commands
174
+ const calendarCommand = program
175
+ .command('calendar')
176
+ .alias('cal')
177
+ .description('Manage calendar events');
178
+
179
+ calendarCommand
180
+ .command('list')
181
+ .description('List calendar events')
182
+ .option('-d, --days <number>', 'Number of days to look ahead', '7')
183
+ .option('-t, --top <number>', 'Maximum number of events', '50')
184
+ .option('--json', 'Output as JSON')
185
+ .action(async (options) => {
186
+ await calendarCommands.list({
187
+ days: parseInt(options.days),
188
+ top: parseInt(options.top),
189
+ json: options.json,
190
+ });
191
+ });
192
+
193
+ calendarCommand
194
+ .command('get')
195
+ .description('Get calendar event by ID')
196
+ .argument('<id>', 'Event ID')
197
+ .option('--json', 'Output as JSON')
198
+ .action(async (id, options) => {
199
+ await calendarCommands.get(id, {
200
+ json: options.json,
201
+ });
202
+ });
203
+
204
+ calendarCommand
205
+ .command('create')
206
+ .description('Create calendar event')
207
+ .argument('<title>', 'Event title')
208
+ .requiredOption('-s, --start <datetime>', 'Start date/time (YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD)')
209
+ .requiredOption('-e, --end <datetime>', 'End date/time (YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD)')
210
+ .option('-l, --location <location>', 'Event location')
211
+ .option('-b, --body <body>', 'Event description')
212
+ .option('-a, --attendees <emails>', 'Attendee emails (comma-separated)', (val) => val.split(','))
213
+ .option('--allday', 'All-day event')
214
+ .option('--json', 'Output as JSON')
215
+ .action(async (title, options) => {
216
+ await calendarCommands.create(title, {
217
+ start: options.start,
218
+ end: options.end,
219
+ location: options.location,
220
+ body: options.body,
221
+ attendees: options.attendees || [],
222
+ allday: options.allday || false,
223
+ json: options.json,
224
+ });
225
+ });
226
+
227
+ calendarCommand
228
+ .command('update')
229
+ .description('Update calendar event')
230
+ .argument('<id>', 'Event ID')
231
+ .option('-t, --title <title>', 'Event title')
232
+ .option('-s, --start <datetime>', 'Start date/time (YYYY-MM-DDTHH:MM:SS)')
233
+ .option('-e, --end <datetime>', 'End date/time (YYYY-MM-DDTHH:MM:SS)')
234
+ .option('-l, --location <location>', 'Event location')
235
+ .option('-b, --body <body>', 'Event description')
236
+ .option('--json', 'Output as JSON')
237
+ .action(async (id, options) => {
238
+ await calendarCommands.update(id, {
239
+ title: options.title,
240
+ start: options.start,
241
+ end: options.end,
242
+ location: options.location,
243
+ body: options.body,
244
+ json: options.json,
245
+ });
246
+ });
247
+
248
+ calendarCommand
249
+ .command('delete')
250
+ .description('Delete calendar event')
251
+ .argument('<id>', 'Event ID')
252
+ .option('--json', 'Output as JSON')
253
+ .action(async (id, options) => {
254
+ await calendarCommands.delete(id, {
255
+ json: options.json,
256
+ });
257
+ });
258
+
259
+ // OneDrive commands
260
+ const onedriveCommand = program
261
+ .command('onedrive')
262
+ .alias('od')
263
+ .description('Manage OneDrive files and folders');
264
+
265
+ onedriveCommand
266
+ .command('ls')
267
+ .description('List files and folders')
268
+ .argument('[path]', 'Path to list (default: root)', '')
269
+ .option('-t, --top <number>', 'Maximum number of items', '100')
270
+ .option('--json', 'Output as JSON')
271
+ .action(async (path, options) => {
272
+ await onedriveCommands.ls(path, {
273
+ top: parseInt(options.top),
274
+ json: options.json,
275
+ });
276
+ });
277
+
278
+ onedriveCommand
279
+ .command('get')
280
+ .description('Get file/folder metadata')
281
+ .argument('<path>', 'Path to file or folder')
282
+ .option('--json', 'Output as JSON')
283
+ .action(async (path, options) => {
284
+ await onedriveCommands.get(path, {
285
+ json: options.json,
286
+ });
287
+ });
288
+
289
+ onedriveCommand
290
+ .command('download')
291
+ .description('Download file from OneDrive')
292
+ .argument('<remote-path>', 'Remote file path')
293
+ .argument('[local-path]', 'Local destination path (default: current directory)')
294
+ .option('--json', 'Output as JSON')
295
+ .action(async (remotePath, localPath, options) => {
296
+ await onedriveCommands.download(remotePath, localPath, {
297
+ json: options.json,
298
+ });
299
+ });
300
+
301
+ onedriveCommand
302
+ .command('upload')
303
+ .description('Upload file to OneDrive')
304
+ .argument('<local-path>', 'Local file path')
305
+ .argument('[remote-path]', 'Remote destination path (default: root with same name)')
306
+ .option('--json', 'Output as JSON')
307
+ .action(async (localPath, remotePath, options) => {
308
+ await onedriveCommands.upload(localPath, remotePath, {
309
+ json: options.json,
310
+ });
311
+ });
312
+
313
+ onedriveCommand
314
+ .command('search')
315
+ .description('Search files in OneDrive')
316
+ .argument('<query>', 'Search query')
317
+ .option('-t, --top <number>', 'Maximum number of results', '50')
318
+ .option('--json', 'Output as JSON')
319
+ .action(async (query, options) => {
320
+ await onedriveCommands.search(query, {
321
+ top: parseInt(options.top),
322
+ json: options.json,
323
+ });
324
+ });
325
+
326
+ onedriveCommand
327
+ .command('share')
328
+ .description('Create sharing link')
329
+ .argument('<path>', 'Path to file or folder')
330
+ .option('--type <type>', 'Link type: view or edit', 'view')
331
+ .option('--scope <scope>', 'Share scope: organization, anonymous, or users', 'organization')
332
+ .option('--json', 'Output as JSON')
333
+ .action(async (path, options) => {
334
+ await onedriveCommands.share(path, {
335
+ type: options.type,
336
+ scope: options.scope,
337
+ json: options.json,
338
+ });
339
+ });
340
+
341
+ onedriveCommand
342
+ .command('invite')
343
+ .description('Invite users to access file (external sharing)')
344
+ .argument('<path>', 'Path to file')
345
+ .argument('<email>', 'Email address(es), comma-separated')
346
+ .option('--role <role>', 'Permission: read or write', 'read')
347
+ .option('--message <msg>', 'Invitation message')
348
+ .option('--no-notify', 'Do not send email notification')
349
+ .option('--json', 'Output as JSON')
350
+ .action(async (path, email, options) => {
351
+ await onedriveCommands.invite(path, email, {
352
+ role: options.role,
353
+ message: options.message,
354
+ notify: options.notify,
355
+ json: options.json,
356
+ });
357
+ });
358
+
359
+ onedriveCommand
360
+ .command('mkdir')
361
+ .description('Create folder')
362
+ .argument('<path>', 'Folder path')
363
+ .option('--json', 'Output as JSON')
364
+ .action(async (path, options) => {
365
+ await onedriveCommands.mkdir(path, {
366
+ json: options.json,
367
+ });
368
+ });
369
+
370
+ onedriveCommand
371
+ .command('rm')
372
+ .description('Delete file or folder')
373
+ .argument('<path>', 'Path to file or folder')
374
+ .option('--force', 'Skip confirmation')
375
+ .option('--json', 'Output as JSON')
376
+ .action(async (path, options) => {
377
+ await onedriveCommands.rm(path, {
378
+ force: options.force,
379
+ json: options.json,
380
+ });
381
+ });
382
+
383
+ // SharePoint commands
384
+ const sharepointCommand = program
385
+ .command('sharepoint')
386
+ .alias('sp')
387
+ .description('Manage SharePoint sites and content');
388
+
389
+ sharepointCommand
390
+ .command('sites')
391
+ .description('List accessible SharePoint sites')
392
+ .option('--search <query>', 'Search for sites')
393
+ .option('-t, --top <number>', 'Maximum number of sites', '50')
394
+ .option('--json', 'Output as JSON')
395
+ .action(async (options) => {
396
+ await sharepointCommands.sites({
397
+ search: options.search,
398
+ top: parseInt(options.top),
399
+ json: options.json,
400
+ });
401
+ });
402
+
403
+ sharepointCommand
404
+ .command('lists')
405
+ .description('List site lists and document libraries')
406
+ .argument('<site>', 'Site URL (hostname:/path) or site ID')
407
+ .option('-t, --top <number>', 'Maximum number of lists', '100')
408
+ .option('--json', 'Output as JSON')
409
+ .action(async (site, options) => {
410
+ await sharepointCommands.lists(site, {
411
+ top: parseInt(options.top),
412
+ json: options.json,
413
+ });
414
+ });
415
+
416
+ sharepointCommand
417
+ .command('items')
418
+ .description('List items in a SharePoint list')
419
+ .argument('<site>', 'Site URL (hostname:/path) or site ID')
420
+ .argument('<list>', 'List ID')
421
+ .option('-t, --top <number>', 'Maximum number of items', '100')
422
+ .option('--json', 'Output as JSON')
423
+ .action(async (site, list, options) => {
424
+ await sharepointCommands.items(site, list, {
425
+ top: parseInt(options.top),
426
+ json: options.json,
427
+ });
428
+ });
429
+
430
+ sharepointCommand
431
+ .command('files')
432
+ .description('List files in site document library')
433
+ .argument('<site>', 'Site URL (hostname:/path) or site ID')
434
+ .argument('[path]', 'Path in document library (default: root)', '')
435
+ .option('-t, --top <number>', 'Maximum number of files', '100')
436
+ .option('--json', 'Output as JSON')
437
+ .action(async (site, path, options) => {
438
+ await sharepointCommands.files(site, path, {
439
+ top: parseInt(options.top),
440
+ json: options.json,
441
+ });
442
+ });
443
+
444
+ sharepointCommand
445
+ .command('download')
446
+ .description('Download file from SharePoint')
447
+ .argument('<site>', 'Site URL (hostname:/path) or site ID')
448
+ .argument('<file-path>', 'Remote file path')
449
+ .argument('[local-path]', 'Local destination path (default: current directory)')
450
+ .option('--json', 'Output as JSON')
451
+ .action(async (site, filePath, localPath, options) => {
452
+ await sharepointCommands.download(site, filePath, localPath, {
453
+ json: options.json,
454
+ });
455
+ });
456
+
457
+ sharepointCommand
458
+ .command('upload')
459
+ .description('Upload file to SharePoint')
460
+ .argument('<site>', 'Site URL (hostname:/path) or site ID')
461
+ .argument('<local-path>', 'Local file path')
462
+ .argument('[remote-path]', 'Remote destination path (default: root with same name)')
463
+ .option('--json', 'Output as JSON')
464
+ .action(async (site, localPath, remotePath, options) => {
465
+ await sharepointCommands.upload(site, localPath, remotePath, {
466
+ json: options.json,
467
+ });
468
+ });
469
+
470
+ sharepointCommand
471
+ .command('search')
472
+ .description('Search SharePoint content')
473
+ .argument('<query>', 'Search query')
474
+ .option('-t, --top <number>', 'Maximum number of results', '50')
475
+ .option('--json', 'Output as JSON')
476
+ .action(async (query, options) => {
477
+ await sharepointCommands.search(query, {
478
+ top: parseInt(options.top),
479
+ json: options.json,
480
+ });
481
+ });
482
+
483
+ // Parse arguments
484
+ program.parse(process.argv);
485
+
486
+ // Show help if no arguments
487
+ if (!process.argv.slice(2).length) {
488
+ program.outputHelp();
489
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "tenantId": "5b4c4b46-4279-4f19-9e5d-84ea285f9b9c",
3
+ "clientId": "091b3d7b-e217-4410-868c-01c3ee6189b6",
4
+ "scopes": [
5
+ "https://graph.microsoft.com/Mail.ReadWrite",
6
+ "https://graph.microsoft.com/Mail.Send",
7
+ "https://graph.microsoft.com/Calendars.ReadWrite",
8
+ "https://graph.microsoft.com/Files.ReadWrite.All",
9
+ "https://graph.microsoft.com/Sites.ReadWrite.All",
10
+ "offline_access"
11
+ ],
12
+ "graphApiUrl": "https://graph.microsoft.com/v1.0",
13
+ "authUrl": "https://login.microsoftonline.com",
14
+ "credsPath": "~/.m365-cli/credentials.json",
15
+ "deviceCodePollInterval": 5,
16
+ "deviceCodeTimeout": 900,
17
+ "tokenRefreshBuffer": 60
18
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "m365-cli",
3
+ "version": "0.1.0",
4
+ "description": "Microsoft 365 CLI - Manage Mail, Calendar, and OneDrive from the command line",
5
+ "type": "module",
6
+ "files": ["bin", "src", "config", "README.md"],
7
+ "main": "src/index.js",
8
+ "bin": {
9
+ "m365": "./bin/m365.js"
10
+ },
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1",
13
+ "link": "npm link",
14
+ "unlink": "npm unlink -g m365-cli"
15
+ },
16
+ "keywords": [
17
+ "microsoft365",
18
+ "office365",
19
+ "graph-api",
20
+ "cli",
21
+ "email",
22
+ "calendar",
23
+ "onedrive"
24
+ ],
25
+ "author": "",
26
+ "repository": { "type": "git", "url": "" },
27
+ "bugs": { "url": "" },
28
+ "homepage": "",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "dependencies": {
34
+ "commander": "^12.0.0"
35
+ }
36
+ }
@@ -0,0 +1,154 @@
1
+ import config from '../utils/config.js';
2
+ import { AuthError } from '../utils/error.js';
3
+
4
+ /**
5
+ * Device Code Flow authentication
6
+ * https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
7
+ */
8
+
9
+ /**
10
+ * Request device code from Microsoft
11
+ */
12
+ export async function requestDeviceCode() {
13
+ const tenantId = config.get('tenantId');
14
+ const clientId = config.get('clientId');
15
+ const scopes = config.get('scopes').join(' ');
16
+ const authUrl = config.get('authUrl');
17
+
18
+ const url = `${authUrl}/${tenantId}/oauth2/v2.0/devicecode`;
19
+
20
+ const response = await fetch(url, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/x-www-form-urlencoded',
24
+ },
25
+ body: new URLSearchParams({
26
+ client_id: clientId,
27
+ scope: scopes,
28
+ }),
29
+ });
30
+
31
+ if (!response.ok) {
32
+ throw new AuthError('Failed to request device code', await response.json());
33
+ }
34
+
35
+ const data = await response.json();
36
+
37
+ return {
38
+ deviceCode: data.device_code,
39
+ userCode: data.user_code,
40
+ verificationUri: data.verification_uri,
41
+ expiresIn: data.expires_in || 900,
42
+ interval: data.interval || 5,
43
+ message: data.message,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Poll for access token
49
+ */
50
+ export async function pollForToken(deviceCode) {
51
+ const tenantId = config.get('tenantId');
52
+ const clientId = config.get('clientId');
53
+ const authUrl = config.get('authUrl');
54
+
55
+ const url = `${authUrl}/${tenantId}/oauth2/v2.0/token`;
56
+
57
+ const response = await fetch(url, {
58
+ method: 'POST',
59
+ headers: {
60
+ 'Content-Type': 'application/x-www-form-urlencoded',
61
+ },
62
+ body: new URLSearchParams({
63
+ client_id: clientId,
64
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
65
+ device_code: deviceCode,
66
+ }),
67
+ });
68
+
69
+ const data = await response.json();
70
+
71
+ // Check for errors
72
+ if (data.error) {
73
+ if (data.error === 'authorization_pending') {
74
+ return { pending: true };
75
+ }
76
+
77
+ if (data.error === 'slow_down') {
78
+ return { slowDown: true };
79
+ }
80
+
81
+ throw new AuthError(
82
+ data.error_description || data.error,
83
+ { error: data.error }
84
+ );
85
+ }
86
+
87
+ // Success - return token data
88
+ return {
89
+ success: true,
90
+ accessToken: data.access_token,
91
+ refreshToken: data.refresh_token,
92
+ expiresIn: data.expires_in || 3600,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Full device code flow
98
+ */
99
+ export async function deviceCodeFlow() {
100
+ // Step 1: Request device code
101
+ console.log('šŸ” Starting authentication...\n');
102
+ const deviceCodeData = await requestDeviceCode();
103
+
104
+ // Step 2: Show user instructions
105
+ console.log('━'.repeat(60));
106
+ console.log('šŸ“± Please authenticate:');
107
+ console.log('');
108
+ console.log(` 1. Open: ${deviceCodeData.verificationUri}`);
109
+ console.log(` 2. Enter code: ${deviceCodeData.userCode}`);
110
+ console.log('');
111
+ console.log('━'.repeat(60));
112
+ console.log('\nā³ Waiting for authentication...\n');
113
+
114
+ // Step 3: Poll for token
115
+ const startTime = Date.now();
116
+ const expiresAt = startTime + deviceCodeData.expiresIn * 1000;
117
+ let interval = deviceCodeData.interval * 1000;
118
+
119
+ while (Date.now() < expiresAt) {
120
+ await new Promise(resolve => setTimeout(resolve, interval));
121
+
122
+ try {
123
+ const result = await pollForToken(deviceCodeData.deviceCode);
124
+
125
+ if (result.success) {
126
+ return {
127
+ accessToken: result.accessToken,
128
+ refreshToken: result.refreshToken,
129
+ expiresIn: result.expiresIn,
130
+ };
131
+ }
132
+
133
+ if (result.slowDown) {
134
+ // Increase polling interval
135
+ interval += 1000;
136
+ }
137
+
138
+ // Keep waiting if pending
139
+ if (result.pending) {
140
+ continue;
141
+ }
142
+ } catch (error) {
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ throw new AuthError('Authentication timed out. Please try again.');
148
+ }
149
+
150
+ export default {
151
+ requestDeviceCode,
152
+ pollForToken,
153
+ deviceCodeFlow,
154
+ };