mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
Vincula o refresh OAuth do Google ao client que emitiu o token, com teste próprio (`google-oauth-client-binding.test.ts`). Validado no worktree combinado. Obrigado!
79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
import { resolvePublicCred } from "../../utils/publicCreds.ts";
|
|
|
|
/**
|
|
* Built-in (env-override-free) Google client credentials per provider.
|
|
*
|
|
* `PROVIDERS[x].clientId` resolves env overrides first
|
|
* (ANTIGRAVITY_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_ID), so once an operator
|
|
* configures a custom OAuth client the resolved value can no longer see the
|
|
* embedded client that issued the refresh tokens of every pre-existing
|
|
* connection. Those connections must keep refreshing against the built-in
|
|
* client of THEIR provider: Google binds a refresh token to the client that
|
|
* issued it and answers any other client with 401 unauthorized_client.
|
|
* gemini and antigravity embed DIFFERENT desktop clients, so the fallback is
|
|
* keyed by provider, not global.
|
|
*/
|
|
export const BUILTIN_ANTIGRAVITY_CLIENT = {
|
|
clientId: resolvePublicCred("antigravity_id"),
|
|
clientSecret: resolvePublicCred("antigravity_alt"),
|
|
} as const;
|
|
|
|
export const BUILTIN_GEMINI_CLIENT = {
|
|
clientId: resolvePublicCred("gemini_id"),
|
|
clientSecret: resolvePublicCred("gemini_alt"),
|
|
} as const;
|
|
|
|
/** Marker recorded at authorize time; the literal id guards client rotation. */
|
|
export type GoogleOauthClientMarker = "builtin" | `custom:${string}` | undefined;
|
|
|
|
function builtinClientFor(provider: string) {
|
|
if (provider === "gemini") return BUILTIN_GEMINI_CLIENT;
|
|
if (provider === "antigravity" || provider === "agy") return BUILTIN_ANTIGRAVITY_CLIENT;
|
|
// Unknown Google-family provider: no embedded client exists to fall back
|
|
// to. The antigravity client would mint Google 401s for tokens it never
|
|
// issued, so refuse loudly instead of guessing.
|
|
throw new Error(`no builtin OAuth client registered for provider: ${provider}`);
|
|
}
|
|
|
|
/**
|
|
* Pick the OAuth client credentials a Google refresh must use.
|
|
*
|
|
* The marker lives in the connection's providerSpecificData.oauthClient:
|
|
* - a string starting with "custom:" records the LITERAL client id that
|
|
* issued the token. The refresh compares it against the currently
|
|
* configured client: matching means the operator's custom client is
|
|
* unchanged and can refresh; a mismatch (the operator swapped to a
|
|
* different custom client after authorization) or a missing/malformed
|
|
* marker means the connection predates per-connection binding or lost
|
|
* its issuer, so the embedded desktop client of the connection's own
|
|
* provider is the only client that can still own that token.
|
|
* - "builtin": authorized with the embedded desktop client.
|
|
*
|
|
* Storing the literal id (not just a boolean) matters because Google binds
|
|
* each refresh token to the exact issuing client; "custom" alone would
|
|
* silently move old connections onto a *different* custom client when the
|
|
* operator rotates credentials.
|
|
*
|
|
* When no custom credentials are configured, both branches resolve to the
|
|
* same built-in client and the choice is moot.
|
|
*/
|
|
export function selectGoogleRefreshClient(
|
|
provider: string,
|
|
oauthClientMarker: GoogleOauthClientMarker,
|
|
configuredClient: { clientId?: string; clientSecret?: string } | null | undefined
|
|
): { clientId: string; clientSecret: string } {
|
|
if (
|
|
typeof oauthClientMarker === "string" &&
|
|
oauthClientMarker.startsWith("custom:") &&
|
|
oauthClientMarker.slice("custom:".length) === configuredClient?.clientId &&
|
|
configuredClient?.clientSecret
|
|
) {
|
|
return {
|
|
clientId: configuredClient.clientId,
|
|
clientSecret: configuredClient.clientSecret,
|
|
};
|
|
}
|
|
const builtin = builtinClientFor(provider);
|
|
return { clientId: builtin.clientId, clientSecret: builtin.clientSecret };
|
|
}
|