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:
@@ -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