diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2716d01..0117d28 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,15 +6,18 @@ on: pull_request: branches: [ "main", "api-v2" ] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Use Node.js 24.x LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24.x' cache: 'npm' @@ -31,10 +34,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Use Node.js 24.x LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '24.x' cache: 'npm' @@ -45,6 +48,11 @@ jobs: cd nest npm ci + - name: Audit Production Dependencies + run: | + cd nest + npm audit --omit=dev --audit-level=critical + - name: Lint run: | cd nest diff --git a/docker-compose.yml b/docker-compose.yml index 3224d6c..14308cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: privileged: true sscan-ipfs: - image: ipfs/kubo:v0.39.0 + image: ipfs/kubo:v0.40.1@sha256:9c70a3dba0b5f362bf99317a02384a194f5c91cf5388abdb8fe7d64d83dd20bb container_name: ${BRANCH_NAME:-main}_sscan-ipfs restart: always environment: @@ -35,13 +35,13 @@ services: - ./docker-data/ipfs:/ipfsdata - ./ipfs/001-configure.sh:/container-init.d/001-configure.sh:ro healthcheck: - test: ["CMD-SHELL", "ipfs swarm peers | head -1"] + test: ["CMD", "ipfs", "id", "--format="] interval: 10s timeout: 5s retries: 5 sscan-nginx: - image: nginx:1.27 + image: nginx:1.30.3-alpine-slim@sha256:d5b51cfc7d55fc7a7bcf4d1d577b9c3738331df56d68f0b1d8ac9795b9470a5a container_name: ${BRANCH_NAME:-main}_sscan-nginx restart: always ports: @@ -56,13 +56,23 @@ services: environment: - NGINX_PORT=${NGINX_PORT} - NEST_PORT=${NEST_PORT} - - GATE_PORT=${GATE_PORT} - - IPFS_PORT=${IPFS_PORT} + - GATE_PORT=8080 + - IPFS_PORT=5001 - IPFS_ADMIN_USER=${IPFS_ADMIN_USER} - IPFS_ADMIN_PASS=${IPFS_ADMIN_PASS} + healthcheck: + test: ["CMD-SHELL", "wget --spider --quiet http://127.0.0.1:$${NGINX_PORT}/"] + interval: 15s + timeout: 5s + start_period: 10s + retries: 3 depends_on: - - sscan-nest - - sscan-ipfs + sscan-nest: + condition: service_healthy + restart: true + sscan-ipfs: + condition: service_healthy + restart: true networks: nwk: diff --git a/nest/.dockerignore b/nest/.dockerignore index b512c09..b6246d1 100644 --- a/nest/.dockerignore +++ b/nest/.dockerignore @@ -1 +1,6 @@ -node_modules \ No newline at end of file +.git +**/._* +coverage +dist +node_modules +npm-debug.log* diff --git a/nest/Dockerfile b/nest/Dockerfile index 258f1b4..d1bdb91 100644 --- a/nest/Dockerfile +++ b/nest/Dockerfile @@ -1,17 +1,19 @@ -# Build stage -FROM node@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 AS build +# syntax=docker/dockerfile:1.7 -WORKDIR /app +ARG NEAR_CLI_RS_VERSION=0.27.0 + +# Keep exact tags for reviewability and digests for reproducibility. +FROM node:24.17.0-alpine3.24@sha256:156b55f92e98ccd5ef49578a8cea0df4679826564bad1c9d4ef04462b9f0ded6 AS node-build -RUN apk add --update python3 make g++\ - && rm -rf /var/cache/apk/* +WORKDIR /app # Copy configuration files +COPY eslint.config.mjs ./ COPY tsconfig*.json ./ COPY package*.json ./ -# Install dependencies from package-lock.json -RUN npm ci +# Install dependencies from package-lock.json. +RUN --mount=type=cache,target=/root/.npm,sharing=locked npm ci # Copy application sources (.ts, .tsx, js) COPY src/ src/ @@ -19,52 +21,72 @@ COPY src/ src/ # Build application (produces dist/ folder) RUN npm run build -# Runtime (production) layer -FROM docker@sha256:7d85d0eda291f1a7ab6df4a9d1802b5ad4cf9145a088bd11188c78dcb5c7392b AS production +# Keep only dependencies needed by the production application. +FROM node-build AS node-production + +RUN npm prune --omit=dev + +# Build near-cli-rs separately so Rust and compiler tooling never reach runtime. +FROM rust:1.96.1-alpine3.24@sha256:a41f7740f8b45d45795624eec13a8b42263cc700f19f7e4e86e04d3dda08a479 AS near-cli-build + +ARG NEAR_CLI_RS_VERSION -# Install dependencies for Rust, build tools, and HIDAPI RUN apk add --no-cache \ - python3 \ - git \ - curl \ - perl \ - protobuf \ - eudev-dev \ build-base \ - pkgconfig \ + eudev-dev \ linux-headers \ - bash + perl \ + pkgconf \ + protobuf + +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + --mount=type=cache,target=/tmp/cargo-target,sharing=locked \ + CARGO_TARGET_DIR=/tmp/cargo-target \ + cargo install near-cli-rs --version "${NEAR_CLI_RS_VERSION}" --locked -# Install Rust -ENV RUSTUP_HOME=/usr/local/rustup -ENV CARGO_HOME=/usr/local/cargo +RUN test "$(near --version)" = "near-cli-rs ${NEAR_CLI_RS_VERSION}" + +# Runtime (production) layer +FROM docker:29.6.1-dind@sha256:66d292e5c26bd33a6f6f61cacb880de2186339a524ecba1ce098dbbaceed6515 AS production -RUN mkdir -p $RUSTUP_HOME $CARGO_HOME \ - && curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y \ - && ln -s $RUSTUP_HOME /root/.rustup \ - && ln -s $CARGO_HOME /root/.cargo -# Add Rust to the PATH environment variable for all subsequent commands -ENV PATH="/usr/local/cargo/bin:$PATH" +ARG NEAR_CLI_RS_VERSION -# Install near-cli-rs for contract verification -RUN cargo install near-cli-rs --locked +LABEL dev.sourcescan.near-cli-rs.version="${NEAR_CLI_RS_VERSION}" -# Set environment variable for near-cli-rs to run in non-interactive mode -ENV NEAR_CI=1 +# Git requires libcurl, while Compose and Buildx are host-side tools here. +RUN apk upgrade --no-cache libcurl \ + && rm -f \ + /usr/local/libexec/docker/cli-plugins/docker-buildx \ + /usr/local/libexec/docker/cli-plugins/docker-compose + +# Keep near-cli-rs non-interactive and point child processes at the embedded daemon. +ENV NEAR_CI=1 \ + DOCKER_HOST=unix:///var/run/docker.sock WORKDIR /app -# Copy node and npm from build stage -COPY --from=build /usr/local /usr/local +# Copy Node and the pinned near CLI into the runtime image. +COPY --from=node-build /usr/lib/libgcc_s.so.1 /usr/lib/ +COPY --from=node-build /usr/lib/libstdc++.so.6* /usr/lib/ +COPY --from=node-build /usr/local/bin/node /usr/local/bin/node +COPY --from=near-cli-build /usr/local/cargo/bin/near /usr/local/bin/near -# Copy dependencies files +# Copy package metadata and production dependencies. COPY package*.json ./ - -# Install runtime dependencies (without dev/test dependencies) -RUN npm ci --omit=dev +COPY --from=node-production /app/node_modules/ ./node_modules/ +COPY docker-healthcheck.js ./ # Copy production build -COPY --from=build /app/dist/ ./dist/ +COPY --from=node-build /app/dist/ ./dist/ + +# Fail the image build if any required runtime binary is unusable. +RUN node --version \ + && near --version \ + && docker --version + +HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \ + CMD ["node", "docker-healthcheck.js"] # Start the application when dockerd is ready -CMD ["sh", "-ec", "rm -f /var/run/docker.pid /var/run/docker.sock /var/run/docker/containerd/containerd.pid /var/run/docker/containerd/containerd.sock /var/run/docker/containerd/containerd.sock.ttrpc /var/run/docker/containerd/containerd-debug.sock; dockerd & until docker info >/dev/null 2>&1; do sleep 1; done; exec node dist/main.js"] +CMD ["sh", "-ec", "rm -f /var/run/docker.pid /var/run/docker.sock /var/run/docker/containerd/containerd.pid /var/run/docker/containerd/containerd.sock /var/run/docker/containerd/containerd.sock.ttrpc /var/run/docker/containerd/containerd-debug.sock; dockerd & until node docker-healthcheck.js --docker-only >/dev/null 2>&1; do sleep 1; done; exec node dist/main.js"] diff --git a/nest/docker-healthcheck.js b/nest/docker-healthcheck.js new file mode 100644 index 0000000..ab28a94 --- /dev/null +++ b/nest/docker-healthcheck.js @@ -0,0 +1,44 @@ +'use strict'; + +const http = require('node:http'); + +function request(options, label) { + return new Promise((resolve, reject) => { + const req = http.get(options, (response) => { + response.resume(); + + if (response.statusCode >= 200 && response.statusCode < 300) { + resolve(); + return; + } + + reject(new Error(`${label} returned HTTP ${response.statusCode}`)); + }); + + req.setTimeout(3000, () => req.destroy(new Error(`${label} timed out`))); + req.on('error', reject); + }); +} + +async function main() { + await request( + { socketPath: '/var/run/docker.sock', path: '/_ping' }, + 'Docker daemon', + ); + + if (process.argv.includes('--docker-only')) { + return; + } + + const port = Number(process.env.NEST_PORT); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error('NEST_PORT must be a valid TCP port'); + } + + await request({ host: '127.0.0.1', port, path: '/' }, 'Verifier API'); +} + +main().catch((error) => { + console.error(error.message); + process.exit(1); +}); diff --git a/nginx/scripts/entrypoint.sh b/nginx/scripts/entrypoint.sh index 2986256..cafe693 100755 --- a/nginx/scripts/entrypoint.sh +++ b/nginx/scripts/entrypoint.sh @@ -4,11 +4,15 @@ set -e # Generate htpasswd from environment variables if [ -n "$IPFS_ADMIN_USER" ] && [ -n "$IPFS_ADMIN_PASS" ]; then echo "Generating htpasswd for user: $IPFS_ADMIN_USER" - echo "$IPFS_ADMIN_USER:$(openssl passwd -apr1 $IPFS_ADMIN_PASS)" > /etc/nginx/.htpasswd + password_hash=$(printf '%s\n' "$IPFS_ADMIN_PASS" | mkpasswd -P 0 -m sha512) + printf '%s:%s\n' "$IPFS_ADMIN_USER" "$password_hash" > /etc/nginx/.htpasswd else echo "Warning: IPFS_ADMIN_USER or IPFS_ADMIN_PASS not set, creating empty htpasswd" touch /etc/nginx/.htpasswd fi +chown root:nginx /etc/nginx/.htpasswd +chmod 640 /etc/nginx/.htpasswd + # Start nginx exec /docker-entrypoint.sh "$@"