fix(zed): harden OAuth lifecycle and live model support
- executors/zed.js: use exact wire values (anthropic, open_ai, google, x_ai) and strip incompatible Vertex safetySettings on the Google path - shared/zedAuth.js: robust callback query parsing, reject garbage PKCS#1 v1.5 decryptions, and thread proxyOptions when fetching LLM tokens - oauth: preserve systemId across authorize/register/exchange lifecycle, renew proxy idle timeout on reuse, and ignore non-callback localhost requests - shared/OAuthModal.js: track owned proxy in flowRef and stop at most once - api/providers/[id]/models: add connection-scoped live Zed model resolver - registry: unhide provider in dashboard - tests: add unit coverage for wire format, native auth, and live models
This commit is contained in:
@@ -29,11 +29,18 @@ import {
|
||||
zedLlmFetch,
|
||||
} from "../shared/zedAuth.js";
|
||||
|
||||
// Wire values for the `provider` field of POST /completions. These are NOT
|
||||
// display names: cloud.zed.dev matches them exactly, and an unrecognized value
|
||||
// fails the whole request with `500 {"message":"An internal server error
|
||||
// occurred."}` before the model is ever looked at. Spellings come from Zed's
|
||||
// own GET /models catalog: `anthropic`, `open_ai`, `google` (note underscore),
|
||||
// `x_ai` follows the same convention — so feeding a catalog value back through
|
||||
// normalizeZedProvider is identity.
|
||||
const ZED_PROVIDER = {
|
||||
anthropic: "Anthropic",
|
||||
openai: "OpenAi",
|
||||
google: "Google",
|
||||
xai: "XAi",
|
||||
anthropic: "anthropic",
|
||||
openai: "open_ai",
|
||||
google: "google",
|
||||
xai: "x_ai",
|
||||
};
|
||||
|
||||
function normalizeZedProvider(value, model) {
|
||||
@@ -55,7 +62,14 @@ function buildProviderRequest(provider, model, body, stream, credentials) {
|
||||
return openaiToClaudeRequest(model, body, true);
|
||||
}
|
||||
if (provider === ZED_PROVIDER.google) {
|
||||
return openaiToGeminiRequest(model, body, true);
|
||||
const geminiRequest = openaiToGeminiRequest(model, body, true);
|
||||
// Zed's hosted Gemini backend speaks the Vertex safety vocabulary, not the
|
||||
// public Gemini API enum the shared translator emits (`OFF`, `CIVIC_INTEGRITY`,
|
||||
// `DANGEROUS_CONTENT`). Drop client-side safetySettings for the Zed Google
|
||||
// path so Zed applies its own defaults — scoped here so native Gemini/
|
||||
// Antigravity is untouched.
|
||||
delete geminiRequest.safetySettings;
|
||||
return geminiRequest;
|
||||
}
|
||||
if (provider === ZED_PROVIDER.openai) {
|
||||
return openaiToOpenAIResponsesRequest(model, body, true, credentials);
|
||||
|
||||
@@ -4,7 +4,6 @@ export default {
|
||||
priority: 10,
|
||||
alias: "zd",
|
||||
uiAlias: "zd",
|
||||
hidden: true,
|
||||
display: {
|
||||
name: "Zed",
|
||||
icon: "code",
|
||||
|
||||
@@ -112,7 +112,14 @@ export function parseZedCallbackPayload(input) {
|
||||
url = new URL(raw);
|
||||
} catch {
|
||||
try {
|
||||
url = new URL(`http://127.0.0.1/?${raw.replace(/^\?/, "")}`);
|
||||
// Accept pathname+query (what the local proxy forwards, e.g.
|
||||
// "/?user_id=..&access_token=.." or "/callback?.."), a bare query,
|
||||
// or a lone query string. Only the query part is parsed — a leading
|
||||
// path must never become part of the first parameter name.
|
||||
const query = raw.includes("?")
|
||||
? raw.slice(raw.indexOf("?") + 1)
|
||||
: raw.replace(/^\?/, "");
|
||||
url = new URL(`http://127.0.0.1/?${query}`);
|
||||
} catch {
|
||||
throw new Error("Invalid Zed callback URL");
|
||||
}
|
||||
@@ -134,6 +141,10 @@ export function parseZedCallbackPayload(input) {
|
||||
export function decryptZedAccessToken(encryptedAccessToken, privateKeyVerifier) {
|
||||
const privateKey = decodeZedPrivateKeyVerifier(privateKeyVerifier);
|
||||
const encrypted = Buffer.from(String(encryptedAccessToken), "base64url");
|
||||
const fail = (oaepError) => {
|
||||
const message = oaepError instanceof Error ? oaepError.message : String(oaepError);
|
||||
throw new Error(`Failed to decrypt Zed access token: ${message}`);
|
||||
};
|
||||
try {
|
||||
return crypto
|
||||
.privateDecrypt(
|
||||
@@ -143,15 +154,21 @@ export function decryptZedAccessToken(encryptedAccessToken, privateKeyVerifier)
|
||||
.toString("utf8");
|
||||
} catch (oaepError) {
|
||||
try {
|
||||
return crypto
|
||||
const text = crypto
|
||||
.privateDecrypt(
|
||||
{ key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING },
|
||||
encrypted,
|
||||
)
|
||||
.toString("utf8");
|
||||
} catch {
|
||||
const message = oaepError instanceof Error ? oaepError.message : String(oaepError);
|
||||
throw new Error(`Failed to decrypt Zed access token: ${message}`);
|
||||
// PKCS#1 v1.5 unpadding is not integrity-checked: a wrong-key decrypt
|
||||
// can "succeed" with garbage bytes instead of throwing. Replacement
|
||||
// characters prove the output is not the real UTF-8 token — fail loudly
|
||||
// rather than storing garbage as a credential.
|
||||
if (text.includes("<22>")) fail(oaepError);
|
||||
return text;
|
||||
} catch (err) {
|
||||
if (err.message.startsWith("Failed to decrypt Zed access token")) throw err;
|
||||
fail(oaepError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,6 +297,7 @@ export async function fetchZedLlmToken(credentials, options = {}) {
|
||||
body: JSON.stringify({ organization_id: organizationId }),
|
||||
signal: options.signal ?? undefined,
|
||||
},
|
||||
options.proxyOptions ?? null,
|
||||
);
|
||||
const token =
|
||||
typeof data?.token === "string" ? data.token : data?.token?.[0] || data?.token?.value;
|
||||
|
||||
Reference in New Issue
Block a user