# SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
# One image, three processes (spec §13). Node 24 LTS, nothing but the app in the final stage, and
# a non-root user that owns nothing it runs.
#
# The `@conusai-kit` and `@conusai-sdk` scopes are served by the ConusAI vendor registry. Its URL
# and token arrive as a BuildKit secret the install stage copies to the user-level ~/.npmrc, the
# only config pnpm trusts for credentials, and never reach a layer
# (docs/how-to/private-package-registries.md).

FROM node:24-slim AS deps
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
    PNPM_HOME=/pnpm \
    CI=1
WORKDIR /app
# The manifests alone, so a source change does not re-resolve the dependency graph.
COPY package.json pnpm-workspace.yaml .npmrc pnpm-lock.yaml ./
COPY config/package.json ./config/
COPY modules/package.json ./modules/
COPY apps/web/package.json ./apps/web/
COPY apps/worker/package.json ./apps/worker/
RUN corepack enable pnpm
# A customer repository commits pnpm-lock.yaml (decision AT-D6), and the install is frozen
# against it: an image is built from exactly the dependency graph the gate ran.
#
# The credential arrives under one secret id per build path: plain `npmrc` for a preset
# single-image build, `conusai_npmrc` for a compose build (the platform prefixes its compose
# secrets so a tenant's own can never collide). Mount both and take whichever was delivered —
# a build that asked for only one id got an empty mount on the other path, and pnpm fell back
# to the public registry where the vendor scopes 404 (stage, deployment 4, 2026-10-20).
#
# The trap removes the copied credential before the shell exits: a file left behind would be
# committed to the cached deps layer, token and all.
RUN --mount=type=secret,id=conusai_npmrc,target=/run/secrets/conusai_npmrc,required=false \
    --mount=type=secret,id=npmrc,target=/run/secrets/npmrc,required=false \
    trap 'rm -f /root/.npmrc' EXIT; \
    if [ -s /run/secrets/conusai_npmrc ]; then \
      cp /run/secrets/conusai_npmrc /root/.npmrc; \
    elif [ -s /run/secrets/npmrc ]; then \
      cp /run/secrets/npmrc /root/.npmrc; \
    fi; \
    pnpm install --frozen-lockfile --ignore-scripts

FROM deps AS build
WORKDIR /app
COPY . .
# config and apps/worker emit JavaScript; apps/web builds the Next.js shell. pnpm runs them in
# dependency order, so config is built before the two that import it.
RUN pnpm -r run build
# The anchor catalogue (`data-guide` and `guideAnchor` literals), then the code-guides bundle # kit:guides
# conusai.json declares as guides.bundle: public content the platform reads out of this image # kit:guides
# and publishes after a healthy deploy (ADR-045); no publication credential is ever in it. # kit:guides
RUN pnpm kit guides:anchors --out guides-anchors.json # kit:guides
RUN node apps/worker/dist/guides-bundle.js /app/.conusai/guides-bundle.json guides-anchors.json # kit:guides
# The image keeps what it runs and nothing else: the formatter, the type-checker and the test
# runner are build-time tools. The workspace links the three processes resolve stay, because they
# are dependencies and not devDependencies.
RUN pnpm prune --prod --ignore-scripts

FROM node:24-slim AS runtime
ENV NODE_ENV=production \
    COREPACK_ENABLE_DOWNLOAD_PROMPT=0
WORKDIR /app
# node:24-slim ships a `node` user (uid 1000). The app owns none of the files it runs, so a
# process that is compromised cannot rewrite its own code.
#
# The next step for size, once a customer's dependency graph has settled, is Next.js's
# `output: "standalone"` for the web process plus `pnpm deploy --prod` for the worker, which
# leaves the build's own toolchain (the compiler binaries) out of the image entirely.
COPY --from=build --chown=root:root /app /app
# Git checkouts created under the service's private umask can carry 0700 directories
# and 0600 files into the build. COPY preserves those modes: root can build them,
# but node cannot even resolve Next through an unsearchable parent (KI-210).
# Keep code root-owned and deny group/other writes while allowing runtime reads
# and directory traversal. X preserves executable files without making data executable.
RUN chmod -R a+rX,go-w /app
USER node
# Every process is started by compose.yaml with its own command; there is no default, because a
# container that silently starts the wrong one is worse than one that does not start.
CMD ["node", "-e", "console.error('start this image with one of the commands in compose.yaml: web, worker or migrate'); process.exit(64)"]
