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.
@@ -0,0 +1,875 @@
1
+ import { getAccessToken } from '../auth/token-manager.js';
2
+ import config from '../utils/config.js';
3
+ import { ApiError, parseGraphError } from '../utils/error.js';
4
+
5
+ /**
6
+ * Microsoft Graph API Client
7
+ * Handles HTTP requests with automatic token refresh
8
+ */
9
+
10
+ class GraphClient {
11
+ constructor() {
12
+ this.baseUrl = config.get('graphApiUrl');
13
+ }
14
+
15
+ /**
16
+ * Make authenticated request
17
+ */
18
+ async request(endpoint, options = {}) {
19
+ const {
20
+ method = 'GET',
21
+ body = null,
22
+ headers = {},
23
+ queryParams = {},
24
+ } = options;
25
+
26
+ // Get access token (auto-refresh if needed)
27
+ const token = await getAccessToken();
28
+
29
+ // Build URL with query parameters
30
+ let url = `${this.baseUrl}${endpoint}`;
31
+ if (Object.keys(queryParams).length > 0) {
32
+ const params = new URLSearchParams(queryParams);
33
+ url += `?${params.toString()}`;
34
+ }
35
+
36
+ // Build request headers
37
+ const requestHeaders = {
38
+ 'Authorization': `Bearer ${token}`,
39
+ 'Content-Type': 'application/json',
40
+ ...headers,
41
+ };
42
+
43
+ // Build request options
44
+ const requestOptions = {
45
+ method,
46
+ headers: requestHeaders,
47
+ };
48
+
49
+ if (body && method !== 'GET') {
50
+ requestOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
51
+ }
52
+
53
+ // Make request
54
+ try {
55
+ const response = await fetch(url, requestOptions);
56
+
57
+ // Handle empty responses (204, etc.)
58
+ if (response.status === 204) {
59
+ return { success: true };
60
+ }
61
+
62
+ // Parse JSON response
63
+ const data = await response.json().catch(() => ({}));
64
+
65
+ // Handle errors
66
+ if (!response.ok) {
67
+ throw parseGraphError(data, response.status);
68
+ }
69
+
70
+ return data;
71
+ } catch (error) {
72
+ if (error instanceof ApiError) {
73
+ throw error;
74
+ }
75
+ throw new ApiError(`Request failed: ${error.message}`, 0);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * GET request
81
+ */
82
+ async get(endpoint, options = {}) {
83
+ return this.request(endpoint, { ...options, method: 'GET' });
84
+ }
85
+
86
+ /**
87
+ * POST request
88
+ */
89
+ async post(endpoint, body, options = {}) {
90
+ return this.request(endpoint, { ...options, method: 'POST', body });
91
+ }
92
+
93
+ /**
94
+ * PATCH request
95
+ */
96
+ async patch(endpoint, body, options = {}) {
97
+ return this.request(endpoint, { ...options, method: 'PATCH', body });
98
+ }
99
+
100
+ /**
101
+ * DELETE request
102
+ */
103
+ async delete(endpoint, options = {}) {
104
+ return this.request(endpoint, { ...options, method: 'DELETE' });
105
+ }
106
+
107
+ /**
108
+ * Mail endpoints
109
+ */
110
+ mail = {
111
+ /**
112
+ * Map friendly folder names to Graph API well-known folder names
113
+ */
114
+ _mapFolderName: (folder) => {
115
+ const folderMap = {
116
+ 'inbox': 'inbox',
117
+ 'sent': 'sentitems',
118
+ 'drafts': 'drafts',
119
+ 'deleted': 'deleteditems',
120
+ 'junk': 'junkemail',
121
+ 'archive': 'archive',
122
+ };
123
+
124
+ // Convert to lowercase for case-insensitive matching
125
+ const lowerFolder = folder.toLowerCase();
126
+
127
+ // Return mapped name if found, otherwise return original (for direct IDs)
128
+ return folderMap[lowerFolder] || folder;
129
+ },
130
+
131
+ /**
132
+ * List messages
133
+ */
134
+ list: async (options = {}) => {
135
+ const { top = 10, folder = 'inbox', select, orderby } = options;
136
+
137
+ const queryParams = {
138
+ '$top': top,
139
+ };
140
+
141
+ if (select) {
142
+ queryParams['$select'] = Array.isArray(select) ? select.join(',') : select;
143
+ } else {
144
+ queryParams['$select'] = 'id,subject,from,receivedDateTime,isRead,hasAttachments';
145
+ }
146
+
147
+ if (orderby) {
148
+ queryParams['$orderby'] = orderby;
149
+ } else {
150
+ queryParams['$orderby'] = 'receivedDateTime desc';
151
+ }
152
+
153
+ // Map friendly folder names to Graph API names
154
+ const mappedFolder = this.mail._mapFolderName(folder);
155
+
156
+ let endpoint = '/me/messages';
157
+ if (mappedFolder && mappedFolder !== 'inbox') {
158
+ endpoint = `/me/mailFolders/${mappedFolder}/messages`;
159
+ }
160
+
161
+ const response = await this.get(endpoint, { queryParams });
162
+ return response.value || [];
163
+ },
164
+
165
+ /**
166
+ * Get message by ID
167
+ */
168
+ get: async (id, options = {}) => {
169
+ const { select, expand } = options;
170
+
171
+ const queryParams = {};
172
+ if (select) {
173
+ queryParams['$select'] = Array.isArray(select) ? select.join(',') : select;
174
+ }
175
+
176
+ // Always expand attachments to get attachment info
177
+ if (expand !== false) {
178
+ queryParams['$expand'] = 'attachments';
179
+ }
180
+
181
+ return this.get(`/me/messages/${id}`, { queryParams });
182
+ },
183
+
184
+ /**
185
+ * Send message
186
+ */
187
+ send: async (message) => {
188
+ const payload = {
189
+ message,
190
+ saveToSentItems: true,
191
+ };
192
+
193
+ return this.post('/me/sendMail', payload);
194
+ },
195
+
196
+ /**
197
+ * Search messages
198
+ */
199
+ search: async (query, options = {}) => {
200
+ const { top = 10 } = options;
201
+
202
+ const queryParams = {
203
+ '$search': `"${query}"`,
204
+ '$top': top,
205
+ '$select': 'id,subject,from,receivedDateTime,isRead,hasAttachments',
206
+ // Note: $orderby not supported with $search
207
+ };
208
+
209
+ const response = await this.get('/me/messages', {
210
+ queryParams,
211
+ headers: {
212
+ 'ConsistencyLevel': 'eventual',
213
+ },
214
+ });
215
+
216
+ return response.value || [];
217
+ },
218
+
219
+ /**
220
+ * List attachments for a message
221
+ */
222
+ attachments: async (id) => {
223
+ const response = await this.get(`/me/messages/${id}/attachments`, {
224
+ queryParams: {
225
+ '$select': 'id,name,size,contentType',
226
+ },
227
+ });
228
+
229
+ return response.value || [];
230
+ },
231
+
232
+ /**
233
+ * Download a specific attachment
234
+ */
235
+ downloadAttachment: async (messageId, attachmentId) => {
236
+ return this.get(`/me/messages/${messageId}/attachments/${attachmentId}`);
237
+ },
238
+ };
239
+
240
+ /**
241
+ * Calendar endpoints
242
+ */
243
+ calendar = {
244
+ /**
245
+ * List calendar events in a time range
246
+ */
247
+ list: async (options = {}) => {
248
+ const { startDateTime, endDateTime, top = 50, select, orderby } = options;
249
+
250
+ if (!startDateTime || !endDateTime) {
251
+ throw new Error('startDateTime and endDateTime are required');
252
+ }
253
+
254
+ const queryParams = {
255
+ 'startDateTime': startDateTime,
256
+ 'endDateTime': endDateTime,
257
+ '$top': top,
258
+ };
259
+
260
+ if (select) {
261
+ queryParams['$select'] = Array.isArray(select) ? select.join(',') : select;
262
+ } else {
263
+ queryParams['$select'] = 'id,subject,start,end,location,isAllDay,bodyPreview';
264
+ }
265
+
266
+ if (orderby) {
267
+ queryParams['$orderby'] = orderby;
268
+ } else {
269
+ queryParams['$orderby'] = 'start/dateTime';
270
+ }
271
+
272
+ const response = await this.get('/me/calendarView', {
273
+ queryParams,
274
+ headers: {
275
+ 'Prefer': 'outlook.timezone="Asia/Shanghai"',
276
+ },
277
+ });
278
+
279
+ return response.value || [];
280
+ },
281
+
282
+ /**
283
+ * Get calendar event by ID
284
+ */
285
+ get: async (id, options = {}) => {
286
+ const { select } = options;
287
+
288
+ const queryParams = {};
289
+ if (select) {
290
+ queryParams['$select'] = Array.isArray(select) ? select.join(',') : select;
291
+ }
292
+
293
+ return this.get(`/me/events/${id}`, {
294
+ queryParams,
295
+ headers: {
296
+ 'Prefer': 'outlook.timezone="Asia/Shanghai"',
297
+ },
298
+ });
299
+ },
300
+
301
+ /**
302
+ * Create calendar event
303
+ */
304
+ create: async (event) => {
305
+ return this.post('/me/events', event, {
306
+ headers: {
307
+ 'Prefer': 'outlook.timezone="Asia/Shanghai"',
308
+ },
309
+ });
310
+ },
311
+
312
+ /**
313
+ * Update calendar event
314
+ */
315
+ update: async (id, updates) => {
316
+ return this.patch(`/me/events/${id}`, updates, {
317
+ headers: {
318
+ 'Prefer': 'outlook.timezone="Asia/Shanghai"',
319
+ },
320
+ });
321
+ },
322
+
323
+ /**
324
+ * Delete calendar event
325
+ */
326
+ delete: async (id) => {
327
+ return this.delete(`/me/events/${id}`);
328
+ },
329
+ };
330
+
331
+ /**
332
+ * SharePoint endpoints
333
+ */
334
+ sharepoint = {
335
+ /**
336
+ * Parse site URL or ID to get site-id
337
+ * Supports three formats:
338
+ * 1. Graph API ID: "hostname,siteId,webId"
339
+ * 2. Site URL: "hostname:/sites/team"
340
+ * 3. Short path: "/sites/team" (auto-completes hostname)
341
+ */
342
+ _parseSite: async (site) => {
343
+ // Format 1: Graph API ID (hostname,guid,guid)
344
+ // Example: "contoso.sharepoint.com,8bfb5166-...,ea772c4f-..."
345
+ if (site.includes(',')) {
346
+ return site;
347
+ }
348
+
349
+ // Format 2: Site URL with explicit hostname
350
+ // Example: "contoso.sharepoint.com:/sites/team"
351
+ if (site.includes(':/')) {
352
+ const match = site.match(/^(.+?):\/(.*)/);
353
+ if (!match) {
354
+ throw new Error('Invalid site URL format. Use "hostname:/path"');
355
+ }
356
+
357
+ const [, hostname, path] = match;
358
+ const endpoint = `/sites/${hostname}:/${path}`;
359
+
360
+ const result = await this.get(endpoint, {
361
+ queryParams: {
362
+ '$select': 'id,name,webUrl',
363
+ },
364
+ });
365
+
366
+ return result.id;
367
+ }
368
+
369
+ // Format 3: Short path (needs hostname lookup)
370
+ // Example: "/sites/team"
371
+ if (site.startsWith('/')) {
372
+ // Get user's SharePoint hostname from profile
373
+ const profile = await this.get('/me', {
374
+ queryParams: {
375
+ '$select': 'mail',
376
+ },
377
+ });
378
+
379
+ // Extract tenant from email (e.g., user@contoso.onmicrosoft.com)
380
+ const email = profile.mail;
381
+ if (!email) {
382
+ throw new Error('Cannot determine SharePoint hostname. Use full format: "hostname:/path"');
383
+ }
384
+
385
+ // Extract domain and build SharePoint hostname
386
+ const domain = email.split('@')[1];
387
+ let hostname;
388
+
389
+ if (domain.endsWith('.onmicrosoft.com')) {
390
+ // Convert tenant.onmicrosoft.com to tenant.sharepoint.com
391
+ const tenant = domain.replace('.onmicrosoft.com', '');
392
+ hostname = `${tenant}.sharepoint.com`;
393
+ } else {
394
+ // Custom domain - try standard SharePoint hostname
395
+ const orgName = domain.split('.')[0];
396
+ hostname = `${orgName}.sharepoint.com`;
397
+ }
398
+
399
+ // Resolve the site
400
+ const endpoint = `/sites/${hostname}:${site}`;
401
+
402
+ const result = await this.get(endpoint, {
403
+ queryParams: {
404
+ '$select': 'id,name,webUrl',
405
+ },
406
+ });
407
+
408
+ return result.id;
409
+ }
410
+
411
+ // Otherwise, assume it's a plain GUID or direct site ID
412
+ return site;
413
+ },
414
+
415
+ /**
416
+ * List accessible sites
417
+ */
418
+ sites: async (options = {}) => {
419
+ const { search, top = 50 } = options;
420
+
421
+ let endpoint;
422
+ let queryParams = {
423
+ '$top': top,
424
+ '$select': 'id,name,displayName,webUrl,description',
425
+ };
426
+
427
+ if (search) {
428
+ // Search sites
429
+ endpoint = '/sites';
430
+ queryParams['$search'] = `"${search}"`;
431
+
432
+ const response = await this.get(endpoint, {
433
+ queryParams,
434
+ headers: {
435
+ 'ConsistencyLevel': 'eventual',
436
+ },
437
+ });
438
+ return response.value || [];
439
+ } else {
440
+ // List followed sites
441
+ endpoint = '/me/followedSites';
442
+ const response = await this.get(endpoint, { queryParams });
443
+ return response.value || [];
444
+ }
445
+ },
446
+
447
+ /**
448
+ * List site lists and document libraries
449
+ */
450
+ lists: async (site, options = {}) => {
451
+ const { top = 100 } = options;
452
+
453
+ const siteId = await this.sharepoint._parseSite(site);
454
+
455
+ const queryParams = {
456
+ '$top': top,
457
+ '$select': 'id,name,displayName,description,webUrl,list',
458
+ };
459
+
460
+ const response = await this.get(`/sites/${siteId}/lists`, { queryParams });
461
+ return response.value || [];
462
+ },
463
+
464
+ /**
465
+ * List items in a list
466
+ */
467
+ items: async (site, listId, options = {}) => {
468
+ const { top = 100 } = options;
469
+
470
+ const siteId = await this.sharepoint._parseSite(site);
471
+
472
+ const queryParams = {
473
+ '$top': top,
474
+ '$expand': 'fields',
475
+ };
476
+
477
+ const response = await this.get(`/sites/${siteId}/lists/${listId}/items`, { queryParams });
478
+ return response.value || [];
479
+ },
480
+
481
+ /**
482
+ * List files in site default document library
483
+ */
484
+ files: async (site, path = '', options = {}) => {
485
+ const { top = 100 } = options;
486
+
487
+ const siteId = await this.sharepoint._parseSite(site);
488
+
489
+ let endpoint;
490
+ if (!path || path === '/' || path === '') {
491
+ endpoint = `/sites/${siteId}/drive/root/children`;
492
+ } else {
493
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
494
+ endpoint = `/sites/${siteId}/drive/root:/${cleanPath}:/children`;
495
+ }
496
+
497
+ const queryParams = {
498
+ '$top': top,
499
+ '$select': 'id,name,size,file,folder,lastModifiedDateTime,webUrl',
500
+ };
501
+
502
+ const response = await this.get(endpoint, { queryParams });
503
+ return response.value || [];
504
+ },
505
+
506
+ /**
507
+ * Download file from SharePoint
508
+ */
509
+ download: async (site, path) => {
510
+ const siteId = await this.sharepoint._parseSite(site);
511
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
512
+ const endpoint = `/sites/${siteId}/drive/root:/${cleanPath}:/content`;
513
+
514
+ // Get access token
515
+ const token = await getAccessToken();
516
+
517
+ // Make direct fetch request to get binary content
518
+ const url = `${this.baseUrl}${endpoint}`;
519
+ const response = await fetch(url, {
520
+ headers: {
521
+ 'Authorization': `Bearer ${token}`,
522
+ },
523
+ });
524
+
525
+ if (!response.ok) {
526
+ const data = await response.json().catch(() => ({}));
527
+ throw parseGraphError(data, response.status);
528
+ }
529
+
530
+ return response;
531
+ },
532
+
533
+ /**
534
+ * Upload file to SharePoint (small files < 4MB)
535
+ */
536
+ upload: async (site, path, content, options = {}) => {
537
+ const { contentType = 'application/octet-stream' } = options;
538
+ const siteId = await this.sharepoint._parseSite(site);
539
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
540
+ const endpoint = `/sites/${siteId}/drive/root:/${cleanPath}:/content`;
541
+
542
+ // Get access token
543
+ const token = await getAccessToken();
544
+
545
+ // Make direct fetch request to send binary content
546
+ const url = `${this.baseUrl}${endpoint}`;
547
+ const response = await fetch(url, {
548
+ method: 'PUT',
549
+ headers: {
550
+ 'Authorization': `Bearer ${token}`,
551
+ 'Content-Type': contentType,
552
+ },
553
+ body: content,
554
+ });
555
+
556
+ if (!response.ok) {
557
+ const data = await response.json().catch(() => ({}));
558
+ throw parseGraphError(data, response.status);
559
+ }
560
+
561
+ return response.json();
562
+ },
563
+
564
+ /**
565
+ * Create upload session for large files
566
+ */
567
+ createUploadSession: async (site, path) => {
568
+ const siteId = await this.sharepoint._parseSite(site);
569
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
570
+ const endpoint = `/sites/${siteId}/drive/root:/${cleanPath}:/createUploadSession`;
571
+
572
+ return this.post(endpoint, {
573
+ item: {
574
+ '@microsoft.graph.conflictBehavior': 'rename',
575
+ },
576
+ });
577
+ },
578
+
579
+ /**
580
+ * Search SharePoint content
581
+ */
582
+ search: async (query, options = {}) => {
583
+ const { top = 50 } = options;
584
+
585
+ const payload = {
586
+ requests: [
587
+ {
588
+ entityTypes: ['driveItem', 'listItem', 'site'],
589
+ query: {
590
+ queryString: query,
591
+ },
592
+ from: 0,
593
+ size: top,
594
+ },
595
+ ],
596
+ };
597
+
598
+ const response = await this.post('/search/query', payload);
599
+
600
+ // Extract hits from response
601
+ const hits = response.value?.[0]?.hitsContainers?.[0]?.hits || [];
602
+ return hits.map(hit => hit.resource);
603
+ },
604
+ };
605
+
606
+ /**
607
+ * OneDrive endpoints
608
+ */
609
+ onedrive = {
610
+ /**
611
+ * Build path for OneDrive item
612
+ */
613
+ _buildPath: (path) => {
614
+ if (!path || path === '/' || path === '') {
615
+ return '/me/drive/root/children';
616
+ }
617
+ // Remove leading/trailing slashes
618
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
619
+ return `/me/drive/root:/${cleanPath}:`;
620
+ },
621
+
622
+ /**
623
+ * List files and folders
624
+ */
625
+ list: async (path = '', options = {}) => {
626
+ const { top = 100, select } = options;
627
+
628
+ let endpoint;
629
+ if (!path || path === '/' || path === '') {
630
+ endpoint = '/me/drive/root/children';
631
+ } else {
632
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
633
+ endpoint = `/me/drive/root:/${cleanPath}:/children`;
634
+ }
635
+
636
+ const queryParams = {
637
+ '$top': top,
638
+ };
639
+
640
+ if (select) {
641
+ queryParams['$select'] = Array.isArray(select) ? select.join(',') : select;
642
+ } else {
643
+ queryParams['$select'] = 'id,name,size,file,folder,lastModifiedDateTime,webUrl';
644
+ }
645
+
646
+ const response = await this.get(endpoint, { queryParams });
647
+ return response.value || [];
648
+ },
649
+
650
+ /**
651
+ * Get item metadata
652
+ */
653
+ getMetadata: async (path, options = {}) => {
654
+ const { select } = options;
655
+
656
+ let endpoint;
657
+ if (!path || path === '/' || path === '') {
658
+ endpoint = '/me/drive/root';
659
+ } else {
660
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
661
+ endpoint = `/me/drive/root:/${cleanPath}`;
662
+ }
663
+
664
+ const queryParams = {};
665
+ if (select) {
666
+ queryParams['$select'] = Array.isArray(select) ? select.join(',') : select;
667
+ }
668
+
669
+ return this.get(endpoint, { queryParams });
670
+ },
671
+
672
+ /**
673
+ * Download file content
674
+ */
675
+ download: async (path) => {
676
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
677
+ const endpoint = `/me/drive/root:/${cleanPath}:/content`;
678
+
679
+ // Get access token
680
+ const token = await getAccessToken();
681
+
682
+ // Make direct fetch request to get binary content
683
+ const url = `${this.baseUrl}${endpoint}`;
684
+ const response = await fetch(url, {
685
+ headers: {
686
+ 'Authorization': `Bearer ${token}`,
687
+ },
688
+ });
689
+
690
+ if (!response.ok) {
691
+ const data = await response.json().catch(() => ({}));
692
+ throw parseGraphError(data, response.status);
693
+ }
694
+
695
+ return response;
696
+ },
697
+
698
+ /**
699
+ * Upload file (small files < 4MB)
700
+ */
701
+ upload: async (path, content, options = {}) => {
702
+ const { contentType = 'application/octet-stream' } = options;
703
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
704
+ const endpoint = `/me/drive/root:/${cleanPath}:/content`;
705
+
706
+ // Get access token
707
+ const token = await getAccessToken();
708
+
709
+ // Make direct fetch request to send binary content
710
+ const url = `${this.baseUrl}${endpoint}`;
711
+ const response = await fetch(url, {
712
+ method: 'PUT',
713
+ headers: {
714
+ 'Authorization': `Bearer ${token}`,
715
+ 'Content-Type': contentType,
716
+ },
717
+ body: content,
718
+ });
719
+
720
+ if (!response.ok) {
721
+ const data = await response.json().catch(() => ({}));
722
+ throw parseGraphError(data, response.status);
723
+ }
724
+
725
+ return response.json();
726
+ },
727
+
728
+ /**
729
+ * Create upload session for large files
730
+ */
731
+ createUploadSession: async (path) => {
732
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
733
+ const endpoint = `/me/drive/root:/${cleanPath}:/createUploadSession`;
734
+
735
+ return this.post(endpoint, {
736
+ item: {
737
+ '@microsoft.graph.conflictBehavior': 'rename',
738
+ },
739
+ });
740
+ },
741
+
742
+ /**
743
+ * Upload chunk to session
744
+ */
745
+ uploadChunk: async (uploadUrl, chunk, start, end, totalSize) => {
746
+ const response = await fetch(uploadUrl, {
747
+ method: 'PUT',
748
+ headers: {
749
+ 'Content-Length': chunk.length.toString(),
750
+ 'Content-Range': `bytes ${start}-${end - 1}/${totalSize}`,
751
+ },
752
+ body: chunk,
753
+ });
754
+
755
+ if (!response.ok && response.status !== 202) {
756
+ const data = await response.json().catch(() => ({}));
757
+ throw parseGraphError(data, response.status);
758
+ }
759
+
760
+ return response.json().catch(() => ({ status: response.status }));
761
+ },
762
+
763
+ /**
764
+ * Search files
765
+ */
766
+ search: async (query, options = {}) => {
767
+ const { top = 50 } = options;
768
+
769
+ const queryParams = {
770
+ '$top': top,
771
+ '$select': 'id,name,size,file,folder,lastModifiedDateTime,webUrl',
772
+ };
773
+
774
+ const endpoint = `/me/drive/root/search(q='${encodeURIComponent(query)}')`;
775
+ const response = await this.get(endpoint, { queryParams });
776
+ return response.value || [];
777
+ },
778
+
779
+ /**
780
+ * Create sharing link
781
+ */
782
+ share: async (path, options = {}) => {
783
+ const { type = 'view', scope = 'organization' } = options;
784
+
785
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
786
+ const endpoint = `/me/drive/root:/${cleanPath}:/createLink`;
787
+
788
+ return this.post(endpoint, {
789
+ type,
790
+ scope,
791
+ });
792
+ },
793
+
794
+ /**
795
+ * Create folder
796
+ */
797
+ mkdir: async (path) => {
798
+ const parts = path.split('/').filter(p => p);
799
+ const folderName = parts.pop();
800
+ const parentPath = parts.join('/');
801
+
802
+ let endpoint;
803
+ if (!parentPath) {
804
+ endpoint = '/me/drive/root/children';
805
+ } else {
806
+ endpoint = `/me/drive/root:/${parentPath}:/children`;
807
+ }
808
+
809
+ return this.post(endpoint, {
810
+ name: folderName,
811
+ folder: {},
812
+ '@microsoft.graph.conflictBehavior': 'fail',
813
+ });
814
+ },
815
+
816
+ /**
817
+ * Delete file or folder
818
+ */
819
+ remove: async (path) => {
820
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
821
+ const endpoint = `/me/drive/root:/${cleanPath}`;
822
+
823
+ return this.delete(endpoint);
824
+ },
825
+
826
+ /**
827
+ * Invite users to access file (external sharing)
828
+ */
829
+ invite: async (path, options = {}) => {
830
+ const {
831
+ recipients = [],
832
+ role = 'read',
833
+ message = '',
834
+ sendInvitation = true,
835
+ requireSignIn = false,
836
+ } = options;
837
+
838
+ if (!recipients || recipients.length === 0) {
839
+ throw new Error('At least one recipient email is required');
840
+ }
841
+
842
+ // Get item ID first
843
+ const cleanPath = path.replace(/^\/+|\/+$/g, '');
844
+ const itemEndpoint = `/me/drive/root:/${cleanPath}`;
845
+ const item = await this.get(itemEndpoint, {
846
+ queryParams: { '$select': 'id,name' },
847
+ });
848
+
849
+ // Prepare recipients array
850
+ const recipientsList = recipients.map(email => ({
851
+ email: typeof email === 'string' ? email : email.email,
852
+ }));
853
+
854
+ // Call invite API
855
+ const inviteEndpoint = `/me/drive/items/${item.id}/invite`;
856
+ const payload = {
857
+ requireSignIn,
858
+ sendInvitation,
859
+ roles: [role],
860
+ recipients: recipientsList,
861
+ };
862
+
863
+ // Add message if provided
864
+ if (message) {
865
+ payload.message = message;
866
+ }
867
+
868
+ return this.post(inviteEndpoint, payload);
869
+ },
870
+ };
871
+ }
872
+
873
+ // Export singleton instance
874
+ const graphClient = new GraphClient();
875
+ export default graphClient;