mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-01 07:02:35 +00:00
* fix(render): implement the /api/* proxy the Blueprint assumed The Blueprint wired GITNEXUS_UPSTREAM_URL into gitnexus-web and its comment claimed docker-server.mjs proxied /api/* to the private server. No such proxy existed -- docker-server.mjs was a pure static file server and nothing in the repo read GITNEXUS_UPSTREAM_URL. So the deployed UI had no reachable backend: GITNEXUS_BACKEND_URL was unset, no window.__GITNEXUS_CONFIG__ was injected, and ui-constants.ts fell back to http://localhost:4747 in the visitor's own browser. Restore the implementation from fork history (ecdcb0ef): - docker-server.mjs: real reverse proxy for /api/*, accepting the scheme-less host:port that `fromService: hostport` yields, with hop-by-hop header stripping, bounded body buffering, retry across an upstream restart, and an idle timeout. Falls back to RENDER_EXTERNAL_URL for backendUrl so the browser's API calls are same-origin and this proxy forwards them. - Dockerfile.cli: honor an injected $PORT (default 4747) instead of hardcoding the port, so Render's port detection and `hostport` agree. Unset GITNEXUS_UPSTREAM_URL disables the proxy entirely, so docker-compose behavior is unchanged. render.yaml is deliberately left alone here -- the Blueprint topology is a later change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(render): name the real env var in URL warnings backendUrl now falls back to RENDER_EXTERNAL_URL, so a malformed RENDER_EXTERNAL_URL was reported as a bad GITNEXUS_BACKEND_URL. Name whichever variable actually supplied the value, and let validHttpUrl quote the operator's raw input rather than the derived string -- so a bad GITNEXUS_UPSTREAM_URL is echoed back as set, not with http:// prepended. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(deploy): rewrite X-Forwarded-For and strip response hop-by-hop headers The API trusts private-network hops and keys its per-IP rate limiters off req.ip, so forwarding the client's X-Forwarded-For chain verbatim let any caller rotate a fake address per request and slip the limits on the expensive analyze/embed routes. Collapse the chain to the entry Render's load balancer appends -- the rightmost, never the leftmost, since the leftmost is client-supplied and spoofable -- or the socket peer when there is no chain. The chain is replaced, not appended, which is what keeps GITNEXUS_TRUST_PROXY=1 correct despite the extra hop. Also drop hop-by-hop headers from the upstream response, not just the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(deploy): extract proxy header helpers Pull the X-Forwarded-For chain collapse and the hop-by-hop strip out of proxyToUpstream into named helpers, matching the file's existing small-helper style (validHttpUrl, readBodyCapped, failGateway). Sharing one stripHopByHopHeaders call site in each direction makes the request/response symmetry visible. No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: tighten comments across the proxy changes Compress the prose in docker-server.mjs, Dockerfile.cli, and the proxy tests to the non-obvious "why" only -- including stating plainly that terminating Origin leaves the API's CSRF guard with no traffic to protect. No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(deploy): keep the restored diff strictly proxy-scoped The restored file carried one line unrelated to the proxy -- an `.ico` entry in `contentTypes`. Drop it so that with GITNEXUS_UPSTREAM_URL unset the static-serving behavior is byte-identical to before, and every hunk in this PR is proxy work. Serving favicon.ico as image/x-icon is worth doing, just not here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(deploy): extract the log-sanitizing helper both env readers duplicated `validHttpUrl` and `numberFromEnv` each inlined the same control-character scrub and 200-char truncation before echoing an operator value into a warning. Name it once so the log-forging guard is a single, obvious thing rather than a pattern a future env reader has to remember to copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(deploy): harden the restored proxy against config and framing edge cases Four findings from a clean-code pass over the restore, each a case where the code was right for the deployment we expect and wrong for one we might get: - numberFromEnv now rejects out-of-range values. Every consumer reads <= 0 as "disabled", so GITNEXUS_PROXY_TIMEOUT_MS=-1 removed the idle timeout outright and hung proxied requests. Warn and keep the documented default instead of obeying a typo. Subsumes the Math.max(1, ...) on retry attempts. - stripHopByHopHeaders drops headers that Connection *names* as single-hop, not just the well-known eight (RFC 7230 6.1). The fixed list alone leaked them. - Retry eligibility follows body framing rather than method (RFC 7230 3.3.3): a bodyless DELETE buffers nothing, so it is replayable, and keying off the method sent it down the stream-once path for no reason. - proxyToUpstream asserts the resolved target's origin. The /api/ route guard already makes req.url host-relative, but that leaves an SSRF boundary resting on a two-step argument; this proxy has exactly one legitimate destination. Deferred, not fixed: clientAddressFor trusts X-Forwarded-For unconditionally, which only holds behind an appending load balancer. That's a trust-boundary decision and lands with the edge token instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(deploy): fold ten copies of the proxy fixture into one, tighten comments The proxy tests each stood up the same four things — a dist/ to serve, a fake upstream, a docker-server pointed at it, and teardown that leaks neither a process nor a temp dir — and differed only in how the upstream misbehaves. Two named helpers plus eight inline copies, ~200 lines of it. withProxy() takes that difference as options: a handler (or null for a port nothing listens on), a late bind for the restart window, a scheme-less upstream URL, and env overrides. Every test now reads as its own setup plus assertions, and what the upstream saw arrives in one ctx object instead of a per-test closure variable. Same 39 tests, same assertions. Verified by mutating the source three ways — rightmost XFF to leftmost, dropping the Connection-list strip, keying hasBody off the method — and confirming exactly the four expected tests fail. Also trimmed the wordier comments on both files down to the load-bearing part. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
131 lines
7.2 KiB
Text
131 lines
7.2 KiB
Text
ARG BUILDPLATFORM
|
|
ARG TARGETPLATFORM
|
|
# Pinned npm version used to replace the bundled npm in the upstream Node
|
|
# image. Bumping requires a coordinated update in Dockerfile.web and
|
|
# gitnexus/Dockerfile.test so all images bootstrap the same npm.
|
|
ARG NPM_VERSION=11.14.1
|
|
|
|
# -- Builder -----------------------------------------------------------
|
|
# Native modules (tree-sitter-*, onnxruntime-node, node-gyp builds for
|
|
# tree-sitter-proto / tree-sitter-swift) require python3 + a C/C++ toolchain.
|
|
# node:22-bookworm-slim
|
|
FROM node:22-bookworm-slim@sha256:9f6d5975c7dca860947d3915877f85607946403fc55349f39b4bc3688448bb6e AS builder
|
|
ARG NPM_VERSION
|
|
|
|
WORKDIR /app
|
|
|
|
RUN npx --yes npm@${NPM_VERSION} install -g npm@${NPM_VERSION}
|
|
|
|
# Toolchain for node-gyp / native builds.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
|
|
|
# Build gitnexus-shared first - gitnexus depends on it as a workspace.
|
|
COPY gitnexus-shared/package.json gitnexus-shared/package-lock.json ./gitnexus-shared/
|
|
RUN npm ci --prefix gitnexus-shared
|
|
COPY gitnexus-shared ./gitnexus-shared
|
|
RUN rm -f gitnexus-shared/tsconfig.tsbuildinfo
|
|
RUN npm run build --prefix gitnexus-shared
|
|
|
|
# Copy the full gitnexus package before installing - `npm ci` triggers
|
|
# `postinstall` (patches tree-sitter-swift, builds the vendored
|
|
# tree-sitter-proto) and `prepare` (compiles TypeScript via scripts/build.js),
|
|
# both of which need the source tree.
|
|
COPY gitnexus ./gitnexus
|
|
RUN npm ci --prefix gitnexus
|
|
|
|
# Drop dev dependencies for a smaller runtime layer.
|
|
RUN npm prune --omit=dev --prefix gitnexus
|
|
|
|
# `npm prune` removes anything not in package.json's dependency tree — which
|
|
# includes the VENDORED tree-sitter grammars (materialized into node_modules/ by
|
|
# postinstall, but not declared as deps) and their freshly-built native bindings.
|
|
# The `serve` image analyzes/parses uploaded repos at runtime, so those grammars
|
|
# must survive into the runtime layer. Re-run the grammar postinstall here in the
|
|
# builder (which still has python3/make/g++ and the hoisted node-addon-api /
|
|
# node-gyp-build) to re-materialize + rebuild them after the prune. This is
|
|
# load-bearing for tree-sitter-c (a core, REQUIRED grammar now vendored, #2116):
|
|
# as a former `dependency` it used to survive prune; vendored, it would not.
|
|
RUN npm run postinstall --prefix gitnexus
|
|
|
|
# -- Runtime -----------------------------------------------------------
|
|
# node:22-bookworm-slim
|
|
FROM node:22-bookworm-slim@sha256:9f6d5975c7dca860947d3915877f85607946403fc55349f39b4bc3688448bb6e AS runtime
|
|
|
|
# curl for the healthcheck; git for cloning; ca-certificates for TLS verification.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends curl git ca-certificates && rm -rf /var/lib/apt/lists/* \
|
|
&& rm -rf /usr/local/lib/node_modules/npm \
|
|
&& rm -rf /usr/local/lib/node_modules/corepack \
|
|
&& rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack
|
|
|
|
WORKDIR /app
|
|
|
|
# Pre-create the data directory and hand it to the unprivileged `node` user
|
|
# so the bind-mounted volume is writable without root.
|
|
RUN mkdir -p /data/gitnexus && chown -R node:node /data
|
|
|
|
COPY --from=builder --chown=node:node /app/gitnexus/dist ./gitnexus/dist
|
|
COPY --from=builder --chown=node:node /app/gitnexus/node_modules ./gitnexus/node_modules
|
|
COPY --from=builder --chown=node:node /app/gitnexus/package.json ./gitnexus/package.json
|
|
COPY --from=builder --chown=node:node /app/gitnexus/scripts/install-duckdb-extension.mjs ./gitnexus/scripts/install-duckdb-extension.mjs
|
|
COPY --from=builder --chown=node:node /app/gitnexus/vendor ./gitnexus/vendor
|
|
|
|
# Expose the `gitnexus` binary on PATH so the documented Docker workflow
|
|
# (`docker compose exec gitnexus-server gitnexus index /workspace/<repo>`)
|
|
# works without users having to invoke `node /app/gitnexus/dist/cli/index.js`.
|
|
# `npm prune --omit=dev` in the builder stage strips `node_modules/.bin/`
|
|
# entries, so the `gitnexus` bin declared in package.json (`dist/cli/index.js`,
|
|
# which already carries `#!/usr/bin/env node` and 755 perms) is otherwise
|
|
# unreachable from $PATH.
|
|
RUN ln -s /app/gitnexus/dist/cli/index.js /usr/local/bin/gitnexus
|
|
|
|
# Bake the LadybugDB FTS extension into the image so BM25 keyword search works
|
|
# at runtime. The server runs the default `load-only` extension policy (the read
|
|
# pool pins `{ policy: 'load-only' }`), so a runtime `LOAD EXTENSION fts` never
|
|
# INSTALLs — the extension must already exist in the runtime user's HOME
|
|
# extension dir, or every keyword search silently degrades (no FTS indexes are
|
|
# written and ranking falls back to vector-only with only a `warning` field).
|
|
# Run the installer as the `node` user with the SAME HOME the server runs under,
|
|
# so `INSTALL fts` materializes the extension under `$HOME/.lbdb/extension` where
|
|
# the runtime `LOAD` resolves it offline. `ENV HOME` is pinned because Docker
|
|
# does not derive HOME from `USER`, so without it build-install and runtime-load
|
|
# would resolve different paths. Requires network egress for the one-time
|
|
# INSTALL; the build fails loudly if it cannot fetch the extension. The DB-size
|
|
# default comes from GITNEXUS_LBUG_MAX_DB_SIZE (single source of truth, matches
|
|
# the runtime) — it only sizes the throwaway scratch DB used to run INSTALL.
|
|
# The second `--verify-only` step re-LOADs the extension in a FRESH process
|
|
# under the same HOME, so a HOME/extension-dir mismatch fails the build here
|
|
# rather than silently degrading keyword search to vector-only at runtime.
|
|
ENV HOME=/home/node \
|
|
GITNEXUS_LBUG_MAX_DB_SIZE=17179869184
|
|
RUN su node -s /bin/sh -c "HOME=/home/node node /app/gitnexus/scripts/install-duckdb-extension.mjs fts" \
|
|
&& su node -s /bin/sh -c "HOME=/home/node node /app/gitnexus/scripts/install-duckdb-extension.mjs fts --verify-only"
|
|
|
|
# Published runtime assets (in package.json `files`). Placed AFTER the DuckDB
|
|
# FTS-extension RUN above so editing hook/skill content does not invalidate that
|
|
# network-fetching cache layer; they have no input dependency on it.
|
|
# `hooks/`: dist/cli/resolve-invocation.js does
|
|
# `require('../../hooks/claude/resolve-analyze-cmd.cjs')` at module load — the
|
|
# single source of truth for the npm-11 npx-crash invocation decision (#1939).
|
|
# Without it, `gitnexus analyze` inside the image crashes with MODULE_NOT_FOUND
|
|
# before it does any work (#2130). `skills/`: the CLI reads the bundled SKILL.md
|
|
# templates from `<pkg>/skills/` for `gitnexus analyze --skills` and `gitnexus
|
|
# setup`/`uninstall`; absent, those degrade silently (placeholder content / zero
|
|
# skills installed). (The web UI bundle `web/`, also in `files`, is deliberately
|
|
# NOT shipped: this builder never builds gitnexus-web, so the image is API-only;
|
|
# the UI is the separate Dockerfile.web image / hosted app.)
|
|
COPY --from=builder --chown=node:node /app/gitnexus/hooks ./gitnexus/hooks
|
|
COPY --from=builder --chown=node:node /app/gitnexus/skills ./gitnexus/skills
|
|
|
|
USER node
|
|
|
|
# The web UI defaults to http://localhost:4747 - keep that contract.
|
|
ENV GITNEXUS_HOME=/data/gitnexus \
|
|
NODE_ENV=production \
|
|
PORT=4747
|
|
|
|
EXPOSE 4747
|
|
|
|
# Bind 0.0.0.0 for the host's mapped port, honoring an injected $PORT (Render
|
|
# sets one). `sh -c` expands it; `exec` keeps the server PID 1 so SIGTERM still
|
|
# reaches it. Platforms can rely on this instead of a dockerCommand override.
|
|
CMD ["sh", "-c", "exec gitnexus serve --host 0.0.0.0 --port \"${PORT:-4747}\""]
|