Addresses the stability + test-coverage warnings from the #119 review: - git-http-backend.service.ts: add `'error'` handlers to child.stdout/stderr. An EventEmitter 'error' with no listener (e.g. EPIPE when the client aborts mid-response) is rethrown by Node as an uncaught exception and crashes the process; now swallowed + logged (never echoed to the client). - TEST INFRA: a jest setupFile shims `navigator`/`MessageChannel` for the `node` testEnvironment. react-dom@18 reads `navigator` at module-init (pulled in via @docmost/editor-ext -> @tiptap/react), so every spec transitively importing the conversion engine — including git-http.service.spec.ts — previously FAILED TO LOAD ("navigator is not defined") and ran ZERO tests. With the shim those specs now run (git-sync integration: 11 suites / 133 tests green). - git-http.service.spec.ts: cover the 503 lock-held push path — `ingestExternalPush` rejecting `GitSyncLockHeldError` -> 503 + Retry-After + "git-sync busy, retry", no double header write (+ the already-headers-sent no-rewrite path). - git-http-backend.service.spec.ts: unit-test run() — child 'error'/'close' before headers -> 500; normal CGI parse+stream; stdout/stderr 'error' (EPIPE) swallowed; synchronous spawn throw -> 500. - page-change.listener.ts: implement OnModuleDestroy to clearTimeout all pending debounce timers on shutdown (+ test). - .env.example: vaults are non-bare working repos, not "bare repos". (Docs deleted by the stray commit were restored in 9cdbce54.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
237 lines
10 KiB
Plaintext
237 lines
10 KiB
Plaintext
# your domain, e.g https://example.com
|
|
APP_URL=http://localhost:3000
|
|
PORT=3000
|
|
|
|
# --- Security / reverse proxy ---
|
|
# The app derives the client IP (req.ip) from the `X-Forwarded-For` header via
|
|
# Fastify `trustProxy`. That header is client-forgeable, so XFF is trusted only
|
|
# from proxies on the configured trusted networks. Deploy this app behind a
|
|
# trusted reverse proxy that SETS/OVERWRITES (not appends) `X-Forwarded-For`
|
|
# with the real client IP. If XFF is trusted from an untrusted source, any
|
|
# per-IP throttling — including the /mcp Basic brute-force limiter — can be
|
|
# bypassed by an attacker who simply spoofs `X-Forwarded-For` to rotate IPs.
|
|
# (The /mcp limiter keeps a global per-email key as an IP-independent backstop,
|
|
# but the per-IP and per-IP+email keys rely on a trustworthy X-Forwarded-For.)
|
|
#
|
|
# TRUST_PROXY controls which proxies are trusted to set X-Forwarded-For.
|
|
# Default (unset/empty): `loopback, linklocal, uniquelocal` — XFF is trusted
|
|
# ONLY from private/loopback proxies, so a public-IP client cannot spoof req.ip.
|
|
# This is the safe default for the common case where the reverse proxy runs on
|
|
# loopback or a private network; req.ip still resolves to the real client.
|
|
# WARNING: this changed the previous default of trust-all. If your reverse proxy
|
|
# sits on a PUBLIC IP, the default will NOT trust its XFF and req.ip will be the
|
|
# proxy's IP — set TRUST_PROXY accordingly. Accepted values:
|
|
# - true restore trust-all (ONLY safe if a trusted proxy ALWAYS overwrites
|
|
# X-Forwarded-For; otherwise clients can spoof their IP)
|
|
# - false never trust X-Forwarded-For (req.ip is the socket peer)
|
|
# - <int> number of trusted proxy hops in front of the app
|
|
# - <list> comma-separated CIDR/IP list of trusted proxies, e.g.
|
|
# `127.0.0.1, 10.0.0.0/8`
|
|
# TRUST_PROXY=
|
|
|
|
# APP_SECRET has a DUAL role: it signs JWTs AND derives the AES-256-GCM key that
|
|
# encrypts stored AI-provider credentials (API keys) at rest. CONSEQUENCE: if you
|
|
# change APP_SECRET after setup, every stored AI API key becomes undecryptable —
|
|
# you must re-enter them in AI settings — and all existing sessions/JWTs are
|
|
# invalidated. Choose it ONCE, keep it stable, and back it up alongside your DB.
|
|
# minimum of 32 characters. Generate one with: openssl rand -hex 32
|
|
APP_SECRET=REPLACE_WITH_LONG_SECRET
|
|
|
|
JWT_TOKEN_EXPIRES_IN=30d
|
|
|
|
DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public"
|
|
REDIS_URL=redis://127.0.0.1:6379
|
|
|
|
# options: local | s3 | azure
|
|
STORAGE_DRIVER=local
|
|
|
|
# S3 driver config
|
|
AWS_S3_ACCESS_KEY_ID=
|
|
AWS_S3_SECRET_ACCESS_KEY=
|
|
AWS_S3_REGION=
|
|
AWS_S3_BUCKET=
|
|
AWS_S3_ENDPOINT=
|
|
AWS_S3_FORCE_PATH_STYLE=
|
|
|
|
# Azure Blob Storage driver config
|
|
AZURE_STORAGE_ACCOUNT_NAME=
|
|
AZURE_STORAGE_ACCOUNT_KEY=
|
|
AZURE_STORAGE_CONTAINER=
|
|
|
|
# default: 50mb
|
|
FILE_UPLOAD_SIZE_LIMIT=
|
|
|
|
# options: smtp | postmark
|
|
MAIL_DRIVER=smtp
|
|
MAIL_FROM_ADDRESS=hello@example.com
|
|
MAIL_FROM_NAME=Docmost
|
|
|
|
# SMTP driver config
|
|
SMTP_HOST=127.0.0.1
|
|
SMTP_PORT=587
|
|
SMTP_USERNAME=
|
|
SMTP_PASSWORD=
|
|
SMTP_SECURE=false
|
|
SMTP_IGNORETLS=false
|
|
|
|
# Postmark driver config
|
|
POSTMARK_TOKEN=
|
|
|
|
# for custom drawio server
|
|
DRAWIO_URL=
|
|
|
|
# Gotenberg URL for server-side PDF export
|
|
GOTENBERG_URL=
|
|
|
|
DISABLE_TELEMETRY=false
|
|
|
|
# Allow other sites to embed Docmost in an iframe.
|
|
IFRAME_EMBED_ALLOWED=false
|
|
|
|
# Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed.
|
|
# Example: https://intranet.example.com,https://portal.example.com
|
|
IFRAME_ALLOWED_ORIGINS=
|
|
|
|
# Enable debug logging in production (default: false)
|
|
DEBUG_MODE=false
|
|
|
|
# Log database queries
|
|
DEBUG_DB=false
|
|
|
|
# Log http requests
|
|
LOG_HTTP=false
|
|
|
|
# MCP server (community): the embedded /mcp endpoint authenticates PER USER.
|
|
# An MCP client authenticates with one of:
|
|
# - HTTP Basic: `Authorization: Basic base64(email:password)` — the user's own
|
|
# Docmost login/password. The server validates the credentials and the MCP
|
|
# session then acts under that user's permissions (edits attributed to them).
|
|
# - Bearer access JWT: `Authorization: Bearer <access-jwt>` (the user's
|
|
# `authToken` cookie value). Validated as an ACCESS token.
|
|
#
|
|
# OPTIONAL service-account fallback. When a request carries NEITHER Basic NOR
|
|
# Bearer credentials and these are set, the MCP session falls back to this
|
|
# shared service account (back-compat; useful for CI/scripts). Leave BLANK to
|
|
# require per-user credentials.
|
|
MCP_DOCMOST_EMAIL=
|
|
MCP_DOCMOST_PASSWORD=
|
|
# MCP_DOCMOST_API_URL=http://127.0.0.1:3000/api
|
|
# Optional shared guard for the /mcp endpoint. When set, every /mcp request must
|
|
# carry a matching `X-MCP-Token` header (separate from `Authorization`, which now
|
|
# carries the per-user credentials). When unset, /mcp relies on the per-user
|
|
# credentials above plus the workspace MCP toggle and network isolation (do not
|
|
# expose the port publicly).
|
|
# MCP_TOKEN=
|
|
# MCP_SESSION_IDLE_MS=1800000
|
|
#
|
|
# AI-AGENT ATTRIBUTION (comments/pages written via MCP are badged as "AI"):
|
|
# attribution is driven by a per-user `is_agent` flag on the users row. There is
|
|
# NO admin UI/API for it — set it out-of-band with SQL. Use a DEDICATED service
|
|
# account for the MCP fallback above and flag ONLY that account, e.g.:
|
|
# UPDATE users SET is_agent = true WHERE email = 'mcp-bot@your-domain';
|
|
# NEVER set is_agent on a human or shared account — every action by that account
|
|
# (including normal human edits) would then be mis-attributed as AI.
|
|
|
|
# Per-embedding-call timeout in milliseconds for the RAG indexer.
|
|
# A slow/hung embeddings endpoint fails after this and the batch continues.
|
|
# AI_EMBEDDING_TIMEOUT_MS=120000
|
|
|
|
# Silence timeout (ms) for streaming chat/agent AI calls AND external-MCP traffic.
|
|
# Bounds time-to-first-byte and the gap BETWEEN chunks (NOT the total turn length),
|
|
# so an arbitrarily long turn that keeps streaming is never cut. Finite so a hung
|
|
# provider is eventually broken instead of leaking forever. Default 900000 (15 min).
|
|
# AI_STREAM_TIMEOUT_MS=900000
|
|
|
|
# Keep-alive recycle window (ms) for streaming chat/agent AI + external-MCP calls.
|
|
# A pooled connection idle longer than this is closed instead of reused, so a
|
|
# NAT / egress firewall / reverse proxy that silently drops idle connections
|
|
# cannot poison a reused socket into a PRE-RESPONSE `read ECONNRESET`. Lower it if
|
|
# your egress drops idle connections faster than ~10s. Default 10000 (10 s).
|
|
# AI_STREAM_KEEPALIVE_MS=10000
|
|
|
|
# Silence timeout (ms) for EXTERNAL-MCP transport ONLY (not the chat provider).
|
|
# Tighter than AI_STREAM_TIMEOUT_MS so a byte-silent/hung MCP server is broken in
|
|
# ~5 min instead of 15. Note it also cuts a legitimately long but byte-silent
|
|
# single tool call (a slow crawl that emits nothing until done) and an SSE
|
|
# transport idling >5 min BETWEEN tool calls. Default 300000 (5 min).
|
|
# AI_MCP_STREAM_TIMEOUT_MS=300000
|
|
|
|
# Total wall-clock cap (ms) for ONE external MCP tool call (app-level, not
|
|
# transport). Aborts a tool that keeps the socket warm (SSE heartbeats / trickle)
|
|
# but never returns a result — which the silence timeout above never breaks.
|
|
# Default 900000 (15 min).
|
|
# AI_MCP_CALL_TIMEOUT_MS=900000
|
|
|
|
# --- Anonymous public-share AI assistant ---
|
|
# Opt-in per workspace (AI settings -> "public share assistant"; off by default).
|
|
# When enabled, anonymous visitors of a published share can ask an AI about that
|
|
# share at POST /api/shares/ai/stream. The assistant is read-only and hard-scoped
|
|
# to the single share tree, but every call spends real tokens on the workspace
|
|
# owner's configured AI provider.
|
|
#
|
|
# DEPLOYMENT REQUIREMENT: the per-IP rate limit on this endpoint is only
|
|
# effective behind a trusted reverse proxy that OVERWRITES (not appends)
|
|
# X-Forwarded-For with the real client IP. The app runs with trustProxy, so
|
|
# without such a proxy an attacker can rotate X-Forwarded-For to evade the
|
|
# per-IP limit. Put this endpoint (and the app) behind a proxy you control that
|
|
# sets X-Forwarded-For to the real client IP.
|
|
#
|
|
# Backstop: a cluster-wide, sliding-window cap per workspace (IP-independent,
|
|
# keyed by the server-resolved workspace id) bounds the owner's bill even if the
|
|
# per-IP limit is fully evaded. It is a COST backstop, not an access control, and
|
|
# FAILS CLOSED if Redis is unavailable (an optional assistant briefly going
|
|
# offline is safer than an unbounded bill). Override the hourly cap below
|
|
# (default: 100 calls per workspace per rolling hour).
|
|
# SHARE_AI_WORKSPACE_MAX_PER_HOUR=100
|
|
#
|
|
# Per-request output-token ceiling for the anonymous assistant (default: 512).
|
|
# Worst-case output per accepted call = agent steps (5) × this value.
|
|
# SHARE_AI_MAX_OUTPUT_TOKENS=512
|
|
#
|
|
# Second cost backstop: a cluster-wide per-workspace rolling-DAY token budget
|
|
# (input re-sent per step + output, summed across every accepted turn). The
|
|
# hourly request cap above bounds how MANY calls run, not how expensive each is,
|
|
# so this caps the owner's actual provider bill directly. Like the request cap it
|
|
# FAILS CLOSED if Redis is unavailable (default: 1,000,000 tokens per workspace
|
|
# per rolling day).
|
|
# SHARE_AI_WORKSPACE_TOKEN_BUDGET_PER_DAY=1000000
|
|
|
|
# --- GIT-SYNC (native two-way Docmost <-> git Markdown sync) ---
|
|
# Master switch. Off by default. When 'true', GIT_SYNC_SERVICE_USER_ID below is
|
|
# REQUIRED (the service account that git-originated create/move/rename/delete are
|
|
# attributed to) — the server refuses to boot with sync enabled and no user id.
|
|
# GIT_SYNC_ENABLED=false
|
|
#
|
|
# Serve the per-space vaults over smart-HTTP (the /git host). Defaults to
|
|
# GIT_SYNC_ENABLED when unset.
|
|
# GIT_SYNC_HTTP_ENABLED=false
|
|
#
|
|
# REQUIRED when GIT_SYNC_ENABLED=true: id of the user that git-originated page
|
|
# operations (create / move / rename / delete) are attributed to.
|
|
# GIT_SYNC_SERVICE_USER_ID=
|
|
#
|
|
# Where the per-space working vaults live (non-bare repos; the engine needs a
|
|
# working tree).
|
|
# Defaults to "<DATA_DIR or ./data>/git-sync".
|
|
# GIT_SYNC_DATA_DIR=
|
|
#
|
|
# Optional remote URL template to mirror each space's vault to (e.g. a git host).
|
|
# Leave unset to keep vaults local-only.
|
|
# GIT_SYNC_REMOTE_TEMPLATE=
|
|
#
|
|
# Path to the SSH private key used when pushing to GIT_SYNC_REMOTE_TEMPLATE.
|
|
# GIT_SYNC_SSH_KEY_PATH=
|
|
#
|
|
# Poll-safety interval in ms — the cadence of the background reconcile cycle
|
|
# (default: 15000).
|
|
# GIT_SYNC_POLL_INTERVAL_MS=15000
|
|
#
|
|
# Debounce window in ms for collapsing bursts of page edits into one sync cycle
|
|
# (default: 2000).
|
|
# GIT_SYNC_DEBOUNCE_MS=2000
|
|
#
|
|
# Defense-in-depth absolute cap on soft-deletes applied per push cycle
|
|
# (default: 5). A non-convergent / phantom-absence cycle can never trash more
|
|
# than this many pages without an explicit override.
|
|
# GIT_SYNC_MAX_DELETES_PER_CYCLE=5
|