c6d3566475
F4: a rename/move + body edit in one diff used to lose the edit (renamed pages
went only into renamesMoves, never updates). Now computePushActions also
emits an updates entry for renames, AND threads the OLD path via a new
UpdateAction.basePath so applyPushActions resolves the 3-way merge base from
the pre-rename file. Without it the base lookup at the new path returns null
and degrades to a 2-way merge that rolls back concurrent Docmost edits; with
it the edited block wins while a concurrent edit to another block survives.
A plain (status M) update carries no basePath and is byte-identical to before.
F5: test the CREATE path stripping conflict markers (autoMergeConflicts on).
F6: .env.example documents GIT_SYNC_REMOTE_TEMPLATE as deferred/inert scaffolding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
248 lines
11 KiB
Bash
248 lines
11 KiB
Bash
# 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.
|
|
|
|
# Agent-roles catalog source: an http(s):// base URL to the catalog's raw files
|
|
# (the server appends /index.json and /bundles/<id>/<lang>.json). This value is
|
|
# baked into the Docker image at build time per branch (see the Dockerfile ARG
|
|
# AI_AGENT_ROLES_CATALOG_URL and the CI build-args). Set it here only to point a
|
|
# local/non-Docker run at a catalog; if unset, the "import role from catalog"
|
|
# admin feature is unavailable. Local-filesystem sources are no longer supported.
|
|
# AI_AGENT_ROLES_CATALOG_URL=
|
|
|
|
# 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=
|
|
#
|
|
# SCAFFOLDING for the DEFERRED remote-push feature (SPEC §7) — NOT yet
|
|
# implemented and currently INERT. The vendored sync engine does not consume
|
|
# this value anywhere (git push to a remote is deferred), so setting it has NO
|
|
# effect today: vaults remain local-only regardless. It is validated and carried
|
|
# only so the wiring is ready for when remote push lands. The intended future
|
|
# shape is a per-space URL template where the literal "{spaceId}" is substituted
|
|
# per space (e.g. git@host:vault-{spaceId}.git).
|
|
# GIT_SYNC_REMOTE_TEMPLATE=
|
|
#
|
|
# 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
|
|
#
|
|
# Watchdog timeout in ms for the spawned `git http-backend` process serving a
|
|
# git smart-HTTP push (default: 120000). A stalled/hung receive-pack is killed
|
|
# after this deadline so it cannot hold the per-space lock forever.
|
|
# GIT_SYNC_BACKEND_TIMEOUT_MS=120000
|
|
#
|