mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
+142
-50
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user