mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(providers): validate M365 Copilot web credentials (#5432)
Integrated into release/v3.8.43 (drift-shed: cherry-picked the real change onto the release tip; stale-base drift dropped).
This commit is contained in:
@@ -86,6 +86,38 @@ export function newChatSessionId(): string {
|
||||
return randomBytes(16).toString("hex");
|
||||
}
|
||||
|
||||
function parsePastedCredential(
|
||||
raw: string
|
||||
): Partial<Pick<M365ConnectionParams, "accessToken" | "chathubPath">> {
|
||||
const value = raw.trim();
|
||||
const parts: Record<string, string> = {};
|
||||
|
||||
for (const segment of value.split(/[;\n]/)) {
|
||||
const separator = segment.indexOf("=");
|
||||
if (separator <= 0) continue;
|
||||
const key = segment.slice(0, separator).trim();
|
||||
const partValue = segment.slice(separator + 1).trim();
|
||||
if (key && partValue) parts[key] = partValue;
|
||||
}
|
||||
|
||||
if (/^wss:\/\/substrate\.office\.com\/m365Copilot\/Chathub\//i.test(value)) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
parts.access_token ||= url.searchParams.get("access_token") || "";
|
||||
parts.chathubPath ||= decodeURIComponent(
|
||||
url.pathname.split("/m365Copilot/Chathub/")[1] || ""
|
||||
);
|
||||
} catch {
|
||||
// Keep any key/value fields already parsed from the pasted text.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: parts.access_token || parts.accessToken,
|
||||
chathubPath: parts.chathubPath || parts.userTenant,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the pasted credential bits. The individual access_token is opaque (JWE),
|
||||
* so it is consumed verbatim. The Chathub path (`user@tenant`) is pasted
|
||||
@@ -95,8 +127,14 @@ export function resolveConnectionParams(
|
||||
credentials: ProviderCredentials | undefined
|
||||
): M365ConnectionParams | { error: string } {
|
||||
const psd = (credentials?.providerSpecificData ?? {}) as JsonRecord;
|
||||
const parsedApiKey =
|
||||
typeof credentials?.apiKey === "string" ? parsePastedCredential(credentials.apiKey) : {};
|
||||
const accessToken =
|
||||
(typeof credentials?.apiKey === "string" && credentials.apiKey) ||
|
||||
parsedApiKey.accessToken ||
|
||||
(typeof credentials?.apiKey === "string" &&
|
||||
credentials.apiKey &&
|
||||
!credentials.apiKey.includes("access_token=") &&
|
||||
credentials.apiKey) ||
|
||||
(typeof psd.accessToken === "string" && psd.accessToken) ||
|
||||
(typeof psd.access_token === "string" && psd.access_token) ||
|
||||
"";
|
||||
@@ -104,6 +142,7 @@ export function resolveConnectionParams(
|
||||
return { error: "Missing M365 Copilot access_token. Paste it as the provider credential." };
|
||||
}
|
||||
const chathubPath =
|
||||
parsedApiKey.chathubPath ||
|
||||
(typeof psd.chathubPath === "string" && psd.chathubPath) ||
|
||||
(typeof psd.userTenant === "string" && psd.userTenant) ||
|
||||
"";
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
validateAdaptaWebProvider,
|
||||
validateClaudeWebProvider,
|
||||
validateGeminiWebProvider,
|
||||
validateCopilotM365WebProvider,
|
||||
validateCopilotWebProvider,
|
||||
validateT3WebProvider,
|
||||
validateJulesProvider,
|
||||
@@ -356,6 +357,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
"adapta-web": validateAdaptaWebProvider,
|
||||
"claude-web": validateClaudeWebProvider,
|
||||
"gemini-web": validateGeminiWebProvider,
|
||||
"copilot-m365-web": validateCopilotM365WebProvider,
|
||||
"copilot-web": validateCopilotWebProvider,
|
||||
"t3-web": validateT3WebProvider,
|
||||
"azure-openai": validateAzureOpenAIProvider,
|
||||
|
||||
@@ -291,6 +291,82 @@ export async function validateCopilotWebProvider({ apiKey, providerSpecificData
|
||||
}
|
||||
}
|
||||
|
||||
function extractM365CredentialParts(raw: string, providerSpecificData: Record<string, unknown>) {
|
||||
const text = raw.trim();
|
||||
const parts: Record<string, string> = {};
|
||||
|
||||
for (const segment of text.split(/[;\n]/)) {
|
||||
const index = segment.indexOf("=");
|
||||
if (index <= 0) continue;
|
||||
const key = segment.slice(0, index).trim();
|
||||
const value = segment.slice(index + 1).trim();
|
||||
if (key && value) parts[key] = value;
|
||||
}
|
||||
|
||||
if (/^wss:\/\/substrate\.office\.com\/m365Copilot\/Chathub\//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] || ""
|
||||
);
|
||||
} catch {
|
||||
// Fall through to the structured key/value parser result.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken:
|
||||
parts.access_token ||
|
||||
parts.accessToken ||
|
||||
(typeof providerSpecificData.access_token === "string"
|
||||
? providerSpecificData.access_token
|
||||
: "") ||
|
||||
(typeof providerSpecificData.accessToken === "string" ? providerSpecificData.accessToken : ""),
|
||||
chathubPath:
|
||||
parts.chathubPath ||
|
||||
parts.userTenant ||
|
||||
(typeof providerSpecificData.chathubPath === "string"
|
||||
? providerSpecificData.chathubPath
|
||||
: "") ||
|
||||
(typeof providerSpecificData.userTenant === "string" ? providerSpecificData.userTenant : ""),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Microsoft 365 Copilot Web token validator ──
|
||||
export async function validateCopilotM365WebProvider({
|
||||
apiKey,
|
||||
providerSpecificData = {},
|
||||
}: any) {
|
||||
const { accessToken, chathubPath } = extractM365CredentialParts(
|
||||
String(apiKey || ""),
|
||||
providerSpecificData
|
||||
);
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Could not extract access_token from the Microsoft 365 Copilot credential",
|
||||
};
|
||||
}
|
||||
|
||||
if (!chathubPath || !chathubPath.includes("@")) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Could not extract the account-specific Chathub path from the credential",
|
||||
};
|
||||
}
|
||||
|
||||
// The live provider uses a SignalR WebSocket. The generic web-cookie /models
|
||||
// probe builds an invalid wss://.../models URL, so validation here confirms
|
||||
// the captured credential shape and lets the executor perform the live check.
|
||||
return {
|
||||
valid: true,
|
||||
error: null,
|
||||
warning: "Credential format accepted. The session is verified when the provider sends a chat.",
|
||||
};
|
||||
}
|
||||
|
||||
// ── t3.chat Web cookie validator ──
|
||||
export async function validateT3WebProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
try {
|
||||
|
||||
@@ -55,6 +55,27 @@ test("resolveConnectionParams accepts access_token in providerSpecificData and a
|
||||
assert.equal(p.host, "substrate.svc.cloud.microsoft");
|
||||
});
|
||||
|
||||
test("resolveConnectionParams parses the pasted OmniRoute credential line", () => {
|
||||
const r = resolveConnectionParams({
|
||||
apiKey: "access_token=tok3; chathubPath=redacted-account@redacted-tenant",
|
||||
});
|
||||
assert.ok(!("error" in r));
|
||||
const p = r as { chathubPath: string; accessToken: string };
|
||||
assert.equal(p.accessToken, "tok3");
|
||||
assert.equal(p.chathubPath, "redacted-account@redacted-tenant");
|
||||
});
|
||||
|
||||
test("resolveConnectionParams parses a pasted Chathub WebSocket URL", () => {
|
||||
const r = resolveConnectionParams({
|
||||
apiKey:
|
||||
"wss://substrate.office.com/m365Copilot/Chathub/redacted-account%40redacted-tenant?access_token=tok4&source=officeweb",
|
||||
});
|
||||
assert.ok(!("error" in r));
|
||||
const p = r as { chathubPath: string; accessToken: string };
|
||||
assert.equal(p.accessToken, "tok4");
|
||||
assert.equal(p.chathubPath, "redacted-account@redacted-tenant");
|
||||
});
|
||||
|
||||
// ── WS URL building ──────────────────────────────────────────────────────
|
||||
|
||||
test("buildWsUrl targets the substrate Chathub with the individual-tier query", () => {
|
||||
|
||||
@@ -2443,6 +2443,37 @@ test("copilot-web validator: empty input → paste prompt", async () => {
|
||||
assert.match(result.error || "", /Paste your access_token/i);
|
||||
});
|
||||
|
||||
// ─── copilot-m365-web validator ──────────────────────────────────────────────
|
||||
|
||||
test("copilot-m365-web validator: accepts pasted OmniRoute credential without /models probe", async () => {
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("should not fetch");
|
||||
};
|
||||
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "copilot-m365-web",
|
||||
apiKey: "access_token=tok; chathubPath=redacted-account@redacted-tenant",
|
||||
});
|
||||
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(result.error, null);
|
||||
assert.match(result.warning || "", /verified when the provider sends a chat/i);
|
||||
});
|
||||
|
||||
test("copilot-m365-web validator: requires chathubPath", async () => {
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("should not fetch");
|
||||
};
|
||||
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "copilot-m365-web",
|
||||
apiKey: "access_token=tok",
|
||||
});
|
||||
|
||||
assert.equal(result.valid, false);
|
||||
assert.match(result.error || "", /Chathub path/i);
|
||||
});
|
||||
|
||||
// ─── t3-web validator ────────────────────────────────────────────────────────
|
||||
|
||||
test("t3-web validator: valid cookies → valid", async () => {
|
||||
|
||||
Reference in New Issue
Block a user