Files
OmniRoute/open-sse/executors/maxai/refresh.ts
Armin Anton” ∴ cabbbe410a feat(providers): add MaxAI — signed OpenAI-compatible provider (chat, tools, vision, image-gen, doc-RAG) (#11461)
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.
2026-09-02 01:55:42 -03:00

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,
};
}