mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
MaxAI joins as a first-class signed provider: 13 chat models discovered live from /models/get_config plus 6 image models, routed through the standard /v1 endpoints with per-request X-Authorization signing, browserless onboarding, prompted tool-calling, vision input, image generation and document RAG. Reconciled on merge — worth reading, because the branch forked 227 commits back and 77 files conflicted. Only five carried MaxAI content; the rest was drift from the older release line and took the tip's side, taking the diff from 113 files to 37 (then 93 as counted against the current base). - executors/index.ts: the tip has since refactored the executor map to lazy dynamic imports, so MaxAI is registered in that shape rather than the branch's static import. - imageRegistry.ts: kept only the maxai block. The branch still carried microsoft-designer-web, which #11754 retired. - models/route.ts: the conflicting hunk was an unrelated Vertex/Anthropic URL change, not MaxAI — tip's side. - volcengine agent-plan/coding-plan registries: git auto-merged both sides and produced a duplicated supportsVision key, which TypeScript rejects (TS1117). Removed. One real integration break that only the combined state shows: the MaxAI entry declared no serviceKinds, which #11392 made required a few hours ago. Provider validation threw at load time and check:provider-consistency crashed outright. Declared ["llm"] — the image kinds derive from imageRegistry, per the convention in that PR's backfill. Every count was measured rather than taken from the branch, and each would have been wrong: reserved prefixes are 402, not the 397 the branch computed from its stale 395 base; providers are 353, not 354. PROVIDER_REFERENCE.md regenerated, the count updated across README/AGENTS.md/llm.txt and its 42 mirrors, package.json and 6 SVGs — every changed line in those files is a digit substitution and nothing else, verified by masking digits and comparing the removed and added sets (90 lines, identical). The executor-map golden snapshot was regenerated: keyCount 133 -> 134. The branch's file-size-baseline.json predates #12411's ratchet re-tightening, so it was discarded rather than merged — taking it would have silently undone that. The three files this PR grows (proxyFetch.ts +20 for the Windows/firefox_150 TLS profile, imageGeneration.ts +12, models/route.ts +48) were entered against the current baseline under one _rebaseline annotation; no other cap moves. Verified: typecheck:core clean, check:provider-consistency OK (269 REGISTRY entries, 353 canonical providers), check:docs-counts exit 0, check-file-size OK, check:cycles OK, and 79/79 across the MaxAI suites plus 21/21 reserved-prefix and 2/2 executor-map-golden. Thanks @arminanton — the provider work itself is thorough; it was the 227 commits of base that needed the attention.
150 lines
5.5 KiB
TypeScript
150 lines
5.5 KiB
TypeScript
/**
|
|
* MaxAI access-token refresh — browserless, via one signed HTTP call.
|
|
*
|
|
* MaxAI issues two tokens: a ~24h `accessToken` and a ~1-year `refreshToken`.
|
|
* The web app refreshes the access token by POSTing the refresh token to
|
|
* `/oauth/refresh_access_token` (web-app chunk 86042, `refreshAccessToken`). That
|
|
* endpoint carries the SAME per-request `X-Authorization` signature as every other
|
|
* MaxAI call (see ./signing.ts) — it is NOT a browser-only OAuth hop. A residential
|
|
* Firefox-TLS client (wreq-js firefox_150, the OmniRoute egress overlay) passes the
|
|
* TLS gate, so OmniRoute mints fresh access tokens itself with no browser.
|
|
*
|
|
* The refresh token is minted out-of-band, once, by the browser Google-OAuth flow
|
|
* (see maxaiBrowserLogin) and only needs re-minting when it itself expires (~yearly).
|
|
* This module handles the routine daily refresh.
|
|
*
|
|
* Request shape (byte-faithful to the web app):
|
|
* POST https://api.maxai.me/oauth/refresh_access_token
|
|
* Authorization: Bearer <refreshToken> // the REFRESH token, not access
|
|
* noAuthLogout: true
|
|
* X-Authorization + X-App/X-Browser headers // standard signing
|
|
* body: {"app":"maxai_webapp"} // the app's `params` -> JSON body
|
|
* -> 200 { data: { access_token } } // a fresh ~24h access JWT
|
|
*
|
|
* The signed path is the BARE pathname (no query string); the `app` field travels
|
|
* in the body. `user_id` folds into the signature and is read from the refresh
|
|
* token's own JWT subject (per the web app), falling back to a provided userId.
|
|
*/
|
|
import { buildMaxaiSignedHeaders } from "./signing.ts";
|
|
import { maxaiStaticHeaders, MAXAI_BASE_URL } from "./protocol.ts";
|
|
import { userIdFromJwt, accessTokenExpiry } from "./credentials.ts";
|
|
import { refreshMaxaiConstants } from "./constantsStore.ts";
|
|
|
|
export const MAXAI_REFRESH_PATH = "/oauth/refresh_access_token";
|
|
|
|
/** How close to expiry (seconds) an access token may be before we refresh it. */
|
|
export const MAXAI_REFRESH_MARGIN_SECONDS = 60 * 60; // 1h
|
|
|
|
export interface MaxaiRefreshInput {
|
|
refreshToken: string;
|
|
deviceId: string;
|
|
/** Optional explicit user id; defaults to the refresh token's JWT subject. */
|
|
userId?: string;
|
|
signal?: AbortSignal | null;
|
|
/** Injectable fetch for tests (defaults to the ambient patched fetch). */
|
|
fetchImpl?: typeof fetch;
|
|
}
|
|
|
|
export interface MaxaiRefreshResult {
|
|
ok: boolean;
|
|
accessToken?: string;
|
|
/** access token expiry (epoch seconds), when a token was minted. */
|
|
expiresAt?: number;
|
|
status: number;
|
|
error?: string;
|
|
}
|
|
|
|
/** True when an access token is missing, unparseable, or within the margin of expiry. */
|
|
export function maxaiAccessTokenNeedsRefresh(
|
|
accessToken: string | null | undefined,
|
|
marginSeconds: number = MAXAI_REFRESH_MARGIN_SECONDS,
|
|
now: () => number = Date.now
|
|
): boolean {
|
|
if (!accessToken) return true;
|
|
const exp = accessTokenExpiry(accessToken);
|
|
if (!exp) return true;
|
|
return exp - now() / 1000 <= marginSeconds;
|
|
}
|
|
|
|
/**
|
|
* Mint a fresh access token from a refresh token via one signed HTTP POST.
|
|
* Never throws; returns a structured result the caller can branch on.
|
|
*/
|
|
export async function maxaiRefreshAccessToken(
|
|
input: MaxaiRefreshInput
|
|
): Promise<MaxaiRefreshResult> {
|
|
const doFetch = input.fetchImpl ?? fetch;
|
|
const userId = input.userId || userIdFromJwt(input.refreshToken) || "";
|
|
if (!input.refreshToken || !input.deviceId || !userId) {
|
|
return { ok: false, status: 0, error: "missing refreshToken, deviceId, or userId" };
|
|
}
|
|
|
|
// Daily refresh is our freshness checkpoint for the signing constants: re-extract
|
|
// from MaxAI's public bundle so a MaxAI-side key/app-version rotation is picked up
|
|
// within a day (self-heal). refreshMaxaiConstants persists a changed set and
|
|
// returns the current-best; on a fetch miss it returns whatever's already stored.
|
|
const constants = await refreshMaxaiConstants({ fetchImpl: doFetch, signal: input.signal });
|
|
if (!constants) {
|
|
return { ok: false, status: 0, error: "MaxAI signing constants unavailable (extraction failed)" };
|
|
}
|
|
|
|
const signed = buildMaxaiSignedHeaders(
|
|
{
|
|
path: MAXAI_REFRESH_PATH,
|
|
userId,
|
|
deviceId: input.deviceId,
|
|
},
|
|
constants
|
|
);
|
|
const headers: Record<string, string> = {
|
|
...maxaiStaticHeaders(),
|
|
...signed,
|
|
Authorization: `Bearer ${input.refreshToken}`,
|
|
noAuthLogout: "true",
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
let res: Response;
|
|
try {
|
|
res = await doFetch(MAXAI_BASE_URL + MAXAI_REFRESH_PATH, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ app: "maxai_webapp" }),
|
|
signal: input.signal ?? undefined,
|
|
});
|
|
} catch (err) {
|
|
return {
|
|
ok: false,
|
|
status: 0,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
};
|
|
}
|
|
|
|
const raw = await res.text().catch(() => "");
|
|
if (res.status !== 200) {
|
|
return { ok: false, status: res.status, error: raw.slice(0, 200) };
|
|
}
|
|
|
|
let accessToken = "";
|
|
try {
|
|
const parsed = JSON.parse(raw) as {
|
|
data?: { access_token?: unknown };
|
|
access_token?: unknown;
|
|
};
|
|
const candidate = parsed?.data?.access_token ?? parsed?.access_token;
|
|
if (typeof candidate === "string") accessToken = candidate;
|
|
} catch {
|
|
return { ok: false, status: res.status, error: "unparseable refresh response" };
|
|
}
|
|
if (!accessToken) {
|
|
return { ok: false, status: res.status, error: "refresh response had no access_token" };
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
accessToken,
|
|
expiresAt: accessTokenExpiry(accessToken) || undefined,
|
|
};
|
|
}
|