mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 08:12:20 +03:00
Compare commits
2 Commits
feat/6736-
...
feat/tinyc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48f04073e9 | ||
|
|
d98737578e |
@@ -2404,5 +2404,15 @@
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"open-sse/executors/tinycms.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"open-sse/executors/tinycmsSigner.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,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";
|
||||
@@ -428,6 +429,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;
|
||||
@@ -64,6 +64,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";
|
||||
@@ -184,6 +185,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
|
||||
@@ -279,6 +282,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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
474
open-sse/executors/tinycmsSigner.ts
Normal file
474
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,
|
||||
@@ -239,6 +240,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,
|
||||
|
||||
@@ -648,3 +648,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +376,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",
|
||||
|
||||
220
tests/unit/provider-tinycms-web.test.ts
Normal file
220
tests/unit/provider-tinycms-web.test.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 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 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";
|
||||
|
||||
// ── 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