diff --git a/src/lib/providers/validation/webProvidersB.ts b/src/lib/providers/validation/webProvidersB.ts index 3d9ddd84d4..6f1069f9ff 100644 --- a/src/lib/providers/validation/webProvidersB.ts +++ b/src/lib/providers/validation/webProvidersB.ts @@ -292,7 +292,7 @@ export async function validateCopilotWebProvider({ apiKey, providerSpecificData } } -function extractM365CredentialParts(raw: string, providerSpecificData: Record) { +export function extractM365CredentialParts(raw: string, providerSpecificData: Record) { const text = raw.trim(); const parts: Record = {}; @@ -304,13 +304,22 @@ function extractM365CredentialParts(raw: string, providerSpecificData: Record, + // so extraction is unchanged. (OmniRoute issue #7078) + if (/^wss:\/\//i.test(text)) { try { const url = new URL(text); - parts.access_token ||= url.searchParams.get("access_token") || ""; - parts.chathubPath ||= decodeURIComponent( - url.pathname.split("/m365Copilot/Chathub/")[1] || "" + const hostOk = /^(?:[\w-]+\.)*(?:m365\.cloud\.microsoft|copilot\.microsoft\.com|substrate\.office\.com)$/i.test( + url.hostname ); + if (hostOk && url.pathname.startsWith("/m365Copilot/Chathub/")) { + parts.access_token ||= url.searchParams.get("access_token") || ""; + parts.chathubPath ||= decodeURIComponent( + url.pathname.split("/m365Copilot/Chathub/")[1] || "" + ); + } } catch { // Fall through to the structured key/value parser result. } diff --git a/tests/unit/m365-web-token-extraction-7078.test.ts b/tests/unit/m365-web-token-extraction-7078.test.ts new file mode 100644 index 0000000000..68ed08b7fc --- /dev/null +++ b/tests/unit/m365-web-token-extraction-7078.test.ts @@ -0,0 +1,48 @@ +// Issue #7078 — Microsoft 365 Copilot web token extraction must accept the current +// m365.cloud.microsoft endpoint (and legacy substrate.office.com / copilot.microsoft.com), +// not just the old substrate.office.com WS host. Verifies access_token + chathubPath parse. +import test from "node:test"; +import assert from "node:assert/strict"; + +const B = await import("../../src/lib/providers/validation/webProvidersB.ts"); +const extract = (raw: string) => + (B as Record).extractM365CredentialParts(raw, {}); + +test("#7078 m365.cloud.microsoft wss URL extracts access_token + chathubPath", () => { + const raw = + "wss://m365.cloud.microsoft/m365Copilot/Chathub/user@tenant.example.com?access_token=TOKEN123"; + const parts = extract(raw); + assert.equal(parts.accessToken, "TOKEN123"); + assert.equal(parts.chathubPath, "user@tenant.example.com"); +}); + +test("#7078 regional subdomain m365.cloud.microsoft also accepted", () => { + const raw = + "wss://eu.m365.cloud.microsoft/m365Copilot/Chathub/user@tenant?access_token=TOKEN456"; + const parts = extract(raw); + assert.equal(parts.accessToken, "TOKEN456"); + assert.equal(parts.chathubPath, "user@tenant"); +}); + +test("#7078 legacy substrate.office.com still works (no regression)", () => { + const raw = + "wss://substrate.office.com/m365Copilot/Chathub/user@tenant?access_token=LEGACY"; + const parts = extract(raw); + assert.equal(parts.accessToken, "LEGACY"); + assert.equal(parts.chathubPath, "user@tenant"); +}); + +test("#7078 key/value string form still parsed", () => { + const raw = "access_token=KV;chathubPath=user@tenant"; + const parts = extract(raw); + assert.equal(parts.accessToken, "KV"); + assert.equal(parts.chathubPath, "user@tenant"); +}); + +test("#7078 m365.cloud.microsoft with explicit :443 port still extracts (hostname, not host)", () => { + const raw = + "wss://m365.cloud.microsoft:443/m365Copilot/Chathub/user@tenant?access_token=TOKENPORT"; + const parts = extract(raw); + assert.equal(parts.accessToken, "TOKENPORT"); + assert.equal(parts.chathubPath, "user@tenant"); +});