m365connector 0.3.7 → 0.3.9
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 +2 -0
- package/package.json +1 -1
- package/src/browser-session.js +208 -10
- package/src/lib/content-reader.js +110 -4
- package/src/token-providers.js +21 -0
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ Then add to your MCP client config:
|
|
|
56
56
|
| `M365C_EDGE_PATH` | auto-detected | Microsoft Edge executable path |
|
|
57
57
|
| `M365C_EDGE_USER_DATA_DIR` | phantomwright `ud-m365` profile when present, else `~/.m365connector/edge-profile` | Persistent Edge user data directory |
|
|
58
58
|
| `M365C_EDGE_PROFILE_DIRECTORY` | `Default` | Edge profile directory inside the user data dir |
|
|
59
|
+
| `M365C_ALLOW_BROWSER_FOCUS` | unset | Set to `1` to allow MCP-driven Edge tabs to come to the foreground for any active-tab request. By default Edge stays in the background unless Microsoft 365 requires user login, MFA, or consent. |
|
|
60
|
+
| `M365C_NEVER_FOCUS_BROWSER` | unset | Set to `1` to prevent MCP-driven Edge tabs from ever coming to the foreground. When user interaction is required, the operation will wait/fail instead of stealing focus. |
|
|
59
61
|
| `M365C_TOKEN_CACHE_PATH` | `~/.m365connector/token-cache.json` | Local token cache path; stores only current unexpired captured tokens |
|
|
60
62
|
|
|
61
63
|
## Tools
|
package/package.json
CHANGED
package/src/browser-session.js
CHANGED
|
@@ -3,11 +3,14 @@ import fs from "node:fs";
|
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
6
|
+
import { execFile, spawn } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
7
8
|
import { WebSocket } from "ws";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_EDGE_PORT = 52367;
|
|
10
11
|
const DEFAULT_NAVIGATION_TIMEOUT_MS = 30000;
|
|
12
|
+
const USER_INTERACTION_FOCUS_REASON = "user-interaction";
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
11
14
|
|
|
12
15
|
function sleep(ms) {
|
|
13
16
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -70,11 +73,96 @@ function requestText(url, options = {}) {
|
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function
|
|
76
|
+
async function createBackgroundTarget(port, url) {
|
|
77
|
+
const version = await requestJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs: 2000 });
|
|
78
|
+
const wsUrl = version?.webSocketDebuggerUrl;
|
|
79
|
+
if (!wsUrl) {
|
|
80
|
+
throw new Error("Browser CDP websocket URL unavailable");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ws = new WebSocket(wsUrl);
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
ws.once("open", resolve);
|
|
86
|
+
ws.once("error", reject);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = await new Promise((resolve, reject) => {
|
|
91
|
+
const timeout = setTimeout(() => {
|
|
92
|
+
ws.off("message", onMessage);
|
|
93
|
+
reject(new Error("CDP timeout: Target.createTarget"));
|
|
94
|
+
}, 5000);
|
|
95
|
+
const onMessage = (raw) => {
|
|
96
|
+
try {
|
|
97
|
+
const message = JSON.parse(String(raw));
|
|
98
|
+
if (message.id !== 1) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
ws.off("message", onMessage);
|
|
103
|
+
if (message.error) {
|
|
104
|
+
reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
105
|
+
} else {
|
|
106
|
+
resolve(message.result || {});
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
ws.off("message", onMessage);
|
|
111
|
+
reject(err);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
ws.on("message", onMessage);
|
|
115
|
+
ws.send(JSON.stringify({
|
|
116
|
+
id: 1,
|
|
117
|
+
method: "Target.createTarget",
|
|
118
|
+
params: {
|
|
119
|
+
url,
|
|
120
|
+
background: true
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const targetId = result?.targetId;
|
|
126
|
+
if (!targetId) {
|
|
127
|
+
throw new Error("Target.createTarget did not return targetId");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const deadline = Date.now() + 5000;
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
const targets = await requestJson(`http://127.0.0.1:${port}/json/list`, { timeoutMs: 2000 });
|
|
133
|
+
const target = targets.find((row) => row.id === targetId);
|
|
134
|
+
if (target?.webSocketDebuggerUrl) {
|
|
135
|
+
return target;
|
|
136
|
+
}
|
|
137
|
+
await sleep(100);
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`Target ${targetId} did not appear in /json/list`);
|
|
140
|
+
} finally {
|
|
141
|
+
try {
|
|
142
|
+
ws.close();
|
|
143
|
+
} catch {
|
|
144
|
+
// ignored
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function findEdgeExecutable() {
|
|
74
150
|
if (process.env.M365C_EDGE_PATH) {
|
|
75
151
|
return expandHome(process.env.M365C_EDGE_PATH);
|
|
76
152
|
}
|
|
77
153
|
|
|
154
|
+
if (process.platform === "win32") {
|
|
155
|
+
const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
|
|
156
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
157
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
158
|
+
const winCandidates = [
|
|
159
|
+
path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
160
|
+
path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
161
|
+
path.join(localAppData, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
162
|
+
];
|
|
163
|
+
return winCandidates.find((candidate) => fs.existsSync(candidate)) || "msedge.exe";
|
|
164
|
+
}
|
|
165
|
+
|
|
78
166
|
const candidates = [
|
|
79
167
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
80
168
|
path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
|
|
@@ -110,6 +198,41 @@ function normalizeHeaderMap(headers = {}) {
|
|
|
110
198
|
return out;
|
|
111
199
|
}
|
|
112
200
|
|
|
201
|
+
async function getFrontmostApplicationName() {
|
|
202
|
+
if (process.platform !== "darwin") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const { stdout } = await execFileAsync("osascript", [
|
|
207
|
+
"-e",
|
|
208
|
+
'tell application "System Events" to get name of first application process whose frontmost is true'
|
|
209
|
+
]);
|
|
210
|
+
const name = stdout.trim();
|
|
211
|
+
return name || null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function restoreFrontmostApplication(name, logger = console) {
|
|
218
|
+
if (process.platform !== "darwin" || !name || name === "Microsoft Edge") {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const escapedName = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
222
|
+
const script = [
|
|
223
|
+
'tell application "System Events"',
|
|
224
|
+
` if exists process "${escapedName}" then`,
|
|
225
|
+
` set frontmost of process "${escapedName}" to true`,
|
|
226
|
+
" end if",
|
|
227
|
+
"end tell"
|
|
228
|
+
].join("\n");
|
|
229
|
+
try {
|
|
230
|
+
await execFileAsync("osascript", ["-e", script], { timeout: 3000 });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.warn?.("[m365connector][browser] failed to restore frontmost application:", err.message);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
113
236
|
export class CdpPage extends EventEmitter {
|
|
114
237
|
constructor(session, target) {
|
|
115
238
|
super();
|
|
@@ -278,12 +401,13 @@ export class CdpPage extends EventEmitter {
|
|
|
278
401
|
}
|
|
279
402
|
|
|
280
403
|
async bringToFront(options = {}) {
|
|
281
|
-
if (!this.session.
|
|
404
|
+
if (!this.session.shouldBringBrowserToFront(options)) {
|
|
282
405
|
this.session.activePageId = this.id;
|
|
283
|
-
return;
|
|
406
|
+
return false;
|
|
284
407
|
}
|
|
285
408
|
await this.send("Page.bringToFront").catch(() => {});
|
|
286
409
|
this.session.activePageId = this.id;
|
|
410
|
+
return true;
|
|
287
411
|
}
|
|
288
412
|
|
|
289
413
|
async clickAt(x, y) {
|
|
@@ -377,11 +501,40 @@ export class BrowserSession extends EventEmitter {
|
|
|
377
501
|
this.edgePath = findEdgeExecutable();
|
|
378
502
|
this.logger = options.logger || console;
|
|
379
503
|
this.allowBrowserFocus = options.allowBrowserFocus ?? process.env.M365C_ALLOW_BROWSER_FOCUS === "1";
|
|
504
|
+
this.neverFocusBrowser = options.neverFocusBrowser ?? process.env.M365C_NEVER_FOCUS_BROWSER === "1";
|
|
380
505
|
this.process = null;
|
|
381
506
|
this.pages = new Map();
|
|
382
507
|
this.activePageId = null;
|
|
383
508
|
}
|
|
384
509
|
|
|
510
|
+
shouldBringBrowserToFront(options = {}) {
|
|
511
|
+
if (this.neverFocusBrowser) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
if (this.allowBrowserFocus) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
return options.force === true && options.reason === USER_INTERACTION_FOCUS_REASON;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
shouldPreserveFrontmostApp() {
|
|
521
|
+
return process.platform === "darwin" && !this.allowBrowserFocus;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async captureFrontmostApp() {
|
|
525
|
+
if (!this.shouldPreserveFrontmostApp()) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
return getFrontmostApplicationName();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async restoreFrontmostApp(name) {
|
|
532
|
+
if (!this.shouldPreserveFrontmostApp()) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
await restoreFrontmostApplication(name, this.logger);
|
|
536
|
+
}
|
|
537
|
+
|
|
385
538
|
async start() {
|
|
386
539
|
if (await this.#isDebugEndpointReady()) {
|
|
387
540
|
return;
|
|
@@ -453,10 +606,23 @@ export class BrowserSession extends EventEmitter {
|
|
|
453
606
|
|
|
454
607
|
async createPage(url = "about:blank", options = {}) {
|
|
455
608
|
await this.start();
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
609
|
+
const previousFrontmostApp = await this.captureFrontmostApp();
|
|
610
|
+
let target = null;
|
|
611
|
+
if (!this.shouldBringBrowserToFront(options)) {
|
|
612
|
+
target = await createBackgroundTarget(this.port, url).catch((err) => {
|
|
613
|
+
this.logger.warn("[m365connector][browser] background target creation failed:", err.message);
|
|
614
|
+
return null;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (!target) {
|
|
618
|
+
target = await requestJson(
|
|
619
|
+
`http://127.0.0.1:${this.port}/json/new?${encodeURIComponent(url)}`,
|
|
620
|
+
{ method: "PUT", timeoutMs: 3000 }
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (!this.shouldBringBrowserToFront(options)) {
|
|
624
|
+
await this.restoreFrontmostApp(previousFrontmostApp);
|
|
625
|
+
}
|
|
460
626
|
const page = new CdpPage(this, target);
|
|
461
627
|
this.pages.set(page.id, page);
|
|
462
628
|
await page.connect();
|
|
@@ -467,11 +633,33 @@ export class BrowserSession extends EventEmitter {
|
|
|
467
633
|
this.emit("bearer", event);
|
|
468
634
|
});
|
|
469
635
|
if (options.active) {
|
|
470
|
-
await page.bringToFront(
|
|
636
|
+
await page.bringToFront();
|
|
637
|
+
}
|
|
638
|
+
if (!this.shouldBringBrowserToFront(options)) {
|
|
639
|
+
await this.restoreFrontmostApp(previousFrontmostApp);
|
|
471
640
|
}
|
|
472
641
|
return page;
|
|
473
642
|
}
|
|
474
643
|
|
|
644
|
+
async requestUserInteraction(page, reason = "Microsoft 365 requires user interaction") {
|
|
645
|
+
if (!page) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
const focused = await page.bringToFront({
|
|
649
|
+
force: true,
|
|
650
|
+
reason: USER_INTERACTION_FOCUS_REASON
|
|
651
|
+
});
|
|
652
|
+
if (focused) {
|
|
653
|
+
this.logger.warn("[m365connector][browser] brought Edge to foreground:", reason);
|
|
654
|
+
} else if (this.neverFocusBrowser) {
|
|
655
|
+
this.logger.warn(
|
|
656
|
+
"[m365connector][browser] user interaction required but browser focus is disabled:",
|
|
657
|
+
reason
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
return focused;
|
|
661
|
+
}
|
|
662
|
+
|
|
475
663
|
async getPage(id) {
|
|
476
664
|
if (this.pages.has(id)) {
|
|
477
665
|
return this.pages.get(id);
|
|
@@ -559,11 +747,15 @@ export function installChromeShim(browserSession) {
|
|
|
559
747
|
if (!page) {
|
|
560
748
|
throw new Error(`Tab not found: ${tabId}`);
|
|
561
749
|
}
|
|
750
|
+
const previousFrontmostApp = await browserSession.captureFrontmostApp();
|
|
562
751
|
if (updateProperties.url) {
|
|
563
752
|
await page.navigate(updateProperties.url);
|
|
564
753
|
}
|
|
565
754
|
if (updateProperties.active) {
|
|
566
|
-
await page.bringToFront(
|
|
755
|
+
await page.bringToFront();
|
|
756
|
+
}
|
|
757
|
+
if (!updateProperties.active || !browserSession.shouldBringBrowserToFront()) {
|
|
758
|
+
await browserSession.restoreFrontmostApp(previousFrontmostApp);
|
|
567
759
|
}
|
|
568
760
|
return { id: page.id, url: page.url, status: page.loaded ? "complete" : "loading" };
|
|
569
761
|
},
|
|
@@ -614,6 +806,12 @@ export function installChromeShim(browserSession) {
|
|
|
614
806
|
}
|
|
615
807
|
return results;
|
|
616
808
|
}
|
|
809
|
+
},
|
|
810
|
+
m365connector: {
|
|
811
|
+
async requestUserInteraction(tabId, reason) {
|
|
812
|
+
const page = await browserSession.getPage(tabId);
|
|
813
|
+
return browserSession.requestUserInteraction(page, reason);
|
|
814
|
+
}
|
|
617
815
|
}
|
|
618
816
|
};
|
|
619
817
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
1
6
|
let jsZipRef = globalThis.JSZip || null;
|
|
2
7
|
let jsZipLoadPromise = null;
|
|
3
8
|
let jsZipLoadFailed = false;
|
|
@@ -40,6 +45,45 @@ function sleep(ms) {
|
|
|
40
45
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
function shouldPreserveFrontmostApp() {
|
|
49
|
+
return process.platform === "darwin" && process.env.M365C_ALLOW_BROWSER_FOCUS !== "1";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function getFrontmostApplicationName() {
|
|
53
|
+
if (!shouldPreserveFrontmostApp()) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await execFileAsync("osascript", [
|
|
58
|
+
"-e",
|
|
59
|
+
'tell application "System Events" to get name of first application process whose frontmost is true'
|
|
60
|
+
]);
|
|
61
|
+
const name = stdout.trim();
|
|
62
|
+
return name || null;
|
|
63
|
+
} catch (_err) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function restoreFrontmostApplication(name, logger = console) {
|
|
69
|
+
if (!shouldPreserveFrontmostApp() || !name || name === "Microsoft Edge") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const escapedName = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
73
|
+
const script = [
|
|
74
|
+
'tell application "System Events"',
|
|
75
|
+
` if exists process "${escapedName}" then`,
|
|
76
|
+
` set frontmost of process "${escapedName}" to true`,
|
|
77
|
+
" end if",
|
|
78
|
+
"end tell"
|
|
79
|
+
].join("\n");
|
|
80
|
+
try {
|
|
81
|
+
await execFileAsync("osascript", ["-e", script], { timeout: 3000 });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.warn?.("[m365connector] failed to restore frontmost application:", err.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
43
87
|
function pickContext(params) {
|
|
44
88
|
if (!params) {
|
|
45
89
|
return null;
|
|
@@ -1214,6 +1258,7 @@ export class ContentReader {
|
|
|
1214
1258
|
|
|
1215
1259
|
async waitForEditorInAnyFrame(tabId, deadline) {
|
|
1216
1260
|
const pollEnd = Math.min(Date.now() + 60000, deadline);
|
|
1261
|
+
let interactionRequested = false;
|
|
1217
1262
|
|
|
1218
1263
|
while (Date.now() < pollEnd) {
|
|
1219
1264
|
try {
|
|
@@ -1221,6 +1266,23 @@ export class ContentReader {
|
|
|
1221
1266
|
target: { tabId, allFrames: true },
|
|
1222
1267
|
func: () => {
|
|
1223
1268
|
const host = location.hostname.toLowerCase();
|
|
1269
|
+
const text = document.body?.innerText?.toLowerCase() || "";
|
|
1270
|
+
const path = location.pathname.toLowerCase();
|
|
1271
|
+
if (
|
|
1272
|
+
host === "login.microsoftonline.com"
|
|
1273
|
+
|| host === "login.live.com"
|
|
1274
|
+
|| path.includes("/common/oauth2")
|
|
1275
|
+
|| text.includes("sign in to your account")
|
|
1276
|
+
|| text.includes("pick an account")
|
|
1277
|
+
|| text.includes("enter password")
|
|
1278
|
+
|| text.includes("stay signed in?")
|
|
1279
|
+
|| text.includes("verify your identity")
|
|
1280
|
+
|| text.includes("approve sign in request")
|
|
1281
|
+
|| text.includes("permission requested")
|
|
1282
|
+
|| text.includes("permissions requested")
|
|
1283
|
+
) {
|
|
1284
|
+
return "interaction";
|
|
1285
|
+
}
|
|
1224
1286
|
if (!host.endsWith(".officeapps.live.com")) {
|
|
1225
1287
|
return null;
|
|
1226
1288
|
}
|
|
@@ -1239,6 +1301,10 @@ export class ContentReader {
|
|
|
1239
1301
|
if (results && results.some((r) => r.result === "ready")) {
|
|
1240
1302
|
return true;
|
|
1241
1303
|
}
|
|
1304
|
+
if (!interactionRequested && results && results.some((r) => r.result === "interaction")) {
|
|
1305
|
+
interactionRequested = true;
|
|
1306
|
+
await this.requestUserInteraction(tabId, "Office Online requires Microsoft 365 sign-in or consent");
|
|
1307
|
+
}
|
|
1242
1308
|
} catch (_err) {
|
|
1243
1309
|
// Frames may not be ready yet; keep polling.
|
|
1244
1310
|
}
|
|
@@ -1248,6 +1314,18 @@ export class ContentReader {
|
|
|
1248
1314
|
return false;
|
|
1249
1315
|
}
|
|
1250
1316
|
|
|
1317
|
+
async requestUserInteraction(tabId, reason) {
|
|
1318
|
+
try {
|
|
1319
|
+
if (chrome.m365connector?.requestUserInteraction) {
|
|
1320
|
+
return await chrome.m365connector.requestUserInteraction(tabId, reason);
|
|
1321
|
+
}
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
this.logger.warn("[m365connector] failed to request browser user interaction:", err?.message || err);
|
|
1324
|
+
}
|
|
1325
|
+
this.logger.warn("[m365connector] browser user interaction required:", reason);
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1251
1329
|
/**
|
|
1252
1330
|
* Scroll through the Word Online document inside the iframe so that all
|
|
1253
1331
|
* virtually-rendered pages get materialised in the DOM. Returns the number
|
|
@@ -1622,14 +1700,15 @@ export class ContentReader {
|
|
|
1622
1700
|
const deadline = Date.now() + DEEP_LINK_BUDGET_MS;
|
|
1623
1701
|
const editUrl = this.buildOfficeEditUrl(webUrl);
|
|
1624
1702
|
let tabId = null;
|
|
1703
|
+
const previousFrontmostApp = await getFrontmostApplicationName();
|
|
1625
1704
|
|
|
1626
1705
|
this.#deepLinkActive = true;
|
|
1627
1706
|
try {
|
|
1628
|
-
const tab = await chrome.tabs.create({ url: editUrl, active:
|
|
1707
|
+
const tab = await chrome.tabs.create({ url: editUrl, active: false });
|
|
1629
1708
|
tabId = tab.id;
|
|
1630
1709
|
|
|
1631
1710
|
await this.waitForTabLoad(tabId, Math.min(15000, deadline - Date.now()));
|
|
1632
|
-
await chrome.tabs.update(tabId, { active:
|
|
1711
|
+
await chrome.tabs.update(tabId, { active: false }).catch(() => null);
|
|
1633
1712
|
|
|
1634
1713
|
// SharePoint auto-submits the WOPI form into a cross-origin iframe where
|
|
1635
1714
|
// Word/PowerPoint Online renders. Wait for the editor to become ready in
|
|
@@ -1674,6 +1753,7 @@ export class ContentReader {
|
|
|
1674
1753
|
// Ignore if tab already closed.
|
|
1675
1754
|
}
|
|
1676
1755
|
}
|
|
1756
|
+
await restoreFrontmostApplication(previousFrontmostApp, this.logger);
|
|
1677
1757
|
}
|
|
1678
1758
|
}
|
|
1679
1759
|
|
|
@@ -1686,6 +1766,7 @@ export class ContentReader {
|
|
|
1686
1766
|
let tabId = null;
|
|
1687
1767
|
let previousActiveTabId = null;
|
|
1688
1768
|
let activatedForDecryption = false;
|
|
1769
|
+
const previousFrontmostApp = await getFrontmostApplicationName();
|
|
1689
1770
|
try {
|
|
1690
1771
|
previousActiveTabId = await this.getActiveTabId();
|
|
1691
1772
|
const tab = await chrome.tabs.create({ url: deepLink, active: false });
|
|
@@ -1707,6 +1788,7 @@ export class ContentReader {
|
|
|
1707
1788
|
let sawEncryptedWrapper = false;
|
|
1708
1789
|
let stableContent = null;
|
|
1709
1790
|
let followedReadMessageUrl = false;
|
|
1791
|
+
let interactionRequested = false;
|
|
1710
1792
|
const startedAt = Date.now();
|
|
1711
1793
|
const pollDeadline = () => sawEncryptedWrapper ? IRM_POLL_MS : BASE_POLL_MS;
|
|
1712
1794
|
|
|
@@ -1734,6 +1816,21 @@ export class ContentReader {
|
|
|
1734
1816
|
seen.add(bodyText);
|
|
1735
1817
|
candidates.push(bodyText);
|
|
1736
1818
|
}
|
|
1819
|
+
const lowerBodyText = (bodyText || "").toLowerCase();
|
|
1820
|
+
const host = location.hostname.toLowerCase();
|
|
1821
|
+
const userInteractionRequired = Boolean(
|
|
1822
|
+
host === "login.microsoftonline.com"
|
|
1823
|
+
|| host === "login.live.com"
|
|
1824
|
+
|| location.pathname.toLowerCase().includes("/common/oauth2")
|
|
1825
|
+
|| lowerBodyText.includes("sign in to your account")
|
|
1826
|
+
|| lowerBodyText.includes("pick an account")
|
|
1827
|
+
|| lowerBodyText.includes("enter password")
|
|
1828
|
+
|| lowerBodyText.includes("stay signed in?")
|
|
1829
|
+
|| lowerBodyText.includes("verify your identity")
|
|
1830
|
+
|| lowerBodyText.includes("approve sign in request")
|
|
1831
|
+
|| lowerBodyText.includes("permission requested")
|
|
1832
|
+
|| lowerBodyText.includes("permissions requested")
|
|
1833
|
+
);
|
|
1737
1834
|
for (const link of document.querySelectorAll("a[href]")) {
|
|
1738
1835
|
const label = `${link.innerText || ""} ${link.getAttribute("aria-label") || ""}`.toLowerCase();
|
|
1739
1836
|
const href = String(link.href || "");
|
|
@@ -1741,11 +1838,19 @@ export class ContentReader {
|
|
|
1741
1838
|
readMessageUrls.push(href);
|
|
1742
1839
|
}
|
|
1743
1840
|
}
|
|
1744
|
-
return { candidates, readMessageUrls };
|
|
1841
|
+
return { candidates, readMessageUrls, userInteractionRequired };
|
|
1745
1842
|
},
|
|
1746
1843
|
args: [EMAIL_SELECTORS]
|
|
1747
1844
|
});
|
|
1748
1845
|
|
|
1846
|
+
if (
|
|
1847
|
+
!interactionRequested
|
|
1848
|
+
&& executions?.some((entry) => entry?.result?.userInteractionRequired)
|
|
1849
|
+
) {
|
|
1850
|
+
interactionRequested = true;
|
|
1851
|
+
await this.requestUserInteraction(tabId, "Outlook requires Microsoft 365 sign-in or consent");
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1749
1854
|
const extraction = this.selectEmailExtraction(executions);
|
|
1750
1855
|
if (extraction.encrypted && extraction.readMessageUrl && !followedReadMessageUrl && tabId !== null) {
|
|
1751
1856
|
followedReadMessageUrl = true;
|
|
@@ -1833,6 +1938,7 @@ export class ContentReader {
|
|
|
1833
1938
|
// Best-effort; the original tab may have been closed.
|
|
1834
1939
|
}
|
|
1835
1940
|
}
|
|
1941
|
+
await restoreFrontmostApplication(previousFrontmostApp, this.logger);
|
|
1836
1942
|
}
|
|
1837
1943
|
}
|
|
1838
1944
|
|
|
@@ -1917,7 +2023,7 @@ export class ContentReader {
|
|
|
1917
2023
|
|
|
1918
2024
|
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
1919
2025
|
timeoutId = setTimeout(finish, timeoutMs);
|
|
1920
|
-
Promise.resolve(chrome.tabs.update(tabId, { url, active:
|
|
2026
|
+
Promise.resolve(chrome.tabs.update(tabId, { url, active: false })).catch(finish);
|
|
1921
2027
|
});
|
|
1922
2028
|
}
|
|
1923
2029
|
|
package/src/token-providers.js
CHANGED
|
@@ -578,6 +578,7 @@ export class GraphTokenProvider {
|
|
|
578
578
|
let lastInfo = null;
|
|
579
579
|
let signInClicked = false;
|
|
580
580
|
let signInDetectCount = 0;
|
|
581
|
+
let interactionRequested = false;
|
|
581
582
|
while (Date.now() < deadline) {
|
|
582
583
|
if (headerToken && this.#acceptToken(headerToken)) {
|
|
583
584
|
return headerToken;
|
|
@@ -625,6 +626,13 @@ export class GraphTokenProvider {
|
|
|
625
626
|
"[m365connector][graph] clicked Graph Explorer sign-in:",
|
|
626
627
|
JSON.stringify(clickResult?.result || {})
|
|
627
628
|
);
|
|
629
|
+
if (!clickResult?.result?.clicked && !interactionRequested) {
|
|
630
|
+
interactionRequested = true;
|
|
631
|
+
await this.browserSession.requestUserInteraction?.(
|
|
632
|
+
this.page,
|
|
633
|
+
"Graph Explorer sign-in is required"
|
|
634
|
+
);
|
|
635
|
+
}
|
|
628
636
|
await this.#completeLoginPopup(deadline);
|
|
629
637
|
await sleep(3000);
|
|
630
638
|
continue;
|
|
@@ -647,6 +655,8 @@ export class GraphTokenProvider {
|
|
|
647
655
|
|
|
648
656
|
async #completeLoginPopup(deadline) {
|
|
649
657
|
let accountClicked = false;
|
|
658
|
+
let interactionRequested = false;
|
|
659
|
+
let noAutoResolveCount = 0;
|
|
650
660
|
while (Date.now() < deadline) {
|
|
651
661
|
const loginPage = await this.browserSession.findPage((target) => {
|
|
652
662
|
try {
|
|
@@ -669,6 +679,7 @@ export class GraphTokenProvider {
|
|
|
669
679
|
const info = result?.result || {};
|
|
670
680
|
if (info.clicked) {
|
|
671
681
|
accountClicked = true;
|
|
682
|
+
noAutoResolveCount = 0;
|
|
672
683
|
const rect = info.rect;
|
|
673
684
|
if (rect && rect.w > 0 && rect.h > 0) {
|
|
674
685
|
await loginPage.clickAt(rect.x + rect.w / 2, rect.y + rect.h / 2).catch(() => {});
|
|
@@ -676,6 +687,16 @@ export class GraphTokenProvider {
|
|
|
676
687
|
this.logger.warn("[m365connector][graph] clicked Microsoft login account:", JSON.stringify(info));
|
|
677
688
|
await sleep(3000);
|
|
678
689
|
} else {
|
|
690
|
+
noAutoResolveCount += 1;
|
|
691
|
+
if (!interactionRequested && noAutoResolveCount >= 2) {
|
|
692
|
+
interactionRequested = true;
|
|
693
|
+
await this.browserSession.requestUserInteraction?.(
|
|
694
|
+
loginPage,
|
|
695
|
+
accountClicked
|
|
696
|
+
? "Microsoft login requires additional verification"
|
|
697
|
+
: "Microsoft login requires user interaction"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
679
700
|
await sleep(1000);
|
|
680
701
|
}
|
|
681
702
|
|