Zod's .optional() accepts undefined but not null; passing null caused a
ZodError in getOutputFileURL on every compressed download request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a compile result is served from cache (fromCache: true), pdfDownloadUrl
is set to the cached format `/download/.../build/editorId-buildId/output/cached/output.pdf`,
which doesn't match the /compressed route. pdfFile.build always contains the
raw buildId regardless of cache state, so use that to construct the URL directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- compilePdfQuality: 'none' | 'ebook' | 'screen' replaces compressPdf: boolean
throughout the stack (types, context, compiler, RequestParser, CompileManager)
- Compile dropdown now shows Normal / Compressed / Draft under "PDF quality"
(ebook=150dpi, screen=72dpi via Ghostscript)
- Download button opens a modal with the same three quality options plus
per-option descriptions and a Ghostscript attribution note; compressed
downloads are served by a new /download/.../output.pdf/compressed endpoint
that fetches the last build from clsi and pipes it through GS on demand
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
compressPdf/setCompressPdf were added to local-compile-context but never
forwarded through DetachCompileProvider, so pdf-compile-button got
undefined for both and the Compressed option was a no-op.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings.pdfCachingMode was never set so ProjectController always sent
'disabled' to the client regardless of ENABLE_PDF_CACHING. Wire it to
a new PDF_CACHING_MODE env var so both server (chunk storage) and client
(chunk requests) are active together.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CI workflow uses an inline deployment spec, not verso-prod-app.yaml,
so the env var must appear in both places.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix auto-compile resetting to true on Typst page refresh: use a separate
one-time marker key (autocompile_typst_default_set) so usePersistedState's
remove-if-default optimisation no longer makes "off" indistinguishable from
"never set"
- Enable PDF content caching: generate output.pdfxref (via qpdf --show-xref)
after every linearisation step in OutputCacheManager._copyFile, and set
ENABLE_PDF_CACHING in k8s so OutputCacheManager actually delivers chunks
- Add optional GS /ebook compression toggle in the Typst compile dropdown
(pdf_size_normal / pdf_size_compressed); runs ghostscript in-place before
the build-cache step so content caching still applies on top
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
typst watch emits "compiled successfully" then immediately starts a new
compile cycle when it detected file changes during the current one (heavy
documents trigger this because ResourceWriter writes many files while
compilation is already underway). The new cycle truncates output.pdf,
so the CompileManager copy gets size=0 → "no PDF produced".
Fix: have typst write to output-typst-snap.pdf (staging path). The
moment "compiled successfully" is detected, atomically rename it to
output.pdf — before the 150 ms diagnostic-flush wait, before yielding
the event loop. The concurrent recompile writes to the staging file
again without touching output.pdf.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After reset --mixed FETCH_HEAD, staged content can differ from both
HEAD and the working tree when new file content has been written.
Without -f, git rm --cached refuses to remove such entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only output.* (output.html, output.pdf, etc.) bypassed the
web server. Companion assets (CSS, JS, images, videos, fonts) from
Quarto/RevealJS presentations went through the web server, hitting the
miscOutputDownload rate limiter (1000 req/hour per project). A heavy
presentation loads 50+ assets per page view and would exhaust the limit
quickly, returning 429 for most assets and leaving the page unstyled.
The build ID is an unguessable random token — the same security model
already used for output.html being served without auth. Routing all
output files through clsi-nginx is safe and also fixes Content-Type
for media files (video/mp4 etc.) now that the mime type fix is in place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.mp4/.webm/etc. were served as application/octet-stream because the
custom types block overrides nginx defaults and was missing media types.
Browsers refuse to play media with a non-media Content-Type, especially
with X-Content-Type-Options: nosniff set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
remote.origin.promisor=true causes git to fetch missing blobs from the
remote during the commit connectivity check, failing if the server is
slow or unavailable. --filter=blob:none on fetch works independently of
promisor config so trees-only fetching is preserved. Existing cached
repos have the config unset on next use.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getAllDocs reads from MongoDB (docstore) which may lag behind the
in-memory document-updater. Without flushing first, edits made in
the Overleaf editor are silently skipped — the pushed file content
matches the pre-edit version so git sees no change and doesn't update
the remote. This is the same flush the zip download uses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
git add -A with a pathspec can silently skip modified files when git's
cached-tree extension still holds a valid tree hash for the subPath
directory (e.g. mtime unchanged after a pull checkout). Replacing with
git rm --cached + git add forces a full rescan regardless of cache state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--depth=1 limits fetch to only the latest commit's tree objects instead
of the full history, dramatically reducing fetch time on repos with
many commits. A git ls-remote check before fetching skips the fetch
entirely when local HEAD already matches remote HEAD, which is the
common case after a push with no external changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
git read-tree only updated the index, leaving HEAD on our previous
local commit. The push commit's diff then included all remote changes
outside subPath as if we made them. reset --mixed also moves HEAD to
FETCH_HEAD, so the new commit's parent is the remote HEAD and the diff
shows only the managed area changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull now reuses the persistent repo cache with --filter=blob:none and
checks out only the configured subPath, so only relevant blobs are
downloaded. Push likewise fetches trees-only and uses git read-tree to
update the index without touching the working tree, preserving files
outside the managed area without downloading their content.
Upserts on pull are now parallelised (p-limit 5) to speed up import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of both issues:
- OOM on Gitea: fresh git init every push → all blobs are "new" →
100% of content sent in every packfile, even images that didn't change
- Slow repush with no changes: fetch+rewrite+add all files O(n) every time
Fix: persist the git working directory at
<data-root>/git-sync-cache/<projectId>/
between pushes. On subsequent pushes:
- git fetch only downloads new commits (delta, not the full tree)
- git add only stages files whose content changed
- git push only sends new/changed blob objects
The first push still sends all objects (unavoidable), but every
subsequent push is proportional to what actually changed.
Remote URL changes (new token) are handled by git remote set-url.
The cached repo is recreated from scratch if it becomes invalid.
Also reverts the binary file exclusion added as a workaround — all
project files (including images) are now included, as originally
intended.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Binary files (images, PDFs, etc.) from getAllFiles are now filtered
through isTextFile before being written to the git working tree.
Text-typed uploaded files (.bib, .cls, .sty) are still pushed; binary
assets are excluded.
Git packs binary files uncompressed and the resulting packfile is
processed fully in memory by the receiving server (git-receive-pack).
Even a handful of images is enough to OOM a memory-constrained Gitea
pod. Binary assets belong in Git LFS or object storage, not in a
regular git object store.
The widget description now explicitly states that binary files are
excluded from the push.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The env var was in server-ce/k8s/verso-prod-app.yaml but the workflow
applies its own inline spec on each deploy, overwriting anything applied
manually. Same fix as was already done for the test workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings in the complete git sync feature developed on git-bridge-test:
Push:
- Force-pushes project files (docs + binary) to a configured git remote
- Fetches remote state first so files outside the managed subPath are
preserved (no more "everything else deleted" issue)
- Configurable branch (default: main), subdirectory, and PDF output path
- PDF pushed as a single file overwrite (other files in same dir kept)
- Independent toggles: push project files, push compiled PDF
- Auto-push on successful compile (localStorage toggle)
- First-push confirmation dialog when pushFiles is on and subPath
would be reset (remembered per project+subPath in localStorage)
Pull:
- Clones remote at depth 1, walks files, upserts text as editable docs
and binary as file uploads; additive only (no deletions for safety)
Security:
- git sync panel and all config (incl. auth token in remote URL) only
visible to project owner — hidden from collaborators and anon users
UX:
- Rail tab uses autorenew icon (in the unfilled font)
- Action buttons wrap on narrow panels / long translations
- Proper error messages surfaced from git subprocess
Infrastructure:
- git installed in the Docker image
- OVERLEAF_ENABLE_GIT_SYNC=true in prod k8s manifest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Branch:
- New "Branch" text field (default: main) stored in gitSyncBranch on
the project. Used for git fetch, symbolic-ref, and push target.
Validated server-side: alphanumeric + / _ . - only, no ..
First-push confirmation:
- When pushFiles is enabled and the user hasn't confirmed for the
current subPath, clicking "Push now" shows an inline warning:
"The <subPath> directory will be completely replaced …"
with "Yes, push and replace" / "Cancel" buttons.
- Confirmation is stored in localStorage keyed by projectId+subPath,
so it's shown again if the subPath is changed.
- Auto-push bypasses the dialog (user already opted in explicitly).
PDF-only replace:
- Already correct with the fetch-first approach: only the specific
file at pdfPath is written; other files in the same directory
(e.g. output/old.pdf) are preserved from the fetched remote state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the push did git-init from scratch and force-pushed, wiping
everything in the remote branch that wasn't part of the Verso project.
New approach:
1. git init + remote add origin + fetch --depth 1 origin/main
2. reset --hard FETCH_HEAD (skipped silently on first push to empty repo)
3. Clear only the managed area (subPath dir, or all non-.git files if
no subPath is configured) so Verso deletions are still reflected
4. Write Verso docs and binary files
5. Write PDF at repo-root-relative pdfPath
6. git add -A + commit + push --force origin HEAD:main
With a subPath configured, everything outside the subPath is fetched
from the remote and preserved in the new commit unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gitSyncEnabled is now false for non-owners, hiding the rail tab
- gitRemote (and all other git sync config) is served as empty string
to non-owners, preventing auth token leakage via meta tags to
collaborators and anonymous token users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pdfPath is now joined against tmpDir (repo root) instead of fileRoot
(subPath dir), so the PDF can be placed anywhere in the repo
independently of where the project files land.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zz.clsiServerId().optional() accepts undefined but rejects null.
?? null was coercing an absent clsiServerId into null, failing Zod
validation with "expected string, received null".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two new boolean fields on the project (gitSyncPushFiles, gitSyncPushPdf,
both default true) let users control what gets pushed independently:
- "Push project files" switch — skip all docs/binary files when off
- "Push compiled PDF" switch — grayed out when no pdfPath is set
The push button and auto-push are disabled when both switches would
result in nothing being pushed. Config is stored in MongoDB so settings
persist per-project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In single-server CE deployments (Verso included) the CLSI does not
return a clsiServerId, so it was always undefined. The push condition
checked pdfPath && pdfBuildId && pdfClsiServerId && userId, meaning the
PDF was silently skipped every time in practice.
clsiServerId is optional in getOutputFileURL (single-server deployments
work without it), so only require pdfPath and pdfBuildId. Also remove
the inner try/catch so PDF fetch errors surface to the user instead of
being swallowed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flex-wrap: wrap lets buttons reflow to a second line instead of
overflowing. flex: 1 1 auto keeps them proportional within each row.
white-space: nowrap prevents individual button labels from breaking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
req.session.user is not the right path in Overleaf/Verso — the session
uses passport's structure. SessionManager.getLoggedInUserId(req.session)
is the standard way all other controllers access the user id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move userId + req.body reads inside try/catch in pushToGit and
pullFromGit so any synchronous throw returns JSON, not an HTML 500
(which made err.data undefined and showed only "Internal Server Error")
- Add extractError() helper in widget that tries err.data.error first,
then err.message, then String(err) — surfaces the actual git failure
message from the server log
- Change rail icon from 'merge' (not in unfilled-symbols list) to
'autorenew' (sync arrows, already in the list) — fixes text showing
when panel is closed and wrong icon when open
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /project/:id/git-sync/pull clones the configured remote at depth 1,
walks all files under the configured subPath, and upserts each into the
Verso project using upsertDocWithPath (text) or upsertFileWithPath
(binary), with full folder creation via mkdirp. The .git directory is
skipped. Pull is additive/update-only — no Verso entities are deleted.
Text vs binary classification uses Settings.textExtensions (same list
the editor uses for file uploads), so .typ, .tex, .md, .yml etc. all
become editable docs while images and PDFs stay as files.
Frontend: "Pull from git" button added alongside "Push now".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- getConfig was outside try/catch so errors returned as HTML 500 instead
of JSON, hiding the real message in the frontend
- git init -b main requires git ≥ 2.28; replaced with git init +
symbolic-ref to support older git versions in the base image
- apt-get update before git install to avoid stale package list failures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The WASM renderer has ongoing stability issues so PDF (server) is now
the default for new projects / browsers that haven't chosen a mode yet.
The Live (browser) option remains available in the compile dropdown but
is labelled "experimental" in italic to set expectations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Icon: change rail tab from integration_instructions to merge
- Auto-push on compile: toggle stored in localStorage, watches compile
context; when compiling goes true→false with no error, triggers a push
automatically (including the latest build's PDF if configured)
- PDF destination path: new gitSyncPdfPath field; backend fetches the
compiled PDF from CLSI (buildId + clsiServerId passed from frontend)
and writes it at the configured path in the repo; silently skipped if
no recent compile or field is blank
- Push now always sends current buildId/clsiServerId so PDF is included
without needing a separate save step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Push now always saves the current form values first, so clicking Push
without a prior Save no longer returns "No git remote configured".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The flag was added to the prod k8s manifest but missing from the
inline deployment spec in the test workflow — causing the Integrations
tab to stay hidden in the test environment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Project files can now be pushed into a subfolder of the target repo
rather than always going to the root. The path is sanitised on the
backend (strips leading/trailing slashes, rejects traversal with ..).
An empty value (default) keeps the existing root behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add git-bridge-test to push trigger branches
- Pass the triggering branch through to the k8s buildkit job via sed
substitution (same pattern as NAV_TITLE), so workflow_dispatch from
any branch also clones the right ref instead of always cloning main
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Git Sync" section in the Integrations rail panel that lets
project owners configure an HTTPS remote URL (with embedded auth token)
and force-push all project files as a single commit.
Backend:
- GitSyncHandler: assembles project docs + binary files into a temp dir,
runs git init/commit/push --force, then cleans up
- GitSyncController: GET/POST /project/:id/git-sync (configure),
POST /project/:id/git-sync/push (trigger)
- Project model: gitRemote field
- Dockerfile: ensures git is present at runtime
- Env flag: OVERLEAF_ENABLE_GIT_SYNC=true (set in k8s manifest)
Frontend:
- GitSyncWidget: URL input + Save + Push Now buttons, success/error feedback
- Integrations panel: shows widget when gitSyncEnabled
- Rail: shows Integrations tab when gitSyncEnabled (was only gitBridgeEnabled)
- i18n: en + fr translations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every call to renderToSvg({ artifactContent }) internally routes through
runWithSession, which calls session.free() after fn resolves. Because the JS
GC may still hold a reference to the first RenderSession wrapper, the next
render's Rc::try_unwrap() panics with 'attempted to take ownership of Rust
value while it was borrowed'.
Fix: use renderer.createModule(data) once to create a persistent RenderSession
that is never freed during the component's lifetime. Subsequent renders call
session.manipulateData({ action: 'reset', data }) (synchronous, no ownership
transfer) + session.renderToSvg({ container }) which routes through
withinOptionSession's renderSession fast-path — bypassing runWithSession and
its session.free() entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace runWithSession + manipulateData + session.renderToSvg with the
direct RenderByContentOptions form: renderer.renderToSvg({ format: 'vector',
artifactContent, container }).
The session-based API kept hitting 'recursive use of an object detected
which would lead to unsafe aliasing in rust' because runWithSession holds
a mutable borrow of the session while renderToSvg also takes one —
regardless of whether you call renderer.renderToSvg({ renderSession }) or
session.renderToSvg(). The content-based form creates and disposes the
session internally without any caller-visible borrow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs fixed:
1. 'recursive use of an object' Rust error: inside runWithSession(), calling
renderer.renderToSvg({ renderSession: session }) passes the session to the
renderer while runWithSession already holds it — double-aliasing the same
Rust object. Fixed by using session.renderToSvg({ container }) directly.
2. Stale preview after edits: concurrent doRender calls (compile finishes
while previous render is still in progress) would both enter runWithSession
simultaneously, causing the Rust error and leaving the view frozen. Fixed
with a render guard (isRenderingRef) that queues the latest vectorData and
flushes it once the current render completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebAssembly.instantiateStreaming() requires 'wasm-unsafe-eval' in the
script-src CSP directive. Unlike 'unsafe-eval', this only permits WASM
compilation and does not allow arbitrary eval() calls.
Needed for the typst.ts WASM preview (both compiler and renderer).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs were causing a brief red error then blank screen:
1. triggerCompile closed over `view` in useCallback deps, so every time
the CodeMirror view reference changed, useEffect terminated and
recreated the entire worker. Fixed by reading view via a ref, making
triggerCompile stable (empty dep array).
2. When 'compiled' arrived before the renderer WASM finished loading,
the old code called setStatus('ready') to silently skip rendering —
this cleared any existing error and left a blank screen. Fixed by
buffering the vectorData in pendingVectorRef and flushing it once
the renderer is ready.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a dual-mode Typst preview: a new "Live (browser)" mode compiles and
renders Typst documents entirely in-browser using typst.ts WASM (28 MB
compiler + 1 MB renderer). The existing server-side PDF mode is preserved
and selectable via a new "Preview mode" section in the recompile dropdown,
visible only for Typst projects.
Architecture:
- Web Worker (typst-preview-worker.ts) runs the WASM compiler; queues
compile requests so only the latest compile runs after each keypress
- TypstWasmPreview component initialises the renderer on the main thread,
listens to changedAt from the compile context, debounces at 400 ms, and
renders SVG into a container div via renderToSvg
- typstPreviewMode ('wasm'|'pdf') is persisted per-project in localStorage
- isTypstProject, changedAt, typstPreviewMode, setTypstPreviewMode are
exposed through both LocalCompileContext and DetachCompileContext
- Fonts loaded from jsDelivr CDN (text subset only) on first use
- Phase 1: single-file Typst only (no #include, no images)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark Alpha 3 as released (drop the "in progress" qualifier) and correct
the bidirectional format export entry: only LaTeX ↔ Typst conversion is
available; DOCX, Markdown and HTML exports are not yet implemented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read-only collaborators and token-link users could publish, unpublish,
and rotate presentation share tokens. Change all three write endpoints
from ensureUserCanReadProject to ensureUserCanAdminProject so only the
project owner can perform these actions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>