fix: release QA hardening across processing, media, security, and CI gates (#649)

A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
This commit is contained in:
SnapOtter
2026-07-27 15:37:30 +08:00
committed by GitHub
parent bc32f86a07
commit d10d0f544f
855 changed files with 54564 additions and 13092 deletions
+142 -50
View File
@@ -8,18 +8,20 @@
# Stage 0: Static FFmpeg/FFprobe binaries
# ============================================
# Multi-arch (amd64 + arm64) static builds for video/audio processing.
FROM mwader/static-ffmpeg:8.1.2 AS ffmpeg
FROM mwader/static-ffmpeg:8.1.2@sha256:33f770f812cbfc3de96c547157fc9faf8bd95a36481753439ffa761045167585 AS ffmpeg
# ============================================
# Stage 0b: Static pdfcpu binary (pure Go, no CGO)
# ============================================
# CGO_ENABLED=0 produces a fully static binary; no cross-compiler needed.
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm@sha256:a1ae6b6c564f3e0072d70081036827a2705dbcf6b38aaa6d97f5de97fe9abdb4 AS pdfcpu-builder
FROM --platform=$BUILDPLATFORM golang:1.25.12-bookworm@sha256:ea341baa9bd5ba6784f6d7161ace70544349a6242d54d34a0fbfd2c4d51c9d58 AS pdfcpu-builder
ARG TARGETOS=linux
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go install github.com/pdfcpu/pdfcpu/cmd/pdfcpu@v0.13.0 \
&& cp "$(find /go/bin -type f -name pdfcpu | head -1)" /tmp/pdfcpu
WORKDIR /build
COPY docker/go-tools/pdfcpu/go.mod docker/go-tools/pdfcpu/go.sum ./
RUN go mod download all && go mod verify && \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -mod=readonly -o /tmp/pdfcpu github.com/pdfcpu/pdfcpu/cmd/pdfcpu
# ============================================
# Stage 1: Build the frontend (Vite + React)
@@ -94,7 +96,7 @@ RUN --mount=type=cache,id=turbo-cache,target=/app/.turbo \
# on Apple Silicon when cross-compiling for linux/amd64.
# caire imports gioui.org/app which requires CGO on Linux, so we use a
# proper C cross-compiler instead of CGO_ENABLED=0.
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm@sha256:a1ae6b6c564f3e0072d70081036827a2705dbcf6b38aaa6d97f5de97fe9abdb4 AS caire-builder
FROM --platform=$BUILDPLATFORM golang:1.25.12-bookworm@sha256:ea341baa9bd5ba6784f6d7161ace70544349a6242d54d34a0fbfd2c4d51c9d58 AS caire-builder
ARG TARGETOS=linux
ARG TARGETARCH
@@ -132,25 +134,26 @@ RUN set -e; \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Build caire and stage to /tmp/caire (stable path for the COPY below).
# Cross-compiled CGO binaries land in $GOPATH/bin/${GOOS}_${GOARCH}/ not $GOPATH/bin/.
# Build caire from a repository-checksummed module graph. The explicit x/image
# requirement lets us patch vulnerable transitive versions without unpinning caire.
WORKDIR /build
COPY docker/go-tools/caire/go.mod docker/go-tools/caire/go.sum ./
RUN go mod download all && go mod verify
RUN set -e; \
NATIVE=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'); \
if [ "$TARGETARCH" = "$NATIVE" ]; then \
go install github.com/esimov/caire/cmd/caire@v1.5.0 && \
cp /go/bin/caire /tmp/caire; \
go build -trimpath -mod=readonly -o /tmp/caire github.com/esimov/caire/cmd/caire; \
elif [ "$TARGETARCH" = "amd64" ]; then \
CC=x86_64-linux-gnu-gcc \
PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
CGO_ENABLED=1 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go install github.com/esimov/caire/cmd/caire@v1.5.0 && \
cp /go/bin/${TARGETOS}_${TARGETARCH}/caire /tmp/caire; \
go build -trimpath -mod=readonly -o /tmp/caire github.com/esimov/caire/cmd/caire; \
elif [ "$TARGETARCH" = "arm64" ]; then \
CC=aarch64-linux-gnu-gcc \
PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
CGO_ENABLED=1 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go install github.com/esimov/caire/cmd/caire@v1.5.0 && \
cp /go/bin/${TARGETOS}_${TARGETARCH}/caire /tmp/caire; \
go build -trimpath -mod=readonly -o /tmp/caire github.com/esimov/caire/cmd/caire; \
fi
# ============================================
@@ -173,8 +176,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libde265-dev libx265-dev libjpeg-dev libpng-dev \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL --retry 3 --retry-delay 5 "https://github.com/strukturag/libheif/releases/download/v${LIBHEIF_VERSION}/libheif-${LIBHEIF_VERSION}.tar.gz" \
| tar xz \
RUN curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 \
--output /tmp/libheif.tar.gz \
"https://github.com/strukturag/libheif/releases/download/v${LIBHEIF_VERSION}/libheif-${LIBHEIF_VERSION}.tar.gz" \
&& printf '%s %s\n' \
"75f530b7154bc93e7ecf846edfc0416bf5f490612de8c45983c36385aa742b42" \
/tmp/libheif.tar.gz | sha256sum --check --strict - \
&& tar -xzf /tmp/libheif.tar.gz \
&& cmake -B build -S "libheif-${LIBHEIF_VERSION}" \
-DCMAKE_INSTALL_PREFIX=/opt/libheif \
-DWITH_EXAMPLES=ON \
@@ -185,9 +194,46 @@ RUN curl -fsSL --retry 3 --retry-delay 5 "https://github.com/strukturag/libheif/
&& cmake --build build -j$(nproc) \
&& cmake --install build
# ============================================
# Stage 2c: Build LibRaw (camera RAW decoder)
# ============================================
# Debian 12 ships LibRaw 0.20.2, which the Debian tracker marks vulnerable to
# five unfixed advisories with no backport planned: CVE-2026-20884,
# CVE-2026-24450 and CVE-2026-24660 (arbitrary code execution, CVSS 9.8) plus
# CVE-2026-20889 and CVE-2026-21413. Trixie's 0.21.4 is still marked vulnerable,
# so a base bump would not clear them; upstream fixed the set in 0.22.1.
# dcraw_emu is the first-choice decoder for user-supplied camera RAW uploads
# (decodeRaw in apps/api/src/lib/format-decoders.ts), which puts attacker bytes
# straight into LibRaw, so both architectures build the fixed line from source
# and the distro package is left out of the runtime entirely.
# Base images match production to avoid shared-library ABI mismatches.
ARG TARGETARCH
FROM libheif-base-${TARGETARCH} AS libraw-builder
ARG LIBRAW_VERSION=0.22.2
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc g++ make curl ca-certificates pkg-config \
libjpeg-dev liblcms2-dev zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
RUN curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 \
--output /tmp/libraw.tar.gz \
"https://www.libraw.org/data/LibRaw-${LIBRAW_VERSION}.tar.gz" \
&& printf '%s %s\n' \
"de86b035655accff8d4010f1a221fdf50d353cb7b1422ba26f14a0db92612cfa" \
/tmp/libraw.tar.gz | sha256sum --check --strict - \
&& tar -xzf /tmp/libraw.tar.gz -C /tmp \
&& cd "/tmp/LibRaw-${LIBRAW_VERSION}" \
&& ./configure --prefix=/opt/libraw --disable-static \
--enable-openmp --enable-jpeg --enable-lcms \
&& make -j"$(nproc)" \
&& make install
# ============================================
# Stage 3: Platform-specific base images
# Pin tags to specific major.minor for reproducible builds.
# Pin tags to a stable upstream series; release digests provide artifact provenance.
# ============================================
FROM node:22-bookworm@sha256:5647be709086c696ff32edaaf1c70cd26d1da6ab2b39c32f3c7b4c4a31957e37 AS base-linux-arm64
# CUDA base must match the remaining GPU AI bundles' torch/onnxruntime wheels
@@ -210,17 +256,15 @@ FROM base-${TARGETOS}-${TARGETARCH} AS production
ARG TARGETARCH
ARG PANDOC_VERSION=3.10
ARG OCR_RUNTIME_INDEX_KEY_ID=
ARG OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64=
ARG OCR_RUNTIME_TRUST_ID=
ARG OCR_RUNTIME_TRUST_PEM_B64=
ARG SNAPOTTER_OFFICIAL_CONTAINER=0
# Public release-verification key pinned independently from the bundle host.
# Official release builds inject these repository variables and opt into the
# exact ABI target. Source builds fail closed unless their operator explicitly
# opts in and provides the same trust identity or mounts a trust file.
ENV OCR_RUNTIME_INDEX_KEY_ID=${OCR_RUNTIME_INDEX_KEY_ID} \
OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64=${OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64} \
SNAPOTTER_OFFICIAL_CONTAINER=${SNAPOTTER_OFFICIAL_CONTAINER}
# Official builds opt into the exact ABI target. Public release-verification
# metadata is validated and written to the image trust store below rather than
# exposed as image environment metadata. Runtime environment overrides remain
# supported for self-hosters who intentionally supply a different trust key.
ENV SNAPOTTER_OFFICIAL_CONTAINER=${SNAPOTTER_OFFICIAL_CONTAINER}
# Pin corepack's cache during image build. It is removed after dependency and
# browser installation so pnpm is not part of the production runtime surface.
@@ -249,7 +293,6 @@ RUN for i in 1 2 3; do apt-get -o Acquire::Retries=3 update && break || sleep $(
tini \
imagemagick \
libjxl-tools \
libraw-dev libraw-bin \
libopenexr-dev \
potrace \
ghostscript \
@@ -291,11 +334,16 @@ RUN for i in 1 2 3; do apt-get -o Acquire::Retries=3 update && break || sleep $(
apt-get install -y --no-install-recommends libcublas-12-6; \
fi \
&& case "$TARGETARCH" in \
amd64) PANDOC_ARCH=amd64 ;; \
arm64) PANDOC_ARCH=arm64 ;; \
amd64) PANDOC_ARCH=amd64; PANDOC_SHA256=d502599878eb29af3ae5f0cb5d559134df96534125d452c7a0674a5bad2c5ecf ;; \
arm64) PANDOC_ARCH=arm64; PANDOC_SHA256=b651c8bfd5a0a2f6650d6c0830131747ef67a1d9c0475b1399626611419e2205 ;; \
*) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 1 ;; \
esac \
&& curl -fsSL "https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-${PANDOC_ARCH}.deb" -o /tmp/pandoc.deb \
&& curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 \
--output /tmp/pandoc.deb \
"https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-${PANDOC_ARCH}.deb" \
&& printf '%s %s\n' "${PANDOC_SHA256}" /tmp/pandoc.deb \
| sha256sum --check --strict - \
&& apt-get install -y --no-install-recommends /tmp/pandoc.deb \
&& rm -f /tmp/pandoc.deb \
&& pandoc --version \
@@ -309,10 +357,22 @@ RUN for i in 1 2 3; do apt-get -o Acquire::Retries=3 update && break || sleep $(
# multi-GB base. They enter the Trivy CVE surface and ride the existing
# apt-get upgrade patching.
RUN install -d /usr/share/postgresql-common/pgdg \
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
&& curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 \
https://www.postgresql.org/media/keys/ACCC4CF8.asc \
-o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
&& curl -fsSL https://packages.redis.io/gpg \
&& printf '%s %s\n' \
"0144068502a1eddd2a0280ede10ef607d1ec592ce819940991203941564e8e76" \
/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
| sha256sum --check --strict - \
&& curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 \
https://packages.redis.io/gpg \
-o /usr/share/keyrings/redis-archive-keyring.asc \
&& printf '%s %s\n' \
"817b5a78358d00ed6b71884d70ad5d2eab9934badca1a34299fdc6a2e4a8ad20" \
/usr/share/keyrings/redis-archive-keyring.asc \
| sha256sum --check --strict - \
&& . /etc/os-release \
&& echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt ${VERSION_CODENAME}-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list \
@@ -325,25 +385,30 @@ RUN install -d /usr/share/postgresql-common/pgdg \
&& rm -rf /var/lib/apt/lists/*
# s6-overlay supervises the embedded service tree (postgres + redis + app).
# Pinned and checksum-verified against the upstream-published .sha256, consistent
# with the repo's digest-pinning posture. For stricter supply-chain pinning,
# replace the .sha256 fetch with a literal hash checked via
# `echo "<hash> <file>" | sha256sum -c -`.
# Pinned and checksum-verified against repository-controlled literal hashes.
# The values were independently matched against the upstream release assets.
ARG S6_OVERLAY_VERSION=3.2.0.2
RUN set -e; \
case "$TARGETARCH" in \
amd64) S6_ARCH=x86_64 ;; \
arm64) S6_ARCH=aarch64 ;; \
amd64) S6_ARCH=x86_64; S6_ARCH_SHA256=59289456ab1761e277bd456a95e737c06b03ede99158beb24f12b165a904f478 ;; \
arm64) S6_ARCH=aarch64; S6_ARCH_SHA256=8b22a2eaca4bf0b27a43d36e65c89d2701738f628d1abd0cea5569619f66f785 ;; \
*) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 1 ;; \
esac; \
cd /tmp; \
base="https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}"; \
for f in "s6-overlay-noarch.tar.xz" "s6-overlay-${S6_ARCH}.tar.xz"; do \
curl -fsSL -O "${base}/${f}"; \
curl -fsSL -O "${base}/${f}.sha256"; \
sha256sum -c "${f}.sha256"; \
tar -C / -Jxpf "${f}"; \
done; \
curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 --output s6-overlay-noarch.tar.xz \
"${base}/s6-overlay-noarch.tar.xz"; \
printf '%s %s\n' \
"6dbcde158a3e78b9bb141d7bcb5ccb421e563523babbe2c64470e76f4fd02dae" \
s6-overlay-noarch.tar.xz | sha256sum --check --strict -; \
curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 --output "s6-overlay-${S6_ARCH}.tar.xz" \
"${base}/s6-overlay-${S6_ARCH}.tar.xz"; \
printf '%s %s\n' "${S6_ARCH_SHA256}" "s6-overlay-${S6_ARCH}.tar.xz" \
| sha256sum --check --strict -; \
tar -C / -Jxpf s6-overlay-noarch.tar.xz; \
tar -C / -Jxpf "s6-overlay-${S6_ARCH}.tar.xz"; \
rm -f /tmp/s6-overlay-*
# Allow ImageMagick to use Ghostscript delegate for EPS (read for decode,
@@ -370,8 +435,20 @@ COPY --from=pdfcpu-builder /tmp/pdfcpu /usr/local/bin/pdfcpu
# LD_LIBRARY_PATH ensures our custom 1.21.2 libs take precedence over distro libheif1.
COPY --from=libheif-builder /opt/libheif/bin/ /usr/local/bin/
COPY --from=libheif-builder /opt/libheif/lib/ /usr/local/lib/
# LibRaw's dcraw_emu, built from source (see the libraw-builder stage). No
# distro libraw is installed, so this is the only RAW decoder in the image.
COPY --from=libraw-builder /opt/libraw/bin/dcraw_emu /usr/local/bin/
COPY --from=libraw-builder /opt/libraw/lib/ /usr/local/lib/
ENV LD_LIBRARY_PATH=/usr/local/lib
RUN ldconfig
# Assert the RAW decoder resolves to the source build and its runtime deps are
# satisfied. Without this a missing shared library would only surface when a
# user uploaded a RAW file, as a silent fall-through to the embedded preview.
RUN ldconfig \
&& [ "$(command -v dcraw_emu)" = "/usr/local/bin/dcraw_emu" ] \
&& ldd /usr/local/bin/dcraw_emu | grep -Eq 'libraw\.so\.[0-9]+ => /usr/local/lib/' \
&& dcraw_emu 2>&1 | grep -q 'dcraw emulator'
# Python venv - Base packages (rarely change, cached aggressively)
# Uses pre-built manylinux wheels where available; gcc/g++ above covers the rest.
@@ -412,6 +489,17 @@ RUN /opt/venv/bin/pip freeze | sha256sum | cut -d' ' -f1 > /opt/venv/.venv-versi
COPY docker/feature-manifest.json /app/docker/feature-manifest.json
COPY packages/ai/python/install_feature.py /app/packages/ai/python/install_feature.py
# Pin public OCR release trust independently from the bundle host. Source
# builds without trust metadata stay unconfigured; official builds fail closed
# unless both values are present, canonical, and identify an Ed25519 key.
COPY docker/write-ocr-runtime-trust.mjs /tmp/write-ocr-runtime-trust.mjs
RUN node /tmp/write-ocr-runtime-trust.mjs \
/app/docker/ocr-runtime-trust.json \
"${OCR_RUNTIME_TRUST_ID}" \
"${OCR_RUNTIME_TRUST_PEM_B64}" \
"${SNAPOTTER_OFFICIAL_CONTAINER}" \
&& rm -f /tmp/write-ocr-runtime-trust.mjs
WORKDIR /app
# The immutable OCR artifact builder runs inside this final image and copies
@@ -464,7 +552,6 @@ RUN apt-get purge -y --auto-remove \
gcc \
g++ \
python3-dev \
libraw-dev \
libopenexr-dev \
libcurl4-openssl-dev \
libdb-dev \
@@ -488,9 +575,14 @@ RUN apt-get purge -y --auto-remove \
libtirpc-dev \
rpcsvc-proto \
&& (corepack disable pnpm || true) \
&& rm -rf /usr/local/share/corepack /root/.cache/node/corepack /root/.cache/pip \
&& rm -f /usr/local/bin/pnpm /usr/local/bin/pnpx \
&& rm -rf /var/lib/apt/lists/* /tmp/*
&& rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack \
/usr/local/share/corepack /root/.cache/node/corepack /root/.cache/pip \
&& rm -f /usr/local/bin/corepack /usr/local/bin/npm /usr/local/bin/npx \
/usr/local/bin/pnpm /usr/local/bin/pnpx \
&& rm -rf /var/lib/apt/lists/* /tmp/* \
# --auto-remove can take a shared library the source-built dcraw_emu needs
# (libgomp1, liblcms2-2) with it. Re-assert the decoder still runs.
&& dcraw_emu 2>&1 | grep -q 'dcraw emulator'
# Copy source code for API (tsx runs TS directly - no build step needed)
COPY apps/api/src ./apps/api/src
@@ -556,7 +648,7 @@ ENV PORT=1349 \
LOGIN_ATTEMPT_LIMIT=30 \
LOG_LEVEL=info \
LOG_DIR=/data/logs \
TRUST_PROXY=true \
TRUST_PROXY=loopback,linklocal,uniquelocal \
OIDC_ENABLED=false \
EXTERNAL_URL=
+10 -6
View File
@@ -15,8 +15,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libde265-dev libx265-dev libjpeg-dev libpng-dev \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "https://github.com/strukturag/libheif/releases/download/v${LIBHEIF_VERSION}/libheif-${LIBHEIF_VERSION}.tar.gz" \
| tar xz \
RUN curl --fail --location --silent --show-error --retry 3 \
--proto '=https' --tlsv1.2 \
--output /tmp/libheif.tar.gz \
"https://github.com/strukturag/libheif/releases/download/v${LIBHEIF_VERSION}/libheif-${LIBHEIF_VERSION}.tar.gz" \
&& printf '%s %s\n' \
"75f530b7154bc93e7ecf846edfc0416bf5f490612de8c45983c36385aa742b42" \
/tmp/libheif.tar.gz | sha256sum --check --strict - \
&& tar -xzf /tmp/libheif.tar.gz \
&& cmake -B build -S "libheif-${LIBHEIF_VERSION}" \
-DCMAKE_INSTALL_PREFIX=/opt/libheif \
-DWITH_EXAMPLES=ON \
@@ -103,11 +109,9 @@ RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Environment for tests
# Non-sensitive test defaults. Authentication credentials belong to the test
# runner or Compose environment rather than the reusable image metadata.
ENV NODE_ENV=test \
AUTH_ENABLED=true \
DEFAULT_USERNAME=admin \
DEFAULT_PASSWORD=admin \
WORKSPACE_PATH=/tmp/test-workspace \
MAX_MEGAPIXELS=100 \
MAX_UPLOAD_SIZE_MB=100 \
+23
View File
@@ -8,6 +8,11 @@ dist
*.db-shm
.env
.env.local
.env.*
!.env.example
.npmrc
.pypirc
.netrc
.DS_Store
test-results
playwright-report
@@ -20,3 +25,21 @@ coverage
!README.md
test-*.png
audit_report.md
# Local agent context and secret material
.license-signing-key
.secrets
.claude
.codex
.local-wiki
AGENTS.md
CLAUDE.md
CONTEXT.md
PRD.md
*.pem
*.key
*.p12
*.pfx
*credentials*.json
*client_secret*.json
*service_account*.json
+53 -10
View File
@@ -287,7 +287,7 @@ PYONNX
# ── Step 5: Download models ──────────────────────────────────────────────
echo "=== Downloading models ==="
python3 << 'PYMODELS'
import json, os, sys, urllib.request, pathlib
import hashlib, hmac, json, os, re, sys, urllib.request
with open("/app/docker/feature-manifest.json") as f:
manifest = json.load(f)
@@ -308,21 +308,51 @@ if not models:
print(f" Downloading {len(models)} model(s)")
SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
REVISION_RE = re.compile(r"^[a-f0-9]{40}$")
def verify_sha256(path, expected, model_id):
digest = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
digest.update(chunk)
actual = digest.hexdigest()
if hmac.compare_digest(actual, expected):
return
try:
os.unlink(path)
except FileNotFoundError:
pass
raise RuntimeError(
f"{model_id} SHA-256 mismatch: expected {expected}, got {actual}"
)
def verify_min_size(path, min_size, model_id):
size = os.path.getsize(path)
if min_size and size < min_size:
raise RuntimeError(
f"{model_id} is {size:,} bytes, expected at least {min_size:,}"
)
return size
for model in models:
model_id = model["id"]
download_fn = model.get("downloadFn")
url = model.get("url")
expected_sha256 = model.get("sha256")
if url:
if not isinstance(expected_sha256, str) or not SHA256_RE.fullmatch(expected_sha256):
raise RuntimeError(f"{model_id} must declare a lowercase SHA-256 digest")
# Direct URL download via urllib
dest = os.path.join(models_dir, model["path"])
os.makedirs(os.path.dirname(dest), exist_ok=True)
print(f" [{model_id}] URL -> {model['path']}", flush=True)
urllib.request.urlretrieve(url, dest)
size = os.path.getsize(dest)
min_size = model.get("minSize", 0)
if min_size and size < min_size:
print(f" WARNING: {model_id} is {size:,} bytes, expected >= {min_size:,}", file=sys.stderr)
size = verify_min_size(dest, model.get("minSize", 0), model_id)
verify_sha256(dest, expected_sha256, model_id)
print(f" [{model_id}] Done ({size:,} bytes)")
elif download_fn == "hf_snapshot":
@@ -330,8 +360,12 @@ for model in models:
args = model["args"]
repo_id = args[0]
local_dir = os.path.join(models_dir, args[1])
revision = model.get("revision")
if not isinstance(revision, str) or not REVISION_RE.fullmatch(revision):
raise RuntimeError(f"{model_id} must pin a 40-character Hugging Face commit")
kwargs = {"repo_id": repo_id, "local_dir": local_dir}
kwargs["revision"] = revision
# Restrict the snapshot when specified. "file" pins one file (single-file
# models like an ONNX weight); "allowPatterns" narrows a multi-file model
@@ -348,11 +382,21 @@ for model in models:
print(f" [{model_id}] HF snapshot: {repo_id} -> {args[1]}", flush=True)
snapshot_download(**kwargs)
expected_path = model.get("path")
if expected_path:
verify_min_size(
os.path.join(models_dir, expected_path),
model.get("minSize", 0),
model_id,
)
print(f" [{model_id}] Done")
elif download_fn == "rembg_session":
if not isinstance(expected_sha256, str) or not SHA256_RE.fullmatch(expected_sha256):
raise RuntimeError(f"{model_id} must declare a lowercase SHA-256 digest")
args = model["args"]
session_name = args[0]
dest = os.path.join(rembg_home, f"{session_name}.onnx")
print(f" [{model_id}] rembg session: {session_name}", flush=True)
# birefnet-matting and birefnet-hr-matting are custom sessions
@@ -365,21 +409,20 @@ for model in models:
"birefnet-hr-matting": "https://github.com/ZhengPeng7/BiRefNet/releases/download/v1/BiRefNet_HR-matting-epoch_135.onnx",
}
if session_name in CUSTOM_BIREFNET:
dest = os.path.join(models_dir, "rembg", f"{session_name}.onnx")
os.makedirs(os.path.dirname(dest), exist_ok=True)
print(f" [{model_id}] Custom BiRefNet -> rembg/{session_name}.onnx", flush=True)
urllib.request.urlretrieve(CUSTOM_BIREFNET[session_name], dest)
size = os.path.getsize(dest)
print(f" [{model_id}] Done ({size:,} bytes)")
else:
from rembg import new_session
# Force CPU-only provider: onnxruntime-gpu segfaults trying to load
# TensorRT libs that aren't present in build containers (no GPU).
new_session(session_name, providers=["CPUExecutionProvider"])
print(f" [{model_id}] Done")
size = os.path.getsize(dest)
verify_sha256(dest, expected_sha256, model_id)
print(f" [{model_id}] Done ({size:,} bytes)")
else:
print(f" WARNING: Unknown download method for {model_id}", file=sys.stderr)
raise RuntimeError(f"Unknown download method for {model_id}")
print(" Model downloads complete")
PYMODELS
+17 -10
View File
@@ -38,7 +38,9 @@ services:
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-1000}
- MAX_USERS=${MAX_USERS:-0}
- SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS:-168}
- TRUST_PROXY=${TRUST_PROXY:-true}
# Believe X-Forwarded-For only from a peer on a private network. Set to
# `true` only if a proxy you control sits in front on a public address.
- TRUST_PROXY=${TRUST_PROXY:-loopback,linklocal,uniquelocal}
- DATABASE_URL=postgres://${POSTGRES_USER:-snapotter}:${POSTGRES_PASSWORD:-snapotter}@postgres:5432/${POSTGRES_DB:-snapotter}
- REDIS_URL=redis://:${REDIS_PASSWORD:-snapotter}@redis:6379
# 1.x upgrade: uncomment to import the old SQLite database on first boot;
@@ -91,9 +93,10 @@ services:
# signal a different-UID process, so docker stop is ungraceful
# ("[FATAL tini] forwarding signal: Operation not permitted" -> SIGKILL).
- KILL
# NOTE: security_opt: [no-new-privileges:true] is intentionally omitted.
# gosu requires setuid to drop from root to the snapotter user.
# Mitigation: cap_drop: ALL limits available capabilities after privilege drop.
# The entrypoint can still drop from root to the snapotter user while this
# prevents setuid binaries from granting privileges to untrusted processes.
security_opt:
- no-new-privileges:true
# NOTE: read_only: true is not set because PUID/PGID remapping requires
# writing to /etc/passwd and /etc/group. Consider using Docker --user flag
# instead of PUID/PGID for read-only rootfs support.
@@ -118,7 +121,7 @@ services:
max-file: "5"
postgres:
image: postgres:17-alpine
image: postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193
container_name: SnapOtter-postgres
environment:
POSTGRES_USER: ${POSTGRES_USER:-snapotter}
@@ -134,8 +137,8 @@ services:
cpus: 2
pids_limit: 256
# The official image starts as root and su-execs down to the postgres user,
# so setuid/setgid + chown caps are required. no-new-privileges and a
# read-only rootfs are omitted for the same privilege-drop reason as the app.
# so setuid/setgid + chown caps are required. A read-only rootfs is omitted
# because the entrypoint must initialize and adjust its data directory.
cap_drop:
- ALL
cap_add:
@@ -144,6 +147,8 @@ services:
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-snapotter} -d ${POSTGRES_DB:-snapotter}"]
interval: 10s
@@ -152,7 +157,7 @@ services:
start_period: 15s
redis:
image: redis:8-alpine
image: redis:8-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
container_name: SnapOtter-redis
command: >-
redis-server
@@ -169,8 +174,8 @@ services:
cpus: 2
pids_limit: 256
# The official image starts as root and gosu-drops to the redis user, so
# setuid/setgid + chown caps are required; no-new-privileges and a read-only
# rootfs are omitted for the same privilege-drop reason as the app.
# setuid/setgid + chown caps are required. A read-only rootfs is omitted
# because the entrypoint must initialize and adjust its data directory.
cap_drop:
- ALL
cap_add:
@@ -179,6 +184,8 @@ services:
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-snapotter}", "--no-auth-warning", "ping"]
interval: 10s
+2 -2
View File
@@ -85,7 +85,7 @@ services:
condition: service_healthy
postgres:
image: postgres:17-alpine
image: postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193
container_name: SnapOtter-test-postgres
environment:
POSTGRES_USER: snapotter
@@ -101,7 +101,7 @@ services:
- /var/lib/postgresql/data
redis:
image: redis:8-alpine
image: redis:8-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
container_name: SnapOtter-test-redis
command: ["redis-server", "--maxmemory-policy", "noeviction", "--appendonly", "no"]
healthcheck:
+17 -10
View File
@@ -37,7 +37,9 @@ services:
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-1000}
- MAX_USERS=${MAX_USERS:-0}
- SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS:-168}
- TRUST_PROXY=${TRUST_PROXY:-true}
# Believe X-Forwarded-For only from a peer on a private network. Set to
# `true` only if a proxy you control sits in front on a public address.
- TRUST_PROXY=${TRUST_PROXY:-loopback,linklocal,uniquelocal}
- DATABASE_URL=postgres://${POSTGRES_USER:-snapotter}:${POSTGRES_PASSWORD:-snapotter}@postgres:5432/${POSTGRES_DB:-snapotter}
- REDIS_URL=redis://:${REDIS_PASSWORD:-snapotter}@redis:6379
# 1.x upgrade: uncomment to import the old SQLite database on first boot;
@@ -90,9 +92,10 @@ services:
# signal a different-UID process, so docker stop is ungraceful
# ("[FATAL tini] forwarding signal: Operation not permitted" -> SIGKILL).
- KILL
# NOTE: security_opt: [no-new-privileges:true] is intentionally omitted.
# gosu requires setuid to drop from root to the snapotter user.
# Mitigation: cap_drop: ALL limits available capabilities after privilege drop.
# The entrypoint can still drop from root to the snapotter user while this
# prevents setuid binaries from granting privileges to untrusted processes.
security_opt:
- no-new-privileges:true
# NOTE: read_only: true is not set because PUID/PGID remapping requires
# writing to /etc/passwd and /etc/group. Consider using Docker --user flag
# instead of PUID/PGID for read-only rootfs support.
@@ -110,7 +113,7 @@ services:
max-file: "5"
postgres:
image: postgres:17-alpine
image: postgres:17-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193
container_name: SnapOtter-postgres
environment:
POSTGRES_USER: ${POSTGRES_USER:-snapotter}
@@ -126,8 +129,8 @@ services:
cpus: 2
pids_limit: 256
# The official image starts as root and su-execs down to the postgres user,
# so setuid/setgid + chown caps are required. no-new-privileges and a
# read-only rootfs are omitted for the same privilege-drop reason as the app.
# so setuid/setgid + chown caps are required. A read-only rootfs is omitted
# because the entrypoint must initialize and adjust its data directory.
cap_drop:
- ALL
cap_add:
@@ -136,6 +139,8 @@ services:
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-snapotter} -d ${POSTGRES_DB:-snapotter}"]
interval: 10s
@@ -144,7 +149,7 @@ services:
start_period: 15s
redis:
image: redis:8-alpine
image: redis:8-alpine@sha256:9d317178eceac8454a2284a9e6df2466b93c745529947f0cd42a0fa9609d7005
container_name: SnapOtter-redis
command: >-
redis-server
@@ -161,8 +166,8 @@ services:
cpus: 2
pids_limit: 256
# The official image starts as root and gosu-drops to the redis user, so
# setuid/setgid + chown caps are required; no-new-privileges and a read-only
# rootfs are omitted for the same privilege-drop reason as the app.
# setuid/setgid + chown caps are required. A read-only rootfs is omitted
# because the entrypoint must initialize and adjust its data directory.
cap_drop:
- ALL
cap_add:
@@ -171,6 +176,8 @@ services:
- FOWNER
- SETGID
- SETUID
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-snapotter}", "--no-auth-warning", "ping"]
interval: 10s
+44 -10
View File
@@ -46,42 +46,50 @@
{
"id": "rembg-u2net",
"downloadFn": "rembg_session",
"args": ["u2net"]
"args": ["u2net"],
"sha256": "8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491"
},
{
"id": "rembg-isnet-general-use",
"downloadFn": "rembg_session",
"args": ["isnet-general-use"]
"args": ["isnet-general-use"],
"sha256": "60920e99c45464f2ba57bee2ad08c919a52bbf852739e96947fbb4358c0d964a"
},
{
"id": "rembg-bria-rmbg",
"downloadFn": "rembg_session",
"args": ["bria-rmbg"]
"args": ["bria-rmbg"],
"sha256": "5b486f08200f513f460da46dd701db5fbb47d79b4be4b708a19444bcd4e79958"
},
{
"id": "rembg-birefnet-general-lite",
"downloadFn": "rembg_session",
"args": ["birefnet-general-lite"]
"args": ["birefnet-general-lite"],
"sha256": "5600024376f572a557870a5eb0afb1e5961636bef4e1e22132025467d0f03333"
},
{
"id": "rembg-birefnet-portrait",
"downloadFn": "rembg_session",
"args": ["birefnet-portrait"]
"args": ["birefnet-portrait"],
"sha256": "1ba1c8ff5a7bbfadc8d8d13fb11d7be793f91f23d9d466549e37a854f6668f99"
},
{
"id": "rembg-birefnet-general",
"downloadFn": "rembg_session",
"args": ["birefnet-general"]
"args": ["birefnet-general"],
"sha256": "58f621f00f5d756097615970a88a791584600dcf7c45b18a0a6267535a1ebd3c"
},
{
"id": "rembg-birefnet-matting",
"downloadFn": "rembg_session",
"args": ["birefnet-matting"]
"args": ["birefnet-matting"],
"sha256": "6065d27c615ea27308f5b88598dd8db116eb07436c7a323ca40d13b2866c309e"
},
{
"id": "rembg-birefnet-hr-matting",
"downloadFn": "rembg_session",
"args": ["birefnet-hr-matting"]
"args": ["birefnet-hr-matting"],
"sha256": "45b7d92ce0e2e75e1c8a564a9e84c87ac9da7f4315d2deeef3674864e809aef3"
}
],
"smokeImports": ["rembg", "onnxruntime"],
@@ -123,12 +131,14 @@
{
"id": "mediapipe-face-detector",
"url": "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/latest/blaze_face_short_range.tflite",
"sha256": "b4578f35940bf5a1a655214a1cce5cab13eba73c1297cd78e1a04c2380b0152f",
"path": "mediapipe/blaze_face_short_range.tflite",
"minSize": 100000
},
{
"id": "mediapipe-face-landmarker",
"url": "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
"sha256": "64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff",
"path": "mediapipe/face_landmarker.task",
"minSize": 1000000
}
@@ -166,6 +176,7 @@
"id": "lama-onnx",
"downloadFn": "hf_snapshot",
"args": ["Carve/LaMa-ONNX", "lama"],
"revision": "c3c0c9e468934d62e79c329e35d82dd09ff8c444",
"file": "lama_fp32.onnx",
"path": "lama/lama_fp32.onnx",
"minSize": 100000000
@@ -174,6 +185,7 @@
"id": "ddcolor-onnx",
"downloadFn": "hf_snapshot",
"args": ["facefusion/models-3.0.0", "ddcolor"],
"revision": "728b9659bd9691bf32cbf7f61af478d94b7ba81e",
"file": "ddcolor.onnx",
"path": "ddcolor/ddcolor.onnx",
"minSize": 50000000
@@ -181,6 +193,7 @@
{
"id": "opencv-colorize-prototxt",
"url": "https://raw.githubusercontent.com/richzhang/colorization/caffe/colorization/models/colorization_deploy_v2.prototxt",
"sha256": "d16418cef8df4ccd703a55ae0ef3960861d5010418f77c90d0a47689998a7169",
"path": "colorize-opencv/colorization_deploy_v2.prototxt",
"minSize": 0
},
@@ -188,6 +201,7 @@
"id": "opencv-colorize-caffemodel",
"downloadFn": "hf_snapshot",
"args": ["BilalSardar/Black-N-White-To-Color", "colorize-opencv"],
"revision": "41c04215906bbd3d8e2a09c07b3e76f42f0cd65e",
"file": "colorization_release_v2.caffemodel",
"path": "colorize-opencv/colorization_release_v2.caffemodel",
"minSize": 100000000,
@@ -196,6 +210,7 @@
{
"id": "opencv-colorize-points",
"url": "https://raw.githubusercontent.com/richzhang/colorization/caffe/colorization/resources/pts_in_hull.npy",
"sha256": "b5dec01315c34f43f1c8c089e84c45ae35d1838d8e77ed0e7ca930f79ffa450e",
"path": "colorize-opencv/pts_in_hull.npy",
"minSize": 0
}
@@ -243,12 +258,13 @@
"id": "sd15-inpainting",
"downloadFn": "hf_snapshot",
"args": ["stable-diffusion-v1-5/stable-diffusion-inpainting", "sd15-inpainting"],
"revision": "8a4288a76071f7280aedbdb3253bdb9e9d5d84bb",
"allowPatterns": ["*.json", "*.txt", "tokenizer/*", "*.fp16.safetensors"],
"path": "sd15-inpainting/unet/diffusion_pytorch_model.fp16.safetensors",
"minSize": 1000000000
}
],
"smokeImports": ["diffusers", "torch"],
"smokeImports": ["diffusers", "torch", "transformers"],
"enablesTools": ["erase-object"]
},
"upscale-enhance": {
@@ -300,24 +316,28 @@
{
"id": "realesrgan-x4plus",
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth",
"sha256": "4fa0d38905f75ac06eb49a7951b426670021be3018265fd191d2125df9d682f1",
"path": "realesrgan/RealESRGAN_x4plus.pth",
"minSize": 60000000
},
{
"id": "realesrgan-x2plus",
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth",
"sha256": "49fafd45f8fd7aa8d31ab2a22d14d91b536c34494a5cfe31eb5d89c2fa266abb",
"path": "realesrgan/RealESRGAN_x2plus.pth",
"minSize": 60000000
},
{
"id": "gfpgan-v1.3",
"url": "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth",
"sha256": "c953a88f2727c85c3d9ae72e2bd4846bbaf59fe6972ad94130e23e7017524a70",
"path": "gfpgan/GFPGANv1.3.pth",
"minSize": 300000000
},
{
"id": "codeformer-pth",
"url": "https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth",
"sha256": "1009e537e0c2a07d4cabce6355f53cb66767cd4b4297ec7a4a64ca4b8a5684b7",
"path": "codeformer/codeformer.pth",
"minSize": 350000000
},
@@ -325,6 +345,7 @@
"id": "codeformer-onnx",
"downloadFn": "hf_snapshot",
"args": ["facefusion/models-3.0.0", "codeformer"],
"revision": "728b9659bd9691bf32cbf7f61af478d94b7ba81e",
"file": "codeformer.onnx",
"path": "codeformer/codeformer.onnx",
"minSize": 100000000
@@ -332,18 +353,21 @@
{
"id": "facexlib-detection",
"url": "https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_Resnet50_Final.pth",
"sha256": "6d1de9c2944f2ccddca5f5e010ea5ae64a39845a86311af6fdf30841b0a5a16d",
"path": "gfpgan/facelib/detection_Resnet50_Final.pth",
"minSize": 100000000
},
{
"id": "facexlib-parsing",
"url": "https://github.com/xinntao/facexlib/releases/download/v0.2.2/parsing_parsenet.pth",
"sha256": "3d558d8d0e42c20224f13cf5a29c79eba2d59913419f945545d8cf7b72920de2",
"path": "gfpgan/facelib/parsing_parsenet.pth",
"minSize": 80000000
},
{
"id": "scunet-color-real",
"url": "https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_psnr.pth",
"sha256": "fa78899ba2caec9d235a900e91d96c689da71c42029230c2028b00f09f809c2e",
"path": "scunet/scunet_color_real_psnr.pth",
"minSize": 3000000
},
@@ -351,6 +375,7 @@
"id": "nafnet-sidd",
"downloadFn": "hf_snapshot",
"args": ["mikestealth/nafnet-models", "nafnet"],
"revision": "cd3a91ddc41c9c3a53ee809b3ddd5263c9e94e51",
"file": "NAFNet-SIDD-width64.pth",
"path": "nafnet/NAFNet-SIDD-width64.pth",
"minSize": 60000000
@@ -413,6 +438,7 @@
"id": "lama-onnx",
"downloadFn": "hf_snapshot",
"args": ["Carve/LaMa-ONNX", "lama"],
"revision": "c3c0c9e468934d62e79c329e35d82dd09ff8c444",
"file": "lama_fp32.onnx",
"path": "lama/lama_fp32.onnx",
"minSize": 100000000
@@ -421,6 +447,7 @@
"id": "codeformer-onnx",
"downloadFn": "hf_snapshot",
"args": ["facefusion/models-3.0.0", "codeformer"],
"revision": "728b9659bd9691bf32cbf7f61af478d94b7ba81e",
"file": "codeformer.onnx",
"path": "codeformer/codeformer.onnx",
"minSize": 100000000
@@ -428,36 +455,42 @@
{
"id": "realesrgan-x4plus",
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth",
"sha256": "4fa0d38905f75ac06eb49a7951b426670021be3018265fd191d2125df9d682f1",
"path": "realesrgan/RealESRGAN_x4plus.pth",
"minSize": 60000000
},
{
"id": "facexlib-detection",
"url": "https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_Resnet50_Final.pth",
"sha256": "6d1de9c2944f2ccddca5f5e010ea5ae64a39845a86311af6fdf30841b0a5a16d",
"path": "gfpgan/facelib/detection_Resnet50_Final.pth",
"minSize": 100000000
},
{
"id": "facexlib-parsing",
"url": "https://github.com/xinntao/facexlib/releases/download/v0.2.2/parsing_parsenet.pth",
"sha256": "3d558d8d0e42c20224f13cf5a29c79eba2d59913419f945545d8cf7b72920de2",
"path": "gfpgan/facelib/parsing_parsenet.pth",
"minSize": 80000000
},
{
"id": "mediapipe-face-detector",
"url": "https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/latest/blaze_face_short_range.tflite",
"sha256": "b4578f35940bf5a1a655214a1cce5cab13eba73c1297cd78e1a04c2380b0152f",
"path": "mediapipe/blaze_face_short_range.tflite",
"minSize": 100000
},
{
"id": "mediapipe-face-landmarker",
"url": "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
"sha256": "64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff",
"path": "mediapipe/face_landmarker.task",
"minSize": 1000000
},
{
"id": "scunet-color-real",
"url": "https://github.com/cszn/KAIR/releases/download/v1.0/scunet_color_real_psnr.pth",
"sha256": "fa78899ba2caec9d235a900e91d96c689da71c42029230c2028b00f09f809c2e",
"path": "scunet/scunet_color_real_psnr.pth",
"minSize": 3000000
}
@@ -537,7 +570,8 @@
{
"id": "faster-whisper-small",
"downloadFn": "hf_snapshot",
"args": ["Systran/faster-whisper-small", "faster-whisper-small"]
"args": ["Systran/faster-whisper-small", "faster-whisper-small"],
"revision": "536b0662742c02347bc0e980a01041f333bce120"
}
],
"smokeImports": ["faster_whisper", "ctranslate2"],
+8
View File
@@ -0,0 +1,8 @@
module github.com/snapotter/snapotter/docker/go-tools/caire
go 1.25.0
require (
github.com/esimov/caire v1.5.0
golang.org/x/image v0.43.0
)
+32
View File
@@ -0,0 +1,32 @@
gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/esimov/caire v1.5.0 h1:7MiLeBW6yBi8OzCcbscGBzTk8OpXjikvkd+s7e9bOyY=
github.com/esimov/caire v1.5.0/go.mod h1:QBxapKatzPhgBo9wkrm51sm1S3534zT+40zLn1TK6aI=
github.com/esimov/pigo v1.4.5 h1:ySG0QqMh02VNALvHnx04L1ScRu66N6XA5vLLga8GiLg=
github.com/esimov/pigo v1.4.5/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+8
View File
@@ -0,0 +1,8 @@
module github.com/snapotter/snapotter/docker/go-tools/pdfcpu
go 1.25.0
require (
github.com/pdfcpu/pdfcpu v0.13.0
golang.org/x/image v0.43.0
)
+28
View File
@@ -0,0 +1,28 @@
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
github.com/hhrutter/pkcs7 v0.2.2 h1:xMoifoVWah1LNym3C0pomEiLmyJyVIBXt/8oTPyPz+8=
github.com/hhrutter/pkcs7 v0.2.2/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
github.com/hhrutter/tiff v1.0.3 h1:POV5xITOE1Lt5FvP24ylft0LyCmHmc8GkJ1SVlvUyk0=
github.com/hhrutter/tiff v1.0.3/go.mod h1:zZDLVY4cp9za2FLrryAaGszwWYAUM6DrRiBR0l//mxA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/pdfcpu/pdfcpu v0.13.0 h1:7maI7K0w4pJsgX9u7eeCsK4+An/+xEVkJwAwyd7/n3M=
github.com/pdfcpu/pdfcpu v0.13.0/go.mod h1:Pz8elxcY3MHc3W65HeeDbuSBvsq+OK+enMVdBsvKCj4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+2 -2
View File
@@ -14,11 +14,11 @@ pillow==12.3.0 --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48d
tqdm==4.67.1 --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2
omegaconf==2.3.0 --hash=sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b
antlr4-python3-runtime==4.9.3 --hash=sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b
requests==2.32.3 --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
requests==2.33.0 --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b
colorlog==6.9.0 --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff
certifi==2025.1.31 --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe
charset-normalizer==3.4.1 --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d
idna==3.10 --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
idna==3.15 --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8
urllib3==2.7.0 --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
coloredlogs==15.0.1 --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934
humanfriendly==10.0 --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477
+2 -2
View File
@@ -14,11 +14,11 @@ pillow==12.3.0 --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e707
tqdm==4.67.1 --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2
omegaconf==2.3.0 --hash=sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b
antlr4-python3-runtime==4.9.3 --hash=sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b
requests==2.32.3 --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
requests==2.33.0 --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b
colorlog==6.9.0 --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff
certifi==2025.1.31 --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe
charset-normalizer==3.4.1 --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1
idna==3.10 --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
idna==3.15 --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8
urllib3==2.7.0 --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
coloredlogs==15.0.1 --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934
humanfriendly==10.0 --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477
+63
View File
@@ -0,0 +1,63 @@
import { createPublicKey } from "node:crypto";
import { writeFileSync } from "node:fs";
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
function fail(message) {
throw new Error(`OCR runtime trust generation failed: ${message}`);
}
function main() {
const [output, trustId = "", encodedPem = "", officialSetting = "0"] = process.argv.slice(2);
if (!output) fail("an output path is required");
if (officialSetting !== "0" && officialSetting !== "1") {
fail("SNAPOTTER_OFFICIAL_CONTAINER must be 0 or 1");
}
if (!trustId && !encodedPem) {
if (officialSetting === "1") fail("official images require public trust metadata");
return;
}
if (!trustId || !encodedPem) fail("public trust metadata is incomplete");
if (!SAFE_ID.test(trustId)) fail("the trust identifier is invalid");
if (!CANONICAL_BASE64.test(encodedPem)) fail("the public key is not canonical base64");
const decodedPem = Buffer.from(encodedPem, "base64");
if (decodedPem.toString("base64") !== encodedPem) {
fail("the public key is not canonical base64");
}
let publicKey;
try {
publicKey = createPublicKey(decodedPem);
} catch {
fail("the public key is not valid PEM");
}
if (publicKey.asymmetricKeyType !== "ed25519") {
fail("the public key is not Ed25519");
}
const trustStore = {
schemaVersion: 1,
keys: [
{
keyId: trustId,
algorithm: "ed25519",
publicKey: decodedPem.toString("utf8"),
},
],
};
writeFileSync(output, `${JSON.stringify(trustStore, null, 2)}\n`, {
encoding: "utf8",
flag: "wx",
mode: 0o444,
});
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : "OCR runtime trust generation failed");
process.exitCode = 1;
}