mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: remove automatic third-party egress of user data + optional strict offline mode (OSM tiles, Scalar fonts, editor fonts, AI model downloads) (#422)
* fix: remove all automatic third-party egress (OSM tiles, Scalar fonts, editor Google Fonts, AI model download fallbacks) Phone-home audit follow-up. The product no longer makes any automatic third-party request; user-initiated click-outs stay, and production now fails closed on missing AI models. 1. GPS leak via OSM tiles: the strip-metadata panel auto-loaded tile.openstreetmap.org tiles encoding the photo's GPS position. The Leaflet mini-map is gone; coordinates render as text plus an explicit View on map link (openstreetmap.org, opens on click only). Removed tile.openstreetmap.org from the CSP img-src, dropped the leaflet dependency, added the viewOnMap i18n key to all 21 locales. 2. Scalar docs fonts: /api/docs loaded Inter and JetBrains Mono from fonts.scalar.com. Scalar now renders with withDefaultFonts: false and both --scalar-font and --scalar-font-code pinned to system stacks; fonts.scalar.com removed from the docs CSP font-src. Verified by injecting GET /api/docs/: config carries withDefaultFonts false and the served page has no fonts.scalar.com reference. 3. Editor Google Fonts: the editor font picker built fonts.googleapis.com stylesheet URLs for 25 web fonts the served CSP already blocked. The remote loading path is deleted; the picker now offers system fonts only, with a SELF_HOSTED_FONTS seam (FontFace API, same origin) for bundling fonts later. Unknown families saved in old documents fall back to the browser default. 4. Python sidecar fails closed on model downloads: new packages/ai/python/offline_guard.py gates every runtime download fallback (inpaint, outpaint, restore, noise_removal, detect_faces, enhance_faces, face_landmarks, red_eye_removal, remove_bg, ocr, transcribe, upscale) behind SNAPOTTER_ALLOW_MODEL_DOWNLOAD=1 with an actionable error. Bundled models keep working untouched. 5. OCR and transcription library-internal downloads: unbundled PaddleOCR language and detection fallbacks now raise the guard error naming the language instead of resolving models over the network; faster-whisper gets local_files_only when downloads are off. 6. GFPGAN and CodeFormer cwd-relative weights: facexlib and codeformer-pip resolve helper weights relative to the process cwd and fetch them from GitHub when absent. They are now symlinked from the installed bundle files under MODELS_PATH/gfpgan/facelib before the libraries load, failing closed when unresolvable. Defense in depth: HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 are set in the runtime image and in the sidecar spawn env; install_feature.py lifts them for user-initiated bundle installs and restores them afterwards (it can run in-process inside the dispatcher). SNAPOTTER_ALLOW_MODEL_DOWNLOAD is documented in .env.example, default off. Validation: typecheck 9/9 workspaces, Biome clean on touched files, 5178 unit tests pass, py_compile on all touched scripts, guard behavior exercised in both dispatcher exec and per-request import modes, zero remaining runtime references to the three hosts. Docker build and live AI inference need post-merge verification on the GPU host. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 * fix: allow AI model downloads by default, make strict offline mode opt-in Product call: ease of use first. The download gating from the previous commit inverts its default: runtime model fetches (public model weights only, never user data) are allowed out of the box so AI tools self-heal, and SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 becomes the explicit strict offline mode for airgapped deployments, where every fallback raises the actionable error instead of fetching. Changes: offline_guard blocks only on an explicit 0/false; the unconditional HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE image ENV is removed and bridge.ts sets those flags for the sidecar only in strict mode; .env.example documents the new default; install_feature's lift/restore stays. All bundled-path preferences, pre-existence checks, and symlink pre-placement remain, so installed bundles never trigger a download. The OSM, Scalar font, and editor font fixes are unchanged. Validation rerun: typecheck 9/9, Biome clean on touched files, 5178 unit tests pass, py_compile on touched scripts, guard behavior verified for unset/1 (allowed) and 0/false (blocked with the new message). Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
const POSTHOG_ORIGINS = ["https://us.i.posthog.com", "https://us-assets.i.posthog.com"];
|
||||
const SENTRY_ORIGINS = ["https://*.ingest.us.sentry.io"];
|
||||
const SCALAR_FONT_ORIGIN = "https://fonts.scalar.com";
|
||||
|
||||
/**
|
||||
* Build a Content-Security-Policy header value.
|
||||
*
|
||||
* Fonts and images are self-hosted only: the docs page renders Scalar with
|
||||
* withDefaultFonts disabled (no fonts.scalar.com), and the metadata panel
|
||||
* shows GPS coordinates as text with a user-initiated map link instead of
|
||||
* auto-loading OpenStreetMap tiles.
|
||||
*
|
||||
* Notes on 'unsafe-inline':
|
||||
* - style-src: Required because the React SPA uses inline styles extensively
|
||||
* (100+ occurrences across 45+ components). Removing it would break the UI.
|
||||
@@ -14,7 +18,7 @@ const SCALAR_FONT_ORIGIN = "https://fonts.scalar.com";
|
||||
*/
|
||||
export function buildCsp(isDocs: boolean): string {
|
||||
const connectSrc = ["'self'", "blob:", "data:", ...POSTHOG_ORIGINS, ...SENTRY_ORIGINS].join(" ");
|
||||
const fontSrc = isDocs ? `'self' data: ${SCALAR_FONT_ORIGIN}` : "'self' data:";
|
||||
const fontSrc = "'self' data:";
|
||||
const scriptSrc = isDocs
|
||||
? "'self' 'unsafe-inline' https://us-assets.i.posthog.com"
|
||||
: "'self' https://us-assets.i.posthog.com";
|
||||
@@ -23,7 +27,7 @@ export function buildCsp(isDocs: boolean): string {
|
||||
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; media-src 'self' blob:; connect-src ${connectSrc}; font-src ${fontSrc}; object-src 'none'; base-uri 'self'; form-action 'self'`;
|
||||
}
|
||||
|
||||
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data: https://tile.openstreetmap.org; media-src 'self' blob:; connect-src ${connectSrc}; font-src ${fontSrc}; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`;
|
||||
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; media-src 'self' blob:; connect-src ${connectSrc}; font-src ${fontSrc}; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`;
|
||||
}
|
||||
|
||||
export function getSecurityHeaders(): Record<string, string> {
|
||||
|
||||
@@ -189,6 +189,11 @@ export async function docsRoutes(app: FastifyInstance): Promise<void> {
|
||||
hideClientButton: true,
|
||||
showDeveloperTools: "never",
|
||||
theme: "default",
|
||||
// Scalar's default typography loads Inter/JetBrains Mono from
|
||||
// fonts.scalar.com at page load. Disable it and pin both font variables
|
||||
// to local system stacks so the docs page makes no third-party requests
|
||||
// (the docs CSP font-src is 'self' data: accordingly).
|
||||
withDefaultFonts: false,
|
||||
customCss: `
|
||||
:root {
|
||||
--scalar-color-1: #09090b;
|
||||
@@ -200,6 +205,7 @@ export async function docsRoutes(app: FastifyInstance): Promise<void> {
|
||||
--scalar-background-3: #e4e4e7;
|
||||
--scalar-border-color: #e4e4e7;
|
||||
--scalar-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--scalar-font-code: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
/* Hide the "Powered by Scalar" sidebar footer link. Scalar exposes no
|
||||
config flag for it (unlike the cloud buttons disabled above). */
|
||||
|
||||
Reference in New Issue
Block a user