Commit Graph
100 Commits
Author SHA1 Message Date
SnapOtter 954cfb01a6 fix: test server registration gaps, Redis subscriber cleanup, atomic settings upsert 2026-06-14 14:30:35 +08:00
SnapOtter 29dd675f42 fix(enterprise): SCIM token generation, GDPR job cancellation, MFA replay, config redaction, encryption validation, SCIM rate limit
- Add POST/DELETE /api/v1/enterprise/scim/token for SCIM bearer token management
- Cancel active BullMQ jobs via requestCancel() before GDPR purge deletes DB rows
- Reject duplicate MFA enrollment when a pending (unverified) secret exists
- Add webhook_destinations to config export REDACTED_KEYS (contains auth headers)
- Validate DATA_ENCRYPTION_KEY and DATA_ENCRYPTION_KEY_PREVIOUS are 64-char hex at startup
- Add 1000 req/min Redis counter rate limit to SCIM auth middleware
- Document SIEM/webhook system coexistence in siem-forward.ts
2026-06-14 14:03:47 +08:00
SnapOtter d86e4585e4 feat(enterprise): add unified webhook system with admin alerts 2026-06-14 12:21:46 +08:00
SnapOtter 0f883fe853 feat(enterprise): add configuration export/import with dry-run and dependency validation 2026-06-14 12:17:40 +08:00
SnapOtter 03e12e6f0b feat(enterprise): add upgrade management endpoints (version, migrations, readiness) 2026-06-14 12:14:52 +08:00
SnapOtter 2aea351f70 feat: add backup status tracking endpoints 2026-06-14 12:11:23 +08:00
SnapOtter 239f85f098 feat: add per-user rate limiting and concurrent job limits
Redis sliding window (sorted set) enforces per-user request rate limits
via the rateLimitPerUser DB setting. Concurrent job limits checked at
the HTTP layer before enqueue via maxConcurrentJobsPerUser setting.
Both default to 0 (unlimited).
2026-06-14 12:09:25 +08:00
SnapOtter fb14f41512 feat: expand Prometheus metrics with request duration, storage, and auth counters 2026-06-14 12:06:06 +08:00
SnapOtter 6237684ec8 feat: extend health endpoints with disk space, S3, storage, and backup checks 2026-06-14 12:01:29 +08:00
SnapOtter 7f62b1bf12 fix: resolve 6 bugs from enterprise audit (SIEM cursor, legal hold join, SCIM role, GDPR self-purge, export OOM, quota check) 2026-06-14 11:04:44 +08:00
SnapOtter b9dac59736 fix: correct SAML idpCert property name for @node-saml/node-saml v5 2026-06-13 23:02:20 +08:00
SnapOtter d397f57667 feat(web): add MFA login prompt and security settings UI
Handle MFA challenge in login flow (TOTP code input after password
verification, recovery code hint, back navigation). Add admin security
settings section with session idle timeout, max sessions per user,
MFA policy selector, SSO enforcement toggle with break-glass username,
and password policy controls. Propagate new i18n keys to all 21 locales.
2026-06-13 23:00:33 +08:00
SnapOtter db6f7bf38a feat(enterprise): add IP allowlisting with CIDR matching and Redis cache
Enterprise-gated onRequest hook that restricts API access to
configured CIDR ranges using Node 22's native BlockList.

- Plugin (ip-allowlist.ts): builds a BlockList from the ipAllowlist
  setting, caches in-process, syncs across instances via Redis pub/sub.
  Exempt paths for health probes, SCIM, SAML/OIDC callbacks.
  Handles IPv4-mapped IPv6 (::ffff:x.x.x.x) transparently.
- Admin API (enterprise/ip-allowlist.ts): GET/PUT endpoints gated by
  security:manage permission and ip_allowlist feature flag.  Validates
  CIDRs, prevents self-lockout, emits IP_ALLOWLIST_UPDATED audit event.
- 32 unit tests covering CIDR matching, validation, exempt paths, IPv6,
  and edge cases (/0, /32, mapped addresses).
2026-06-13 22:54:06 +08:00
SnapOtter 1787be35fe feat(enterprise): add TOTP MFA with enrollment, verification, and recovery codes 2026-06-13 22:49:00 +08:00
SnapOtter a1b5c6d2c3 feat(enterprise): add SCIM 2.0 provisioning (Users + Groups) 2026-06-13 22:42:55 +08:00
SnapOtter 0c4468a004 feat(enterprise): add SSO enforcement mode with break-glass admin 2026-06-13 22:32:16 +08:00
SnapOtter 54132d1833 feat(enterprise): add SAML 2.0 SSO with SP-initiated login
Implements SAML SSO using @node-saml/node-saml, gated behind
SAML_ENABLED env var and the saml_sso enterprise license feature.

- SAML env vars (entity ID, callback URL, IdP SSO URL, IdP cert,
  auto-create/auto-link users, default role, provider name,
  username/email attribute mapping) with validation in superRefine
- SAML plugin with three routes: metadata (GET), login (GET),
  and ACS callback (POST with form-urlencoded content type parser)
- Callback uses the shared external-auth resolver for user
  resolution (same pattern as OIDC: match/link/create/deny)
- Auth config endpoint exposes samlEnabled and samlProviderName
- Session loginMethod detection updated for SAML auth provider
- Frontend login page shows SAML SSO button when enabled
- i18n strings for SAML error messages across all 21 locales
2026-06-13 22:27:56 +08:00
SnapOtter 6920035f5a refactor: extract external auth resolver from OIDC for SAML reuse
Move user resolution logic (match by externalId, auto-link by email,
auto-create with user limit check) into a shared module that both
OIDC and SAML callbacks can use. Includes sanitizeUsername and
findUniqueUsername helpers. Preserves all existing OIDC behavior
and audit events.
2026-06-13 22:20:22 +08:00
SnapOtter 8b349b0341 feat: make password policy configurable via admin settings 2026-06-13 22:12:38 +08:00
SnapOtter 9fa23f4543 feat: add per-tool permission model with category and per-tool modes 2026-06-13 22:11:03 +08:00
SnapOtter 3cc4ef6895 feat: add session idle timeout and concurrent session limit
Idle timeout: reads `sessionIdleTimeoutMinutes` from settings, tracks
last activity in Redis (with Postgres fallback on cache miss), and
invalidates sessions that exceed the configured idle window.

Concurrent session limit: reads `maxSessionsPerUser` from settings
and evicts oldest sessions (FIFO) when a new login exceeds the cap.

Both features are opt-in (disabled when value is 0 or absent).
2026-06-13 22:07:31 +08:00
SnapOtter 7ed043ec53 feat(db): add identity columns (lastActivity, toolPermissions, TOTP) 2026-06-13 22:04:14 +08:00
SnapOtter 8eefa46981 chore: install @node-saml/node-saml and otpauth for Phase 3 identity 2026-06-13 22:04:03 +08:00
SnapOtter c7e9b1ddc6 feat(web): add storage dashboard and team retention settings
Add per-team storage breakdown table to the usage section (from
GET /api/v1/admin/usage teamStorage data). Add storage quota (MB)
and retention (hours) inline fields to the teams section, saving
via PUT /api/v1/teams/:id. Add i18n keys to all 21 locale files.
2026-06-13 21:20:12 +08:00
SnapOtter dd2a50799a feat(enterprise): add GDPR user/team data purge with audit redaction 2026-06-13 21:13:22 +08:00
SnapOtter b6a8226837 feat(enterprise): add GDPR user data export (async) 2026-06-13 21:07:28 +08:00
SnapOtter fa7da7ce0c feat(enterprise): add legal hold with cleanup bypass 2026-06-13 21:01:04 +08:00
SnapOtter b60f550b3f feat(enterprise): add per-team retention overrides with deleteAfter
Compute a deleteAfter timestamp on job creation when the enterprise
team_retention_overrides feature is enabled. The cleanup sweep now
deletes storage for jobs past their deleteAfter deadline, running
independently of the global TTL setting.
2026-06-13 20:55:17 +08:00
SnapOtter d064286559 feat: add team-level storage quotas with enforcement on upload and save 2026-06-13 20:49:28 +08:00
SnapOtter aaa8a37c9b feat: add storage usage tracking with DB counters and reconciliation
Increment users.storageUsed on file upload/save, decrement on delete
(per-user via GREATEST to prevent negatives). Add per-team storage
breakdown to GET /api/v1/admin/usage. Weekly reconciliation job
(3 AM Sunday) recomputes counters from actual userFiles sums.
2026-06-13 20:43:52 +08:00
SnapOtter 7e5843cd40 feat(db): add data lifecycle columns (deleteAfter, legalHold, quotas, retention) 2026-06-13 20:39:38 +08:00
SnapOtter 3b7f44e50e fix: sort enterprise exports for Biome lint compliance 2026-06-13 17:07:49 +08:00
SnapOtter de7bbcc18d test: update permission count tests for new enterprise permissions 2026-06-13 17:07:39 +08:00
SnapOtter 1cf1f47d6f feat: add request correlation IDs to audit logs and response headers 2026-06-13 17:04:27 +08:00
SnapOtter c1dc27f248 feat(enterprise): add audit log archival with crash-safe state machine 2026-06-13 16:57:20 +08:00
SnapOtter d3f30a2f5d feat(enterprise): add SIEM webhook forwarding with circuit breaker 2026-06-13 16:53:32 +08:00
SnapOtter ab88b9ad0d feat: add webhook delivery module with retry and backoff 2026-06-13 16:49:47 +08:00
SnapOtter 895e29e93f feat(enterprise): add tamper-resistant audit mode with HMAC integrity 2026-06-13 16:48:09 +08:00
SnapOtter 913dd6bbe1 feat(enterprise): add audit log export endpoint (CSV/JSON) 2026-06-13 16:45:11 +08:00
SnapOtter c6f9a29687 feat(web): complete audit log viewer with IP column and all event types 2026-06-13 16:42:10 +08:00
SnapOtter 3016571c2b feat(web): add data retention settings to admin UI 2026-06-13 16:39:22 +08:00
SnapOtter 2520cdd556 feat: add AES-256-GCM encryption at rest for sensitive settings 2026-06-13 16:35:28 +08:00
SnapOtter 36f083ba64 feat(audit): add TOOL_EXECUTED logging with opt-in setting
Add isToolAuditEnabled() helper that checks the auditToolOperations
DB setting (off by default) or falls back to the enterprise
audit_export feature flag. The createToolRoute factory now emits a
TOOL_EXECUTED audit entry on successful tool execution when enabled,
using a fire-and-forget pattern so a failed audit write never blocks
the tool response.
2026-06-13 16:31:49 +08:00
SnapOtter 37b2b9c2ee feat(audit): extensible event type system with shared constants 2026-06-13 16:27:53 +08:00
SnapOtter 5d2f520d78 feat(audit): capture IP address, make TRUST_PROXY configurable 2026-06-13 16:25:58 +08:00
SnapOtter f6d5479334 test: add enterprise feature mock utilities 2026-06-13 16:22:01 +08:00
SnapOtter 86d6f50ea6 feat: add enterprise Phase 1-4 feature flags and new permissions 2026-06-13 16:20:24 +08:00
SnapOtter cafd2d8b65 feat(db): add audit integrity/requestId columns, user_preferences table, audit indexes 2026-06-13 16:17:17 +08:00
SnapOtter b8b6b0a44a fix(web): update dropzone to accept all file types, not just images 2026-06-13 15:50:52 +08:00
SnapOtter 3114323d07 fix(web): add modality to fuse search keys, merge docs+files in fullscreen grid 2026-06-13 15:43:50 +08:00
SnapOtter 888d243a65 feat(web): show icon + text labels on modality filter tabs 2026-06-13 15:39:32 +08:00
SnapOtter cc06c802c1 feat(web): icon-only modality tabs, merge documents and files 2026-06-13 15:35:19 +08:00
SnapOtter dab31d6e32 fix(web): wrap modality tabs to prevent overflow in narrow sidebar 2026-06-13 15:31:12 +08:00
SnapOtter 1181b1fe22 fix(web): restore preview panel rendering for all file types
The home-page preview panel always rendered ImageViewer regardless of the
uploaded file's modality, causing videos, audio, PDFs, and data files to
show "Preview not available". Root cause: the preview branching only
checked for a blob URL and assumed all files were images.

Fix: branch on currentEntry.previewKind so the existing MediaPlayerView,
DocumentView, and a file-info fallback are activated for their respective
modalities. Also widen the AppLayout dropzone from image-only to all file
types so non-image files can reach the home page in the first place.
2026-06-13 15:19:58 +08:00
SnapOtter 8adceaf92d feat(web): modality filter tabs in sidebar tool panel
Add horizontal filter tabs (All, Image, Video, Audio, Documents, Files)
above the search bar so users can instantly scope the 157-tool catalog to
one modality. Active tab uses the modality accent color. In "all" mode
file-modality tools remain merged into the document section; selecting a
specific modality tab shows only that modality with its own header.
2026-06-13 15:19:48 +08:00
SnapOtter 3db1b3bafd feat(web): stronger modality section visual hierarchy in tool catalog 2026-06-13 15:00:47 +08:00
SnapOtter 22cccd46c4 feat(web): fuzzy search with fuse.js for typo-tolerant tool discovery 2026-06-13 14:58:25 +08:00
SnapOtter 2a2151d7dd fix(doc-engine): treat qpdf exit code 3 (warnings) as success, not failure 2026-06-13 14:58:16 +08:00
SnapOtter 82fb798142 feat(web): modality tabs on fullscreen grid page
Add a row of filter tabs (All, Image, Video, Audio, Documents,
Data & Files) at the top of the fullscreen tool grid. Selecting a
tab filters the visible tools by modality, and the existing search
still works within the filtered set.

Also registers the Wand2 icon needed by the new video-effects
category header.
2026-06-13 14:45:16 +08:00
SnapOtter 7daaa11e7d refactor(shared): modality-scoped tool categories, dissolve ai category
Replace the flat 12-category structure with 27 modality-scoped
sub-categories. The cross-modality "ai" category is dissolved: each
AI tool moves into its natural modality's sub-category (e.g.
remove-background -> image/enhance, auto-subtitles -> video/subtitles,
ocr -> documents/convert, transcribe-audio -> audio/convert).

New category hierarchy:
- Image (8): essentials, adjustments, enhance, watermark, layout,
  utilities, optimization, format
- Video (5): video-edit, video-convert, video-effects,
  video-subtitles, video-metadata
- Audio (4): audio-edit, audio-convert, audio-effects, audio-metadata
- Documents (5): pdf-organize, pdf-edit, pdf-security, pdf-optimize,
  doc-convert
- Data (2): data, archives

No tool IDs, routes, or backend behavior changed. Category labels
updated across all 21 locales.
2026-06-13 14:45:06 +08:00
SnapOtter 492da820f8 fix(web): resolve feature install status sync and mutual exclusivity (#214) 2026-06-13 13:44:37 +08:00
SnapOtter 4e14e00a4c test(matrix): multi-modality format matrix covering video, audio, document, and data tools 2026-06-13 12:59:45 +08:00
SnapOtter e6e4ff921d test(matrix): settings variation matrix for tool-specific option combinations
Static data-driven test covering 90 tools with 551 variations. For each
tool that has configurable settings, every enum value, boundary number,
and boolean flag is exercised through the HTTP endpoint. The test asserts
no 500s occur and validates response shape for 200/202 results.

Coverage spans image (29 tools), video (20), audio (15), PDF/document
(21), and data (5) modalities.
2026-06-13 12:01:01 +08:00
SnapOtter cba3a7a392 chore: initialize CLA signature store 2026-06-13 11:35:36 +08:00
SnapOtter 92c9bea3c7 test(fixtures): add sample files for all 37 missing accepted formats
Video (13): mov, webm, avi, m4v, mts, m2ts, 3gp, flv, wmv, mpg, mpeg, ts, ogv
Audio (9): flac, aac, m4a, ogg, opus, wma, aiff, amr, ac3
Subtitle (1): ass
Image RAW (5): cr2 (Canon), nef (Nikon), arw (Sony), orf (Olympus), rw2 (Panasonic)
Document (7): doc, ppt, odp, xls, ods, htm, markdown
Data/archive (2): yml, zip

Media files generated locally with ffmpeg from existing tiny.mp4/wav.
RAW files downloaded from rawsamples.ch and filesamples.com.
Office files downloaded from filesamples.com.
All validated with ffprobe/file magic bytes.
2026-06-13 11:17:20 +08:00
SnapOtter 47b6984be0 fix(landing): add www redirect, noindex JS chunks, and website badge
Edge worker 301-redirects www.snapotter.com to snapotter.com to stop
Google from splitting domain authority. Adds X-Robots-Tag: noindex on
/_next/static/ and /_next/data/ paths to reclaim crawl budget wasted
on JS chunks. Adds snapotter.com website badge to README so the
highest-authority backlink (GitHub) points to the main site.
2026-06-13 11:09:02 +08:00
SnapOtter 82991b6a41 fix(api): fix batch user-files delete recursive CTE query
The refactored CTE used = ANY(${validIds}::uuid[]) which Drizzle
serializes as a scalar text parameter with a ::uuid[] cast. Since
the user_files.id column is text (not uuid), PostgreSQL rejects
the text = uuid comparison. Replace with IN (...) via sql.join()
to pass each ID as a plain text parameter matching the column type.
2026-06-13 10:55:14 +08:00
SnapOtter e2c57144dd fix(api): batch user-files delete to eliminate N+1 queries 2026-06-13 10:37:32 +08:00
SnapOtter 5397f9b21c fix(ai): broaden SSRF pre-scan regex to cover srcset, poster, formaction, @import 2026-06-13 10:36:12 +08:00
SnapOtter fc718c1684 fix(jobs): strip internal paths from all worker SSE error frames
Closes #71. Several error paths in the worker could leak internal
filesystem paths (/tmp/workspace, /data/ai/venv, /app) through SSE
frames, resultPayload objects, and Redis batch-error lists. The
existing stripInternalPaths call at worker.ts line 345 only covered
the single-file processToolJob catch block.

Wrapped 6 additional call sites:
- processPipelineStep: prevError from DB and catch errorMsg
- processPipelineFinalize: composed errorMsg reaching SSE,
  recordChildOutcome, and resultPayload
- processBatchChild: catch error reaching recordChildOutcome
  and resultPayload
- processBatchFinalize: manifest errorMsg from DB rows
- recordChildOutcome (batch-progress.ts): defense-in-depth
  strip before Redis rpush

Added 10 unit tests for stripInternalPaths covering /tmp, /data,
/app, /opt, /home, /workspace, multi-path messages, safe passthrough,
and pipeline-step wrapping.
2026-06-13 10:36:01 +08:00
SnapOtter b564932083 fix(api): wrap role rename and delete in transactions 2026-06-13 10:35:28 +08:00
SnapOtter 3c4433583b fix(tools): surface chars count in pdf-to-text result payload 2026-06-13 10:34:43 +08:00
SnapOtter 39543d6512 fix(docker): make venv bootstrap upgrade-aware with pip-freeze stamp
On 1.x-to-2.0 upgrade the /data/ai/venv already existed with stale
packages, so the entrypoint skipped the cp from /opt/venv and 2.0
Python tools failed with ImportError (#85).

Dockerfile now writes a SHA-256 of `pip freeze` to /opt/venv/.venv-version
at build time. The entrypoint compares that stamp against the copy on the
volume; on mismatch it does a clean nuke + re-copy and resets
installed.json so the UI correctly shows bundles as needing reinstall.
Models in /data/ai/models survive the refresh, so bundle reinstall only
reruns pip (model downloads are idempotent).

Backward compatible: if the image has no stamp (old build), the
entrypoint falls through to the existing skip-if-exists behavior.
2026-06-13 10:31:22 +08:00
SnapOtter 51666cdd5f feat(tools): 2.0 phase 5 wave 5b - ai pool: ocr-pdf, transcription, background composites (5 tools) (#226) 2026-06-13 10:19:47 +08:00
SnapOtter 6e1b9865f1 feat(tools): 2.0 phase 5 wave 5a - image gap-fill (11 tools) (#225) 2026-06-13 10:19:16 +08:00
SnapOtter fc7c1f850e feat(tools): 2.0 phase 5 wave 4 - office, ebooks, data, archives (14 tools) (#224) 2026-06-13 10:19:11 +08:00
SnapOtter 638288e196 feat(tools): 2.0 phase 5 wave 3b - audio depth (14 tools) (#222) 2026-06-13 10:19:06 +08:00
SnapOtter 5f98b48593 feat(tools): 2.0 phase 5 wave 3a - video depth (22 tools) (#221) 2026-06-13 10:19:00 +08:00
SnapOtter 2f39e38162 feat(tools): 2.0 phase 5 wave 2 - pdf depth (21 tools) (#220) 2026-06-13 10:18:55 +08:00
SnapOtter ae1337901d feat(tools)!: SnapOtter 2.0 phase 4 wave 1: 45 core tools across all modalities (#219) 2026-06-13 10:18:49 +08:00
SnapOtter d647d8ed19 feat(modality)!: SnapOtter 2.0 phase 3 modality framework: media/doc engines, pool routing, display modes (#218) 2026-06-13 10:18:39 +08:00
SnapOtter c451b939c7 feat(jobs)!: SnapOtter 2.0 phase 2 job spine: async queues, worker pools, object storage, admin dashboard (#217) 2026-06-13 10:17:13 +08:00
SnapOtterandGitHub 1c724d5d21 feat(db)!: SnapOtter 2.0 phase 1 foundation: postgres, migrator, compose stack (#216)
* 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
2026-06-13 10:15:23 +08:00
SnapOtterandGitHub 4ec39c556f test: testing overhaul -- CI e2e gates, parallel suites, generated matrices, mutation testing (#215)
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().
2026-06-10 22:01:13 +08:00
SnapOtter 3b8d529b44 fix(ci): revert rembg to 2.0.62 (2.0.75 requires numpy>=2.3)
rembg 2.0.75 pulls in numpy>=2.3.0 which conflicts with our pinned
numpy==1.26.4 and would break the entire AI dependency chain. The two
rembg CVEs (SSRF + path traversal) are in its server/CLI components
which we don't use; they're already in the pip-audit ignore list.
2026-06-10 21:21:39 +08:00
SnapOtter 8792080982 fix(deps): patch Dependabot security alerts
- Pillow 11.1.0 -> 12.2.0 (6 CVEs: OOB writes, decompression bomb, DoS)
- rembg 2.0.62 -> 2.0.75 (SSRF + path traversal in server component)
- @fastify/static ^8.1.0 -> ^9.1.3 (path traversal + route guard bypass)
- Remove redundant @fastify/static pnpm override
- Dismiss stale esbuild alert (already at 0.28.0)
- Dismiss file-type alert (16.5.4 is dev-only via @types/potrace)
2026-06-10 19:08:08 +08:00
SnapOtter 7c70c60b9e fix(ai): surface actionable fix for libGL.so.1 missing on headless installs
Proxmox LXC and other headless Linux installs lack libgl1, causing
all AI tools to show a misleading "install opencv-python-headless"
error even though the pip package is already installed. Detect the
libGL ImportError and suggest `apt-get install -y libgl1` instead.
2026-06-10 18:59:30 +08:00
SnapOtter 03600de4ef docs: correct API endpoints in llms.txt
The pipeline endpoints were documented as /api/v1/pipelines CRUD routes
that do not exist; the real routes live under /api/v1/pipeline/
(execute, save, list, delete, batch). The X-API-Key header was never
supported; API keys authenticate via Authorization: Bearer. Also adds
the openapi.yaml and llms-full.txt endpoints and the explicit batch
route.
2026-06-10 11:52:36 +08:00
SnapOtter 5cbfe61ea8 chore: remove superseded one-off scripts
generate-meme-thumbs.ts is superseded by build-templates.mjs which
generates thumbnails and the manifest in one pass. test-docker-fixes.sh
was a one-off Docker debugging script with no remaining references.
2026-06-10 10:48:01 +08:00
SnapOtter 78679c57f4 fix(test): update import paths for cleanup.test.ts after move to integration 2026-06-10 00:24:42 +08:00
SnapOtter 5b3b3c7632 fix(test): mock db/index.js in unit tests that import API modules
Five unit tests crashed because they imported modules that transitively
reached db/index.ts, which eagerly calls `new Database()` at module load
time. Added vi.mock for db/index.js matching the pattern used by 16
other passing API unit tests.

Moved cleanup.test.ts to tests/integration/ since it uses a real SQLite
database, runs migrations, and inserts rows.
2026-06-09 23:34:36 +08:00
SnapOtter 60e3ac2210 fix: resolve hardcoded /app paths and loosen mediapipe pin for native installs
Path resolution for the feature manifest and install script was hardcoded
to /app/..., which only works inside the Docker container. Native installs
(e.g. Proxmox at /opt/snapotter) hit "No such file or directory" errors.

Resolve both paths relative to the source file location via import.meta.url
so they work regardless of where the project is installed.

Also loosen mediapipe==0.10.21 to >=0.10.21 in requirements.txt and
requirements-gpu.txt to match the feature manifest. The exact pin has no
cp313 wheel, so it fails on Python 3.13 (Debian 13 default). mediapipe
0.10.35 ships py3-none universal wheels that resolve cleanly.

Reported-by: MickLesk (community-scripts/ProxmoxVE#14720)
2026-06-09 23:18:02 +08:00
SnapOtter edea82ba27 fix(ui): show repair UI when AI bundle models are broken (#214) 2026-06-09 23:09:34 +08:00
SnapOtter 8e15fe7a3f feat(i18n): add repair UI strings for broken AI bundles 2026-06-09 23:08:26 +08:00
SnapOtter 27a56c774b fix: allow reinstall of AI bundles with broken model files (#214) 2026-06-09 23:07:38 +08:00
SnapOtter 3196ba63d5 refactor: export verifyBundleModels with unit tests 2026-06-09 23:06:07 +08:00
SnapOtter 96764ec375 fix(test): align analytics-env test with ANALYTICS_ENABLED=true default
The test still expected ANALYTICS_ENABLED to default to false, but
commit a3cd33f3 changed the default to true for the consent flow.
2026-06-08 19:32:00 +08:00
SnapOtter 931e15a6bd fix(ui): constrain preview dropzone height in pipeline builder
The empty-state dropzone in the Automate preview panel was expanding
to fill the full 38% panel height, causing overlap and clipping of
the "Import from Library" button. Constrain to max 120px and tighten
spacing for a cleaner layout.
2026-06-08 18:41:55 +08:00
SnapOtter a3cd33f37f fix: restore ANALYTICS_ENABLED=true default for consent flow
The security hardening commit changed this to false, which prevents
the analytics consent page from appearing on first login. Users see
"disabled by administrator" instead of being asked to opt in/out.

With ANALYTICS_ENABLED=true, the SDK initializes but NO user data is
sent until the user explicitly accepts on the consent page. The consent
page is the privacy safeguard, not the server-side flag. Self-hosters
who want to fully disable analytics can still set ANALYTICS_ENABLED=false.
2026-06-08 18:07:04 +08:00
SnapOtter 0631c41eb9 fix(docs): read version from root package.json for nav badge
The docs version badge was reading from apps/docs/package.json which
gets out of sync during manual releases. Read from the monorepo root
package.json instead so it always matches the released version.
2026-06-08 17:56:18 +08:00