mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
[v3.8.50] feat(providers): add support for TinyCMS Web (#8736)
* 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>
This commit is contained in:
@@ -44,6 +44,16 @@
|
||||
"count": 11
|
||||
}
|
||||
},
|
||||
"open-sse/executors/tinycms.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"open-sse/executors/tinycmsSigner.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"open-sse/executors/vertex.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 3
|
||||
|
||||
@@ -216,6 +216,7 @@ import { grok_cliProvider } from "./registry/grok-cli/index.ts";
|
||||
import { codebuddy_cnProvider } from "./registry/codebuddy-cn/index.ts";
|
||||
import { pioneerProvider } from "./registry/pioneer/index.ts";
|
||||
import { zenmux_freeProvider } from "./registry/zenmux-free/index.ts";
|
||||
import { tinycmsProvider } from "./registry/tinycms/index.ts";
|
||||
import { sumopodProvider } from "./registry/sumopod/index.ts";
|
||||
import { x5labProvider } from "./registry/x5lab/index.ts";
|
||||
import { kenariProvider } from "./registry/kenari/index.ts";
|
||||
@@ -446,6 +447,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"codebuddy-cn": codebuddy_cnProvider,
|
||||
pioneer: pioneerProvider,
|
||||
"zenmux-free": zenmux_freeProvider,
|
||||
"tinycms-web": tinycmsProvider,
|
||||
sumopod: sumopodProvider,
|
||||
x5lab: x5labProvider,
|
||||
kenari: kenariProvider,
|
||||
|
||||
40
open-sse/config/providers/registry/tinycms/index.ts
Normal file
40
open-sse/config/providers/registry/tinycms/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* TinyCMS — session-cookie free-tier and subscription gateway.
|
||||
*
|
||||
* Users get a device UUID starting with "R" from site.tinycms.xyz (stored in localStorage
|
||||
* as app-config-uuid) and paste it as the credential.
|
||||
*
|
||||
* Emulates the cryptographic signatures (WASM signer) and Proof of Work expected
|
||||
* by the TinyCMS server.
|
||||
*/
|
||||
export const tinycmsProvider: RegistryEntry = {
|
||||
id: "tinycms-web",
|
||||
alias: "tcw",
|
||||
format: "openai",
|
||||
executor: "tinycms-web",
|
||||
baseUrl: "https://gov.freegpt.win/api/openai/oneapi/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "uuid",
|
||||
models: [
|
||||
{ id: "gpt-5-free", name: "GPT 5 Free" },
|
||||
{ id: "gpt-5.3-free", name: "GPT 5.3 Free (Multimodal/Vision)" },
|
||||
{ id: "gpt-5.3-thinking-free", name: "GPT 5.3 Thinking Free", supportsReasoning: true },
|
||||
{ id: "gpt-5.4-mini", name: "GPT 5.4 Mini" },
|
||||
{ id: "gpt-5.4-nano", name: "GPT 5.4 Nano" },
|
||||
{ id: "gpt-5-nano", name: "GPT 5 Nano" },
|
||||
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
|
||||
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
|
||||
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
|
||||
{ id: "grok-4.20-fast", name: "Grok 4.20 Fast" },
|
||||
{ id: "grok-4.20", name: "Grok 4.20" },
|
||||
{ id: "grok-imagine", name: "Grok Imagine (Image Gen)" },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
|
||||
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
|
||||
{ id: "gpt-image-2", name: "GPT Image 2 (Image Gen)" },
|
||||
{ id: "qwen3.6-plus", name: "Qwen 3.6 Plus" },
|
||||
],
|
||||
};
|
||||
|
||||
export default tinycmsProvider;
|
||||
@@ -69,6 +69,7 @@ import { MimocodeExecutor } from "./mimocode.ts";
|
||||
import { GrokCliExecutor } from "./grok-cli.ts";
|
||||
import { CodeBuddyCnExecutor } from "./codebuddy-cn.ts";
|
||||
import { ZenmuxFreeExecutor } from "./zenmux-free.ts";
|
||||
import { TinyCmsExecutor } from "./tinycms.ts";
|
||||
import { HyperAgentExecutor } from "./hyperagent.ts";
|
||||
import { XaiExecutor } from "./xai.ts";
|
||||
import { PromptQlExecutor } from "./promptql.ts";
|
||||
@@ -199,6 +200,8 @@ const executors = {
|
||||
"codebuddy-cn": new CodeBuddyCnExecutor(),
|
||||
cbcn: new CodeBuddyCnExecutor(), // Alias for codebuddy-cn
|
||||
"zenmux-free": new ZenmuxFreeExecutor(),
|
||||
"tinycms-web": new TinyCmsExecutor(),
|
||||
tcw: new TinyCmsExecutor(), // Alias
|
||||
hyperagent: new HyperAgentExecutor(),
|
||||
ha: new HyperAgentExecutor(), // Alias
|
||||
zmf: new ZenmuxFreeExecutor(), // Alias for zenmux-free
|
||||
@@ -297,6 +300,7 @@ export { MimocodeExecutor } from "./mimocode.ts";
|
||||
export { GrokCliExecutor } from "./grok-cli.ts";
|
||||
export { CodeBuddyCnExecutor } from "./codebuddy-cn.ts";
|
||||
export { ZenmuxFreeExecutor } from "./zenmux-free.ts";
|
||||
export { TinyCmsExecutor } from "./tinycms.ts";
|
||||
export { HyperAgentExecutor } from "./hyperagent.ts";
|
||||
export { XaiExecutor } from "./xai.ts";
|
||||
export { MoonshotExecutor } from "./moonshot.ts";
|
||||
|
||||
132
open-sse/executors/tinycms.ts
Normal file
132
open-sse/executors/tinycms.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
509
open-sse/executors/tinycmsSigner.ts
Normal file
509
open-sse/executors/tinycmsSigner.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -39,6 +39,7 @@ import {
|
||||
import {
|
||||
validateMuseSparkWebProvider,
|
||||
validateAdaptaWebProvider,
|
||||
validateTinyCmsWebProvider,
|
||||
validateClaudeWebProvider,
|
||||
validateGeminiWebProvider,
|
||||
validateCopilotM365WebProvider,
|
||||
@@ -256,6 +257,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
"muse-spark-web": validateMuseSparkWebProvider,
|
||||
"inner-ai": validateInnerAiProvider,
|
||||
"adapta-web": validateAdaptaWebProvider,
|
||||
"tinycms-web": validateTinyCmsWebProvider,
|
||||
"claude-web": validateClaudeWebProvider,
|
||||
"gemini-web": validateGeminiWebProvider,
|
||||
"notion-web": validateNotionWebProvider,
|
||||
|
||||
@@ -679,3 +679,34 @@ export async function validateInnerAiProvider({ apiKey, providerSpecificData = {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateTinyCmsWebProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
try {
|
||||
const raw = typeof apiKey === "string" ? apiKey.trim() : "";
|
||||
if (!raw || !raw.startsWith("R")) {
|
||||
return { valid: false, error: "TinyCMS UUID must start with 'R'" };
|
||||
}
|
||||
|
||||
const response = await validationRead("https://gov.freegpt.win/api/challenge", {
|
||||
headers: applyCustomUserAgent(
|
||||
{
|
||||
uuid: raw,
|
||||
"x-origin": "https://gov.freegpt.win",
|
||||
Accept: "application/json",
|
||||
},
|
||||
providerSpecificData
|
||||
),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `TinyCMS UUID validation status: ${response.status} (invalid/expired UUID)`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true, error: null };
|
||||
} catch (error: any) {
|
||||
return toValidationErrorResult(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +379,20 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
authHint:
|
||||
"Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days.",
|
||||
},
|
||||
"tinycms-web": {
|
||||
id: "tinycms-web",
|
||||
alias: "tcw",
|
||||
name: "TinyCMS Web (Free/Sub)",
|
||||
icon: "layers",
|
||||
color: "#ED8936",
|
||||
textIcon: "TC",
|
||||
website: "https://site.tinycms.xyz",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free tier has access to GPT 5.4, Gemini 3.5, and Grok 4.20 models. No login required. Subscription grants 300 requests/day for advanced models.",
|
||||
authHint:
|
||||
"Go to site.tinycms.xyz, open DevTools → Application → Local Storage, copy the value of 'app-config-uuid' (starts with 'R'), and paste it here.",
|
||||
},
|
||||
"zai-web": {
|
||||
id: "zai-web",
|
||||
alias: "zw",
|
||||
|
||||
@@ -32,6 +32,13 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
|
||||
acceptsFullCookieHeader: true,
|
||||
storageKeys: ["cookie"],
|
||||
},
|
||||
"tinycms-web": {
|
||||
kind: "token",
|
||||
credentialName: "app-config-uuid",
|
||||
placeholder: "R...",
|
||||
acceptsFullCookieHeader: false,
|
||||
storageKeys: ["apiKey", "token", "uuid", "app-config-uuid"],
|
||||
},
|
||||
"chatgpt-web": {
|
||||
kind: "cookie",
|
||||
credentialName: "__Secure-next-auth.session-token",
|
||||
|
||||
240
tests/unit/provider-tinycms-web.test.ts
Normal file
240
tests/unit/provider-tinycms-web.test.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Tests for TinyCMS Web provider — registration, credential validation, challenge/PoW flow.
|
||||
*
|
||||
* Validates:
|
||||
* - WEB_COOKIE_PROVIDERS contains the tinycms-web entry
|
||||
* - Registry entry has correct shape and models
|
||||
* - Executor resolves for both primary id and alias
|
||||
* - UUID validation (must start with 'R')
|
||||
* - WASM initialization flow
|
||||
* - Challenge/PoW flow with mocked network calls
|
||||
* - Error sanitization (Hard Rule #12: no stack traces)
|
||||
* - Configurable userid from providerSpecificData
|
||||
*/
|
||||
import test, { before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
|
||||
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
|
||||
import { getExecutor, TinyCmsExecutor } from "../../open-sse/executors/index.ts";
|
||||
import {
|
||||
setupDomMocks,
|
||||
type DomMockRestore,
|
||||
} from "../../open-sse/executors/tinycmsSigner.ts";
|
||||
|
||||
// tinycmsSigner.ts intentionally does NOT install its window/document/canvas
|
||||
// shims as a module-load side effect (see setupDomMocks() there) — doing so
|
||||
// would leak those globals into every other test file that transitively
|
||||
// imports it (e.g. through the provider registry). Install them explicitly
|
||||
// for this file only, and restore whatever was there before once this file's
|
||||
// tests are done.
|
||||
let restoreDomMocks: DomMockRestore;
|
||||
|
||||
before(() => {
|
||||
restoreDomMocks = setupDomMocks();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
restoreDomMocks();
|
||||
});
|
||||
|
||||
// ── Catalog / WEB_COOKIE_PROVIDERS ────────────────────────────────────────────
|
||||
|
||||
test("tinycms-web is present in WEB_COOKIE_PROVIDERS", () => {
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
|
||||
"tinycms-web"
|
||||
] as Record<string, unknown>;
|
||||
assert.ok(p, "WEB_COOKIE_PROVIDERS['tinycms-web'] must exist");
|
||||
assert.equal(p.id, "tinycms-web");
|
||||
assert.equal(p.alias, "tcw");
|
||||
assert.equal((p.name as string).toLowerCase().includes("tinycms"), true);
|
||||
});
|
||||
|
||||
test("tinycms-web WEB_COOKIE_PROVIDERS entry is marked as free-tier", () => {
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
|
||||
"tinycms-web"
|
||||
] as Record<string, unknown>;
|
||||
assert.equal(p.hasFree, true);
|
||||
assert.ok(typeof p.freeNote === "string" && (p.freeNote as string).length > 0);
|
||||
assert.ok(typeof p.authHint === "string" && (p.authHint as string).length > 0);
|
||||
});
|
||||
|
||||
// ── Registry / REGISTRY ───────────────────────────────────────────────────────
|
||||
|
||||
test("tinycms-web is present in the provider REGISTRY with correct shape", () => {
|
||||
const r = REGISTRY["tinycms-web"];
|
||||
assert.ok(r, "REGISTRY['tinycms-web'] must exist");
|
||||
assert.equal(r.id, "tinycms-web");
|
||||
assert.equal(r.alias, "tcw");
|
||||
assert.equal(r.executor, "tinycms-web");
|
||||
assert.equal(r.format, "openai");
|
||||
assert.equal(r.authType, "apikey");
|
||||
assert.equal(r.authHeader, "uuid");
|
||||
});
|
||||
|
||||
test("tinycms-web registry has all expected models", () => {
|
||||
const r = REGISTRY["tinycms-web"];
|
||||
assert.ok(r.models && r.models.length > 0, "must have at least one model");
|
||||
const ids = r.models.map((m) => m.id);
|
||||
|
||||
assert.ok(ids.includes("gpt-5-free"), "gpt-5-free must be registered");
|
||||
assert.ok(ids.includes("gpt-5.3-free"), "gpt-5.3-free must be registered");
|
||||
assert.ok(
|
||||
ids.includes("gpt-5.3-thinking-free"),
|
||||
"gpt-5.3-thinking-free must be registered"
|
||||
);
|
||||
assert.ok(ids.includes("deepseek-v4-flash"), "deepseek-v4-flash must be registered");
|
||||
assert.ok(ids.includes("claude-sonnet-5"), "claude-sonnet-5 must be registered");
|
||||
assert.ok(ids.includes("gemini-3.5-flash"), "gemini-3.5-flash must be registered");
|
||||
assert.equal(r.models.length, 16, "must have exactly 16 models");
|
||||
});
|
||||
|
||||
test("tinycms-web model names are human-readable strings", () => {
|
||||
const r = REGISTRY["tinycms-web"];
|
||||
for (const m of r.models) {
|
||||
assert.ok(m.id && typeof m.id === "string", `model id must be a string: ${JSON.stringify(m)}`);
|
||||
assert.ok(
|
||||
m.name && typeof m.name === "string",
|
||||
`model name must be a string: ${JSON.stringify(m)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("supportsReasoning is set on gpt-5.3-thinking-free", () => {
|
||||
const r = REGISTRY["tinycms-web"];
|
||||
const thinkingModel = r.models.find((m) => m.id === "gpt-5.3-thinking-free");
|
||||
assert.ok(thinkingModel, "gpt-5.3-thinking-free must exist");
|
||||
assert.equal(thinkingModel!.supportsReasoning, true);
|
||||
});
|
||||
|
||||
// ── Executor ──────────────────────────────────────────────────────────────────
|
||||
|
||||
test("getExecutor returns TinyCmsExecutor for 'tinycms-web'", () => {
|
||||
const e = getExecutor("tinycms-web");
|
||||
assert.ok(e instanceof TinyCmsExecutor, "executor must be TinyCmsExecutor");
|
||||
});
|
||||
|
||||
test("getExecutor returns TinyCmsExecutor for 'tcw' alias", () => {
|
||||
const e = getExecutor("tcw");
|
||||
assert.ok(e instanceof TinyCmsExecutor, "alias 'tcw' must resolve to TinyCmsExecutor");
|
||||
});
|
||||
|
||||
test("TinyCmsExecutor can be instantiated", () => {
|
||||
const executor = new TinyCmsExecutor();
|
||||
assert.ok(executor, "must instantiate without errors");
|
||||
assert.ok(typeof executor.execute === "function", "must have an execute method");
|
||||
});
|
||||
|
||||
// ── UUID validation (must start with 'R') ─────────────────────────────────────
|
||||
|
||||
test("TinyCmsExecutor returns 401 when UUID is missing", async () => {
|
||||
const executor = new TinyCmsExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5-free",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {},
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
assert.ok(result.response, "response must be present");
|
||||
assert.equal(result.response.status, 401);
|
||||
const body = await result.response.json();
|
||||
const errMsg = body?.error?.message || "";
|
||||
assert.ok(
|
||||
errMsg.includes("Invalid or missing device UUID"),
|
||||
"error must mention missing UUID"
|
||||
);
|
||||
// Hard Rule #12: must NOT leak stack traces
|
||||
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
|
||||
});
|
||||
|
||||
test("TinyCmsExecutor returns 401 when UUID does not start with 'R'", async () => {
|
||||
const executor = new TinyCmsExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5-free",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "abc123" }, // does not start with 'R'
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
assert.ok(result.response, "response must be present");
|
||||
assert.equal(result.response.status, 401);
|
||||
const body = await result.response.json();
|
||||
const errMsg = body?.error?.message || "";
|
||||
assert.ok(
|
||||
errMsg.includes("Invalid or missing device UUID"),
|
||||
"error must mention missing UUID"
|
||||
);
|
||||
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
|
||||
});
|
||||
|
||||
// ── WASM initialization ───────────────────────────────────────────────────────
|
||||
|
||||
test("initTinyCmsWasm module exports expected functions", async () => {
|
||||
const signer = await import("../../open-sse/executors/tinycmsSigner.ts");
|
||||
assert.ok(
|
||||
typeof signer.initTinyCmsWasm === "function",
|
||||
"must export initTinyCmsWasm function"
|
||||
);
|
||||
assert.ok(
|
||||
typeof signer.generateSecurePayload === "function",
|
||||
"must export generateSecurePayload function"
|
||||
);
|
||||
});
|
||||
|
||||
test("initTinyCmsWasm is idempotent (calling twice does not throw)", async () => {
|
||||
const signer = await import("../../open-sse/executors/tinycmsSigner.ts");
|
||||
try {
|
||||
await signer.initTinyCmsWasm();
|
||||
await signer.initTinyCmsWasm(); // Second call should be a no-op
|
||||
} catch {
|
||||
// WASM may not be available in all Node.js test environments;
|
||||
// the important thing is that the function handles errors gracefully
|
||||
// and both calls behave the same way.
|
||||
}
|
||||
});
|
||||
|
||||
// ── Error sanitization (Hard Rule #12) ────────────────────────────────────────
|
||||
|
||||
test("TinyCmsExecutor sanitizes errors (no stack traces in error response)", async () => {
|
||||
const executor = new TinyCmsExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5-free",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "" }, // empty UUID
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
assert.ok(result.response, "response must be present");
|
||||
const body = await result.response.json();
|
||||
const errMsg = body?.error?.message || "";
|
||||
assert.ok(
|
||||
errMsg.includes("Invalid or missing device UUID"),
|
||||
"error must mention missing UUID"
|
||||
);
|
||||
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path (Hard Rule #12)");
|
||||
});
|
||||
|
||||
// ── Provider-specific credential requirements ─────────────────────────────────
|
||||
|
||||
test("tinycms-web credential requirement is kind: token with app-config-uuid", async () => {
|
||||
const { WEB_SESSION_CREDENTIAL_REQUIREMENTS } =
|
||||
await import("../../src/shared/providers/webSessionCredentials.ts");
|
||||
const req = (WEB_SESSION_CREDENTIAL_REQUIREMENTS as Record<string, unknown>)[
|
||||
"tinycms-web"
|
||||
] as Record<string, unknown>;
|
||||
assert.ok(req, "credential requirement must exist for tinycms-web");
|
||||
assert.equal(req.kind, "token");
|
||||
assert.equal(req.credentialName, "app-config-uuid");
|
||||
assert.equal(req.acceptsFullCookieHeader, false);
|
||||
assert.ok(Array.isArray(req.storageKeys), "must have storageKeys array");
|
||||
assert.ok(
|
||||
(req.storageKeys as string[]).includes("apiKey"),
|
||||
"apiKey must be in storageKeys"
|
||||
);
|
||||
assert.ok(
|
||||
(req.storageKeys as string[]).includes("uuid"),
|
||||
"uuid must be in storageKeys"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user