Commit Graph
586 Commits
Author SHA1 Message Date
SnapOtter 6f85b3d12a fix(api): support sharp 0.35 types 2026-06-29 16:09:30 +08:00
dependabot[bot]andGitHub af7cd77e84 chore(deps): bump the production-deps group with 16 updates
Bumps the production-deps group with 16 updates:

| Package | From | To |
| --- | --- | --- |
| [@scalar/fastify-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/fastify) | `1.60.0` | `1.62.0` |
| [@sentry/node](https://github.com/getsentry/sentry-javascript) | `10.59.0` | `10.62.0` |
| [bullmq](https://github.com/taskforcesh/bullmq) | `5.79.1` | `5.79.2` |
| [fastify](https://github.com/fastify/fastify) | `5.8.5` | `5.9.0` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.2.0` | `4.3.0` |
| [playwright](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.38.2` | `5.38.6` |
| [sharp](https://github.com/lovell/sharp) | `0.35.1` | `0.35.2` |
| [tar](https://github.com/isaacs/node-tar) | `7.5.16` | `7.5.19` |
| [lucide](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide) | `1.21.0` | `1.22.0` |
| [@sentry/react](https://github.com/getsentry/sentry-javascript) | `10.59.0` | `10.62.0` |
| [pdfjs-dist](https://github.com/mozilla/pdf.js) | `6.0.227` | `6.1.200` |
| [posthog-js](https://github.com/PostHog/posthog-js) | `1.391.9` | `1.395.0` |
| [react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook) | `5.3.2` | `5.3.3` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1073.0` | `3.1075.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.1073.0` | `3.1075.0` |


Updates `@scalar/fastify-api-reference` from 1.60.0 to 1.62.0
- [Release notes](https://github.com/scalar/scalar/releases)
- [Changelog](https://github.com/scalar/scalar/blob/main/integrations/fastify/CHANGELOG.md)
- [Commits](https://github.com/scalar/scalar/commits/HEAD/integrations/fastify)

Updates `@sentry/node` from 10.59.0 to 10.62.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.59.0...10.62.0)

Updates `bullmq` from 5.79.1 to 5.79.2
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v5.79.1...v5.79.2)

Updates `fastify` from 5.8.5 to 5.9.0
- [Release notes](https://github.com/fastify/fastify/releases)
- [Commits](https://github.com/fastify/fastify/compare/v5.8.5...v5.9.0)

Updates `js-yaml` from 4.2.0 to 4.3.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.0/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

Updates `playwright` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.0...v1.61.1)

Updates `posthog-node` from 5.38.2 to 5.38.6
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.38.6/packages/node)

Updates `sharp` from 0.35.1 to 0.35.2
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.1...v0.35.2)

Updates `tar` from 7.5.16 to 7.5.19
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.16...v7.5.19)

Updates `lucide` from 1.21.0 to 1.22.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.22.0/packages/lucide)

Updates `@sentry/react` from 10.59.0 to 10.62.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.59.0...10.62.0)

Updates `pdfjs-dist` from 6.0.227 to 6.1.200
- [Release notes](https://github.com/mozilla/pdf.js/releases)
- [Commits](https://github.com/mozilla/pdf.js/compare/v6.0.227...v6.1.200)

Updates `posthog-js` from 1.391.9 to 1.395.0
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.391.9...posthog-js@1.395.0)

Updates `react-hotkeys-hook` from 5.3.2 to 5.3.3
- [Release notes](https://github.com/JohannesKlauss/react-keymap-hook/releases)
- [Changelog](https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JohannesKlauss/react-keymap-hook/compare/v.5.3.2...v5.3.3)

Updates `@aws-sdk/client-s3` from 3.1073.0 to 3.1075.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1075.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.1073.0 to 3.1075.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1075.0/lib/lib-storage)

---
updated-dependencies:
- dependency-name: "@scalar/fastify-api-reference"
  dependency-version: 1.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: "@sentry/node"
  dependency-version: 10.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: bullmq
  dependency-version: 5.79.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: fastify
  dependency-version: 5.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: playwright
  dependency-version: 1.61.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: posthog-node
  dependency-version: 5.38.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: sharp
  dependency-version: 0.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: tar
  dependency-version: 7.5.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: lucide
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: "@sentry/react"
  dependency-version: 10.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: pdfjs-dist
  dependency-version: 6.1.200
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: posthog-js
  dependency-version: 1.395.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: react-hotkeys-hook
  dependency-version: 5.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1075.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.1075.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 05:44:44 +00:00
SnapOtterandGitHub 0cdd560ac4 feat: add Sign PDF tool (draw/type/upload signatures, place on a PDF) (#370)
Draw, type, or upload a signature and place resizable/rotatable copies across PDF pages; output flattened server-side with PyMuPDF. Visual electronic signature, not cryptographic. New interactive-sign display mode (pdf.js + Konva) and a custom docs-pool route.
2026-06-29 11:01:25 +08:00
SnapOtterandGitHub 1c202c6ef0 feat(analytics): upload web source maps to Sentry + tie release to build (#369)
* feat(analytics): upload web source maps to Sentry + tie release to build

Web crash reports were unusable: the bundle ships minified with no source
maps uploaded, and every build reported as the frozen APP_VERSION, so a
Sentry error showed an unreadable stack under a single release.

- Add @sentry/vite-plugin: emit hidden source maps and upload them by debug
  id when SENTRY_AUTH_TOKEN is present (published Docker build only), then
  delete the maps so they never ship. No-op for dev and the source archive.
- Set the Sentry release from SENTRY_RELEASE / VITE_SENTRY_RELEASE (the Docker
  build passes the release version), falling back to APP_VERSION.
- Relax beforeSend so app bundle frames keep a host-stripped path (Sentry needs
  it to match the uploaded map) while the instance hostname, error message, and
  PII stay stripped. Filesystem paths still collapse to the basename.
- Wire the Dockerfile (sentry_auth_token build secret + SENTRY_RELEASE arg/env)
  and the release docker job.

* fix(analytics): point source map upload at the snapotter org (project node)
2026-06-29 10:47:30 +08:00
SnapOtterandGitHub 19b515dc5f chore: use "200+ tools" for the tool-count claim across public surfaces (#364)
* 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.
2026-06-28 21:49:14 +08:00
SnapOtterandGitHub 8e9452e650 fix: settings dialog and settings API correctness bugs
Seven correctness fixes in the admin settings dialog and settings API: AdminSecuritySettings save echoing read-only/redacted keys; the server persisting the ******** mask over real OIDC/SIEM secrets on a settings round trip; the Tools panel missing its settings:write gate; three swallowed errors (ToolsSection save, ApiKeys generate and delete); and generatePassword omitting a special char under passwordRequireSpecial. Adds an integration regression test for the secret-mask no-op.
2026-06-28 21:37:38 +08:00
SnapOtterandGitHub 63a03d26f2 feat: pipeline templates, analytics opt-out, 83 conversion presets, positioning + e2e modernization
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.
2026-06-28 18:57:53 +08:00
SnapOtterandGitHub 88af8d46fb fix(tools): honest content-type for edit-metadata pass-through (#350)
edit-metadata writes EXIF tags in place and streams the original bytes
back, but the response content-type defaulted to image/jpeg for any
format outside a small map. A BMP/PSD/PPM download then claimed to be a
JPEG, and the nightly tool-x-format matrix tried to Sharp-decode it and
threw "unsupported image format".

Map every format validateImageBuffer can report to its real MIME type,
and switch the matrix's pixel-decode gate from a fragile denylist to an
allowlist of formats this libvips build is guaranteed to decode. Niche
raster types streamed back untouched now carry an honest content-type
we simply don't pixel-verify.

Verified green across all 157 tools x 34 formats with FULL_MATRIX=1.
2026-06-25 02:20:41 +08:00
SnapOtterandGitHub 078743d6b2 fix(nightly): qpdf in test image, NUL-byte settings, AI sub-path fuzz exclude (#348)
Third round - the prior fixes unblocked these deeper failures on the nightly:

- Docker E2E: the patches/ fix (#346) let the build finish, so tests now
  run - and fail with 'spawnSync qpdf ENOENT'. Dockerfile.test installed
  imagemagick/ghostscript/exiftool but never qpdf, which the PDF tools and
  fixture-integrity checks need. Add it.
- NUL-byte 500 (real robustness bug Schemathesis found): a settings string
  containing U+0000 hits the jobs.settings jsonb insert and Postgres rejects
  it ('invalid byte sequence for encoding UTF8: 0x00'), 500ing tools like
  html-to-image. Strip NUL bytes from settings before the insert (NUL is
  never meaningful in tool settings). api typecheck passes.
- Schemathesis: the AI exclude (#346) only anchored on the tool id at the
  path end, so AI sub-endpoints like /passport-photo/analyze were still
  fuzzed and 501'd. Extend the regex to allow an optional sub-path.
2026-06-24 21:59:02 +08:00
SnapOtterandGitHub 8f4235d2c6 fix(enterprise): ship enterprise package in prod image + S3, analytics, tracing, queue fixes (#342)
* fix(enterprise): ship enterprise pkg in prod image, full license features, tracing key fallback

docker/Dockerfile: COPY packages/enterprise manifest+src into the production stage.
Without it, apps/api's workspace link to @snapotter/enterprise dangles and every
import() throws (silently caught), so all 19 enterprise features failed closed
(enterprise.active=false) regardless of a valid license.

scripts/generate-license.mjs: sync PLAN_FEATURES with packages/enterprise/src/license.ts
so a --plan enterprise license unlocks all 19 features (was 8) and team unlocks 8.

apps/api/src/tracing.ts: accept SNAPOTTER_LICENSE_KEY as a fallback to LICENSE_KEY so
distributed_tracing activates with the same key as the rest of the app.

* fix(docker): keep scripts/bake-analytics.mjs in build context

.dockerignore excluded the whole scripts/ dir (PR #82, V1 hardening), but
docker/Dockerfile later added 'COPY scripts/bake-analytics.mjs' for the analytics
bake step. A clean production image build therefore fails with
'scripts/bake-analytics.mjs: not found'. The published image build is gated off in
CI so this latent break went unnoticed. Exclude scripts/* but re-include the one
file the Dockerfile needs.

* fix: S3 upload stream, analytics bake reaches API, dedupe retention field, reconcile orphan jobs

storage-s3.ts: wrap the upload AsyncIterable in Readable.from() so @aws-sdk/lib-storage
accepts it. STORAGE_MODE=s3 file uploads failed with 'Body Data is unsupported format'
for every tool because a bare async generator is not a Readable.

docker/Dockerfile: COPY the builder-baked analytics baked.ts into the API runtime stage.
The API re-copied the committed (off) baked.ts from the build context, so the
SNAPOTTER_ANALYTICS build arg had no effect on the API -- and since the SPA reads
/api/v1/config/analytics, analytics was off everywhere regardless of the arg.

settings-dialog.tsx: remove the duplicate tempFileMaxAgeHours control under Data
Retention; it bound the same setting key as the File Management control with a different
default, so editing either silently overwrote the other.

apps/api/src/index.ts: reconcile orphaned job rows (empty tool_id, never enqueued to
BullMQ) at boot so they don't sit in processing/queued forever and inflate the per-user
concurrent-job count and the upgrade-check in-flight gate.

* fix(web): style the SSO login buttons (they referenced undefined theme tokens)

The OIDC/SAML 'Sign in with <provider>' buttons used bg-secondary /
text-secondary-foreground, which the web theme never defines (it has primary,
background, foreground, muted, border, card, primary-subtle). Those classes resolved
to nothing, so the buttons rendered as bare unstyled text on the login page.

Restyle: the optional (non-enforced) buttons become white-card outline buttons with a
key icon and an orange hover tint, secondary to the primary Login button; the
SSO-enforced buttons become solid primary with the icon.

* fix: gate S3 behind license, custom-role enterprise perms, wire retention UI, cleanup

S3 is a licensed feature, but shipping packages/enterprise in every image removed the
implicit gate, so STORAGE_MODE=s3 worked without a license. Enforce
isFeatureEnabled('s3_storage') at boot and fail fast if unlicensed.

Custom roles can now be granted security:manage / compliance:manage / webhooks:manage
(roles.ts ALL_PERMISSIONS + the Roles UI) so admins can build least-privilege
compliance/security roles instead of only the built-in admin role.

retentionSweep now reads the jobsRetentionDays / auditRetentionDays DB settings the
System Settings UI writes (env vars become the fallback default), mirroring how the
temp-file sweep reads tempFileMaxAgeHours. Previously those two UI controls were no-ops.

Cleanup: drop the never-set snapotter_storage_bytes gauge and the unused
MAX_WORKSPACE_SIZE_GB env var; emit tool_client_error to PostHog from the web
ErrorBoundary (client crashes were not reaching analytics); add the Python
OpenTelemetry packages so the innermost sidecar.<script> span exports; fix the stale
'only local storage' line in the docs; delete two e2e-analytics specs that tested the
removed consent UI.

* fix(env): restore MAX_WORKSPACE_SIZE_GB default

security-auth-hardening.test.ts asserts env.MAX_WORKSPACE_SIZE_GB defaults to 10, so
the var is an intentional (tested) default, not dead code. Removing it in the cleanup
commit broke that unit test. Keep the declaration.
2026-06-24 17:27:59 +08:00
SnapOtterandGitHub c2ae334c81 fix(api): make OpenAPI spec ASCII-only so Schemathesis can load it (#344)
The nightly Schemathesis job failed with a schema-loading error:
'unacceptable character #x0080: control characters are not allowed'. The
served openapi.yaml contained 215 em dashes (U+2014) and box-drawing
section dividers (U+2500); Schemathesis's strict YAML parser mis-decodes
those multi-byte UTF-8 sequences as C1 control chars and refuses to load
the schema, so no fuzz checks ran. (PyYAML is lenient, which is why the
local yaml.safe_load check passed.)

Replace every non-ASCII char with ASCII '-'. This also clears an
em-dash style-rule violation. Add a docs.test.ts guard asserting the
served spec is ASCII-only, with a clear message, so this can't regress.

Pre-existing issue (the dashes predate this branch); surfaced while
verifying CI is green.
2026-06-24 17:14:10 +08:00
SnapOtterandGitHub 6917a8b0c7 fix(test): repair integration suite after analytics column/endpoint removal (#340)
* fix(test): repair integration suite after analytics column/endpoint removal

#336 moved analytics to a build-time bake: migration 0005 dropped the
users.analytics_enabled and analytics_consent_* columns and removed the
PUT /api/v1/user/analytics endpoint. Two integration tests were left
referencing the old shape and went red on main (13 failures):

- migrate-from-sqlite.test.ts built 1.x SQLite fixtures whose users table
  declared the analytics columns. The generic SELECT *-based importer then
  tried to INSERT them into the 2.0 target, which no longer has those
  columns, failing with Postgres 42703 and rolling back the whole import
  (cascading to all 12 assertions). 1.x never had analytics columns, so the
  fixtures are corrected to drop them. Also removed the now-dead analytics
  entries from the importer's TS/BOOL conversion sets.

- analytics.test.ts asserted the removed PUT endpoint returns 404 but sent
  the request unauthenticated, so the global auth preHandler answered 401
  first. It now authenticates, reaching Fastify's not-found handler (404).

Also removed the stale /api/v1/user/analytics path from openapi.yaml.

Verified locally: full platform integration bucket 1029 passed / 0 failed;
monorepo typecheck clean.

* test(e2e): drop orphaned analytics-consent dismissal calls

#336 deleted the entire analytics consent system (consent page, consent
module, and PUT /api/v1/user/analytics), but six tests/e2e files still
PUT to that removed endpoint to 'dismiss analytics consent.' The calls
were silent no-ops (Playwright request.put / fetch don't throw on 4xx),
so they passed while hitting a dead route.

There is no consent prompt to dismiss anymore, so remove the calls:
- auth.setup.ts / qa-auth.setup.ts: keep the waitForFunction that syncs on
  login completion, drop the now-unused token capture, the dead PUT, and
  the stale 'consent guard' comments.
- rbac / rbac-full / gui-settings-rbac / gui-settings-expanded specs: the
  re-login blocks existed solely to obtain a token for the PUT (reLoginData
  was used nowhere else and the block was the tail of each helper), so
  remove the whole block. The meaningful create-user/login/change-password
  work is untouched.

Verified: no /api/v1/user/analytics refs remain in tests/e2e; biome clean
(no unused vars).
2026-06-24 13:30:18 +08:00
SnapOtterandGitHub 5d36ac06d8 feat(analytics): build-time bake + telemetry depth (#336)
Bake PostHog + Sentry into the published Docker image (SNAPOTTER_ANALYTICS
build arg, codegen script). Delete entire consent system. Move event emission
to BullMQ worker. Add cross-tier identity stitching, Sentry performance
tracing on both tiers, frontend funnel events. Fix stateful regex bug.

86 files changed, 1593 insertions(+), 3747 deletions(-)
2026-06-24 11:05:39 +08:00
SnapOtterandGitHub a53038ed96 feat(automate): make the pipeline builder fully multi-modal (#335)
* feat(shared): add outputModality to Tool metadata for crossing tools

* feat(shared): add modalityForExtension, toolInputModality, toolOutputModality

* fix(pipeline): route finalize and parent jobs to the pipeline's modality pool

* feat(automate): add ConvertAudioControls pipeline step (exemplar)

* feat(automate): add video tool settings controls to pipelines

* feat(automate): add audio tool settings controls to pipelines

* feat(automate): add document tool settings controls to pipelines

* feat(automate): add chart-maker settings control to pipelines

* feat(automate): warn on modality-incompatible pipeline steps

* feat(automate): add single-file download button for pipeline results

* refactor(automate): modality-aware icons, nav handler rename, mobile size bar

* test(pipeline): cover audio, document, file, and cross-modality chains

* i18n(automate): translate the modality-warning tooltip

* test(pipeline): gate media-pool routing assertion on ffmpeg availability
2026-06-24 00:21:41 +08:00
SnapOtterandGitHub 35e18d8b79 fix: GPU deployment robustness (6 fixes from end-to-end testing on an RTX 4070) (#334)
* fix(docker): pin CUDA base to 12.6 so the GPU image starts on R560+ drivers

The amd64 base nvidia/cuda:12.9.2-cudnn-runtime bakes a cuda>=12.9 driver gate enforced by nvidia-container-toolkit at container start, so the image fails to launch on common production drivers (e.g. 570.x / CUDA 12.8). The AI bundles are all cu126 wheels and the image installs libcublas-12-6, so 12.9 was misaligned with the workload. Pin to nvidia/cuda:12.6.3-cudnn-runtime-ubuntu24.04 to match the wheels and lower the driver floor to R560+.

* fix(ai): broaden OOM detection so the rembg lighter-model fallback fires

onnxruntime/CUDA allocation failures surface as 'Failed to allocate memory for requested buffer', CUBLAS_STATUS_ALLOC_FAILED, or bad_alloc, not just 'out of memory'. The background-removal and transparency-fixer fallback-to-lighter-model paths only matched the literal 'out of memory', so the fallback was dead code and transparency-fixer (default birefnet-hr-matting) always failed with an allocation error. Add isMemoryAllocError() and use it in both checks.

* fix(ai): use bundled PaddleOCR models so OCR runs offline

ocr.py passed no model dirs to PaddleOCR, so PaddleX resolved models from ~/.paddlex and downloaded them from HuggingFace at runtime (slow first use, broken air-gapped), ignoring the models the OCR bundle ships in MODELS_PATH; it also pulled doc-orientation/unwarping models that are not bundled. Pin detection, recognition and textline models to the bundled dirs in MODELS_PATH (per language) and disable use_doc_orientation_classify / use_doc_unwarping, with per-component fallback when a model is absent. Verified: OCR runs with zero HuggingFace requests.

* fix(docker): add CAP_KILL so container shutdown is graceful

cap_drop: ALL without re-adding KILL meant tini (PID 1, root) could not forward SIGTERM to the gosu-dropped snapotter process (root minus CAP_KILL cannot signal a different UID). docker stop logged '[FATAL tini] forwarding signal: Operation not permitted', never delivered the signal, and fell back to SIGKILL after the 10s timeout. Add KILL to cap_add in both compose files. Verified: docker stop completes in 0s with SIGTERM delivered (exit 143) and no FATAL tini.

* fix(ai): serialize bundle installs against AI jobs to prevent sidecar segfault

A feature bundle install rewrites the shared Python venv (pip + copytree of site-packages/*.so) as a background subprocess, with no coordination against AI tool jobs that dlopen native libs (torch / onnxruntime CUDA) from the same venv; a job loading a shared object while it is overwritten segfaults the sidecar. Add a process-wide async mutex (venv-lock.ts): bridge.run() acquires it before every AI script and the install route holds it across the installer subprocess. Both run in the same Node process so a module-level lock suffices. Verified: concurrent install + AI job produces zero segfaults and the job serializes behind the install.

* fix(ai): make the venv lock read/write so concurrent AI jobs are not serialized

The first cut used an exclusive mutex, which (a) deferred the dispatcher spawn by a microtask and broke unit tests that synchronously drive the mocked spawn, and (b) serialized AI jobs against each other, removing the dispatcher's by-id request multiplexing. Make it a writer-preferring read/write lock: AI jobs are shared readers (with a synchronous fast path so spawn still happens in-tick) and a bundle install is the exclusive writer. Verified: all 764 AI unit tests pass.

* fix(ai): degrade OCR to Tesseract on CPU-only hosts instead of segfaulting

The amd64 AI bundle ships paddlepaddle-gpu, whose native libs dlopen
libcuda.so.1 at import and segfault on a host without a GPU (libcuda is the
driver lib, injected only by nvidia-container-toolkit on GPU hosts). The
segfault crashed the shared long-lived AI dispatcher and, after a few attempts,
tripped the bridge crash-recovery permanent-disable, wedging all AI until a
container restart. The standalone ocr tool defaults to quality=balanced
(PaddleOCR), so it hit this on every CPU-only deployment; ocr-pdf already
hardcoded Tesseract and was unaffected.

ocr.py now gates the PaddleOCR tiers on gpu_available(): balanced/best
transparently fall back to fast (Tesseract, CPU-capable) when no usable GPU is
present, and run_paddleocr_v5/run_paddleocr_vl refuse before importing paddle so
the GPU build is never dlopen'd on CPU. GPU hosts are unchanged.

Verified on a CPU-only Windows/WSL2 box: ocr returns Tesseract text across
repeated runs with the dispatcher staying healthy (no wedge).
2026-06-23 18:39:51 +08:00
SnapOtterandGitHub 32c1192d63 fix(passport-photo): require the face-detection bundle, not just background-removal (#329)
* fix(passport-photo): require the face-detection bundle, not just background-removal

Passport Photo runs face-landmark detection (face_landmarks.py, gated to the
face-detection bundle) before background removal (background-removal bundle),
but it was only declared under and guarded against background-removal. A user
who installed only Background Removal passed every JS-side check, then hit a
late "feature_not_installed" from the Python dispatcher gate when the analyze
step ran face landmarks, and the UI never told them Face Detection was needed.

- shared: add TOOL_EXTRA_BUNDLES + getRequiredBundlesForTool so a tool can
  declare more than one required bundle (passport-photo needs background-removal
  and face-detection). enablesTools is untouched, so the one-tool-per-bundle
  invariant still holds.
- api: isToolInstalled() now checks every required bundle; add
  getFirstMissingBundleForTool() so the analyze and base routes, pipeline (both
  guards) and batch report the bundle the user actually still needs.
- web: the proactive install prompt (tool-page) and features-store treat a tool
  as installed only when all required bundles are present, and point the prompt
  at the first missing one (sequential install, no new UI).

Refs #327

* test(passport-photo): deterministic integration coverage for the two-bundle guard

Boots the real API with an isolated DATA_DIR and controls installed.json to
prove the HTTP route behavior end-to-end:
- nothing installed -> 501 naming background-removal
- only background-removal installed -> 501 naming face-detection (issue #327)
- both installed -> guard passes (not 501)
- base route reports face-detection too

Refs #327
2026-06-22 23:31:18 +08:00
SnapOtterandGitHub 8952e9ba47 fix: harden against three production Sentry crashes (#328)
Three production crashes from the snapotter/node Sentry project.

feature-status (NODE-12): a valid-JSON-but-wrong-shape installed.json
crashed boot via Object.keys(data.bundles). readInstalled() now
normalizes any unusable shape to { bundles: {} }, and the boot recovery
call is wrapped so cleanup can never fatal startup.

image-viewer (NODE-15/17/18): drag-to-pan read .x off an undefined
use-gesture memo on pointerUp or a pinch-into-pan. A guarded pure helper
(resolvePanStart) now falls back to the live pan offset.

Fastify (NODE-14): raised pluginTimeout to 60s so slow self-hosted boots
do not fatal at @fastify/static.
2026-06-22 23:25:22 +08:00
SnapOtterGitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
717de2577a chore(deps): bump the production-deps group across 1 directory with 18 updates (#326)
Bumps the production-deps group with 18 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@scalar/fastify-api-reference](https://github.com/scalar/scalar/tree/HEAD/integrations/fastify) | `1.59.3` | `1.60.0` |
| [@sentry/node](https://github.com/getsentry/sentry-javascript) | `10.57.0` | `10.59.0` |
| [bullmq](https://github.com/taskforcesh/bullmq) | `5.78.1` | `5.79.1` |
| [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | `5.9.0` | `5.9.3` |
| [ipaddr.js](https://github.com/whitequark/ipaddr.js) | `2.3.0` | `2.4.0` |
| [papaparse](https://github.com/mholt/PapaParse) | `5.5.3` | `5.5.4` |
| [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) | `8.21.0` | `8.22.0` |
| [playwright](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.0` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.37.0` | `5.38.2` |
| [sharp](https://github.com/lovell/sharp) | `0.35.1` | `0.35.2` |
| [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) | `6.4.7` | `6.4.8` |
| [lucide](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide) | `1.18.0` | `1.21.0` |
| [@sentry/react](https://github.com/getsentry/sentry-javascript) | `10.57.0` | `10.59.0` |
| [posthog-js](https://github.com/PostHog/posthog-js) | `1.386.6` | `1.391.9` |
| [react-image-crop](https://github.com/dominictobias/react-image-crop) | `11.0.10` | `11.1.2` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.17.0` | `7.18.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1068.0` | `3.1073.0` |
| [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) | `3.1068.0` | `3.1073.0` |



Updates `@scalar/fastify-api-reference` from 1.59.3 to 1.60.0
- [Release notes](https://github.com/scalar/scalar/releases)
- [Changelog](https://github.com/scalar/scalar/blob/main/integrations/fastify/CHANGELOG.md)
- [Commits](https://github.com/scalar/scalar/commits/HEAD/integrations/fastify)

Updates `@sentry/node` from 10.57.0 to 10.59.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.57.0...10.59.0)

Updates `bullmq` from 5.78.1 to 5.79.1
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v5.78.1...v5.79.1)

Updates `fast-xml-parser` from 5.9.0 to 5.9.3
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.9.0...v5.9.3)

Updates `ipaddr.js` from 2.3.0 to 2.4.0
- [Changelog](https://github.com/whitequark/ipaddr.js/blob/main/Changes.md)
- [Commits](https://github.com/whitequark/ipaddr.js/compare/v2.3.0...v2.4.0)

Updates `papaparse` from 5.5.3 to 5.5.4
- [Release notes](https://github.com/mholt/PapaParse/releases)
- [Changelog](https://github.com/mholt/PapaParse/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mholt/PapaParse/compare/5.5.3...5.5.4)

Updates `pg` from 8.21.0 to 8.22.0
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.22.0/packages/pg)

Updates `playwright` from 1.60.0 to 1.61.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

Updates `posthog-node` from 5.37.0 to 5.38.2
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.38.2/packages/node)

Updates `sharp` from 0.35.1 to 0.35.2
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.1...v0.35.2)

Updates `astro` from 6.4.7 to 6.4.8
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/astro@6.4.8/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.4.8/packages/astro)

Updates `lucide` from 1.18.0 to 1.21.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.21.0/packages/lucide)

Updates `@sentry/react` from 10.57.0 to 10.59.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.57.0...10.59.0)

Updates `posthog-js` from 1.386.6 to 1.391.9
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/posthog-js@1.386.6...posthog-js@1.391.9)

Updates `react-image-crop` from 11.0.10 to 11.1.2
- [Release notes](https://github.com/dominictobias/react-image-crop/releases)
- [Commits](https://github.com/dominictobias/react-image-crop/compare/11.0.10...11.1.2)

Updates `react-router-dom` from 7.17.0 to 7.18.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router-dom@7.18.0/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.18.0/packages/react-router-dom)

Updates `@aws-sdk/client-s3` from 3.1068.0 to 3.1073.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1073.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.1068.0 to 3.1073.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1073.0/lib/lib-storage)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1073.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.1073.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: "@scalar/fastify-api-reference"
  dependency-version: 1.60.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: "@sentry/node"
  dependency-version: 10.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: "@sentry/react"
  dependency-version: 10.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: astro
  dependency-version: 6.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: bullmq
  dependency-version: 5.79.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: fast-xml-parser
  dependency-version: 5.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: ipaddr.js
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: lucide
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: papaparse
  dependency-version: 5.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
- dependency-name: pg
  dependency-version: 8.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: playwright
  dependency-version: 1.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: posthog-js
  dependency-version: 1.391.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: posthog-node
  dependency-version: 5.38.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: react-image-crop
  dependency-version: 11.1.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: react-router-dom
  dependency-version: 7.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-deps
- dependency-name: sharp
  dependency-version: 0.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 22:16:14 +08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c8371c3cd1 chore(deps-dev): bump the dev-deps group with 5 updates (#297)
Bumps the dev-deps group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.0` |
| [@testcontainers/postgresql](https://github.com/testcontainers/testcontainers-node) | `12.0.2` | `12.0.3` |
| [@testcontainers/redis](https://github.com/testcontainers/testcontainers-node) | `12.0.2` | `12.0.3` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.19.21` | `22.20.0` |
| [vitepress-plugin-llms](https://github.com/okineadev/vitepress-plugin-llms) | `1.13.1` | `1.13.2` |


Updates `@playwright/test` from 1.60.0 to 1.61.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

Updates `@testcontainers/postgresql` from 12.0.2 to 12.0.3
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v12.0.2...v12.0.3)

Updates `@testcontainers/redis` from 12.0.2 to 12.0.3
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v12.0.2...v12.0.3)

Updates `@types/node` from 22.19.21 to 22.20.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vitepress-plugin-llms` from 1.13.1 to 1.13.2
- [Release notes](https://github.com/okineadev/vitepress-plugin-llms/releases)
- [Commits](https://github.com/okineadev/vitepress-plugin-llms/compare/v1.13.1...v1.13.2)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: "@testcontainers/postgresql"
  dependency-version: 12.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: "@testcontainers/redis"
  dependency-version: 12.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: "@types/node"
  dependency-version: 22.20.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-deps
- dependency-name: vitepress-plugin-llms
  dependency-version: 1.13.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 21:42:30 +08:00
SnapOtterandGitHub 1fec97111b fix(docker): make storage writable under non-root/foreign UIDs (TrueNAS, OpenShift) (#299)
The entrypoint only fixed volume permissions when started as root (chown +
gosu-drop to snapotter). Launched under a non-root/foreign UID (TrueNAS app
user, Kubernetes runAsUser, OpenShift) it did no permission setup, so /data and
/tmp/workspace -- owned by uid 999 from the image -- were not writable by the
running user. Uploads and processing then failed with a cryptic EACCES
("workspace folder is not writable") and AI bundle installs failed the same way,
while health checks still reported the container healthy.

- entrypoint: source new entrypoint-lib.sh; verify writability up front when
  non-root, and as snapotter after chown when root (catches root-squashed
  mounts), failing fast with an actionable message (which dir, uid/gid, how to
  fix) instead of a late, cryptic EACCES
- Dockerfile: own /data and /tmp/workspace as snapotter:0, group-writable with
  setgid, so an arbitrary UID with the root supplementary group (OpenShift /
  Kubernetes fsGroup) can write; keep /opt/venv world-readable for the AI venv
  bootstrap under arbitrary UIDs
- api: assert storage writability at boot (lib/storage-writable.ts), failing
  fast with the same guidance even when the entrypoint is bypassed
- docs: add a Storage permissions section (named volumes, bind mounts, TrueNAS,
  Kubernetes/OpenShift) and cross-link it from the security guide

Fixes #230
2026-06-22 16:58:59 +08:00
SnapOtterandGitHub 3d9ff1e0d2 fix(api): decode RAW via LibRaw first so DNG processes at full resolution (#289) (#290)
RAW (DNG) processing crashed on ImageMagick's deprecated ufraw-batch
delegate, which fails on modern formats such as iPhone ProRAW DNG.

Root cause: the dcraw_emu (LibRaw) decode tier read the wrong output path.
dcraw_emu APPENDS the output extension (raw-in-X.dng -> raw-in-X.dng.tiff)
but the code looked for raw-in-X.tiff (replaced extension), so readFile threw
on every RAW, the tier silently fell through to ufraw, and the 24MB TIFF
leaked into the temp dir on each attempt.

- Repair the dcraw_emu output path; clean it up in finally (fixes the leak)
- Prefer LibRaw full decode over embedded-preview extraction so a
  full-resolution RAW is never silently returned as a reduced-size preview
  (sample DNG: was 1024x683 preview, now 3474x2314 full)
- Add RAW decode regression tests (DNG full-resolution + all 6 RAW formats);
  these were absent, which let the bug ship
- Install libraw-bin on CI test runners so dcraw_emu is actually exercised
2026-06-22 09:54:04 +08:00
Luciano GodoyandGitHub ce02ce1348 fix(api): respect RATE_LIMIT_PER_MIN for tool routes (#272)
Tool endpoints (/api/v1/tools/*) now honor the RATE_LIMIT_PER_MIN env var instead of a hardcoded 60/min: `0` disables per-tool limiting, `>0` uses the configured value, and unset falls back to 60. Merged on top of the section-based route refactor (#280).

Fixes #271.
2026-06-21 23:39:55 +08:00
SnapOtterandGitHub dba8a85a80 fix(jobs): pre-warm QueueEvents to kill first-sync-wait flake (#285)
The csv-json integration test intermittently timed out at 30000ms on
the first worker-backed job in a fork. Root cause: waitForJob() creates
the BullMQ QueueEvents consumer lazily on first use, and a fresh consumer
reads the Redis events stream from "$" (the tail at the moment its run
loop starts). A trivial tool can publish its completed:<id> event before
the brand-new consumer positions itself, so waitUntilFinished() never
sees the event and blocks for the full sync-wait window. In tests
SYNC_WAIT_MS is floored at 30000ms, exactly the vitest per-test budget,
so the stall surfaces as an opaque timeout instead of a 202 fallback.
This is also a latent production latency bug: the first synchronous tool
request after each boot could hang up to the 8s prod window.

Fix: warmQueueEvents() eagerly constructs and connects every pool's
consumer at spine startup, before any job is enqueued, so each consumer
is positioned at the stream tail up front and never misses a completion.
Awaited in the test spine (deterministic for the first request) and fired
non-blocking at prod boot (a slow Redis must not stall startup).

Adds a regression guard in job-spine.test.ts that drops the cached
consumers, warms explicitly, and asserts a fast job's completion is
captured on the first sync-wait.

Verified: 3 parallel stress runs (276 file-runs across all pools), zero
timeouts; targeted job-spine + csv-json suites green; typecheck clean.
2026-06-21 23:22:59 +08:00
SnapOtterandGitHub f75cc328ac fix(security): numeric CIDR matching for IPv6 SSRF allow/deny
Replace textual prefix matching in isPrivateIPv6() with numeric bit-prefix matching via ipaddr.js. Closes reachable classifier bypasses for IPv4-compatible IPv6 (::a.b.c.d), site-local fec0::/10, and the full fe80::/10 link-local span, on top of the IPv4-mapped forms. Embedded IPv4 (mapped and compatible) is run through the full isPrivateIPv4 classifier; unparseable input fails closed.

Follow-up hardening to f64cbdda.

Reported-by: tonghuaroot
2026-06-21 23:20:28 +08:00
SnapOtter 7a3b4e6b3b fix(security): basename-sanitize file-preview paths (CodeQL js/path-injection)
The resolve()+startsWith containment check was correct but CodeQL did not
recognize it. Apply path.basename() to the name in resolveWithinPreviewDir -- a
sanitizer CodeQL recognizes -- so every id-derived preview path is provably a
single filename inside the preview dir. Behaviour is unchanged for valid ids
(already charset-validated); containment check kept as a backstop.
2026-06-21 14:32:16 +08:00
SnapOtter bdadb843d8 fix(security): close remaining high-severity CodeQL alerts
- svg-sanitize.ts: strip each dangerous element repeatedly until stable with
  whitespace-tolerant end tags, defeating nested/overlapping tags (closes 5
  incomplete-multi-character-sanitization + 1 bad-tag-filter; the prior
  single-pass regex could leave a residual <script>/<iframe>).
- file-preview.ts: add a resolve()+containment barrier (the path-traversal
  guard CodeQL recognizes) on top of the id charset check (closes 9
  path-injection).
- metadata.ts: bound the XMP namespace:name key segments so parseXmp cannot
  backtrack polynomially (closes js/polynomial-redos).
- analytics-disabled.spec.ts: match analytics by URL host, not substring
  (closes 4 incomplete-url-substring-sanitization).

typecheck + lint green; svg (119), preview (22), metadata (164) tests pass.
2026-06-21 13:47:22 +08:00
SnapOtter ae4fc1decf fix(security): explicit per-route rate limits (CodeQL js/missing-rate-limiting)
All /api/ routes are already covered by the global @fastify/rate-limit
(index.ts:283), but CodeQL doesn't model the global registration and flagged
every route without an explicit per-route limit. Added tuned config.rateLimit
to 45 routes: stricter on auth/credential routes (mfa/saml/oidc/api-keys,
15-30/min), moderate on writes (60/min), generous on reads/polls (300/min =
the global default). Real defense-in-depth on sensitive routes. 3 alerts on
non-route code (a preHandler hook, the rate-limiter's own DB lookup, a test
helper) are documented false-positives covered by the global limiter.
2026-06-21 11:49:02 +08:00
SnapOtter c1cd8712f4 fix(security): close file-preview path-injection + tighten subtitle detection
- file-preview.ts: validate the :id URL param against a safe charset and
  restrict the original-name extension to alphanumerics before they feed
  filesystem paths (closes 9 CodeQL js/path-injection; defense-in-depth on
  top of the existing DB lookup).
- media-input.ts: require the SRT/VTT timecode structure to detect a subtitle
  rather than a bare '-->' (closes CodeQL js/bad-tag-filter; also rejects
  non-subtitle files that merely contain '-->').
2026-06-21 11:31:39 +08:00
SnapOtter 6e1b9cd3cc fix(lint): resolve #280 Lint failures (route formatting + landing import sort)
#280 left 10 section-prefixed custom-route files mis-indented and one
unsorted import block in the new landing section-index page. Fixed via
biome formatter (api) and manual import sort (landing). No config change
(biome.json is hook-protected); no suppression. pnpm lint + typecheck
now exit 0.
2026-06-21 02:58:46 +08:00
SnapOtter 8001ec97f1 Merge branch 'fix/biome-lint-debt' into chore/consolidate-v2.0.0 2026-06-21 02:31:28 +08:00
SnapOtter 719457e111 Merge branch 'feat/section-based-tool-urls' into chore/consolidate-v2.0.0 2026-06-21 02:09:14 +08:00
SnapOtter 37841e3369 Merge branch 'security/hardening-audit' into chore/consolidate-v2.0.0 2026-06-21 02:08:50 +08:00
SnapOtter 19dc6ba554 fix(lint): clear remaining biome errors (unused code, optional chains, non-null assertions, effect deps) 2026-06-21 01:49:48 +08:00
SnapOtter 7561f2a8c3 fix(lint): make husky hook executable; rename backend useS3 to isS3Enabled (clears 17 false-positive useHookAtTopLevel) 2026-06-21 01:34:58 +08:00
SnapOtter fc205c7428 fix(api): section override for backwards-compat alias routes (adjust-colors) 2026-06-20 12:02:00 +08:00
SnapOtter 414f126a33 docs(api): section-prefix OpenAPI tool paths 2026-06-20 11:55:22 +08:00
SnapOtter 10bb73ffc1 feat(api): section-prefix all custom tool route literals 2026-06-20 11:47:59 +08:00
SnapOtter 68dd7dabaa feat(api): section-prefix factory + batch routes via apiToolPath (+section validation) 2026-06-20 11:44:36 +08:00
SnapOtter 0acc8ca751 chore: post-2.0 QA hygiene across api hardening and qa metadata 2026-06-20 10:50:20 +08:00
SnapOtterandGitHub 19d9ed181a fix(audio): low-samplerate ogg encode + post-2.0 QA hygiene
Quality-VBR ogg (libvorbis -q:a) fixes 8 kHz 'encoder setup failed' in both ogg paths; drop empty COOKIE_SECRET ENV (app auto-generates); emit real bundle extractedSize; fix stale image-pad/compress-pdf QA specs.
2026-06-20 10:41:50 +08:00
SnapOtter d61226496b fix(security): harden rate limits, Redis auth, resource caps, and error sanitization
- Lower LOGIN_ATTEMPT_LIMIT default from 30 to 10 (brute-force protection)
- Lower RATE_LIMIT_PER_MIN default from 1000 to 300
- Add Redis authentication (requirepass) with REDIS_PASSWORD env var
- Add Redis maxmemory 512mb cap to prevent unbounded growth
- Add mem_limit: 1g to Postgres and Redis containers
- Strip internal file paths from all error responses (defense-in-depth)
- Add startup warnings for default admin/Postgres/Redis credentials
- Update security test expectations for new defaults
2026-06-20 00:53:11 +08:00
SnapOtterandGitHub 2bd3e2302a fix: post-2.0 audit bug fixes (404 route, worker logging, outpaint gate, pandoc path)
Surgical post-2.0 fixes: SPA 404 route, worker logging, outpaint gate, pandoc path resolution.
2026-06-19 18:46:52 +08:00
SnapOtter 620552569e feat!: SnapOtter 2.0.0
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.
2026-06-17 14:33:09 +08:00
SnapOtter 7a205eeb12 fix(api): isolate the factory validation scratch dir from the worker's
The tool factory created its per-request validation scratch dir at tmpdir()/snapotter-scratch/<jobId> -- the same path the BullMQ worker uses for job processing (worker.ts scratchRoot()/<jobId>) whenever SCRATCH_PATH is unset. For sync tools this is harmless because the factory blocks on the worker before its finally cleanup runs. For "long" tools the factory returns 202 immediately and then rm's that dir, racing the worker that is writing the job's input there -- so tools that stage input in the worker scratch dir (epub-convert, to-epub) intermittently failed with ENOENT. Production never hit this because it sets SCRATCH_PATH, diverging the two paths. Give the factory's validation dir a -prep suffix so its cleanup can never touch the worker's job dir.
2026-06-17 14:28:41 +08:00
SnapOtter 37b9c6d7bf fix(api): reject non-PDF inputs in pdf-to-image
acceptedInputs is [.pdf], but pdf-to-image validated by calling mupdf.openDocument(buf, 'application/pdf'); mupdf sniffs the real format and opens JPEGs/PNGs/etc. as 1-page image-documents, returning 200. So non-PDF (incl. truncated/hostile) inputs were accepted, violating the contract and the hostile-input robustness check -- the one pre-existing failure surfaced by the full integration run. Gate all three endpoints (convert/info/preview) on the %PDF- magic bytes. Verified: truncated.jpg -> 400, valid PDF -> 200; the hostile-inputs test passes.
2026-06-17 14:28:41 +08:00
SnapOtter 965501aef9 fix(api): log only genuine processing faults at error level
The earlier worker error-logging change logged every job failure at error level, including expected InputValidationErrors (e.g. 'needs at least two audio files') -- flooding error logs with non-actionable user-input rejections (visible across the integration run). Skip validation errors (matched by name, which survives the BullMQ boundary); genuine faults still log at error, and all failures still reach the OTel span.
2026-06-17 14:28:41 +08:00
SnapOtter c483897452 fix(api): drop app-logger import from media-input; update stale errors mock
The full unit suite surfaced two issues from earlier commits on this branch. (1) Importing the app logger into media-input.ts pulled logger.ts -- which builds its pino file transport at module load via join(env.LOG_DIR, ...) -- into the unit-test import graph, throwing at collection time wherever LOG_DIR is unset (integration tests set it; unit tests do not). A low-level modality handler should not depend on the app logger, and a corrupt upload is an expected user error, so drop the import and keep the clean validation message. (2) tool-factory-route.test.ts mocked errors.js without the new friendlyError export; add it.
2026-06-17 14:28:41 +08:00
SnapOtter 6220885e0c fix(api): narrow friendlyError matching to avoid collapsing valid messages
The raw-dump detector matched broad content keywords ('conversion failed', 'pixel format', bare 'ffprobe') that appear in legitimate validation messages (e.g. 'SVG conversion failed'), which would wrongly collapse them to the generic error. Narrow it to the unambiguous 'ffmpeg/ffprobe exited N:' prefix, python tracebacks, and crashes; longer/multi-line raw dumps are still caught by the length/line-count check. Found during self-review.
2026-06-17 14:28:41 +08:00
SnapOtter e52e0bb3d5 fix(api): propagate lenient structural-validation flag to batch + pipeline
Main already reuses the per-modality input handlers in batch and pipeline
(#244), so the modality-aware reuse from the video QA sweep was redundant.
Port only the remaining unique piece: pass lenient: skipStructuralValidation
to the handler so batched/pipelined PDF tools that opt out of structural
validation behave like the single-file factory path. Keeps main's safer
explicit image-decode chain (HEIC/RAW/SVG/autoOrient) and AI fileId threading.
2026-06-17 14:28:22 +08:00
SnapOtter 3fc4149143 fix(api): stop leaking raw ffprobe stderr in media validation errors
An unrecognized/corrupt media upload threw an InputValidationError whose message embedded the raw ffprobe stderr ('ffprobe exited 1: ...'). Return a clean 'Unrecognized <kind> file' message to the client and log the raw probe failure via logger.warn instead. Found via the round-2 hostile-input gap test.
2026-06-17 14:22:16 +08:00