SkateSpots/src/lib/translationGuard.ts
SkateSpots d2a24679f0 feat(chat): on-device translation pilot (English ↔ Spanish)
- @xenova/transformers 2.17.2 in a Web Worker; Opus-MT en↔es ONNX q8 packs
  self-hosted on the relay VPS (/models + CORS); ONNX Runtime 1.14 WASM
  self-hosted in public/ort/ (37MB) loaded from ../ort/.
- Hard network guard: only model GET/HEAD (no query) and /ort/ assets allowed.
  Message text travels only via postMessage; 9 tests prove fetch is never called
  with the text and every other request is blocked.
- TranslateMessage in DMs + crew chat; Settings auto-translate off by default;
  memory-only translations; per-model download progress; 12 UI strings in all
  24 locales.
- Both model directions verified from the hosted files; tsc, 138 tests, build
  and smoke green.
2026-10-27 10:49:54 +07:00

50 lines
1.7 KiB
TypeScript

/**
* Network guard for the translation worker.
*
* The worker is allowed to fetch exactly two kinds of URLs:
* 1. the self-hosted model files (GET/HEAD only), and
* 2. same-origin ONNX runtime assets (.wasm/.mjs/.js).
*
* Everything else throws before the request leaves the worker. Message text is
* never put into a URL or a request body; it only travels through
* `postMessage` between the UI and the worker.
*/
export interface TranslationGuardOptions {
modelOrigin: string;
modelPathPrefix: string;
appOrigin: string;
/** Only the self-hosted ONNX runtime (e.g. `/ort/`) may be fetched app-side. */
appAssetPathPrefix: string;
}
const APP_ASSET = /\.(?:wasm|mjs|js)$/i;
export function createGuardedFetch(
realFetch: typeof fetch,
options: TranslationGuardOptions,
): typeof fetch {
return (async (input: RequestInfo | URL, init?: RequestInit) => {
const request = input instanceof Request ? input : null;
const raw = typeof input === 'string'
? input
: input instanceof URL
? input.href
: input.url;
const url = new URL(raw, request?.url ?? options.appOrigin);
const method = (init?.method ?? request?.method ?? 'GET').toUpperCase();
const read = method === 'GET' || method === 'HEAD';
const isModel = url.origin === options.modelOrigin
&& url.pathname.startsWith(options.modelPathPrefix)
&& !url.search
&& read;
const isAppAsset = url.origin === options.appOrigin
&& url.pathname.startsWith(options.appAssetPathPrefix)
&& APP_ASSET.test(url.pathname)
&& read;
if (!isModel && !isAppAsset) {
throw new Error('blocked network request');
}
return realFetch(input as RequestInfo, init);
}) as typeof fetch;
}