msoutlook-mcp 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/README.md +134 -0
- package/dist/api/calendar.d.ts +90 -0
- package/dist/api/calendar.d.ts.map +1 -0
- package/dist/api/calendar.js +102 -0
- package/dist/api/calendar.js.map +1 -0
- package/dist/api/client.d.ts +14 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +93 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/contacts.d.ts +43 -0
- package/dist/api/contacts.d.ts.map +1 -0
- package/dist/api/contacts.js +45 -0
- package/dist/api/contacts.js.map +1 -0
- package/dist/api/mail.d.ts +90 -0
- package/dist/api/mail.d.ts.map +1 -0
- package/dist/api/mail.js +148 -0
- package/dist/api/mail.js.map +1 -0
- package/dist/api/people.d.ts +28 -0
- package/dist/api/people.d.ts.map +1 -0
- package/dist/api/people.js +16 -0
- package/dist/api/people.js.map +1 -0
- package/dist/auth/browser-login.d.ts +26 -0
- package/dist/auth/browser-login.d.ts.map +1 -0
- package/dist/auth/browser-login.js +398 -0
- package/dist/auth/browser-login.js.map +1 -0
- package/dist/auth/index.d.ts +34 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +89 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/session-store.d.ts +38 -0
- package/dist/auth/session-store.d.ts.map +1 -0
- package/dist/auth/session-store.js +163 -0
- package/dist/auth/session-store.js.map +1 -0
- package/dist/auth/token-extractor.d.ts +46 -0
- package/dist/auth/token-extractor.d.ts.map +1 -0
- package/dist/auth/token-extractor.js +126 -0
- package/dist/auth/token-extractor.js.map +1 -0
- package/dist/auth/token-refresh.d.ts +23 -0
- package/dist/auth/token-refresh.d.ts.map +1 -0
- package/dist/auth/token-refresh.js +133 -0
- package/dist/auth/token-refresh.js.map +1 -0
- package/dist/browser/cookie-import.d.ts +30 -0
- package/dist/browser/cookie-import.d.ts.map +1 -0
- package/dist/browser/cookie-import.js +446 -0
- package/dist/browser/cookie-import.js.map +1 -0
- package/dist/constants.d.ts +27 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +39 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +20 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/auth-tools.d.ts +6 -0
- package/dist/tools/auth-tools.d.ts.map +1 -0
- package/dist/tools/auth-tools.js +116 -0
- package/dist/tools/auth-tools.js.map +1 -0
- package/dist/tools/calendar-tools.d.ts +6 -0
- package/dist/tools/calendar-tools.d.ts.map +1 -0
- package/dist/tools/calendar-tools.js +168 -0
- package/dist/tools/calendar-tools.js.map +1 -0
- package/dist/tools/contact-tools.d.ts +6 -0
- package/dist/tools/contact-tools.d.ts.map +1 -0
- package/dist/tools/contact-tools.js +105 -0
- package/dist/tools/contact-tools.js.map +1 -0
- package/dist/tools/mail-tools.d.ts +6 -0
- package/dist/tools/mail-tools.d.ts.map +1 -0
- package/dist/tools/mail-tools.js +196 -0
- package/dist/tools/mail-tools.js.map +1 -0
- package/dist/utils/http.d.ts +15 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +48 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../src/api/mail.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAiBrF;AAMD,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,kBAAkB,UAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAGzF;AAMD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACvC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBrE;AAMD,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAanG;AAMD,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9F;AAMD,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9F;AAMD,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;AAMD,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,GAAE,SAAS,GAAG,UAAU,GAAG,YAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtH;AAMD,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAI3F;AAMD,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7D;AAMD,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAMhF;AAMD,wBAAsB,WAAW,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAMzD;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAErE;AAMD,wBAAsB,iBAAiB,CAAC,GAAG,SAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAMpE;AAMD,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D"}
|
package/dist/api/mail.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail API — email CRUD, search, and folder operations.
|
|
3
|
+
*/
|
|
4
|
+
import { owaGet, owaPost, owaPatch, owaDelete } from './client.js';
|
|
5
|
+
export async function listMessages(opts = {}) {
|
|
6
|
+
const folder = opts.folder ?? 'Inbox';
|
|
7
|
+
const params = {
|
|
8
|
+
'$top': String(opts.top ?? 20),
|
|
9
|
+
'$orderby': opts.orderBy ?? 'ReceivedDateTime desc',
|
|
10
|
+
'$select': (opts.select ?? [
|
|
11
|
+
'Id', 'Subject', 'BodyPreview', 'From', 'ToRecipients', 'ReceivedDateTime',
|
|
12
|
+
'IsRead', 'HasAttachments', 'Importance', 'Flag', 'ConversationId', 'WebLink',
|
|
13
|
+
]).join(','),
|
|
14
|
+
};
|
|
15
|
+
if (opts.skip)
|
|
16
|
+
params['$skip'] = String(opts.skip);
|
|
17
|
+
if (opts.filter)
|
|
18
|
+
params['$filter'] = opts.filter;
|
|
19
|
+
if (opts.search)
|
|
20
|
+
params['$search'] = `"${opts.search}"`;
|
|
21
|
+
const res = await owaGet(`/MailFolders/${folder}/messages`, params);
|
|
22
|
+
return res.value;
|
|
23
|
+
}
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Get message
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
export async function getMessage(id, includeAttachments = false) {
|
|
28
|
+
const params = includeAttachments ? { '$expand': 'Attachments' } : undefined;
|
|
29
|
+
return owaGet(`/messages/${id}`, params);
|
|
30
|
+
}
|
|
31
|
+
export async function sendEmail(opts) {
|
|
32
|
+
const toRecipients = opts.to.map(addr => ({
|
|
33
|
+
EmailAddress: { Address: addr },
|
|
34
|
+
}));
|
|
35
|
+
const ccRecipients = opts.cc?.map(addr => ({ EmailAddress: { Address: addr } }));
|
|
36
|
+
const bccRecipients = opts.bcc?.map(addr => ({ EmailAddress: { Address: addr } }));
|
|
37
|
+
await owaPost('/sendmail', {
|
|
38
|
+
Message: {
|
|
39
|
+
Subject: opts.subject,
|
|
40
|
+
Body: { ContentType: opts.bodyType ?? 'Text', Content: opts.body },
|
|
41
|
+
ToRecipients: toRecipients,
|
|
42
|
+
...(ccRecipients?.length ? { CcRecipients: ccRecipients } : {}),
|
|
43
|
+
...(bccRecipients?.length ? { BccRecipients: bccRecipients } : {}),
|
|
44
|
+
Importance: opts.importance ?? 'Normal',
|
|
45
|
+
},
|
|
46
|
+
SaveToSentItems: opts.saveToSentItems !== false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// Create draft
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
export async function createDraft(opts) {
|
|
53
|
+
const toRecipients = opts.to.map(addr => ({ EmailAddress: { Address: addr } }));
|
|
54
|
+
const ccRecipients = opts.cc?.map(addr => ({ EmailAddress: { Address: addr } }));
|
|
55
|
+
const bccRecipients = opts.bcc?.map(addr => ({ EmailAddress: { Address: addr } }));
|
|
56
|
+
return owaPost('/messages', {
|
|
57
|
+
Subject: opts.subject,
|
|
58
|
+
Body: { ContentType: opts.bodyType ?? 'Text', Content: opts.body },
|
|
59
|
+
ToRecipients: toRecipients,
|
|
60
|
+
...(ccRecipients?.length ? { CcRecipients: ccRecipients } : {}),
|
|
61
|
+
...(bccRecipients?.length ? { BccRecipients: bccRecipients } : {}),
|
|
62
|
+
Importance: opts.importance ?? 'Normal',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Reply
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
export async function replyToMessage(id, body, replyAll = false) {
|
|
69
|
+
const action = replyAll ? 'replyall' : 'reply';
|
|
70
|
+
await owaPost(`/messages/${id}/${action}`, {
|
|
71
|
+
Comment: body,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// Forward
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
export async function forwardMessage(id, to, comment) {
|
|
78
|
+
await owaPost(`/messages/${id}/forward`, {
|
|
79
|
+
ToRecipients: to.map(addr => ({ EmailAddress: { Address: addr } })),
|
|
80
|
+
Comment: comment ?? '',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
// Mark read/unread
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
export async function markMessageRead(id, isRead = true) {
|
|
87
|
+
await owaPatch(`/messages/${id}`, { IsRead: isRead });
|
|
88
|
+
}
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
// Flag
|
|
91
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
export async function flagMessage(id, status = 'Flagged') {
|
|
93
|
+
await owaPatch(`/messages/${id}`, { Flag: { FlagStatus: status } });
|
|
94
|
+
}
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// Move
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
export async function moveMessage(id, destinationFolderId) {
|
|
99
|
+
return owaPost(`/messages/${id}/move`, {
|
|
100
|
+
DestinationId: destinationFolderId,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
// Delete
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
export async function deleteMessage(id) {
|
|
107
|
+
await owaDelete(`/messages/${id}`);
|
|
108
|
+
}
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
// Search
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
export async function searchMessages(query, top = 20) {
|
|
113
|
+
return listMessages({
|
|
114
|
+
search: query,
|
|
115
|
+
top,
|
|
116
|
+
select: ['Id', 'Subject', 'BodyPreview', 'From', 'ReceivedDateTime', 'IsRead', 'WebLink'],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
+
// Folders
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
export async function listFolders() {
|
|
123
|
+
const res = await owaGet('/mailfolders', {
|
|
124
|
+
'$top': '50',
|
|
125
|
+
'$select': 'Id,DisplayName,UnreadItemCount,TotalItemCount,ChildFolderCount',
|
|
126
|
+
});
|
|
127
|
+
return res.value;
|
|
128
|
+
}
|
|
129
|
+
export async function getFolder(idOrName) {
|
|
130
|
+
return owaGet(`/mailfolders/${idOrName}`);
|
|
131
|
+
}
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
// Unread count
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
export async function getUnreadMessages(top = 10) {
|
|
136
|
+
return listMessages({
|
|
137
|
+
filter: 'IsRead eq false',
|
|
138
|
+
top,
|
|
139
|
+
orderBy: 'ReceivedDateTime desc',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
// Send draft
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
export async function sendDraft(draftId) {
|
|
146
|
+
await owaPost(`/messages/${draftId}/send`, {});
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=mail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.js","sourceRoot":"","sources":["../../src/api/mail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* People and directory lookup API.
|
|
3
|
+
*/
|
|
4
|
+
export interface Person {
|
|
5
|
+
Id?: string;
|
|
6
|
+
DisplayName: string;
|
|
7
|
+
GivenName?: string;
|
|
8
|
+
Surname?: string;
|
|
9
|
+
EmailAddresses?: Array<{
|
|
10
|
+
Name?: string;
|
|
11
|
+
Address: string;
|
|
12
|
+
}>;
|
|
13
|
+
Phones?: Array<{
|
|
14
|
+
Number: string;
|
|
15
|
+
Type: string;
|
|
16
|
+
}>;
|
|
17
|
+
JobTitle?: string;
|
|
18
|
+
Department?: string;
|
|
19
|
+
OfficeLocation?: string;
|
|
20
|
+
CompanyName?: string;
|
|
21
|
+
UserPrincipalName?: string;
|
|
22
|
+
ScoredEmailAddresses?: Array<{
|
|
23
|
+
Address: string;
|
|
24
|
+
RelevanceScore: number;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export declare function searchPeople(query: string, top?: number): Promise<Person[]>;
|
|
28
|
+
//# sourceMappingURL=people.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"people.d.ts","sourceRoot":"","sources":["../../src/api/people.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,MAAM,WAAW,MAAM;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3E;AAMD,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAO7E"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* People and directory lookup API.
|
|
3
|
+
*/
|
|
4
|
+
import { owaGet } from './client.js';
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Search people
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
export async function searchPeople(query, top = 10) {
|
|
9
|
+
const res = await owaGet('/people', {
|
|
10
|
+
'$search': `"${query}"`,
|
|
11
|
+
'$top': String(top),
|
|
12
|
+
'$select': 'DisplayName,GivenName,Surname,ScoredEmailAddresses,JobTitle,Department,UserPrincipalName',
|
|
13
|
+
});
|
|
14
|
+
return res.value;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=people.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"people.js","sourceRoot":"","sources":["../../src/api/people.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAsBrC,gFAAgF;AAChF,gBAAgB;AAChB,gFAAgF;AAEhF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAa,EAAE,GAAG,GAAG,EAAE;IACxD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAwB,SAAS,EAAE;QACzD,SAAS,EAAE,IAAI,KAAK,GAAG;QACvB,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC;QACnB,SAAS,EAAE,0FAA0F;KACtG,CAAC,CAAC;IACH,OAAO,GAAG,CAAC,KAAK,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright-based browser login flow for Outlook Web App.
|
|
3
|
+
*
|
|
4
|
+
* Ported closely from msteams-mcp's browser auth approach:
|
|
5
|
+
* - Uses the system default browser (Edge on Windows, Chrome on macOS)
|
|
6
|
+
* - Imports Microsoft SSO cookies from the user's real browser profile,
|
|
7
|
+
* enabling silent authentication without typing credentials
|
|
8
|
+
* - Headless first, visible browser only as last resort
|
|
9
|
+
* - Handles stale SingletonLock files from crashed sessions
|
|
10
|
+
*/
|
|
11
|
+
export declare function headlessLogin(): Promise<LoginResult | null>;
|
|
12
|
+
export declare function headedLogin(clearCookiesFirst?: boolean): Promise<LoginResult | null>;
|
|
13
|
+
export interface LoginResult {
|
|
14
|
+
upn: string;
|
|
15
|
+
/** How authentication was completed. */
|
|
16
|
+
method: 'token-cache' | 'headless-sso' | 'headed-browser';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Login to Outlook Web.
|
|
20
|
+
*
|
|
21
|
+
* @param forceNew - When true, clear the saved session and force a full
|
|
22
|
+
* re-authentication (mirrors msteams-mcp's `forceNew` option).
|
|
23
|
+
*/
|
|
24
|
+
export declare function browserLogin(forceNew?: boolean): Promise<LoginResult | null>;
|
|
25
|
+
export declare function headlessTokenRefresh(): Promise<boolean>;
|
|
26
|
+
//# sourceMappingURL=browser-login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-login.d.ts","sourceRoot":"","sources":["../../src/auth/browser-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA6QH,wBAAsB,aAAa,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAkCjE;AA4BD,wBAAsB,WAAW,CAAC,iBAAiB,UAAQ,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAiExF;AAMD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,MAAM,EAAE,aAAa,GAAG,cAAc,GAAG,gBAAgB,CAAC;CAC3D;AAMD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,QAAQ,UAAQ,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAiBhF;AAMD,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAE7D"}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright-based browser login flow for Outlook Web App.
|
|
3
|
+
*
|
|
4
|
+
* Ported closely from msteams-mcp's browser auth approach:
|
|
5
|
+
* - Uses the system default browser (Edge on Windows, Chrome on macOS)
|
|
6
|
+
* - Imports Microsoft SSO cookies from the user's real browser profile,
|
|
7
|
+
* enabling silent authentication without typing credentials
|
|
8
|
+
* - Headless first, visible browser only as last resort
|
|
9
|
+
* - Handles stale SingletonLock files from crashed sessions
|
|
10
|
+
*/
|
|
11
|
+
import { chromium } from 'playwright';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
import { OWA_URL, LOGIN_TIMEOUT_MS } from '../constants.js';
|
|
17
|
+
import { getBrowserProfileDir, writeSessionState, writeTokenCache, clearSession, } from './session-store.js';
|
|
18
|
+
import { extractTokensFromLocalStorage, getOwaLocalStorage } from './token-extractor.js';
|
|
19
|
+
import { importMicrosoftCookies } from '../browser/cookie-import.js';
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Browser channel
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
/** Microsoft login domains — redirect to these means we're not authenticated. */
|
|
24
|
+
const LOGIN_DOMAINS = [
|
|
25
|
+
'login.microsoftonline.com',
|
|
26
|
+
'login.live.com',
|
|
27
|
+
'login.microsoft.com',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Predicate evaluated INSIDE the browser. Returns true once an OWA-scoped MSAL
|
|
31
|
+
* access token is present in localStorage. This is the real signal that auth
|
|
32
|
+
* has completed — we deliberately do NOT check the `olk-isauthed` flag, whose
|
|
33
|
+
* value is "1" (not "true") and varies, while this token check is what the
|
|
34
|
+
* token extractor itself relies on.
|
|
35
|
+
*/
|
|
36
|
+
function owaAccessTokenPresent() {
|
|
37
|
+
return Object.keys(localStorage).some(k => k.includes('accesstoken') && k.includes('outlook.office.com'));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Determine the Playwright channel to use.
|
|
41
|
+
*
|
|
42
|
+
* Priority:
|
|
43
|
+
* 1. MSOUTLOOK_BROWSER env var (explicit override)
|
|
44
|
+
* 2. macOS system default browser (read from LaunchServices)
|
|
45
|
+
* 3. Platform fallback: Chrome on macOS/Linux, Edge on Windows
|
|
46
|
+
*/
|
|
47
|
+
function getBrowserChannel() {
|
|
48
|
+
const override = process.env.MSOUTLOOK_BROWSER?.trim().toLowerCase();
|
|
49
|
+
if (override && override !== 'chromium' && override !== 'bundled')
|
|
50
|
+
return override;
|
|
51
|
+
if (process.platform === 'darwin') {
|
|
52
|
+
const detected = getMacOSDefaultBrowser();
|
|
53
|
+
if (detected) {
|
|
54
|
+
logger.debug(`macOS default browser detected: ${detected}`);
|
|
55
|
+
return detected;
|
|
56
|
+
}
|
|
57
|
+
return 'chrome'; // macOS fallback (same as msteams-mcp)
|
|
58
|
+
}
|
|
59
|
+
return process.platform === 'win32' ? 'msedge' : 'chrome';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Detect the macOS default browser by reading the LaunchServices plist.
|
|
63
|
+
* Returns a Playwright channel name or undefined if unrecognised.
|
|
64
|
+
*/
|
|
65
|
+
function getMacOSDefaultBrowser() {
|
|
66
|
+
try {
|
|
67
|
+
const plistPath = path.join(process.env.HOME ?? '', 'Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist');
|
|
68
|
+
const json = execSync(`plutil -convert json -o - "${plistPath}"`, {
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
timeout: 3000,
|
|
71
|
+
});
|
|
72
|
+
const data = JSON.parse(json);
|
|
73
|
+
const handlers = data.LSHandlers ?? [];
|
|
74
|
+
const httpsHandler = handlers.find(h => h.LSHandlerURLScheme === 'https');
|
|
75
|
+
const bundleId = (httpsHandler?.LSHandlerRoleAll ?? '').toLowerCase();
|
|
76
|
+
if (bundleId.includes('microsoft.edgemac') || bundleId.includes('edge'))
|
|
77
|
+
return 'msedge';
|
|
78
|
+
if (bundleId.includes('google.chrome') || bundleId.includes('chrome'))
|
|
79
|
+
return 'chrome';
|
|
80
|
+
// Firefox and Safari don't have Playwright channels — fall through to platform default
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
// SingletonLock cleanup (same as msteams-mcp)
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
function cleanupStaleSingletonLock(profileDir) {
|
|
91
|
+
const lockPath = path.join(profileDir, 'SingletonLock');
|
|
92
|
+
if (!fs.existsSync(lockPath))
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
const linkTarget = fs.readlinkSync(lockPath);
|
|
96
|
+
const match = linkTarget.match(/-(\d+)$/);
|
|
97
|
+
if (match) {
|
|
98
|
+
const pid = parseInt(match[1], 10);
|
|
99
|
+
try {
|
|
100
|
+
process.kill(pid, 0); // signal 0 = check if process exists
|
|
101
|
+
return; // still running — don't remove
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// process is gone — remove stale lock
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
fs.unlinkSync(lockPath);
|
|
108
|
+
logger.debug('Removed stale SingletonLock');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// Shared helpers
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
function hasSavedBrowserProfile() {
|
|
118
|
+
const dir = getBrowserProfileDir();
|
|
119
|
+
return fs.existsSync(dir) && fs.readdirSync(dir).length > 0;
|
|
120
|
+
}
|
|
121
|
+
async function extractAndCacheTokens(context) {
|
|
122
|
+
const state = await context.storageState();
|
|
123
|
+
writeSessionState(state);
|
|
124
|
+
const ls = getOwaLocalStorage(state);
|
|
125
|
+
if (!ls) {
|
|
126
|
+
logger.debug('Could not find Outlook origin in session state');
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const tokens = extractTokensFromLocalStorage(ls);
|
|
130
|
+
if (!tokens) {
|
|
131
|
+
logger.debug('Could not extract OWA tokens from localStorage');
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const cache = {
|
|
135
|
+
owaToken: tokens.owaToken,
|
|
136
|
+
owaTokenExpiry: tokens.owaTokenExpiry.getTime(),
|
|
137
|
+
graphToken: tokens.graphToken,
|
|
138
|
+
graphTokenExpiry: tokens.graphTokenExpiry?.getTime(),
|
|
139
|
+
refreshToken: tokens.refreshToken,
|
|
140
|
+
tenantId: tokens.tenantId,
|
|
141
|
+
upn: tokens.upn,
|
|
142
|
+
extractedAt: Date.now(),
|
|
143
|
+
};
|
|
144
|
+
writeTokenCache(cache);
|
|
145
|
+
return tokens.upn ?? 'unknown';
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Launch a persistent context, clean up stale locks first.
|
|
149
|
+
* Falls back to removing the lock and retrying once if a lock conflict occurs.
|
|
150
|
+
*/
|
|
151
|
+
async function launchContext(profileDir, headless, channel) {
|
|
152
|
+
cleanupStaleSingletonLock(profileDir);
|
|
153
|
+
const launch = () => chromium.launchPersistentContext(profileDir, {
|
|
154
|
+
headless,
|
|
155
|
+
channel,
|
|
156
|
+
viewport: { width: 1280, height: 800 },
|
|
157
|
+
acceptDownloads: false,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
return await launch();
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
164
|
+
if (msg.includes('ProcessSingleton') || msg.includes('SingletonLock')) {
|
|
165
|
+
logger.debug('Profile lock conflict — removing lock and retrying');
|
|
166
|
+
const lockPath = path.join(profileDir, 'SingletonLock');
|
|
167
|
+
try {
|
|
168
|
+
fs.unlinkSync(lockPath);
|
|
169
|
+
}
|
|
170
|
+
catch { /* ignore */ }
|
|
171
|
+
return launch();
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Wait for OWA to authenticate AND for MSAL tokens to be present in localStorage.
|
|
178
|
+
* Uses a redirect-detection approach (like msteams-mcp) for fast failure on
|
|
179
|
+
* unauthenticated sessions, so we don't sit at a blank login page for 30s+.
|
|
180
|
+
*
|
|
181
|
+
* @returns true if authenticated with tokens, false if redirected to login
|
|
182
|
+
*/
|
|
183
|
+
async function waitForOwaAuth(context, timeoutMs) {
|
|
184
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
185
|
+
// Detect redirect to Microsoft login — fast signal that the session is invalid
|
|
186
|
+
let redirectedToLogin = false;
|
|
187
|
+
const onNavigation = (frame) => {
|
|
188
|
+
if (frame === page.mainFrame() && LOGIN_DOMAINS.some(d => frame.url().includes(d))) {
|
|
189
|
+
redirectedToLogin = true;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
page.on('framenavigated', onNavigation);
|
|
193
|
+
try {
|
|
194
|
+
await page.goto(OWA_URL, { waitUntil: 'domcontentloaded' });
|
|
195
|
+
// Give MSAL 5 s to either redirect to login or silently authenticate.
|
|
196
|
+
// If redirected to login page, return false immediately.
|
|
197
|
+
const deadline = Date.now() + Math.min(timeoutMs, 5_000);
|
|
198
|
+
while (Date.now() < deadline) {
|
|
199
|
+
if (redirectedToLogin)
|
|
200
|
+
return false;
|
|
201
|
+
await page.waitForTimeout(100);
|
|
202
|
+
}
|
|
203
|
+
if (redirectedToLogin)
|
|
204
|
+
return false;
|
|
205
|
+
// No login redirect — session appears valid. Now wait for the MSAL token.
|
|
206
|
+
await page.waitForFunction(owaAccessTokenPresent, { timeout: timeoutMs });
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
page.off('framenavigated', onNavigation);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
214
|
+
// MSAL token polling (mirrors msteams-mcp waitForTokenRefresh)
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* When the browser session is valid (no login redirect) but MSAL tokens are
|
|
218
|
+
* still being acquired, poll localStorage in-browser until tokens appear.
|
|
219
|
+
* This is the case where session cookies are alive but the hour-long access
|
|
220
|
+
* token has expired — OWA JS silently acquires new ones.
|
|
221
|
+
*
|
|
222
|
+
* Polls in-browser to avoid deserialising the full session state on every
|
|
223
|
+
* check. Returns the UPN once tokens are available.
|
|
224
|
+
*/
|
|
225
|
+
async function waitForMsalTokens(context, timeoutMs = 20_000) {
|
|
226
|
+
const page = context.pages()[0];
|
|
227
|
+
if (!page)
|
|
228
|
+
return false;
|
|
229
|
+
logger.debug('Waiting for MSAL to acquire tokens...');
|
|
230
|
+
const interval = 1_000;
|
|
231
|
+
const deadline = Date.now() + timeoutMs;
|
|
232
|
+
while (Date.now() < deadline) {
|
|
233
|
+
const ready = await page.evaluate(owaAccessTokenPresent).catch(() => false);
|
|
234
|
+
if (ready) {
|
|
235
|
+
logger.debug(`MSAL tokens appeared after ${Math.round((Date.now() - (deadline - timeoutMs)) / 1000)}s`);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
await page.waitForTimeout(interval);
|
|
239
|
+
}
|
|
240
|
+
logger.debug('MSAL token wait timed out');
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
// Headless login (primary)
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
export async function headlessLogin() {
|
|
247
|
+
logger.debug('Attempting headless (silent) login...');
|
|
248
|
+
const profileDir = getBrowserProfileDir();
|
|
249
|
+
const channel = getBrowserChannel();
|
|
250
|
+
let context = null;
|
|
251
|
+
try {
|
|
252
|
+
context = await launchContext(profileDir, true, channel);
|
|
253
|
+
const authenticated = await waitForOwaAuth(context, 5_000);
|
|
254
|
+
if (!authenticated) {
|
|
255
|
+
logger.debug('Headless: session invalid or expired (login redirect detected)');
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
// Session is valid. Check if MSAL tokens are already there, or wait
|
|
259
|
+
// up to 20s for OWA JS to silently acquire them (mirrors msteams-mcp).
|
|
260
|
+
const tokensReady = await waitForMsalTokens(context, 20_000);
|
|
261
|
+
if (!tokensReady) {
|
|
262
|
+
logger.debug('Headless: MSAL tokens did not appear — falling back to headed login');
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const upn = await extractAndCacheTokens(context);
|
|
266
|
+
if (!upn)
|
|
267
|
+
return null;
|
|
268
|
+
logger.info(`Headless login succeeded (${upn})`);
|
|
269
|
+
return { upn, method: 'headless-sso' };
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
logger.debug('Headless login failed', err instanceof Error ? err.message : String(err));
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
await context?.close().catch(() => { });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
|
+
// Headed login (fallback)
|
|
281
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
282
|
+
/** Inject a visible progress overlay into the page (mirrors msteams-mcp). Failures are silently ignored — purely cosmetic. */
|
|
283
|
+
async function showOverlay(page, phase) {
|
|
284
|
+
const phases = {
|
|
285
|
+
pending: { icon: '⋯', title: "You're signed in!", detail: 'Setting up your Outlook connection...', bg: '#5b5fc7' },
|
|
286
|
+
saving: { icon: '⋯', title: 'Saving your session...', detail: "So you won't need to log in again.", bg: '#5b5fc7' },
|
|
287
|
+
done: { icon: '✓', title: 'All done!', detail: 'This window will close automatically.', bg: '#107c10' },
|
|
288
|
+
error: { icon: '✕', title: 'Something went wrong', detail: 'Please try again.', bg: '#c42b1c' },
|
|
289
|
+
};
|
|
290
|
+
const p = phases[phase];
|
|
291
|
+
try {
|
|
292
|
+
await page.evaluate(({ icon, title, detail, bg }) => {
|
|
293
|
+
const existing = document.getElementById('msoutlook-mcp-overlay');
|
|
294
|
+
if (existing)
|
|
295
|
+
existing.remove();
|
|
296
|
+
const overlay = document.createElement('div');
|
|
297
|
+
overlay.id = 'msoutlook-mcp-overlay';
|
|
298
|
+
Object.assign(overlay.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: '999999', fontFamily: "'Segoe UI',system-ui,sans-serif" });
|
|
299
|
+
overlay.innerHTML = `<div style="background:white;border-radius:12px;padding:40px 48px;max-width:420px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.3)"><div style="width:64px;height:64px;border-radius:50%;background:${bg};color:white;font-size:32px;display:flex;align-items:center;justify-content:center;margin:0 auto 24px">${icon}</div><h2 style="margin:0 0 12px;font-size:20px;font-weight:600;color:#242424">${title}</h2><p style="margin:0;font-size:14px;color:#616161;line-height:1.5">${detail}</p></div>`;
|
|
300
|
+
document.body.appendChild(overlay);
|
|
301
|
+
}, p);
|
|
302
|
+
}
|
|
303
|
+
catch { /* cosmetic — ignore */ }
|
|
304
|
+
}
|
|
305
|
+
export async function headedLogin(clearCookiesFirst = false) {
|
|
306
|
+
logger.info('Opening browser for interactive login...');
|
|
307
|
+
const profileDir = getBrowserProfileDir();
|
|
308
|
+
const channel = getBrowserChannel();
|
|
309
|
+
let context = null;
|
|
310
|
+
let browserClosed = false;
|
|
311
|
+
try {
|
|
312
|
+
context = await launchContext(profileDir, false, channel);
|
|
313
|
+
context.on('close', () => { browserClosed = true; });
|
|
314
|
+
// For force_new: clear browser cookies before importing fresh ones
|
|
315
|
+
if (clearCookiesFirst) {
|
|
316
|
+
await context.clearCookies();
|
|
317
|
+
logger.debug('Browser cookies cleared for force_new login');
|
|
318
|
+
}
|
|
319
|
+
// Import Microsoft SSO cookies from the user's real browser — enables
|
|
320
|
+
// instant silent sign-in without typing credentials (same as msteams-mcp).
|
|
321
|
+
await importMicrosoftCookies(context, channel);
|
|
322
|
+
const authenticated = await waitForOwaAuth(context, LOGIN_TIMEOUT_MS);
|
|
323
|
+
const page = context.pages()[0];
|
|
324
|
+
if (!authenticated) {
|
|
325
|
+
// Login redirect — user needs to sign in manually; wait for them
|
|
326
|
+
logger.info('Waiting for you to complete sign-in in the browser...');
|
|
327
|
+
if (page) {
|
|
328
|
+
await page.waitForFunction(owaAccessTokenPresent, { timeout: LOGIN_TIMEOUT_MS });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Session authenticated — show progress overlay while we save
|
|
332
|
+
if (page) {
|
|
333
|
+
await showOverlay(page, 'pending');
|
|
334
|
+
await page.waitForTimeout(800);
|
|
335
|
+
await showOverlay(page, 'saving');
|
|
336
|
+
}
|
|
337
|
+
const upn = await extractAndCacheTokens(context);
|
|
338
|
+
if (!upn) {
|
|
339
|
+
if (page)
|
|
340
|
+
await showOverlay(page, 'error');
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
if (page) {
|
|
344
|
+
await showOverlay(page, 'done');
|
|
345
|
+
await page.waitForTimeout(1200); // let user read "All done!"
|
|
346
|
+
}
|
|
347
|
+
logger.info(`Headed login succeeded (${upn}). Browser closing.`);
|
|
348
|
+
return { upn, method: 'headed-browser' };
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (browserClosed) {
|
|
352
|
+
logger.error('Login aborted — browser was closed before authentication completed.');
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
logger.error('Headed login failed', err instanceof Error ? err.message : String(err));
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
if (context && !browserClosed) {
|
|
361
|
+
await context.close().catch(() => { });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
366
|
+
// Main entry point
|
|
367
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
368
|
+
/**
|
|
369
|
+
* Login to Outlook Web.
|
|
370
|
+
*
|
|
371
|
+
* @param forceNew - When true, clear the saved session and force a full
|
|
372
|
+
* re-authentication (mirrors msteams-mcp's `forceNew` option).
|
|
373
|
+
*/
|
|
374
|
+
export async function browserLogin(forceNew = false) {
|
|
375
|
+
if (forceNew) {
|
|
376
|
+
clearSession();
|
|
377
|
+
logger.info('Forced re-login — cleared previous session and token cache.');
|
|
378
|
+
}
|
|
379
|
+
if (!forceNew && hasSavedBrowserProfile()) {
|
|
380
|
+
const result = await headlessLogin();
|
|
381
|
+
if (result)
|
|
382
|
+
return result;
|
|
383
|
+
logger.info('Headless login failed — falling back to visible browser...');
|
|
384
|
+
}
|
|
385
|
+
else if (!forceNew) {
|
|
386
|
+
logger.info('No saved browser profile — opening visible browser for first-time setup...');
|
|
387
|
+
}
|
|
388
|
+
// Pass clearCookiesFirst=true for force_new so the browser profile's cookies
|
|
389
|
+
// are wiped before importing fresh SSO cookies (mirrors msteams-mcp forceNewLogin).
|
|
390
|
+
return headedLogin(forceNew);
|
|
391
|
+
}
|
|
392
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
393
|
+
// Token refresh alias
|
|
394
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
395
|
+
export async function headlessTokenRefresh() {
|
|
396
|
+
return (await headlessLogin()) !== null;
|
|
397
|
+
}
|
|
398
|
+
//# sourceMappingURL=browser-login.js.map
|