From e7b342c4ca77f5cb3605bdb3227bdeb332a92e22 Mon Sep 17 00:00:00 2001 From: Dextheking1 Date: Fri, 23 Oct 2026 03:49:02 +0000 Subject: [PATCH 1/3] fix(core): validate credential server before sending the key serverUrl returned credential.metadata.server verbatim and modelsFor sends the API key to it as a bearer token. Parse the value first: https only, http on loopback, anything else falls back to the default server. --- packages/core/src/plugin/provider/mars.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/plugin/provider/mars.ts b/packages/core/src/plugin/provider/mars.ts index 4d3a304c2ca3..4db06562831a 100644 --- a/packages/core/src/plugin/provider/mars.ts +++ b/packages/core/src/plugin/provider/mars.ts @@ -27,7 +27,16 @@ const ModelsResponse = Schema.Struct({ }) function serverUrl(value: Credential.Key) { - return typeof value.metadata?.server === "string" ? value.metadata.server : defaultServer + const raw = value.metadata?.server + if (typeof raw !== "string") return defaultServer + try { + const url = new URL(raw) + const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" + if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return defaultServer + return `${url.origin}${url.pathname.replace(/\/+$/, "").replace(/\/v1$/i, "")}` + } catch { + return defaultServer + } } function modelsFor(http: HttpClient.HttpClient, value: Credential.Key) { From a862ede6a68e56a6a70105fdf7e791a332b5b3e1 Mon Sep 17 00:00:00 2001 From: Dextheking1 Date: Fri, 23 Oct 2026 04:04:16 +0000 Subject: [PATCH 2/3] fix(core): validate MARSAI_URL with the same server policy defaultServer took MARSAI_URL as-is, so a remote http:// value became the fallback and the key went to it in plaintext whenever the credential had no server of its own. Run it through the same normalizeServer check as credential servers; on failure use the hardcoded https default. --- packages/core/src/plugin/provider/mars.ts | 26 +++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/core/src/plugin/provider/mars.ts b/packages/core/src/plugin/provider/mars.ts index 4db06562831a..3886bd9b0090 100644 --- a/packages/core/src/plugin/provider/mars.ts +++ b/packages/core/src/plugin/provider/mars.ts @@ -9,7 +9,22 @@ import { Credential } from "../../credential.js" import { Integration } from "../../integration.js" import { Provider } from "../../provider.js" -const defaultServer = (process.env.MARSAI_URL ?? "https://marsai.click").replace(/\/+$/, "").replace(/\/v1$/i, "") +const fallbackServer = "https://marsai.click" + +function normalizeServer(raw: string) { + try { + const url = new URL(raw) + const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" + if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return undefined + return `${url.origin}${url.pathname.replace(/\/+$/, "").replace(/\/v1$/i, "")}` + } catch { + return undefined + } +} + +// MARSAI_URL gets the same check as a credential server. Without it the +// fallback itself could point the key at a remote host over plaintext http. +const defaultServer = (process.env.MARSAI_URL ? normalizeServer(process.env.MARSAI_URL) : undefined) ?? fallbackServer const integrationID = Integration.ID.make("marsai") const providerID = Provider.ID.make("marsai") const ModelsResponse = Schema.Struct({ @@ -29,14 +44,7 @@ const ModelsResponse = Schema.Struct({ function serverUrl(value: Credential.Key) { const raw = value.metadata?.server if (typeof raw !== "string") return defaultServer - try { - const url = new URL(raw) - const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" - if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) return defaultServer - return `${url.origin}${url.pathname.replace(/\/+$/, "").replace(/\/v1$/i, "")}` - } catch { - return defaultServer - } + return normalizeServer(raw) ?? defaultServer } function modelsFor(http: HttpClient.HttpClient, value: Credential.Key) { From fc877db6d4a91c3ca2f8ff0abcf2635da1d9fe08 Mon Sep 17 00:00:00 2001 From: Dextheking1 Date: Fri, 23 Oct 2026 04:04:16 +0000 Subject: [PATCH 3/3] fix(core): drop server when importing legacy key credentials The import copied legacy auth.json metadata into the stored key credential verbatim. No first-party writer puts a server on a key credential (the opencode console flow writes it on OAuth credentials of its own integration), so the field only ever arrives as file-controlled text. Drop it during the import; imported keys use the configured default server. The serverUrl validation stays as a second layer. --- .../20260805200742_import_legacy_credentials.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/core/src/database/migration/20260805200742_import_legacy_credentials.ts b/packages/core/src/database/migration/20260805200742_import_legacy_credentials.ts index 3345500b97f2..0aa348713b80 100644 --- a/packages/core/src/database/migration/20260805200742_import_legacy_credentials.ts +++ b/packages/core/src/database/migration/20260805200742_import_legacy_credentials.ts @@ -80,7 +80,17 @@ export function importLegacyCredentials(tx: Parameters name !== "server")) + : undefined, + }) : value.type === "wellknown" ? Credential.Key.make({ type: "key", key: value.token }) : Credential.OAuth.make({