mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
* feat(providers): add support for TinyCMS Web including WASM-based cryptographic signing and Proof-of-Work emulation * feat(providers): add unit tests, ESLint suppressions, and fix hardcoded userid for TinyCMS Web - Add unit tests for WASM init, UUID validation, challenge flow (15 tests) - Add WASM source comment explaining binary origin - Replace hardcoded userid with dynamic provider-specific data - Add ESLint suppressions for no-explicit-any in WASM bridge code - Add explanatory comments for DOM shim (runtime WASM-bindgen, not test mocks) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(providers): extract TinyCMS DOM shims into an explicit setup function tinycmsSigner.ts installed its window/document/HTMLCanvasElement/ CanvasRenderingContext2D shims for the wasm-bindgen glue as a module-load side effect. That meant merely importing the module (even transitively, e.g. through the provider registry from an unrelated test) mutated global state for the rest of the test process. Extract the shim installation into setupDomMocks(), which returns a restore callback: - initTinyCmsWasm() calls it once before instantiating the WASM module (production path — unchanged behavior, still automatic). - tests/unit/provider-tinycms-web.test.ts now calls it explicitly in a `before` hook and restores the previous globals in `after`, so the shims never leak into other test files. As a side effect, replacing five separate `as any` casts with a single typed `global as Record<string, any>` handle drops the file's no-explicit-any count from 5 to 1; eslint-suppressions.json updated to match. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(providers): regenerate PROVIDER_REFERENCE.md for tinycms-web Mechanical `npm run gen:provider-reference` run after merging release/ v3.8.50 into this branch — the generated table was stale for both the new tinycms-web entry this PR adds and the release's own cheaperinference addition. Total providers 290 -> 292, Web Cookie Providers 31 -> 32. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
|
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
|
|
import { initTinyCmsWasm, generateSecurePayload } from "./tinycmsSigner.ts";
|
|
|
|
const CHAT_URL = "https://gov.freegpt.win/api/openai/oneapi/v1/chat/completions";
|
|
const CHALLENGE_URL = "https://gov.freegpt.win/api/challenge";
|
|
|
|
let publicIp: string | null = null;
|
|
let lastIpFetch = 0;
|
|
|
|
async function getPublicIp(): Promise<string> {
|
|
const now = Date.now();
|
|
if (publicIp && now - lastIpFetch < 300000) {
|
|
return publicIp;
|
|
}
|
|
try {
|
|
const res = await fetch("https://api64.ipify.org?format=json");
|
|
const json = (await res.json()) as { ip: string };
|
|
publicIp = json.ip;
|
|
lastIpFetch = now;
|
|
return publicIp;
|
|
} catch {
|
|
return publicIp || "127.0.0.1";
|
|
}
|
|
}
|
|
|
|
async function fetchChallenge(uuid: string): Promise<any> {
|
|
const res = await fetch(CHALLENGE_URL, {
|
|
method: "GET",
|
|
headers: {
|
|
"uuid": uuid,
|
|
"x-origin": "https://gov.freegpt.win",
|
|
"Accept": "application/json",
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to fetch challenge: ${res.status}`);
|
|
}
|
|
return await res.json();
|
|
}
|
|
|
|
export class TinyCmsExecutor extends BaseExecutor {
|
|
constructor() {
|
|
super("tinycms-web", { id: "tinycms-web", baseUrl: CHAT_URL });
|
|
}
|
|
|
|
async execute(input: ExecuteInput) {
|
|
const { body, credentials, signal } = input;
|
|
const bodyObj = (body || {}) as Record<string, any>;
|
|
|
|
// TinyCMS uses 'uuid' header for identification
|
|
const uuid = String(credentials?.apiKey ?? "").trim();
|
|
if (!uuid || !uuid.startsWith("R")) {
|
|
return makeErrorResult(
|
|
401,
|
|
"TinyCMS: Invalid or missing device UUID (must start with 'R')",
|
|
body,
|
|
CHAT_URL
|
|
);
|
|
}
|
|
|
|
try {
|
|
await initTinyCmsWasm();
|
|
|
|
const ip = await getPublicIp();
|
|
const challengeObj = await fetchChallenge(uuid);
|
|
|
|
const timestamp = Date.now().toString();
|
|
const nonceJs =
|
|
typeof crypto !== "undefined" && crypto.randomUUID
|
|
? crypto.randomUUID()
|
|
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
|
|
const securePayload = generateSecurePayload(
|
|
uuid,
|
|
timestamp,
|
|
nonceJs,
|
|
challengeObj.challenge,
|
|
ip,
|
|
challengeObj.difficulty
|
|
);
|
|
|
|
const signedHeaders: Record<string, string> = {
|
|
uuid: uuid,
|
|
"x-origin": "https://gov.freegpt.win",
|
|
referer: "https://gov.freegpt.win/",
|
|
"x-secure-challenge-id": challengeObj.challengeId,
|
|
"x-secure-challenge-expires-at": String(challengeObj.expiresAt),
|
|
"x-secure-challenge-version": challengeObj.version,
|
|
"x-secure-signature": securePayload.signature,
|
|
"x-secure-fingerprint": securePayload.fingerprint,
|
|
"x-secure-client-ip": securePayload.client_ip,
|
|
"x-secure-pow-seed-nonce": String(securePayload.pow.seed_nonce),
|
|
"x-secure-pow-nonce": String(securePayload.pow.nonce),
|
|
"x-secure-pow-hash": securePayload.pow.hash,
|
|
"x-secure-pow-difficulty": String(securePayload.pow.difficulty),
|
|
"x-secure-timestamp": timestamp,
|
|
"x-secure-nonce": nonceJs,
|
|
"x-secure-version": securePayload.v,
|
|
"x-session-id": nonceJs,
|
|
// Use configurable userid from providerSpecificData if present, otherwise generate one
|
|
// from the UUID (the server uses it for request attribution, not auth).
|
|
userid: String(credentials?.providerSpecificData?.userid ?? "") || uuid.slice(0, 20),
|
|
Accept: bodyObj.stream ? "text/event-stream" : "application/json",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
};
|
|
|
|
const fetchOptions: RequestInit = {
|
|
method: "POST",
|
|
headers: signedHeaders,
|
|
body: JSON.stringify(bodyObj),
|
|
signal,
|
|
};
|
|
|
|
const response = await fetch(CHAT_URL, fetchOptions);
|
|
return {
|
|
status: response.status,
|
|
headers: Object.fromEntries(response.headers.entries()),
|
|
body: response.body,
|
|
};
|
|
} catch (err: any) {
|
|
return makeErrorResult(
|
|
500,
|
|
`TinyCMS Error: ${err.message}`,
|
|
body,
|
|
CHAT_URL
|
|
);
|
|
}
|
|
}
|
|
}
|