ms365-mcp-server 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.
- package/README.md +95 -362
- package/dist/index.js +144 -209
- package/dist/utils/ms365-auth-enhanced.js +202 -87
- package/dist/utils/ms365-operations.js +58 -4
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ const DEFAULT_TENANT_ID = "common";
|
|
|
23
23
|
// Configuration directory and file paths
|
|
24
24
|
const CONFIG_DIR = path.join(os.homedir(), '.ms365-mcp');
|
|
25
25
|
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
26
|
+
const DEVICE_CODE_FILE = path.join(CONFIG_DIR, 'device-code.json');
|
|
26
27
|
/**
|
|
27
28
|
* Enhanced Microsoft 365 authentication manager with device code flow support
|
|
28
29
|
*/
|
|
@@ -259,7 +260,7 @@ export class EnhancedMS365Auth {
|
|
|
259
260
|
return 'device';
|
|
260
261
|
}
|
|
261
262
|
/**
|
|
262
|
-
* Save token using secure credential store
|
|
263
|
+
* Save token using secure credential store (simplified single account)
|
|
263
264
|
*/
|
|
264
265
|
async saveToken(token, authType) {
|
|
265
266
|
try {
|
|
@@ -270,8 +271,8 @@ export class EnhancedMS365Auth {
|
|
|
270
271
|
account: token.account,
|
|
271
272
|
authType: authType
|
|
272
273
|
};
|
|
273
|
-
|
|
274
|
-
await credentialStore.setCredentials(
|
|
274
|
+
// Always use a single account key for simplicity
|
|
275
|
+
await credentialStore.setCredentials('ms365-user', tokenData);
|
|
275
276
|
logger.log('Saved MS365 access token securely');
|
|
276
277
|
}
|
|
277
278
|
catch (error) {
|
|
@@ -279,11 +280,11 @@ export class EnhancedMS365Auth {
|
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
282
|
/**
|
|
282
|
-
* Load stored token using secure credential store
|
|
283
|
+
* Load stored token using secure credential store (simplified single account)
|
|
283
284
|
*/
|
|
284
|
-
async loadStoredToken(
|
|
285
|
+
async loadStoredToken() {
|
|
285
286
|
try {
|
|
286
|
-
return await credentialStore.getCredentials(
|
|
287
|
+
return await credentialStore.getCredentials('ms365-user');
|
|
287
288
|
}
|
|
288
289
|
catch (error) {
|
|
289
290
|
logger.error('Error loading stored token:', error);
|
|
@@ -329,22 +330,14 @@ export class EnhancedMS365Auth {
|
|
|
329
330
|
/**
|
|
330
331
|
* Get authenticated Microsoft Graph client
|
|
331
332
|
*/
|
|
332
|
-
async getGraphClient(
|
|
333
|
-
|
|
334
|
-
if (!accountKey) {
|
|
335
|
-
const accounts = await this.listAuthenticatedAccounts();
|
|
336
|
-
if (accounts.length === 0) {
|
|
337
|
-
throw new Error('No authenticated accounts found. Please authenticate first.');
|
|
338
|
-
}
|
|
339
|
-
accountKey = accounts[0];
|
|
340
|
-
}
|
|
341
|
-
const storedToken = await this.loadStoredToken(accountKey);
|
|
333
|
+
async getGraphClient() {
|
|
334
|
+
const storedToken = await this.loadStoredToken();
|
|
342
335
|
if (!storedToken) {
|
|
343
336
|
throw new Error('No stored token found. Please authenticate first.');
|
|
344
337
|
}
|
|
345
338
|
// Check if token is expired
|
|
346
339
|
if (storedToken.expiresOn < Date.now()) {
|
|
347
|
-
await this.refreshToken(
|
|
340
|
+
await this.refreshToken();
|
|
348
341
|
}
|
|
349
342
|
const client = Client.init({
|
|
350
343
|
authProvider: (done) => {
|
|
@@ -356,8 +349,8 @@ export class EnhancedMS365Auth {
|
|
|
356
349
|
/**
|
|
357
350
|
* Refresh access token
|
|
358
351
|
*/
|
|
359
|
-
async refreshToken(
|
|
360
|
-
const storedToken = await this.loadStoredToken(
|
|
352
|
+
async refreshToken() {
|
|
353
|
+
const storedToken = await this.loadStoredToken();
|
|
361
354
|
if (!storedToken?.account) {
|
|
362
355
|
throw new Error('No account information available. Please re-authenticate.');
|
|
363
356
|
}
|
|
@@ -384,29 +377,15 @@ export class EnhancedMS365Auth {
|
|
|
384
377
|
/**
|
|
385
378
|
* Check if user is authenticated
|
|
386
379
|
*/
|
|
387
|
-
async isAuthenticated(
|
|
388
|
-
|
|
389
|
-
if (!accountKey) {
|
|
390
|
-
const accounts = await this.listAuthenticatedAccounts();
|
|
391
|
-
if (accounts.length === 0) {
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
// Check if any account has valid authentication
|
|
395
|
-
for (const account of accounts) {
|
|
396
|
-
if (await this.isAuthenticated(account)) {
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
const storedToken = await this.loadStoredToken(accountKey);
|
|
380
|
+
async isAuthenticated() {
|
|
381
|
+
const storedToken = await this.loadStoredToken();
|
|
403
382
|
if (!storedToken) {
|
|
404
383
|
return false;
|
|
405
384
|
}
|
|
406
385
|
// If token is expired, try to refresh
|
|
407
386
|
if (storedToken.expiresOn < Date.now()) {
|
|
408
387
|
try {
|
|
409
|
-
await this.refreshToken(
|
|
388
|
+
await this.refreshToken();
|
|
410
389
|
return true;
|
|
411
390
|
}
|
|
412
391
|
catch (error) {
|
|
@@ -425,33 +404,160 @@ export class EnhancedMS365Auth {
|
|
|
425
404
|
/**
|
|
426
405
|
* Clear stored authentication data
|
|
427
406
|
*/
|
|
428
|
-
async resetAuth(
|
|
407
|
+
async resetAuth() {
|
|
408
|
+
try {
|
|
409
|
+
await credentialStore.deleteCredentials('ms365-user');
|
|
410
|
+
await this.clearDeviceCodeState();
|
|
411
|
+
logger.log('Cleared stored authentication tokens');
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logger.error('Error clearing authentication data:', error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Save device code state to file
|
|
419
|
+
*/
|
|
420
|
+
async saveDeviceCodeState(state) {
|
|
421
|
+
try {
|
|
422
|
+
fs.writeFileSync(DEVICE_CODE_FILE, JSON.stringify(state, null, 2));
|
|
423
|
+
logger.log('Saved device code state to file');
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
logger.error('Error saving device code state:', error);
|
|
427
|
+
throw new Error('Failed to save device code state');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Load device code state from file
|
|
432
|
+
*/
|
|
433
|
+
async loadDeviceCodeState() {
|
|
429
434
|
try {
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
await credentialStore.deleteCredentials(accountKey);
|
|
433
|
-
logger.log(`Cleared stored authentication tokens for account: ${accountKey}`);
|
|
435
|
+
if (!fs.existsSync(DEVICE_CODE_FILE)) {
|
|
436
|
+
return null;
|
|
434
437
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
438
|
+
const stateData = fs.readFileSync(DEVICE_CODE_FILE, 'utf8');
|
|
439
|
+
const state = JSON.parse(stateData);
|
|
440
|
+
// Check if device code has expired
|
|
441
|
+
const now = Date.now();
|
|
442
|
+
const elapsed = (now - state.startTime) / 1000;
|
|
443
|
+
if (elapsed > state.expiresIn) {
|
|
444
|
+
// Device code has expired, clean up
|
|
445
|
+
await this.clearDeviceCodeState();
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return state;
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
logger.error('Error loading device code state:', error);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Clear device code state file
|
|
457
|
+
*/
|
|
458
|
+
async clearDeviceCodeState() {
|
|
459
|
+
try {
|
|
460
|
+
if (fs.existsSync(DEVICE_CODE_FILE)) {
|
|
461
|
+
fs.unlinkSync(DEVICE_CODE_FILE);
|
|
462
|
+
logger.log('Cleared device code state file');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
logger.error('Error clearing device code state:', error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Complete device code authentication using saved state
|
|
471
|
+
*/
|
|
472
|
+
async completeDeviceCodeAuth() {
|
|
473
|
+
const deviceCodeState = await this.loadDeviceCodeState();
|
|
474
|
+
if (!deviceCodeState) {
|
|
475
|
+
return false; // No pending device code authentication
|
|
476
|
+
}
|
|
477
|
+
if (!await this.loadCredentials()) {
|
|
478
|
+
throw new Error('MS365 credentials not configured');
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
// Use the raw MSAL token endpoint to check if authentication completed
|
|
482
|
+
const tokenUrl = `https://login.microsoftonline.com/${this.credentials.tenantId}/oauth2/v2.0/token`;
|
|
483
|
+
const response = await fetch(tokenUrl, {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
headers: {
|
|
486
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
487
|
+
},
|
|
488
|
+
body: new URLSearchParams({
|
|
489
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
490
|
+
device_code: deviceCodeState.deviceCode,
|
|
491
|
+
client_id: this.credentials.clientId,
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
const result = await response.json();
|
|
495
|
+
if (response.ok && result.access_token) {
|
|
496
|
+
// Authentication completed successfully
|
|
497
|
+
// Microsoft's token response doesn't include account info, so we need to get it from the token
|
|
498
|
+
let username = 'authenticated-user';
|
|
499
|
+
try {
|
|
500
|
+
// Try to decode the access token to get user info
|
|
501
|
+
const tokenParts = result.access_token.split('.');
|
|
502
|
+
if (tokenParts.length >= 2) {
|
|
503
|
+
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
|
|
504
|
+
username = payload.upn || payload.unique_name || payload.preferred_username || 'authenticated-user';
|
|
448
505
|
}
|
|
449
506
|
}
|
|
507
|
+
catch (decodeError) {
|
|
508
|
+
logger.log('Could not decode token for username, using default');
|
|
509
|
+
}
|
|
510
|
+
const tokenResponse = {
|
|
511
|
+
accessToken: result.access_token,
|
|
512
|
+
refreshToken: result.refresh_token || '',
|
|
513
|
+
expiresOn: new Date(Date.now() + (result.expires_in * 1000)),
|
|
514
|
+
account: {
|
|
515
|
+
username: username,
|
|
516
|
+
homeAccountId: `${username}.${this.credentials.tenantId}`,
|
|
517
|
+
environment: 'login.microsoftonline.com',
|
|
518
|
+
tenantId: this.credentials.tenantId,
|
|
519
|
+
localAccountId: username
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
await this.saveToken(tokenResponse, 'device');
|
|
523
|
+
await this.clearDeviceCodeState();
|
|
524
|
+
logger.log('MS365 device code authentication completed successfully');
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
else if (result.error === 'authorization_pending') {
|
|
528
|
+
// Still waiting for user to complete authentication
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
else if (result.error === 'expired_token') {
|
|
532
|
+
// Device code has expired
|
|
533
|
+
await this.clearDeviceCodeState();
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// Other error - don't clear device code state, just return false
|
|
538
|
+
logger.error('Device code authentication error:', result);
|
|
539
|
+
return false;
|
|
450
540
|
}
|
|
451
541
|
}
|
|
452
542
|
catch (error) {
|
|
453
|
-
logger.error('Error
|
|
543
|
+
logger.error('Error completing device code authentication:', error);
|
|
544
|
+
// Don't clear device code state on network/other errors
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get pending device code info from saved state
|
|
550
|
+
*/
|
|
551
|
+
async getPendingDeviceCodeInfo() {
|
|
552
|
+
const deviceCodeState = await this.loadDeviceCodeState();
|
|
553
|
+
if (!deviceCodeState) {
|
|
554
|
+
return null;
|
|
454
555
|
}
|
|
556
|
+
return {
|
|
557
|
+
verificationUri: deviceCodeState.verificationUri,
|
|
558
|
+
userCode: deviceCodeState.userCode,
|
|
559
|
+
message: deviceCodeState.message
|
|
560
|
+
};
|
|
455
561
|
}
|
|
456
562
|
/**
|
|
457
563
|
* Get authentication URL for device code flow
|
|
@@ -487,36 +593,47 @@ export class EnhancedMS365Auth {
|
|
|
487
593
|
return new Promise((resolve, reject) => {
|
|
488
594
|
const deviceCodeRequest = {
|
|
489
595
|
scopes: SCOPES,
|
|
490
|
-
deviceCodeCallback: (response) => {
|
|
596
|
+
deviceCodeCallback: async (response) => {
|
|
491
597
|
const deviceCodeInfo = {
|
|
492
598
|
verificationUri: response.verificationUri,
|
|
493
599
|
userCode: response.userCode,
|
|
494
600
|
message: response.message
|
|
495
601
|
};
|
|
496
|
-
//
|
|
497
|
-
const
|
|
498
|
-
.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
resolve(deviceCodeInfo);
|
|
602
|
+
// Save device code state for later completion
|
|
603
|
+
const deviceCodeState = {
|
|
604
|
+
deviceCode: response.deviceCode,
|
|
605
|
+
userCode: response.userCode,
|
|
606
|
+
verificationUri: response.verificationUri,
|
|
607
|
+
expiresIn: response.expiresIn,
|
|
608
|
+
startTime: Date.now(),
|
|
609
|
+
message: response.message
|
|
610
|
+
};
|
|
611
|
+
try {
|
|
612
|
+
await this.saveDeviceCodeState(deviceCodeState);
|
|
613
|
+
logger.log(`Device code authentication started: ${response.verificationUri} - ${response.userCode}`);
|
|
614
|
+
// Return device code info immediately
|
|
615
|
+
resolve(deviceCodeInfo);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
reject(error);
|
|
619
|
+
}
|
|
515
620
|
}
|
|
516
621
|
};
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
622
|
+
// Start the device code flow - we need this to run to get the device code
|
|
623
|
+
// The callback will resolve our promise, and we'll handle the auth later
|
|
624
|
+
msalClient.acquireTokenByDeviceCode(deviceCodeRequest).then((result) => {
|
|
625
|
+
// If this completes immediately (unlikely but possible), save the token
|
|
626
|
+
if (result) {
|
|
627
|
+
this.saveToken(result, 'device').catch(() => {
|
|
628
|
+
// Ignore save errors here since we primarily care about getting the device code
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}).catch((error) => {
|
|
632
|
+
// Only reject if we haven't already resolved with device code info
|
|
633
|
+
// The most common case is that the user hasn't completed auth yet
|
|
634
|
+
if (error.errorCode !== 'user_cancelled' && error.errorCode !== 'authorization_pending') {
|
|
635
|
+
logger.error('Device code flow error:', error);
|
|
636
|
+
}
|
|
520
637
|
});
|
|
521
638
|
});
|
|
522
639
|
}
|
|
@@ -535,12 +652,6 @@ export class EnhancedMS365Auth {
|
|
|
535
652
|
hasPendingAuth() {
|
|
536
653
|
return this.pendingAuth !== null;
|
|
537
654
|
}
|
|
538
|
-
/**
|
|
539
|
-
* Get pending device code info
|
|
540
|
-
*/
|
|
541
|
-
getPendingDeviceCodeInfo() {
|
|
542
|
-
return this.pendingAuth?.deviceCodeInfo || null;
|
|
543
|
-
}
|
|
544
655
|
/**
|
|
545
656
|
* Setup credentials interactively
|
|
546
657
|
*/
|
|
@@ -612,10 +723,14 @@ export class EnhancedMS365Auth {
|
|
|
612
723
|
};
|
|
613
724
|
}
|
|
614
725
|
/**
|
|
615
|
-
*
|
|
726
|
+
* Get current authenticated user (secure - only your own info)
|
|
616
727
|
*/
|
|
617
|
-
async
|
|
618
|
-
|
|
728
|
+
async getCurrentUser() {
|
|
729
|
+
const storedToken = await this.loadStoredToken();
|
|
730
|
+
if (storedToken && storedToken.expiresOn > Date.now()) {
|
|
731
|
+
return storedToken.account?.username || 'authenticated-user';
|
|
732
|
+
}
|
|
733
|
+
return null;
|
|
619
734
|
}
|
|
620
735
|
/**
|
|
621
736
|
* Get authentication URL without opening browser (redirect flow)
|
|
@@ -389,16 +389,14 @@ export class MS365Operations {
|
|
|
389
389
|
return false;
|
|
390
390
|
}
|
|
391
391
|
if (criteria.to) {
|
|
392
|
-
const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase()
|
|
393
|
-
recipient.name.toLowerCase().includes(criteria.to.toLowerCase()));
|
|
392
|
+
const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
|
|
394
393
|
if (!toMatch)
|
|
395
394
|
return false;
|
|
396
395
|
}
|
|
397
396
|
if (criteria.cc) {
|
|
398
397
|
// Handle case where ccRecipients might be undefined
|
|
399
398
|
const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
|
|
400
|
-
message.ccRecipients.some(recipient => recipient.address.toLowerCase()
|
|
401
|
-
recipient.name.toLowerCase().includes(criteria.cc.toLowerCase()));
|
|
399
|
+
message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
|
|
402
400
|
if (!ccMatch)
|
|
403
401
|
return false;
|
|
404
402
|
}
|
|
@@ -640,5 +638,61 @@ export class MS365Operations {
|
|
|
640
638
|
throw error;
|
|
641
639
|
}
|
|
642
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Get current user's email address
|
|
643
|
+
*/
|
|
644
|
+
async getCurrentUserEmail() {
|
|
645
|
+
try {
|
|
646
|
+
const graphClient = await this.getGraphClient();
|
|
647
|
+
const user = await graphClient
|
|
648
|
+
.api('/me')
|
|
649
|
+
.select('mail,userPrincipalName')
|
|
650
|
+
.get();
|
|
651
|
+
return user.mail || user.userPrincipalName || '';
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
logger.error('Error getting current user email:', error);
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Search for emails addressed to the current user (both TO and CC recipients)
|
|
660
|
+
*/
|
|
661
|
+
async searchEmailsToMe(additionalCriteria = {}) {
|
|
662
|
+
try {
|
|
663
|
+
const userEmail = await this.getCurrentUserEmail();
|
|
664
|
+
// First search for emails where user is in TO field
|
|
665
|
+
const toCriteria = {
|
|
666
|
+
...additionalCriteria,
|
|
667
|
+
to: userEmail
|
|
668
|
+
};
|
|
669
|
+
// Then search for emails where user is in CC field
|
|
670
|
+
const ccCriteria = {
|
|
671
|
+
...additionalCriteria,
|
|
672
|
+
cc: userEmail
|
|
673
|
+
};
|
|
674
|
+
// Execute both searches in parallel
|
|
675
|
+
const [toResults, ccResults] = await Promise.all([
|
|
676
|
+
this.searchEmails(toCriteria),
|
|
677
|
+
this.searchEmails(ccCriteria)
|
|
678
|
+
]);
|
|
679
|
+
// Combine results and remove duplicates based on email ID
|
|
680
|
+
const allMessages = [...toResults.messages, ...ccResults.messages];
|
|
681
|
+
const uniqueMessages = allMessages.filter((message, index, array) => array.findIndex(m => m.id === message.id) === index);
|
|
682
|
+
// Sort by received date (newest first)
|
|
683
|
+
uniqueMessages.sort((a, b) => new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime());
|
|
684
|
+
// Apply maxResults limit if specified
|
|
685
|
+
const maxResults = additionalCriteria.maxResults || 50;
|
|
686
|
+
const limitedMessages = uniqueMessages.slice(0, maxResults);
|
|
687
|
+
return {
|
|
688
|
+
messages: limitedMessages,
|
|
689
|
+
hasMore: uniqueMessages.length > maxResults || toResults.hasMore || ccResults.hasMore
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
logger.error('Error searching emails addressed to me:', error);
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
643
697
|
}
|
|
644
698
|
export const ms365Operations = new MS365Operations();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ms365-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|