Regenerate the social/OG card (200+ tools, Private file processing, self-hosted infrastructure) and sync to landing/web/docs; update banner, press kit, package + OpenAPI + Docker Hub descriptions, a leaked docs count, and the English About string.
* docs: add 1.x-to-2.0 migration guide and upgrade notice
Adds MIGRATING.md with backup and upgrade steps, plus a short
"coming from 1.x?" callout in README and the docs upgrade guide
pointing existing users at it.
* fix: replace stale image-only and pre-rename data copy across product
SnapOtter grew from an image-only tool into a 5-modality suite
(Image, Video, Audio, PDF, Files), but copy in several places never
caught up. Fixes:
- dropzone.defaultFormats (i18n): every non-English locale still had
the pure pre-2.0 image-only format list; English omitted Files
entirely. Corrected across all 21 locales.
- settings.about.appDescription (i18n): "document, and data" workflow
copy updated to "PDF, and file" across all 21 locales.
- constants.ts: Files category's raw name was still "Data Files".
- Landing hero subtitle, JSON-LD schema, llms.txt, and 7 spots in the
competitor-comparison pages.
- Docs: VitePress config, supported-formats, deployment, and an
architecture.md modality-naming nit.
- OpenAPI description, root package.json description/keyword, and a
GitHub issue template dropdown option.
DOCKERHUB.md's separate "v1.x, image tools only" pre-release notice
is left untouched since 2.0 hasn't published to Docker Hub yet.
* test: update dropzone format-hint assertion to match corrected copy
The expected string still had the stale image-only/duplicated
PDF-Documents text from before the dropzone.defaultFormats fix.
* fix(landing): correct PDF tool count to 28 in alternatives copy
The pdf section has 28 tools (section.test.ts asserts bySection('pdf')=28). PR #363 corrected the docs breakdown but the alternatives pages still said 40 PDF tools (the document-modality count, not the pdf section) with 200 for the rest. Update to 28 PDF tools and 212 for the non-PDF remainder.
* chore: use "200+ tools" for the tool-count claim across public surfaces
Replaces the exact '240 tools' count (which drifts as tools are added) with the stable '200+ tools' on README, the Docker Hub overview, landing pages, the docs site (meta, homepage, search), the API self-description, the demo OG tag, llms.txt, package.json, the branding readme, and the en/nl app strings. Per-modality breakdown tables stay exact. Leaves the architecture doc's technical 'tool routes' figure, an internal vitest comment, and a QA report line unchanged. Updates the two tests that assert the docs strings.
Lands five integrated branches: pipeline templates (#355), analytics opt-out (#354), 83 conversion presets bringing the catalog to 240 tools (#356), self-hosted positioning (#353), and e2e modernization (#351).
Integration fixes: aligned stale web analytics tests with the opt-out/allow-list model, closed 3 CodeQL incomplete-sanitization alerts in the i18n generator, resolved settings/index/docs/format-matrix conflicts, and corrected tool counts to 240.
* fix(deps): patch gray-matter onto js-yaml 4.2.0 (close js-yaml DoS alert)
js-yaml 3.14.2 (quadratic-complexity DoS in merge-key handling, GHSA
patched only in 4.2.0) was kept in the tree by a scoped pnpm override
"gray-matter>js-yaml": "^3.14.1" that exempted gray-matter from the
global js-yaml>=4.2.0 override. gray-matter is a build-time-only
transitive dep of the docs site (vitepress-plugin-llms,
@sugarat/theme-shared) and pinned 3.x because it calls the removed
yaml.safeLoad / yaml.safeDump APIs.
Remove the exemption so gray-matter resolves js-yaml 4.2.0, and add a
pnpm patch renaming safeLoad->load / safeDump->dump (the 4.x
equivalents; load is safe by default). js-yaml 3.x is now gone from the
lockfile.
Verified: gray-matter parse+stringify smoke test passes on 4.2.0; full
VitePress docs build green (177 pages, llms plugin parses all tool
frontmatter with no safeLoad/safeDump error).
* docs(ai): document rembg 2.0.69 pin and advisory non-reachability
The patched rembg 2.0.75 pulls a numpy 2.x closure (numpy>=2.3,
scipy>=1.16, scikit-image>=0.26) that is incompatible with the
numpy==1.26.4-locked AI stack (realesrgan 0.3.0 and codeformer-pip 0.0.4
break on numpy 2.x). Both open rembg advisories are unreachable in this
codebase: rembg is used purely as a library (never the `rembg s`
server), and new_session() only receives allowlisted model names
(remove_bg.py ALLOWED_MODELS), never user-controlled paths. Record this
rationale next to the pin; the Dependabot alerts are dismissed as
not_used.
jsdom@29.1.1 imports undici's internal lib/handler/wrap-handler.js, removed
in 8.5.0 -- so forcing undici>=8.5.0 broke ~27 jsdom unit files. Removing the
override lets pnpm resolve undici to the latest patched in-range versions
(6.27.0/7.28.0/8.5.0, all above the vulnerable ranges) while jsdom keeps
working. Unit suite restored to 188 files / 4908 tests.
Add scoped axe accessibility audit (a11y.spec.ts, device-a11y.spec.ts)
scanning home, one tool per modality, editor, and login across desktop
chromium and mobile-chromium in EN and AR locales. Uses a committed
baseline (a11y-baseline.json) to gate on NEW critical/serious violations
while documenting existing debt.
Add device-visual.spec.ts with curated screenshots (home, resize tool,
settings dialog) on mobile-chromium and tablet-chromium. Six darwin
baselines generated; linux baselines deferred to the existing
update-visual-baselines workflow.
Trivial a11y fixes applied:
- Login page: outer div -> main (fixes landmark-one-main, reduces region)
- Editor page: outer div -> main for both desktop and mobile gate
- AppLayout main: add tabIndex={-1} for skip-link focusability
Updated DEVICE_SPECS regex to route device-visual and device-a11y specs.
Added @axe-core/playwright as a devDependency.
Bump all workspace package versions and APP_VERSION to 2.0.0, marking the
official 2.0 release. Removes the stale 1.x .release-notes.md artifact
(semantic-release regenerates release notes). The 2.0/multimodality docs
and rebrand already landed on main via #254 and #261, so this carries only
the version designation forward from the rebrand branch.
BREAKING CHANGE: SnapOtter 2.0 - the platform re-architecture (Postgres 17 +
Redis 8 + BullMQ durable jobs, 157 tools across five modalities) is the 2.0
release line, replacing the 1.x SQLite single-container architecture.
* fix(docs): keep gray-matter on js-yaml 3 so the docs site builds
The js-yaml >=4.2.0 override from #257 forced js-yaml 4 onto gray-matter (used by vitepress and vitepress-plugin-llms), which calls the removed yaml.safeLoad and broke `vitepress build`. Scope a gray-matter>js-yaml ^3.14.1 override so gray-matter keeps the v3 API (build-time, trusted frontmatter only) while app code stays on js-yaml 4.2.0+.
* docs: add per-tool reference pages for all 157 tools, with a modality sidebar
Generate /tools/<id> pages for the 104 tools that lacked one (video 29, audio 17, document 36, data 10, and 12 newer image tools), matching the existing page format (API endpoint, parameters from the OpenAPI spec, curl example, response, notes). Async/AI tools document the 202+SSE flow and feature-bundle requirement.
Sidebar: add Video / Audio / PDF & Documents / Data groups with per-tool links, fold the 12 new image tools into the existing image categories, and replace the placeholder rest.md-anchor group. Docs site builds cleanly (157 pages, no dead links).
Resolve the actionable Dependabot alerts via pnpm overrides (for transitive deps) and a Python pin bump.
- pnpm overrides: esbuild >=0.28.1 (the lone high-severity alert), @babel/core >=7.29.6, @opentelemetry/core >=2.8.0, js-yaml >=4.2.0, qs >=6.15.2, uuid >=11.1.1, yaml >=2.8.3
- rembg 2.0.62 -> 2.0.75 in requirements.txt and requirements-gpu.txt
Verified: pnpm install, typecheck, lint, and full build all pass.
NOT included: the astro advisory requires Astro 5 -> 6 (a major, breaking framework upgrade), which warrants its own migration PR rather than a security bump.
* docs: rebrand from image-only to multi-modality across docs and metadata
SnapOtter expanded from image-only to 157 tools across 5 modalities
(image, video, audio, document/PDF, data). Update all product-level
copy, metadata, and i18n that still framed it as an image-only tool.
- README, package.json, root llms.txt: multi-modality framing, 157 tools
- OpenAPI info + tags, generated /llms.txt tagline (docs.ts)
- VitePress docs site: hero, getting-started, architecture, security,
deployment, configuration, developer, supported-formats
- i18n: 10 product keys across all 21 locales (hero, app description,
privacy notes, AI features, progress messages, getting-started)
- web/demo/landing meta + privacy copy, COMMUNITY_GUIDE, .env.example
Stale tool counts (53/50+/52/70+/35) corrected to 157 throughout.
Database/container deployment claims left unchanged (out of scope).
* docs: fix stale post-rebrand test assertions and README language list
- tests/e2e-docs/homepage.spec.ts: assert the current docs homepage (file toolkit, 157 tools, 5 modalities) instead of the old image-only strings
- tests/unit/api/docs-route.test.ts: sync the reproduced llms.txt tagline with docs.ts
- README.md: 21 languages with the correct list (add Swedish and Chinese Traditional, drop Czech which is not supported)
* docs: correct 2.0 architecture references (Postgres 17 + Redis 8, 3-container stack)
The docs and metadata still described the 1.x stack (SQLite, single container, p-queue). Update them to the current 2.0 reality.
- README: replace the broken single-container `docker run` quick-start with the real Docker Compose stack (app + Postgres 17 + Redis 8); fix the "no Redis, no Postgres" feature bullet
- package.json: description no longer claims a single container
- apps/docs: rewrite database.md for Postgres; configuration.md DB_PATH -> DATABASE_URL + REDIS_URL; architecture.md SQLite/p-queue/better-sqlite3 -> Postgres/BullMQ/pg and add media-engine + doc-engine; developer/security/deployment/docker-tags/getting-started/contributing compose examples now include postgres + redis; index.md + api/ai.md AI count 16 -> 19
- SECURITY.md: Drizzle (SQLite) -> (PostgreSQL)
- landing: enterprise/FeatureHighlights single-container wording; TrustSignals/ToolGrid 150+ -> 157 (dynamic); Pricing/FAQ 15 -> 19 AI tools
* docs(api): document all video, audio, document, and data tool endpoints in OpenAPI
The spec covered only image tools; the Scalar UI and the generated /llms.txt and /llms-full.txt inherited that gap. Add the 104 missing tool endpoints so the API docs match the code.
- Video: 29 endpoints (most long/async; auto-subtitles is AI)
- Audio: 17 (transcribe-audio is AI)
- Document/PDF: 36 (ocr-pdf is AI; conversions are long/async)
- Data: 10
- Image: 12 newer tools (background-replace, blur-background AI; histogram/lqip-placeholder/sprite-sheet custom responses; barcode-generate uses a JSON body)
Each schema is derived from the tool's Zod validator and executionHint (fast -> 200, long -> 202+SSE, AI adds 501 FeatureNotInstalledError, multi-file inputs as arrays), referencing the existing shared schemas. Tool path entries: 64 -> 168. Spec parses as valid YAML with no duplicate paths and only known $refs.
* feat(infra): add dev compose stack with postgres and redis
* fix(infra): comment dev env defaults until wired; harden dev compose restart and start_period
* chore(deps): add pg driver and testcontainers for postgres migration
* feat(db): translate schema to drizzle pg-core (timestamptz, boolean, pgEnum, jsonb)
Schema translation (apps/api/src/db/schema.ts):
- sqlite-core -> pg-core, all 10 tables preserved 1:1
- integer(mode:'timestamp') -> timestamp({ withTimezone: true })
- integer(mode:'boolean') -> boolean
- jobs.status text enum -> pgEnum('job_status') with same 4 values
- 7 columns changed from text to jsonb: jobs.inputFiles, jobs.settings,
pipelines.steps, apiKeys.permissions, roles.permissions,
auditLog.details, userFiles.toolChain
- settings.value stays text, jobs.error stays text, jobs.progress stays real
jsonb call-site sweep (removed JSON.stringify on writes, JSON.parse on reads):
- apps/api/src/routes/roles.ts: permissions read/write (3 sites)
- apps/api/src/routes/api-keys.ts: permissions write + read (2 sites)
- apps/api/src/routes/audit-log.ts: details read (1 site)
- apps/api/src/routes/pipeline.ts: steps write + read (2 sites)
- apps/api/src/routes/progress.ts: inputFiles write (2 sites)
- apps/api/src/routes/tool-factory.ts: toolChain read + write (2 sites)
- apps/api/src/routes/user-files.ts: toolChain read + write (4 sites)
- apps/api/src/permissions.ts: roles.permissions read (1 site)
- apps/api/src/lib/audit.ts: details write (1 site)
- apps/api/src/plugins/auth.ts: apiKeys.permissions read (1 site)
* refactor(db): type jsonb columns via $type and note raw CTE conversion requirements
* feat(db): archive sqlite migrations and generate postgres baseline
* chore(db): dockerignore legacy migrations, add archive breadcrumb, fix trailing newline
* feat(db): pg pool connection, advisory-locked boot migrations, DATABASE_URL config
* fix(db): friendly fatal on unreachable postgres, idempotent closeDb, lock-key convention note
* refactor(db): async drizzle calls in plugins, lib, permissions
* fix(api): analytics never throws, typed permission guard, single-query session invalidation
* refactor(db): async drizzle calls across all routes and bootstrap
Convert every route file and index.ts from sync SQLite drizzle
patterns to async node-postgres drizzle:
- .all() removed (bare await on select)
- .get() converted to destructured [row] = await ...
- .run() removed (bare await on insert/update/delete)
- .changes replaced with .rowCount (null-guarded) in progress.ts
- sqlite import removed from user-files.ts; raw CTEs converted to
await db.execute(sql`...`) with postgres-dialect recursive CTEs
- ChainRow types updated: tool_chain is parsed jsonb (string[] | null),
created_at is Date (timestamptz) with no * 1000 conversion
- All requirePermission() guard calls awaited (security: unawaited
async guard returns truthy Promise, bypassing permission check)
- All hasEffectivePermission() and getPermissions() calls awaited
- All auditLog() calls awaited (preserves write-before-response order)
- trackEvent() and captureException() left un-awaited (fire-and-forget
by design, guaranteed never-throw)
- ensureAnonymousUser(), startCleanupCron(), recoverStaleJobs() awaited
in bootstrap sequence
- ensureInstanceId() and ensureDefaultSettings() made async
Files converted: 14 (index.ts + 12 route files + tools/index.ts)
* fix(db): await async checkStorageQuota in user-files upload/save routes
* fix(db): await checkStorageQuota in save-result route (missed second call site)
* feat(db): sqlite-to-postgres migrator with CLI and first-boot import
* fix(db): migrator error context, honest force semantics, boot-hook fatal, null-variance tests
* test: run suite against per-file postgres databases via testcontainers
- Add tests/global-setup.ts: spins up a Postgres testcontainer,
creates a migrated template database once per vitest run.
- Rewrite tests/setup/per-fork-env.ts: each test file (forks pool)
clones the template into its own database via CREATE DATABASE ...
TEMPLATE, preserving the same per-file isolation granularity.
- Update vitest.config.ts: add globalSetup, pg alias, update comment.
- Fix tests/integration/test-server.ts: remove DB_PATH mkdir, async
runMigrations, async db operations, remove SQLite WAL checkpoint.
- Fix 21 unit test db/index mocks: add pool and closeDb exports.
- Fix 8 unit test files: add async/await for now-async permission,
audit, and analytics functions.
- Fix 18 integration test files: convert sync .run()/.all()/.get()
to async drizzle patterns, add async to callbacks.
- Production change: apps/api/src/routes/teams.ts: cast COUNT(*)
to ::int so Postgres returns a number instead of bigint string.
* fix(db): seed built-in roles, reject NUL bytes, cast COUNT, serialize job persists
- Seed built-in roles (admin, editor, user) at boot via ensureBuiltinRoles()
with onConflictDoNothing, restoring data that legacy SQLite migration 0007
provided via INSERT statements (the pg baseline is DDL-only).
- Reject NUL bytes in login credentials with 401 (postgres rejects \x00 in
text columns; valid usernames never contain NUL, matching 1.x behavior).
- Cast COUNT(*)::int in user-files, audit-log, and roles listing queries so
postgres returns a JS number instead of bigint-as-string.
- Serialize fire-and-forget job progress DB writes per jobId so the final
"completed" status is never overwritten by a late-arriving "processing"
write (race condition exposed by async postgres round-trips).
* test: fix teams race, seed roles in test server, poll for job status
- Add missing await to resetTeams() in teams PUT beforeEach (the async
delete raced with the subsequent insert under postgres).
- Call ensureBuiltinRoles() in test server bootstrap so integration tests
have the same built-in roles as production.
- Replace fixed 100ms flushPersist delay with a polling helper that waits
for terminal job status, eliminating timing-dependent failures caused by
postgres network round-trip latency.
* test: make heic temp-file cleanup assertion resilient to concurrent workers
Use a set-based diff instead of raw file count when checking that
decodeHeic cleans up temp files. Other concurrent test workers can
create heic-in-*/heic-out-* files in the shared tmpdir, inflating the
"after" count and causing spurious failures under full-suite load.
* fix(db): align builtin-role seed to post-0010 legacy state; test polish
* feat(docker): three-container compose (app, postgres, redis) with boot wait and migrations
* fix(docker): set TEST_DATABASE_URL so containerized tests skip testcontainers
* chore(docker): test compose project name, clearer 1.x upgrade comment, unref probe timer
* feat(enterprise): enforce D15 license boundary; move s3 storage into packages/enterprise
* fix(enterprise): restore lazy aws-sdk loading; community installs load no s3 code at boot
* fix(enterprise): boundary check catches dynamic imports; document getS3 concurrency
* feat(db)!: SnapOtter 2.0 phase 1 foundation: postgres, migrator, compose stack
BREAKING CHANGE: SQLite is no longer the runtime database. Deployments now
require Postgres (and Redis, used from phase 2). Existing installs migrate
with SQLITE_MIGRATE_PATH or 'pnpm --filter @snapotter/api migrate:sqlite'.
* fix(ci): postgres service + fresh e2e database per run; ignore unfixable torch CVE-2025-3000
Closes the "e2e never runs in CI" hole. Adds per-PR e2e smoke gate,
nightly full-suite workflows, parallel vitest forks (per-fork DBs),
Playwright parallel/serial/visual projects against production builds,
metadata-generated test suites (drift guards, hostile inputs, format
matrix, pairwise settings, property-based fuzz), Stryker mutation
testing, Schemathesis API fuzz, coverage ratchet, and fixes for three
session-poisoning bugs that caused 200+ serial-bucket failures.
Bug fix included: favicon/split/bulk-rename could hang clients forever
when ZIP streaming failed after reply.hijack().
- fix(ocr): change paddlepaddle-gpu from --extra-index-url to --index-url
for the CUDA 12.6 package index. With --extra-index-url, pip could
resolve from PyPI (CUDA 11 build) instead of the cu126 index, causing
"libcusolver.so.11: undefined symbol" errors on CUDA 12 containers.
- revert version to 1.17.1 (v1.17.2 release was deleted)
- fix(rate-limit): treat RATE_LIMIT_PER_MIN=0 as unlimited (50k/min)
instead of blocking all requests. @fastify/rate-limit interprets
max:0 as "allow zero requests," breaking fresh container startups.
- fix(docker): add libgles2 for MediaPipe face detection tools.
blur-faces, red-eye-removal, enhance-faces, and passport-photo
failed with "libGLESv2.so.2 not found" on all headless containers.
- fix(docker/arm64): remove conflicting system libheif1 to avoid
ABI symbol mismatch with our custom libheif 1.21.2 build.
heif-convert failed with "undefined symbol: heif_get_plugin_directories."
- fix(docker/arm64): pre-install wheel+setuptools in base Python venv
so basicsr can build from source on arm64 (no pre-built wheel).
This unblocks upscale-enhance and photo-restoration bundles.
Token-Permissions (0 -> 10): Set permissions: {} at workflow top level
across all 7 workflows, moved write scopes to per-job minimum.
SAST (0 -> 10): Added CodeQL workflow for JavaScript/TypeScript and
Python analysis on push, PR, and weekly schedule.
Vulnerabilities (0 -> ~8): Added 13 pnpm overrides to patch transitive
dependency vulnerabilities (38 -> 2 remaining, both in dev-only tools).
Pinned-Dependencies (5 -> 8-9): Pinned all Docker FROM images to SHA
digests, pinned pip-audit version in CI, pinned pip version in
Dockerfile.
- Bump all workspace package versions to 1.17.0
- Update APP_VERSION constant and OpenAPI spec
- Update AI tool count from 15 to 16 across docs and i18n
- Update tool table with AI Canvas Expand, Meme Generator, Beautify
- Add image editor, OIDC, and 20 languages to README features
- Add release notes for v1.17.0
- Add JSON-LD structured data and SEO improvements to landing/docs
Auth: login rate limit 30/min (was 500), global rate limit 1000/min (was
unlimited), password/username max lengths on all Zod schemas, session
invalidation on role change, API key legacy scan bounded to 100 keys.
SVG: hardened regex sanitizer with CDATA stripping, XML entity decoding,
set/animate/iframe/embed blocking, comprehensive data: URI blocking,
use element external href blocking. 11 attack payload fixtures added.
SSRF: fixed DNS rebinding TOCTOU by pinning resolved IPs via custom
HTTP/HTTPS agents. Added 6to4 and NAT64 to blocked IPv6 ranges.
Docker: capability dropping (cap_drop ALL + minimal cap_add), resource
limits (4g/8g mem, 512/1024 pids), healthcheck timeout, password
removed from startup banner, default password warning comments.
Network: CSP and HSTS applied in all environments (not just production),
stack traces removed from all error responses, internal paths stripped
from error details, per-route rate limits on uploads (60/min) and URL
fetches (200/hour).
Files: exclusive temp file creation (O_EXCL), disk space circuit
breaker, per-user storage quotas, settings payload 64KB size guard.
Python sidecar: script name allowlist in dispatcher, minimal environment
for subprocess spawns.
Dependencies: fixed 6 production CVEs (drizzle-orm, fastify, fast-uri,
@fastify/static, next, archiver/lodash). Pinned all GitHub Actions to
SHA hashes.
114 security tests added. Full OWASP Top 10 penetration test matrix
verified against production Docker container (30/30 pass after
hardening).
captureException now checks isRequestOptedIn before forwarding errors
to Sentry, closing a gap where server errors leaked to an external
service even when no user had consented. The PII scrubbing regex is
also fixed: he[ic]f? failed to match .heic due to word-boundary
behavior and is replaced with hei[cf]? which correctly covers .heic,
.heif, and .hei.
Adds 88 new analytics tests across unit, integration, and e2e layers
proving PostHog/Sentry are never invoked when analytics is disabled or
users have not consented, plus full 7-day reminder lifecycle coverage.
- Bump APP_VERSION to 1.15.11 (was hardcoded at 1.15.9, causing
health endpoint to report wrong version in Docker images)
- Fix cpu_fallback_packages() splitting --index-url into separate
pip install arguments, breaking torch install on CPU-only amd64
- Add Cloudflare Pages deployment for landing page (snapotter.com) and
docs (docs.snapotter.com)
- Create deploy-landing.yml and update deploy-docs.yml workflows
- Update CI to ignore apps/landing/** paths
- Fix logo transparency (remove white background) across all apps
- Recreate social-preview.png with SnapOtter branding
- Update all docs URLs from GitHub Pages to docs.snapotter.com
- Update VitePress config: light theme default, fix llms.txt paths
- Add .vitepress/cache/ and .env.* to gitignore