Compare commits

...
804 Commits
Author SHA1 Message Date
yu-i-i ae07e781ab Reference Picker: Drag-and-drop badges
- Make badges draggable with the mouse
  - Replace Insert and Replace modes with a single Apply action
  - Add a hint for the keyboard shortcut
  - Reorganize directory structure
2026-11-06 14:45:52 +01:00
Nico Mexis 5c9fef4d03 Fix user projects export script (#258) 2026-11-04 18:29:28 +01:00
Musicminion 6c4829eea4 Track changes: fix permissions (#271)
* track-changes: enforce write and review permissions on review actions

 - Toggling track changes and accepting changes need write access
 - Resolving, reopening and deleting a thread use ensureUserCanDeleteOrResolveThread
 - Deleting a reply needs write or review access, and reviewers may only delete their own replies

* track-changes: leave the reviewer own-reply check out of deleteMessage

 - Deleting a reply is limited by the route only, so reviewers can still delete any reply
2026-11-04 18:28:56 +01:00
Musicminion 2da513262a Sandboxed compiles: apply the memory limit through HostConfig (#270)
- Memory was set at the top level of the create options, which Docker ignores, so compile containers had no memory limit
 - Move it into HostConfig with the 1 GiB the comment intended (the old value was 1 TiB)
2026-11-04 18:11:53 +01:00
yu-i-i 5c6ab5fad3 Zotero: clean up the code; add collections support 2026-11-04 17:51:47 +01:00
yu-i-i ca8fd03a2e Bib Editor: the initial commit 2026-11-04 17:51:41 +01:00
Juan José Sánchez Medina 01d5002ea9 Zotero: fix: cloning projects failed with zotero (#253)
walks every project's folder tree and rewrites legacy Zotero linkedFileData to the new key names.
2026-11-04 14:47:57 +01:00
Tobias Hallmen 5b6154bcde Track-changes: read all docs for the review panel Overview and change authors (#262)
* track-changes: list ranges of all docs in the review panel Overview

GET /project/:project_id/ranges returned document-updater's
/project/:id/ranges, which only covers docs currently loaded in Redis.
Comments and tracked changes in any doc nobody had opened since it was
last unloaded (or since document-updater restarted) were missing from
the Overview tab until that file was opened.

Flush the project's loaded docs to docstore first (POST
/project/:id/flush writes them back without unloading them), then read
the ranges of all non-deleted docs from docstore. After the flush
docstore is complete and current, so there is nothing to merge; this is
simpler than overlaying document-updater's live ranges on docstore's.
The response keeps the shape the frontend decodes ([{ id, ranges }]),
and docs without ranges get {} as document-updater returned for them.

The flush makes the request cost proportional to the number of loaded
docs with unflushed changes, and docstore unarchives the project's docs
if they were archived; on very large projects opening the Overview may
therefore be slower than before.

* track-changes: flush before listing tracked change authors

getChangesUsers read docstore directly. A tracked change still in
document-updater (not yet flushed to Mongo) was not seen, so the review
panel could not name its author once they left the project. Flush the
project first, as getAllRanges does since the previous commit.

Reported in ayaka-notes/ayakaleaf-pro#108.
2026-11-04 14:12:27 +01:00
Tobias Hallmen 21165d882e Git-bridge: reject personal access tokens of suspended users (#261)
Suspended users cannot log in to web, but their git-bridge personal
access tokens kept working for clone/pull/push. Treat a token whose
owner is suspended like an unknown token in GitBridgePATManager.getUserId,
which backs both /oauth/token/info and the token checks on the
/api/v0/docs routes, so git-bridge gets a 401.
2026-11-04 14:11:24 +01:00
Tobias Hallmen a683019198 Git-bridge: nginx: only expose git-bridge project endpoints under /git/ (#260)
The /git/ location proxied every path to git-bridge, which made its
internal routes reachable from the internet: /status, /health_check,
/metrics, /diags, /api/<project>/<key>/postback and the unauthenticated
DELETE /api/projects/<project id> used by web on project expiry.

Forward only /git/<24 hex project id>[.git]/... (the git smart-HTTP
endpoints, which git-bridge protects with Oauth2Filter) and return 404
for everything else under /git/. Web talks to git-bridge directly
(GIT_BRIDGE_HOST:GIT_BRIDGE_PORT), not through nginx, so it is not
affected.
2026-11-04 14:10:09 +01:00
Baiqiao Zhang f1b9809f35 Fix: set analyticsId when registering via the Sign Up page (#256)
registerNewUser() requires analyticsId (UserCreator throws
'bug: attributes.analyticsId is missing'), so every sign-up through the
registration-page module failed with a 500. Same fix as #208 for admin-tools.

Fixes #255
2026-10-07 14:45:40 +02:00
yu-i-i 738bebb743 README.md for v6.3.0-v5.1 2026-09-27 18:25:22 +02:00
yu-i-i 80e3226d64 Enable by default editor tabs and review mode switcher in toolbar 2026-09-27 18:25:22 +02:00
yu-i-i 7601071d18 Python script runner: initial commit
- restore upstream code removed by commit b90a850
  - migrate restored code to modules
  - improve Run / Stop button design
2026-09-27 18:25:05 +02:00
yu-i-i 7bc2c7f2fa Enable themed modals 2026-09-27 18:11:28 +02:00
hnd0hng f0ac2f4051 Feat: add word and sentence counts for selected text 2026-09-27 18:11:27 +02:00
yu-i-i 79b145a852 Image for conversion should not depend on TEX_LIVE_DOCKER_IMAGE_ROOT 2026-09-27 18:11:27 +02:00
yu-i-i e5a7e28291 Remove copyright note and 'Digital Science' 2026-09-27 18:11:27 +02:00
yu-i-i 0e520296e8 Linked URL: fix deps 2026-09-27 18:11:27 +02:00
yu-i-i dc36597117 Zotero: fix for 6.3.0 2026-09-27 18:11:27 +02:00
yu-i-i ccc1c57ccd Github Sync: fix for 6.3.0 2026-09-27 18:11:27 +02:00
yu-i-i 404623dc25 Admin Tools: fix for 6.3.0 2026-09-27 18:11:27 +02:00
Juan Zuloaga 4552eef3f6 OIDC: enable non standard claims in admin check (#230)
* OIDC: enable non standard claims in admin check

* give priority to UserInfo
2026-09-27 18:11:26 +02:00
yu-i-i 8e95ddfec1 Remove parasitic @misc{noauthor_notitle_nodate} entries 2026-09-22 02:41:44 +02:00
yu-i-i dcb2eb1b80 Sandboxed compiles: fix cannot compile cyrillic 2026-09-22 02:41:44 +02:00
Rudy Ercek b840b6524c Fix: zip archive max uncompressed size 2026-09-22 02:41:44 +02:00
yu-i-i cd516bdc12 Reference picker: fix show existing key as unexistent in Insert mode 2026-09-22 02:41:44 +02:00
yu-i-i b3a57cf3e2 Update README.md for 6.2.0 2026-09-22 02:41:44 +02:00
yu-i-i fb567698f6 Add syscalls to seccomp profile, fixes #194 2026-09-22 02:41:44 +02:00
yu-i-i 94f3c564a6 Admin tools: improve Active Projects UI
- do not show the project owner, email and connections number columns
 - add mailto: all active users of a project when clicking the project title
 - fix error handling
2026-09-22 02:41:44 +02:00
yu-i-i 08bc4c65dc Fix: hide Git Bridge, GitHub Sync and Zotero UI elements when the corresponding feature is disabled 2026-09-22 02:41:43 +02:00
yu-i-i 07a77cafd9 Reference picker: improve usability
- show tags for nonexistent references
- click a badge to find the corresponding reference
2026-09-22 02:41:43 +02:00
yu-i-i f93211fd24 Add variables to enable experimental features: pandoc conversion and editor tabs 2026-09-22 02:41:43 +02:00
yu-i-i fea291b72e Registration page: the initial commit 2026-09-22 02:41:43 +02:00
yu-i-i 479c9fce50 Git bridge, Template gallery: remove integration with the old editor code 2026-09-22 02:41:43 +02:00
yu-i-i 590bf506ac Admin tools: handle bad SMTP configuration error on account creation 2026-09-22 02:41:43 +02:00
yu-i-i 539e155d6b Admin tools: remove themed pages env var; style admin header
- themed pages now is enabled by default upstream
  - make the header background red on admin manage urers/projects pages
  - fix minor bug
2026-09-22 02:41:43 +02:00
yu-i-i bd5507262e Github Sync: enhance token handling and post-sync indexing
- support file-based token cipher
  - reindex references after sync
  - fix typo and clean up code
2026-09-22 02:41:43 +02:00
yu-i-i 9afab5ade1 Zotero: improve integration and reference import
- use OAuth instead of tokens
  - add an integration card
  - improve error handling
  - add BibLaTeX format support
  - fix indexing of linked .bib files on creation
2026-09-22 02:41:43 +02:00
yu-i-i a262362712 Linked URL: adapt for yarn; bump node to 24.14.1 2026-09-22 02:41:42 +02:00
David Rotermund 345bc57e36 Fix admin socket metadata handling and footer HTML rendering in admin-tools 2026-09-22 02:41:42 +02:00
Pierpaolo Vittorini 24e2fdaab5 Admin tools, Authentication: Fix: set analyticsId when creating a new user 2026-09-22 02:41:42 +02:00
yu-i-i f71de2eacc Zotero: allow collaborators to refresh files imported from Zotero; show importer's name in the header 2026-09-22 02:41:42 +02:00
yu-i-i d5e240b4b5 Admin tools: move styles to modules directory; fix form for dark theme 2026-09-22 02:41:42 +02:00
yu-i-i de2744f834 Github Sync: initial commit 2026-09-22 02:41:42 +02:00
yu-i-i ac73b428ff Git integration: use named export for settings page widget
remove stray console.log
2026-09-22 02:41:42 +02:00
Synésio Neto aa40894fb0 Fix a greyed out create file button after the Zotero Integration was merged. 2026-09-22 02:41:42 +02:00
yu-i-i 04b76c7f84 Sandboxed compiles: cwd argument was added to CommandRunner
see upstream commit ce6f9b8
2026-09-22 02:41:41 +02:00
yu-i-i fdabfef9b0 Patches handled by yarn 2026-09-22 02:41:41 +02:00
yu-i-i 425594757e Revert "[clsi] remove locking from docker actions (#32373)"
This reverts commit d66a856baa.
2026-09-22 02:41:41 +02:00
yu-i-i 2fd95a172b Reference picker: normal search instead of fuzzy; overleafying modal 2026-09-22 02:41:41 +02:00
David Rotermund d0e78d61d9 Reference picker 2026-09-22 02:41:41 +02:00
yu-i-i 30e127603e Authentication: fix ldap searchAttributes for passport-ldapauth 3.0.1 2026-09-22 02:41:41 +02:00
David Rotermund 22c294fa3a Zotero Integration (#164) 2026-09-22 02:41:41 +02:00
David Rotermund 6558c49c82 Logo Tools (#158)
* Logo Tools

* Moved to /tools/logo; Checks dependencies first and stops if they are not there; Added information about 3_remove_branding_from_projectpage.sh in README ; reordered the commands of 1_convert.sh to avoid the logo-horizontal.png problem
2026-09-22 02:41:41 +02:00
yu-i-i 8426f59658 Git Bridge: fix incorrect token creation date display 2026-09-22 02:41:41 +02:00
David Rotermund 5185548301 Admin tools: introduce Activ Projects tab on the Manage Site page
- code moved to modules/admin-tools (yu-i-i)
2026-09-22 02:41:41 +02:00
yu-i-i 8fa8b0dd83 Git Bridge: route PAT validation endpoint to correct router 2026-09-22 02:41:40 +02:00
yu-i-i bce42463b6 Sandboxed compiles: avoid destroying containers of recently accessed projects
see upstream commit a9c4138
  here is an appropriate DockerRunner.mjs patch
2026-09-22 02:41:40 +02:00
yu-i-i 36a09efb6a Admin tools: themed dashboard 2026-09-22 02:41:40 +02:00
Tao Chen 4587d6935f Git Bridge: support git-bridge configuration via env in nginx
- generate overleaf.conf dynamically
- support GIT_BRIDGE_HOST and GIT_BRIDGE_PORT
- configure client_max_body_size for /git using GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE
2026-09-22 02:41:40 +02:00
yu-i-i 31f8bec536 Linked URL: bump node to 24.13.0 2026-09-22 02:41:40 +02:00
yu-i-i dac79ec9eb Git Bridge: integrations-panel code was moved 2026-09-22 02:41:40 +02:00
yu-i-i f65fed52b5 Authentication: fix uid ldap login after email parsing was added to rate limiter 2026-09-22 02:41:40 +02:00
yu-i-i ec41d3c9a2 Sandboxed compiles: convert DockerRunner to ESM 2026-09-22 02:41:40 +02:00
yu-i-i 3ebbb3761c Update README.md 2026-09-22 02:41:40 +02:00
yu-i-i 43d5d5f535 Sandboxed compiles: add scripts to add/strip repo prefix from texlive images 2026-09-22 02:41:40 +02:00
yu-i-i 033c14449f Add env variable to enable restoring project/file from history snapshot 2026-09-22 02:41:40 +02:00
yu-i-i f56514ad7e Admin tools: minor improvements
- enable Projects button in navbar for admin projects page
- enable copy of user ID in user info modal
- add null check in UserListController (thanks @moschlar)
2026-09-22 02:41:40 +02:00
yu-i-i 285d346eb6 Git Bridge: Add git integration 2026-09-22 02:41:39 +02:00
yu-i-i b18298c8b0 Sandboxed compiles: set imageName to default if undefined in project 2026-09-22 02:41:39 +02:00
yu-i-i d018510a33 Authentication: update passport-ldapauth to 3.0.1; fix minor issues 2026-09-22 02:41:39 +02:00
yu-i-i c1ee595528 Linked URL: prevent SSRF via DNS rebinding; minor fixes 2026-09-22 02:41:39 +02:00
yu-i-i 6ffb19c722 Admin tools: optimize user/project list performance
Add frontend pagination
Add search debouncing
Render modals conditionally
Remove unnecessary sorting
2026-09-22 02:41:39 +02:00
yu-i-i a20c47b454 Update README.md 2026-09-22 02:41:39 +02:00
yu-i-i ed02401de5 Sandboxed Compiles: pass imageName in Copy project 2026-09-22 02:41:39 +02:00
yu-i-i da765c2092 Admin tools: fix minor bugs 2026-09-22 02:41:39 +02:00
yu-i-i bb788215ba Sandboxed compiles: add arm64 support to clsi seccomp profile 2026-09-22 02:41:38 +02:00
yu-i-i a0acf2f72a Sandboxed Compiles: support configurable texlive image root via env var 2026-09-22 02:41:38 +02:00
yu-i-i 46d23c6c8b Admin Tools: Fix EXTERNAL_AUTH is 'none' 2026-09-22 02:41:38 +02:00
yu-i-i c554423be7 Admin Tools: Fix sorting 2026-09-22 02:41:38 +02:00
yu-i-i 78e4ccc4c9 Symbol Palette: fix for 6.1.0 2026-09-22 02:41:38 +02:00
yu-i-i bcddf9b0e7 Track changes: Fix import after upstream converted several files to ESM 2026-09-22 02:41:38 +02:00
yu-i-i 1b6e9fc501 Authentication: Fix import after upstream converted some files to ESM 2026-09-22 02:41:38 +02:00
yu-i-i b50ef8c754 Admin Tools: Fix for 6.1.0 migraton 2026-09-22 02:41:38 +02:00
Guilhem Poy 97cd11f439 Add support for EMAIL_SMTP_PASS_FILE
Small feature to provide the email password from a file rather than directly from the env variable OVERLEAF_EMAIL_SMTP_PASS. Useful when used in conjunction with Docker Compose secrets.
2026-09-22 02:41:38 +02:00
yu-i-i 9f29a11826 Admin Tools: Fix user name
fix user name is not updated

fix first_name/last_name is undefined
2026-09-22 02:41:38 +02:00
yu-i-i 7a671ab244 Admin Tools: Manage users and Manage projects pages 2026-09-22 02:41:37 +02:00
yu-i-i 6607edb882 Hotfix 5.5.6-ext-3.3 2026-09-22 02:41:37 +02:00
yu-i-i e2b99a2e34 Linked URL: add sanitize and normalize 2026-09-22 02:41:37 +02:00
yu-i-i 8c011b6a8a README for extended CE 2026-09-22 02:41:37 +02:00
yu-i-i d7601aa404 Add service for Linked URL feature 2026-09-22 02:41:37 +02:00
yu-i-i 9c5be38261 Sandboxed compiles: adjust clsi-profile 2026-09-22 02:41:37 +02:00
yu-i-i d59885e4e7 Track changes: Fix import after upstream converted DocumentUpdaterHandler and CollaboratorsGetter to ESM 2026-09-22 02:41:37 +02:00
yu-i-i eb16c6680d Authentication: Fix import after upstream converted AuthenticationController to ESM 2026-09-22 02:41:37 +02:00
yu-i-i a28e6445dd Authentication: Fix import after upstream converted PermissionsManager, RateLimiterMiddleware, UserCreator and SAMLIdentityManager to ESM 2026-09-22 02:41:36 +02:00
yu-i-i 80f7ad1f31 Track changes: Fix import after upstream converted ChatManager, UserInfoManager and UserInfoController to ESM 2026-09-22 02:41:36 +02:00
Rudy ERCEK f4f0e13630 Add MAX_UPLOAD_SIZE environment variable 2026-09-22 02:41:36 +02:00
yu-i-i 0e54cc56d5 Fix import after upstream converted AuthorizationMiddleware to ESM 2026-09-22 02:41:36 +02:00
yu-i-i 886014d466 Track changes: Use getProjectRanges introduced in the upstream 2026-09-22 02:41:36 +02:00
yu-i-i c7af64b81c Rename @node-saml/node-saml patch for v5.1.0 2026-09-22 02:41:36 +02:00
yu-i-i 89d73ac7a7 Fix imports after upstream converted UserController and CaptchaMiddleware to ESM 2026-09-22 02:41:36 +02:00
yu-i-i 64529664ca Fix import after upstream converted ErrorController to ESM 2026-09-22 02:41:36 +02:00
yu-i-i 3eca3de988 Bring back deleteFile removed upstream 2026-09-22 02:41:36 +02:00
yu-i-i 8f8f8992f0 Symbol palette: ui components were moved to shared folder 2026-09-22 02:41:36 +02:00
yu-i-i 7e2224dd70 Move Sympol Palette code to modules, closes #63 2026-09-22 02:41:36 +02:00
yu-i-i f60d5db8c2 passport-saml was updated to v5.1.0 2026-09-22 02:41:35 +02:00
yu-i-i c83f53d0b6 Fix: always include repository prefix in sandboxed compiles image name 2026-09-22 02:41:35 +02:00
yu-i-i 361b03b44b Remove LDAP admin check via group search in ldap-passport 2026-09-22 02:41:35 +02:00
yu-i-i a9bf9f7f63 toggleSymbolPalette now in useEditorPropertiesContext 2026-09-22 02:41:35 +02:00
yu-i-i 8d05573e96 Set use-ai capability to false 2026-09-22 02:41:35 +02:00
yu-i-i 8d6dbd1aab Track changes: convert to mjs 2026-09-22 02:41:35 +02:00
yu-i-i 3f1e136d50 Track changes: use getTrackedChangesUserIds introduced in the mainstream 2026-09-22 02:41:35 +02:00
yu-i-i 9e7209fd31 Remove OVERLEAF_OIDC_DISABLE_JIT_ACCOUNT_CREATION in favor of OVERLEAF_OIDC_ALLOWED_EMAIL_DOMAINS 2026-09-22 02:41:35 +02:00
Synésio Neto dce0c52f1b Introduce an environment variable to allow JIT OIDC users creation based on their email address domain. 2026-09-22 02:41:35 +02:00
yu-i-i 40a45e6dc9 Introduce an environment variable to control user creation in OIDC authentication, closes #47 2026-09-22 02:41:35 +02:00
yu-i-i 44c25117d4 Avoid DEP0174 by removing async from callback-based getGroupPolicyForUser 2026-09-22 02:41:35 +02:00
yu-i-i a4d443f9f3 Fix login page 2026-09-22 02:41:34 +02:00
yu-i-i 0f428c3eb1 Symbol Palette: make close button visible 2026-09-22 02:41:34 +02:00
yu-i-i 119258ea95 Refactor Sandboxed Compiles 2026-09-22 02:41:34 +02:00
yu-i-i 63e1c74a8d Symbol Palette: improve keyboard input experience 2026-09-22 02:41:34 +02:00
yu-i-i 6487e204e5 Symbol Palette: get rid of @reach/tabs 2026-09-22 02:41:34 +02:00
yu-i-i 8c6109a90d Add 'poll' to clsi seccomp profile, fixes minted. Thanks, David. 2026-09-22 02:41:34 +02:00
yu-i-i 59b9cc740a Template Gallery: add support
Make Template Gallery optional; rename environment variables
Refactor Template Gallery; resolves #38 and #39:
  - replace free-text license input with a select box
  - improve visual presentation of modals and enhance keyboard interaction
Template Gallery: replace markdown-it with marked
Fix template publishing to align with upstream project context changes
Template gallery: ui components were moved to shared folder
Fix style of Templates button in navbar
Template gallery: Fix `OLModal` usage (see upstream commit 19b3834)
Template gallery: Fix import after upstream converted `CompileManager`, `ClsiManager` and `RateLimiterMiddleware` to ESM
Template gallery: Fix import after upstream converted `AuthenticationController`, `ProjectGetter`, ProjectLocator` and `DocumentUpdaterHandler` to ESM
Template Gallery: migration to 6.1.0
  - enable menu for editor redisign
  - move frontend code to modules
Template gallery: add template management authorization middleware
  - middleware to enforce template access rules
  - introduce template manager role
  - fix minor issues
Template Gallery: fix for 6.2.0
Template Gallery: fix for 6.3.0 and show language in the details
2026-09-22 02:38:49 +02:00
yu-i-i 870bf7c3a7 Track changes / comments: update backend to support frontend changes 2026-09-18 23:10:17 +02:00
yu-i-i b102d1e401 Allow EXTERNAL_AUTH to be undefined, fixes #26 2026-09-18 23:09:13 +02:00
yu-i-i 1d8e5fb92e Symbol palette: switch to 'OL' UI components and apply minor cosmetic changes 2026-09-18 23:09:13 +02:00
yu-i-i 0c8d307575 Make OVERLEAF_OIDC_USER_ID_FIELD support 'email' as a value 2026-09-18 23:09:13 +02:00
yu-i-i f3e5ca9e26 See upstream commit 42ee56e 2026-09-18 23:09:13 +02:00
yu-i-i f793a3965a Fix glitches in symbol palette after switching to Bootstrap 5 2026-09-18 23:09:13 +02:00
yu-i-i 48c4af7545 Whitelist /oidc/login endpoint, fixes #21 2026-09-18 23:09:12 +02:00
yu-i-i 5e4965e784 Add ENV variables to control SAML signature validation 2026-09-18 23:09:12 +02:00
yu-i-i 51e1c3e08b Re-export doLogout (was removed from exports in commit b9fb636). 2026-09-18 23:09:12 +02:00
yu-i-i f3d4a51e79 Refactor authentication code; add OIDC support 2026-09-18 23:02:41 +02:00
yu-i-i 54440bdb58 Allow adding extra flags to LaTeX compiler through environment variable 2026-09-18 20:56:25 +02:00
yu-i-i 6097d93c90 Enable LDAP and SAML authentication support 2026-09-18 20:48:52 +02:00
yu-i-i 3a8ef14900 Enable Symbol Palette 2026-09-18 20:42:16 +02:00
yu-i-i 79c2579d3b Allow selecting a TeX Live image for a project 2026-09-18 20:42:00 +02:00
Sam Van den Vonder e76dc99a6f Enable Sandboxed Compiles feature 2026-09-18 20:41:44 +02:00
yu-i-i 236f577974 Enable track changes and comments feature 2026-09-18 20:41:26 +02:00
yu-i-i 65ca4000b9 Redirect non-existing links to Overleaf page 2026-09-18 20:41:15 +02:00
e039ad26c5 [editor-core] extract logic for determining root doc from snapshot (#37043)
* [editor-core] hold the one threshold that decides which store owns the file tree

HISTORY_FILE_TREE_STAGE is five literals across the services that consume this
library -- web backend, web editor, real-time, third-party-datastore,
github-sync -- linked by comments saying which other copies they are aligned
with. What the docstring here says is what goes wrong: this is the single thing
that decides which storage is authoritative, a project crosses it once with no
way back, and a copy that drifts sends a whole service's writes to the store
nothing reads any more. Silently, and for good.

overleaf-editor-core is where it goes: every one of the five already depends on
it, the editor included, and the library that defines what a file tree is can say
where one lives. The consumers adopt it separately; this is the definition they
adopt.

historyIsSourceOfTruth goes with it, and refuses a stage that is neither absent
nor a number rather than reading it as low -- web's copy coerced, which turned a
corrupt stage into a permanent mongo write. Absent stays "below the gate": a
project that never migrated has no stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] hold which doc a project opens on

History records it as `main: true` on the file, and nothing derives it: every
writer that creates a project has to record it, or the project compiles nothing
until the user picks a file. So the rule for choosing one, and the operations
that record it, go where a file tree is defined rather than with any one writer.

The rule is the one web has always applied, over a snapshot instead of a mongo
tree: of the docs that declare a document class, the one nearest the project
root, alphabetically among equals, so that the same files always give the same
answer. A project that already records a root doc keeps it -- that is a user's
choice and not something to re-derive.

Whether a doc could be the one is asked of content rather than of a store to read
it from: a writer creating a project has each doc's content in its hands as it
stores it, so reading them back would be a second read of every one. The choice
is then made from the pathnames alone.

The extension list joins the others in text_file_defaults for the reason they are
there: a writer holding a shorter copy leaves a project with nothing to compile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] say why a store built for a consumer does not need getBlob

ReadWriteBlobStore is BlobStore without the lookup by hash, and the type said so
without saying why a consumer can do without it. getBlob is reached only for a
file stored as a bare hash, and one of those does not leave history-v1:
chunk_store's lazyLoadHistoryFiles attaches blob metadata to every snapshot and
change it serves, so a file arrives already knowing its own lengths.

That is what the contract rests on, and a store that reimplemented the
classification rule to answer the lookup anyway would be a second opinion on
whether a file is editable. The reasoning was what was missing, rather than the
type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] say what makes a doc a root doc candidate

The sentence naming the two halves of the check had lost the first one, and read
"named as a root doc may be named". It is the extension that is tested: one of
DEFAULT_ROOT_DOC_EXTENSIONS, and a document class declared in the content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] hold the doc flags a file's metadata can carry in one place

`main` is not the only flag a doc carries -- `mainBibliography` marks the file
references are added to -- and both live in the same metadata, which
SetFileMetadataOperation replaces wholesale rather than merges. So every writer
that sets one has to write the others out again, and every reader that asks
whether a file is a doc has to know the whole list.

root_doc was spelling all three of those for itself against `main` alone: it
tested the key by hand, refused a file whose metadata held anything else, and
cleared the metadata to `{}` when moving the root doc elsewhere. Against a file
that is also the main bibliography, the first two refuse a legitimate root doc
and the third silently drops the bibliography.

The helpers answer it once: which keys are doc flags, whether metadata holds only
those, and what metadata with one flag changed looks like. root_doc uses them
throughout, and the schema takes both flags on one file.

Co-Authored-By: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] ask the snapshot which doc carries a flag

Finding the file a doc flag is set on is the same search whichever flag is being
asked about, and it belongs to the snapshot rather than to the caller: it walks
the snapshot's own pathnames, and it is the snapshot that knows a file's
metadata without its content being loaded.

root_doc's mainPathname is that search for `main`, and now delegates. It gains
the check it was missing: a file whose metadata holds anything besides doc flags
came from outside the editor, so a stale `main` on it does not make it the root
doc -- which is already what setMainPathnameOperations refuses to write.

Co-Authored-By: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] drop mainPathname for the snapshot method behind it

It became a second name for `snapshot.getPathnameWithDocFlag('main')` and nothing
in the library was left calling it but chooseRootDoc, which asks the snapshot
directly now. Its two tests go with it: Snapshot's own cover the same two answers,
and more of them.

Callers outside the library ask the snapshot too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: b3f503aa3e94ea8b6524d83f910fe33dc381c0a5
2026-09-17 12:08:31 +00:00
Eric Mc Sween 0ec99c7e3e Merge pull request #37028 from overleaf/em-bibtex-stream-parser
[web] add a streaming bibtex parser

GitOrigin-RevId: 5397ba546a3d5b3436a80eab0467244969b280d0
2026-09-17 12:08:28 +00:00
Eric Mc Sween 0f22a44d1a Merge pull request #37005 from overleaf/em-bibtex-grammar-fixes
[web] match biber in three bibtex parsing edge cases

GitOrigin-RevId: da7e2c0b9f137e7debfe26f09c531a61a135e40e
2026-09-17 12:08:25 +00:00
Eric Mc Sween 7d3d60f2ba Merge pull request #36920 from overleaf/em-shared-frontend-backend
Move shared bibtex model classes to services/web/shared

GitOrigin-RevId: ef768d4f7bcf7b9c4b41844450b0256ecb46fdae
2026-09-17 12:08:21 +00:00
6f784e08c4 Update Organizations card copy on plans page (#36859)
* Update Organizations card text and bullets on plans page

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomas- <2176518+thomas-@users.noreply.github.com>
Co-authored-by: Thomas Mees <thomas.mees@overleaf.com>
GitOrigin-RevId: 4eaf5eb547801314c8c8852c1d68fa51532819ac
2026-09-17 12:08:18 +00:00
ThomasandClaude Opus 5 60531d63b7 [web] Add the Back to School group sale banner to the plans page (#36956)
* [web] Add the Back to School group sale banner to the plans page

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 596b23006fce7504fcdfd6341cf93429c08acc8b
2026-09-17 12:08:14 +00:00
Thomas bd8dbe3f11 Merge pull request #32999 from overleaf/tm-objectid-admin-lookup
Extend Project URL Lookup to resolve arbitrary ObjectIds

GitOrigin-RevId: c4006ff8cae38f4141da9cfa153e407e83f0ee7f
2026-09-17 12:08:11 +00:00
Domagoj Kriskovic 48402647b9 [library-sync] List Zotero collections in a sync modal (#36884)
* [library-sync] Zotero collections sync modal

* Update COLLECTIONS_MAX_PAGES to improve request handling and clarify comments

GitOrigin-RevId: f689aa24ca1189b99ad322aa06885f5f969ed81f
2026-09-17 12:08:02 +00:00
Domagoj Kriskovic 79a14571a2 Extract userHasRefProviderFeature and rename refSyncState.synced to syncAvailable
GitOrigin-RevId: 330a7061e88a5179569a7d436f083a8761c472cd
2026-09-17 12:07:59 +00:00
Antoine Clausse 0eac1f1f80 [web] Tear down pro-value-framing (#37016)
* Tear down `pro-value-framing`

* Revert "[web] Add pro-value-framing split test copy (#36628)"

This reverts commit 25b0736af47a3a32f4e1dd108ab0eb14c1bc13e3.

* `make cleanup_unused_locales`

GitOrigin-RevId: 7fdaee52685f8dc415cfa52f28d7fd071740d7e1
2026-09-17 12:07:50 +00:00
Antoine ClausseandClaude Opus 5 943edd29f3 [monorepo] lint .mts/.cts files (#37001)
* [monorepo] lint .mts/.cts files

ESLint's `files` globs listed `{js,jsx,mjs,cjs,ts,tsx}` but never the
module-scoped TypeScript extensions, so every `.mts` file in the repo was
silently skipped with "File ignored because no matching configuration was
supplied" -- 21 files today, all in services/web, and more as backend code
migrates to TypeScript (`.mts` is the extension runtime-loaded backend TS
has to use, since services/web has no `"type": "module"`).

Each extension list now carries the c/m variants of the languages it
already allowed, canonicalised to one of:

    {js,cjs,mjs}                      JS only
    {js,cjs,mjs,jsx}                  JS + JSX
    {js,cjs,mjs,ts,cts,mts}           JS + TS, no JSX
    {ts,cts,mts,tsx}                  TS only
    {js,cjs,mjs,jsx,ts,cts,mts,tsx}   everything

No list gains a language it did not already have, and none gains JSX
where it was previously excluded, so backend globs keep excluding
jsx/tsx and TS-only rule blocks keep excluding JS.

Both the extension declaration and the plugin/parser block in
services/web/eslint.config.mjs are widened together: widening only the
rule blocks makes `@overleaf/*` rules match files the plugin
registration does not, which fails with "could not find plugin
@overleaf".

The root global-ignores shim (`services/**/*.{mjs,cjs,ts,tsx}` and
friends) is deliberately left alone -- it replicates ESLint v8's file
walker so pre-existing debt in never-linted files stays off the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix eslint errors

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 084d0d054db9db6b5e3dd26de56d14358ab5edf7
2026-09-17 12:07:47 +00:00
Olzhas Askar a234c3cb27 Merge pull request #36952 from overleaf/oa-your-subscriptions
[web] Translate your subscriptions

GitOrigin-RevId: 20b249d8d94a33474d34ee8575c8e41667b6cf0f
2026-09-17 12:07:44 +00:00
Miguel Serrano f3a93c681e [CE/SP] Set REQ_VALIDATION_MODE=enforce-log (#37021)
Set enforce-log mode for `req` access violations in CE/SP.

GitOrigin-RevId: a870ae602a1920c8e36032af4a429e5478959c99
2026-09-17 12:07:35 +00:00
Tim Down c3d962c075 Add the SheerID student verification module (#36704)
GitOrigin-RevId: a3673c09e185c8f857d5da3e706fa9a310fba6d4
2026-09-17 12:07:32 +00:00
Jakob Ackermann 37e27bb22e [saas-e2e] reduce disk IO and webpack overhead (#36997)
* [monorepo] reduce writes from chrome in cypress

Chrome puts its shared memory in /dev/shm, and docker's default 64MB is far
too small for it - which is why Cypress passes --disable-dev-shm-usage,
spilling those segments to files on the container's writable layer instead.
Under one spec that is a few hundred MB of write-then-delete churn; under the
CI fan-out it was most of the disk bandwidth on the whole box.

So every service built from dockerfiles/cypress gets shm_size 1gb and every
cypress config drops the flag again: the SaaS e2e service, web's CI and local
component-test services plus its standalone cypress one, and server-ce's e2e
and native cypress services. Both e2e-open variants inherit the setting
through `extends`; the native one is a service of its own, and would be left
running Chrome against 64MB once the flag is gone.

* [monorepo] adopt cypress-pnp-reporter

Materializing the reporter packages copied 267 packages / 3779 files
(59MB) into /tmp on every container start, once per spec. Resolving them
through PnP inside Cypress's Electron process instead drops that to
zero: the entrypoint shell went from 55.6MB of write_bytes to 0.

* [web] serve built assets from a prepared buffer between builds

Serving a chunk cost ~2ms whatever its size, and an editor page pulls ~144
of them, so webpack-dev-server burned 2-3 cores through an e2e run purely
answering requests for output that had not changed since the last build.

It is not bytes: cutting the bundle 2.6x with a cheaper devtool moved CPU
by 8%, and an empty 304 cost nearly as much as the full body. It is the
per-request work - reading the asset out of the in-memory filesystem and
re-hashing the whole body for a strong ETag, on every request.

Three things a build does that this has to respect, each found by tracing
the hooks rather than by reading them:

- `done` fires twice per build, for the compilation and for the manifest,
  both with the same hash. A flag would have been cleared by the second
  while the first was still running, so the gate is a counter.
- An incremental `done` reports only the assets it rewrote - 16, not 630 -
  so asset names accumulate. Resetting them on `invalid` left the
  manifest's two behind and nothing was served from here again.
- webpack-dev-middleware holds requests during a rebuild. This sits in
  front of it, so it steps aside for that window and drops the cache when
  a rebuild starts. Whether a response can be kept is then decided as it
  ends rather than as it began: an epoch bumped on `invalid` names the
  build its bytes were read from, and only a response from the current
  build with no rebuild in flight is stored. A rebuild starting
  mid-response can no longer write the previous build's bytes in behind
  the cache being dropped, which eviction by asset name could not catch
  anyway - an incremental build does not name the assets it left alone.

Measured on the editor entrypoint, 16 concurrent clients:
0.26-0.31s -> 0.045-0.050s of webpack CPU per page load, and 0.0455s
immediately after a rebuild. Same chunk graph, byte-identical output.

GitOrigin-RevId: 08b4ab2d665414069bc01b4763dd64faa9b4e5cb
2026-09-17 12:07:28 +00:00
KristinaandJimmy Domagala-Tang 44910202f3 [web] open comments and changes via url (#36546)
* [web] open a comment from a URL

* [web] add "View comment" button to digest email

* [web] open a tracked change from a URL

* [web] add "View suggestion" button to digest email

* [web] open the review panel from an open=review-panel URL

* [web] drop the digest email CTA and collapse the deep links to t=review

* [web] add an Open project button to the section overflow row and plain-text view links

* [web] validate the comment and change deep-link ids as object ids

* [web] collapse the deep-link resolvers into a single DeepLink component

* fix: scrolling into view when below the fold

* fix: address claude review

* fix: style adjustments

* fix: updating types to accomodate accepted/rejected changes

---------

Co-authored-by: Jimmy Domagala-Tang <jimmy.domagala-tang@overleaf.com>
GitOrigin-RevId: dab535591a93e1c3849a6b2a0dcc3e3934354385
2026-09-17 12:07:22 +00:00
Eric Mc Sween 5dc8c2b0c3 Merge pull request #36949 from overleaf/em-bibtex-crlf-upload-fix
[web] fix bibtex parser dropping fields on CRLF/CR line endings

GitOrigin-RevId: 6174bedeed92a014c2d7210afb6f39387e8608a8
2026-09-17 12:07:19 +00:00
Jakob AckermannandClaude Opus 5 04871d6049 [editor-core] extract more shared utils (#36954)
* [editor-core] share the blob hash and the doc-vs-file rule

The git blob hash and the rule that decides whether a file is a doc or a binary
file both have to reach the same answer in every service that writes to history,
so they move into overleaf-editor-core rather than being spelled out again on
each side.

blob_utils takes over lib/blob_string_length.js: hashing and the string-length
rule answered two halves of the same question -- what history needs to know
about content before it can store it -- and each carried the same warning about
staying off index.js, which is bundled for the browser. One module states it
once, and blobForFile joins them, describing content on disk as the blob it
amounts to. The hashing functions are flat rather than a namespace of their own:
in a module that also holds the string-length rule, a bare fromStream says
nothing about what it makes. history-v1's blob store and recover_zip follow the
module to its new name.

file_type_detector holds the doc-vs-file rule, Node-only and deep-imported for
the same reason. It takes textExtensions, editableFilenames and maxDocLength as
configuration rather than reading settings: the limit is web's 2 MiB and not
editor-core's own 3 MiB, and the surrogate check covers both halves of a pair
where editor-core's covers the high half.

buildSetContentOperations' test spelled the blob header out for itself, with the
separator as a literal NUL byte, which had git reading the file as binary. It
hashes through blobHashFromString now, so it does not spell the header at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] name the client a change came from on the origin itself

`EditorOrigin` carried an `editorId` that only an editor change could have. What
the id is for -- recognising a change again, so history does not apply a resend
twice and the client does not apply its own change back -- is a property of the
client that submitted it rather than of what it did, so it moves onto `Origin` as
an optional `historyClientId` and `EditorOrigin` goes.

- `Origin` takes `(kind, historyClientId)` and answers `getHistoryClientId()`;
  `dropHistoryClientId()` is what the chunk store uses to strip the id from all
  but a client's latest change, which now leaves the rest of the origin alone --
  a restore that a client submitted stays a restore.
- `toRaw` is built a layer at a time: the base writes the kind and the id, a
  subclass spreads that and adds the fields it owns.
- The named kinds are their own schemas and discriminate on `kind`, so a payload
  claiming one is checked against that shape and the error says what it got
  wrong. The catch-all cannot join them -- its kind is any string -- so it sits
  alongside and is refined to refuse the named kinds, which it used to swallow:
  `{kind: 'restore'}` validated as a kind and nothing else, then read back as a
  restore that recorded nothing about what it restored.
- `lib/origin/*.js` gains `// @ts-check`. Without it the annotations in there
  were decorative: the lazily required subclasses were `any`, so nothing handed
  to them was checked, and `fromRaw` narrows with `in` to drop the catch-all
  variant, which a kind check cannot.

The callers that stamp the id -- real-time's handshake flag, web's applyChange
body -- come with the rest of the branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] recognise a change a writer that is not an editor placed

The counterpart of `isSameEditorChange` for writers that are not an editor
client: an integration that commits a batch on a user's behalf has to recognise
that batch coming back, because a lost response does not say whether the commit
landed and resubmitting one that did applies its removes twice.

It compares the same three fields, and for the same reason -- a rebase rewrites a
change's operations but leaves its origin, authors and timestamp alone -- with
the timestamp compared as a point in time rather than as a string, so that two
spellings of the same instant cannot read as different changes. github-sync is
the first caller, and comes with the rest of the branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] load an editable file's content, or say it has none

Only an editable file has content: any other kind is bytes addressed by hash, and
hollow data records the size of content it does not have. Both answer
`getContent()` with null, so every caller that wants the content of a doc loaded
it, checked the data's class, and reached for its own error when the check failed.
`File#loadEager` does that once and answers the data that always has the content.

`buildSetContentOperations` takes a `ReadWriteBlobStore` -- everything but
looking a blob up by hash, which is all it takes to build operations over content
-- so a caller holding a store that cannot answer `getBlob` can still use it. Its
metadata argument is `FileMetadata` rather than `Object`, which is what the schema
already says a file's metadata is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [editor-core] say that a pair of operations that do not fit is unprocessable

compose and transform threw a bare Error when the two operations do not describe
the same content. Everything that answers a client for these -- history-v1's
import routes, the git-bridge push, project-history's resync -- decides on
UnprocessableError, so a bare Error reached them as an internal failure: a 500,
which reads as worth retrying, for a pair that fails the same way every time.
A client that resubmits on a 500 resubmits forever.

The guards that cannot be reached through the API keep their bare Error: a
TextOperation composed with something that is not one, and the branch whose
message says it should not happen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [history-v1] hash blobs with the rule editor-core holds

storage/lib/blob_hash.js was a bluebird-era implementation of the same git blob
hash editor-core holds, exported from the storage barrel and used across the
storage layer, the api and the tests. All of it reads the shared rule now, and
the test that existed only to check the copy is gone -- editor-core's suite
covers the same non-ASCII case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 5a0c1e9a06170a0164c01a26a7bb2d205726d531
2026-09-17 12:07:15 +00:00
Miguel Serrano 3f171d500f [CE] Update dependencies on Dockerfile-base (#36996)
Bump REBUILD_AFTER to trigger a dependency install

GitOrigin-RevId: fa6b969778e696ea57f2e9ec1fada040b205b83e
2026-09-17 12:07:12 +00:00
Jakob Ackermann 7cebd46047 [monorepo] fix error code for failed ESM import (#36946)
GitOrigin-RevId: 3576cba8f3b18c749b45b8f4baaaaaeb8d598c6a
2026-09-17 12:06:56 +00:00
Jakob Ackermann 2317fd6edb [web] remove generic analytics service proxy (#36934)
GitOrigin-RevId: 19b3613b7cc84e73ad556067da1b3cd2f956e1c5
2026-09-17 12:06:50 +00:00
Eric Mc Sween e136bcad05 Merge pull request #36937 from overleaf/em-tpds-app-ts-refs
Point the third-party-datastore deployment at app.ts

GitOrigin-RevId: b0013f02d91a98dbc382c23ea781ec07efdf1eb5
2026-09-17 12:06:33 +00:00
Jakob AckermannandClaude Opus 5 6caa5fc3a3 [monorepo] read empty content from its hash, in a shared blob store base (#36831)
The hash of empty content is a property of the content rather than of anything
stored: every project's empty file has it, whether or not a blob was ever written
under it, and a file may reference it before anything has. Eight blob stores were
each paying a lookup -- a database query, an HTTP request, a read of a cache that
offline mode may be the only thing standing in for -- to be told what the hash
already says, and clsi had already reinvented the answer on its own.

overleaf-editor-core gains BlobStoreBase, which answers getString for that hash
and delegates the rest to the fetchString its subclasses provide. getObject comes
with it: four stores spelled out the same JSON.parse of getString, and a fifth
wrapped it in a log line.

Adopted by history-v1's BlobStore, BackupBlobStore, HashCheckBlobStore and
recover_zip's RecoveryBlobStore, by project-history's BlobStore, by clsi's
BlobStore, and by web's HistoryBlobStore, SimpleBlobStore and FrontendBlobStore.
history-v1 keeps its own answers in getStream, getBlob and getBlobs, which no
other store implements. Writing and copying a blob are untouched, so a project
that wrote empty content still has a blob for it and a read answers the same
either way.

Two paths could not handle a project holding a tracked change or a comment at
all. Loading such a file needs the ranges blob its rangesHash points at, and
hashing one again needs to write it back, so a store missing getObject or
putObject fails outright:

- recover_zip died on `blobStore.getObject is not a function`, making every such
  project unrecoverable. The test covering it asserts a tracked delete is absent
  from the zip, which the script already asked for and never reached.
- GET /projects/:project_id/latest/hashed_content returned 500 from the same
  cause, then would have hit a missing putObject in snapshot.store.
  HashCheckBlobStore becomes a class over the base and gains putObject.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
GitOrigin-RevId: c84f62465beff6cf77ffac26aa772984e1b554cb
2026-09-17 12:06:30 +00:00
Jakob Ackermann 7608d738a6 [monorepo] speed up development in Codespaces (#36912)
* [monorepo] put volumes of ephemeral test containers on tmpfs

* [github] reduce IOWAIT in codespaces

* [monorepo] avoid yarn exec wrapper process for dev-env services

* [monorepo] exec from sh -c wrappers in dev-env services

* [github] reduce bloat with overlay2 storage driver

* [monorepo] reduce memory usage with yarn pnp

* [monorepo] add retries to docker builds

* [monorepo] fix formatting for .pnp.register.mjs

* [server-ce] test: fix re-use of users across reconfiguring

* [monorepo] adopt incoming resolve context conditions

GitOrigin-RevId: fe5d810a064f15cf8ca9f8a292b91ac054a578b0
2026-09-17 12:06:24 +00:00
Jakob Ackermann 5cbf586b70 [web] log failed create/refresh actions for linked files (#36925)
GitOrigin-RevId: 127ca0fe976d704468842107f6fe6d0200acdb6c
2026-09-17 12:06:21 +00:00
Jakob AckermannandClaude Opus 5 7ae8db6b12 [editor-core] extract shared pieces for history-ot in editor (#36828)
* [editor-core] share the pieces the editor, real-time and history-v1 all need

Project-level history-OT has the browser, real-time and history-v1 all reason
about the same changes, so the rules they reason with move into the shared
library rather than being reimplemented on each side.

- `origin/editor_origin.js` plus `EDITOR_ORIGIN_KIND` and its schema entry: the
  origin an editor client's change carries, holding the editorId that scopes a
  change's timestamp.
- `change_identity.js`: how a submission is recognised when it comes back, so a
  resend of a change that already landed is deduplicated rather than applied
  twice.
- `rebase.js`: transforming a change onto concurrent ones, in the same order on
  both sides, which is what lets the server acknowledge a change with only the
  version it landed at.
- `text_file_defaults.js`: the doc-vs-file extension lists, now read from here
  by web's settings instead of being spelled out again.
- `blob_string_length.js`: the string length of a blob, used by history-v1's
  blob store and by web when it writes an uploaded file into history.
- `file_tree_diff.js`: file-tree operations to a diff, used by
  project-history's FileTreeDiffGenerator.
- `Blob.EMPTY_HASH`, so callers can answer for the empty blob without storing
  it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [project-history] build the file-tree diff with the shared helper

FileTreeDiffGenerator's hand-rolled walk over add/edit/move/delete operations
becomes a call to `buildFileTreeDiff` from overleaf-editor-core, so the editor
and project-history read a file-tree change the same way. Adds the unit tests
the decaffeinated original never had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [web] read the editable-file lists from overleaf-editor-core

web's settings spelled out the text extensions and editable filenames that
decide whether a file becomes a doc, and every other service that has to make
that call carried its own copy. The lists now live in overleaf-editor-core and
web builds settings.textExtensions and settings.editableFilenames from them,
keeping the ADDITIONAL_TEXT_EXTENSIONS override for Server CE/Pro on top.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [history-v1] read a blob's string length from overleaf-editor-core

Two copies of getStringLengthOfFile lived here, the one in recover_zip.js
carrying a TODO asking for exactly this. Both now import the shared
implementation, which the blob store keeps re-exporting for backupArchiver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
GitOrigin-RevId: 31948332f416f56848e3b0dfb5142a4c9f93f508
2026-09-17 12:06:17 +00:00
Eric Mc Sween 2024f85941 Merge pull request #36842 from overleaf/em-tpds-typescript
Convert third-party-datastore to TypeScript

GitOrigin-RevId: 028172063d0cfe03c0f105f48ab66020c79ea50e
2026-09-17 12:06:14 +00:00
Domagoj Kriskovic a9ad734cf4 [web] Delete library data when a deleted user expires (#36781)
* Delete library data when an expired deleted user is cleaned up

* Refactor MongoDB session handling

* Remove transaction handling from deleteAllLibraryDataForUser function

* use findUserIdsToKeep rather than filterDanglingUserIds

GitOrigin-RevId: 9e82f6bf943809b55a8b042d9240f99dbc635711
2026-09-17 12:05:57 +00:00
ThomasandClaude Opus 4.8 42b29f0107 Support separate header logo images for light and dark themes (#36362)
Server Pro/CE admins can set a custom header logo via OVERLEAF_HEADER_IMAGE_URL,
but the same image is used for both the light and dark navbar themes. A logo
with good contrast on one background can be hard to see on the other (e.g. a
dark logo on the dark dashboard navbar).

Add two optional env vars, OVERLEAF_HEADER_IMAGE_URL_LIGHT and
OVERLEAF_HEADER_IMAGE_URL_DARK, each falling back to OVERLEAF_HEADER_IMAGE_URL
when unset, so existing configurations are unaffected. The light/dark fallback is
resolved server-side and exposed to the frontend as customLogo (light-effective)
and customLogoDark (dark-effective); the two theme-aware navbars (project
dashboard and library) pick between them using the existing overall-theme hook,
so the logo tracks both the user's theme setting and live OS theme changes.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: 587a5679c650717cba6251e6d612146f434e8730
2026-09-17 12:05:54 +00:00
ilkin-overleaf b4b12caf48 Merge pull request #36833 from overleaf/ii-workspaces-collection
[web] Create workspaces collection indexes

GitOrigin-RevId: a84d404e660e6c19eb288712bcf020e1ba4a90d0
2026-09-17 12:05:51 +00:00
Antoine Clausse 09c564fbc4 [web] Cleanup plans-2026-phase-1 - phase 5: unused files in pricing pages (#36688)
* Remove unused Pug files related to Pricing

* `make cleanup_unused_locales`

* Remove unused `descriptionMobile`

* `make cleanup_unused_locales`

* Remove dead CSS

* Remove non-EN dead keys

* Remove "ai-assist" dead CSS

* Remove "ai-assist" listeners

* Remove `plans-new-tabs` and related

* Remove dead SCSS variables

* Remove more dead CSS (mobile)

GitOrigin-RevId: 08c452f66fe5b4d5bca4bb8cf2171254f960bc71
2026-09-17 12:05:47 +00:00
Jessica Lawshe 24802e485b Merge pull request #35956 from overleaf/jel-domain-grace-period-notification
[web] Combine grace period and failed verification notififcations on group domains table

GitOrigin-RevId: 0178421af235ee59a9b8c3a521ac1a8e87c744a6
2026-09-17 12:05:34 +00:00
Jessica Lawshe 69b585caf7 Merge pull request #36752 from overleaf/jel-group-sso-email-check
[web] Stop enforcing requested email for group SSO matches linked account

GitOrigin-RevId: 4988ecf76069d531e3aea892d07a134252c30323
2026-09-17 12:05:30 +00:00
roo hutton e6ceb4fec4 Merge pull request #36864 from overleaf/rh-36644-cancel-spinner-fallback
Fix cancellation flow if downgrade upsell plan is not available

GitOrigin-RevId: c34f7f37057d5e0c08cbdd5f8524a7c5f00ae8b4
2026-09-17 12:05:27 +00:00
Eric Mc Sween b19f7fb6ad Merge pull request #36873 from overleaf/em-remove-duplicate-citation-key-helper
Remove inaccurate citation key helper text

GitOrigin-RevId: 91cff6c47f27980910e059084b14809586f5dae4
2026-09-17 12:05:23 +00:00
David 5e615cd261 Merge pull request #36620 from overleaf/copilot/fix-default-trash-view
Default Trash nav item to the current location

GitOrigin-RevId: 939284aecc4a002394430e9e2850125825d599de
2026-09-17 12:05:20 +00:00
John Lees-Miller df5c9fd8ea Merge pull request #36881 from overleaf/jlm-recurly-stripe-payment-heal-review
Heal migrated payment methods before renewal

GitOrigin-RevId: 72e33e73b3ca0b771ff0124e8a59e12118922573
2026-09-17 12:05:09 +00:00
Jessica Lawshe 5523f0a6e9 Merge pull request #36700 from overleaf/tm-institutions-api-timeout-abort-handling
Handle client-side v1 timeouts gracefully in InstitutionsAPI, bump reconfirmation-lapsed timeout to 60s

GitOrigin-RevId: ecb2b61e257fdd41cadb353da3af98fec0cdf617
2026-09-17 12:04:40 +00:00
Jessica Lawshe 700bc74f22 Merge pull request #35969 from overleaf/jel-domains-table-internal-admin
[web] Group domains table for internal admins

GitOrigin-RevId: 8c16a22502e333d9c604b322ba66703f9c17d100
2026-09-17 12:04:34 +00:00
Jessica Lawshe 09cd43eb17 Merge pull request #36011 from overleaf/jel-domain-capture-sales-email
[web] Weekly domain capture digest email

GitOrigin-RevId: 6682d1d7fcbb179615dbc009d2da8266113a3b4b
2026-09-17 12:04:28 +00:00
Andrew Rumble 52f25234cd Merge pull request #36462 from overleaf/lg-security-mongoose
[Security upgrade] bump mongoose to 8.24.1 in web

GitOrigin-RevId: 89373303435aec06c2e1efde1dd54f1d70c4dfe1
2026-09-17 12:04:17 +00:00
David 90be47c5e1 Merge pull request #36649 from overleaf/dp-cleanup-library-feature-flag
Remove the overleaf-library feature flag

GitOrigin-RevId: ebeabd43c9e44dbc2344b4e8e7e5b9095fcc86e0
2026-09-17 12:04:13 +00:00
Domagoj Kriskovic 6c802c03a6 Fix dark-mode white flash when navigating between dashboard and library (#36785)
* Fix dark-mode white flash when navigating between dashboard and library

* Use userSettings object in layout-base.pug

* Move initial theme styles in a new file

* Sanitize data-initial-theme with a generic getInitialTheme helper

* Add getInitialTheme stub

GitOrigin-RevId: 80812093bbf75dc2a6f6f24710324a31a1930265
2026-09-17 12:04:10 +00:00
Eric Mc Sween 2bbdc22496 Merge pull request #36844 from overleaf/em-document-migration-tags
Document the auxiliary and nonblocking migration tags

GitOrigin-RevId: ad16fe6b4f3e13606fff1d18f543cca0c2268ee2
2026-09-17 12:03:59 +00:00
Malik Glossop 2a489d6517 Merge pull request #35985 from overleaf/mg-history-ot-comments-preserve
HistoryOT - Preserve comments on cut/paste/undo

GitOrigin-RevId: 3c53da9abe599ab80ccc20960a46c6380d597831
2026-09-17 12:03:53 +00:00
Malik Glossop 183c054991 Merge pull request #36411 from overleaf/mg-writefull-countdown
Use shared ceiling time value for writefull paywall countdowns

GitOrigin-RevId: de77dd274028db58d23044e8aaea9cf0ad570037
2026-09-17 12:03:49 +00:00
Malik Glossop 863e33af78 Merge pull request #35982 from overleaf/mg-history-ot-comments
HistoryOT - Fix comments being dropped and crashing on detached ranges

GitOrigin-RevId: 0906a55a51a6fda9ac828efcbe72989573161261
2026-09-17 12:03:46 +00:00
Anna Claire Fields 19d97a75c6 [web] log the failing tag colour in the validation rollout (#36858)
GitOrigin-RevId: 9fa5a7bc380edf85a1b266e10c5add495744619b
2026-09-17 12:03:42 +00:00
Brian Gough 7aa941794e Merge pull request #36860 from overleaf/bg-allow-integer-in-sync-requests
Allow integer values for h and v parameters in proxySyncPdf

GitOrigin-RevId: 66f303a5940f6976dcfe27ebf90da8a9ec24e98a
2026-09-17 12:03:37 +00:00
Jakob Ackermann 9b44fd075b [monorepo] downgrade ranges ids back to strings for now (#36857)
GitOrigin-RevId: 6c85715bc85adc2f641bfd0e9f3bb6a8f2c839c2
2026-09-17 12:03:33 +00:00
Jakob AckermannandClaude Opus 5 91bf8f13ba [validation-tools] add a throw+log mode to parseReq (#36821)
REQ_VALIDATION_MODE=enforce-log rejects schema failures exactly as 'enforce'
does, but logs them first at instrumented call sites, so the visibility built
for the 'log' rollout (sanitized issues, caller trace, logFields values, per
schema/signature dedup) survives moving out of 'log' mode.

opts no longer change the outcome in this mode: logOnly does not suppress the
throw, the error is classified from the primary schema (not the fallback), and
fallbackSchema is only re-parsed to tag the log entry -- 'enforced-fallback-
passed' marks a rejection the looser pre-refinement schema would have avoided,
i.e. one the tightened schema newly introduces.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 0743322990e6c570ffbae166b4586285a18221df
2026-09-17 12:03:25 +00:00
Jakob AckermannandThomas Mees e276104da8 Relax split test import schema to allow staging export fields (#36832)
* Relax split test import schema to allow staging export fields

The staging split test export includes fields (variant.active, version.__v,
splitTest.customerIoEnabled) that aren't otherwise written by the app but
are present in Mongo exports. The strict zod schema was rejecting the
import; mark these as optional so a staging export can be imported into a
local dev environment.

* [web] allow split-test import in dev-env and CI

* [web] ensure that the zod schema for split-tests covers all fields

---------

Co-authored-by: Thomas Mees <thomas.mees@overleaf.com>
GitOrigin-RevId: eab633e8498e351a64dc9a9dad85759e80744da0
2026-09-17 12:03:21 +00:00
Thomas 0e7eff712c [web] [b2b] mark selected schemas as non-strict (#36780)
Two of the B2B request schemas flagged in #36577 reject bodies we don't
want to reject:

- POST /manage/groups/:id/settings/sso: the group settings frontend
  spreads the SSO config document it fetched from the GET route back into
  the POST body, so the request carries `_id`, `__v`, `validated`,
  `enabled` and `useSettingsUKAMF` that the handler recomputes or ignores.
- POST /manage/groups/:id/invites: an internal script sends an unsupported
  `expiresAt` alongside the email; the handler only reads `email`.

Swap z.strictObject() for z.object() on both, with a comment on each
explaining why it's deliberately non-strict.

GitOrigin-RevId: 5ff94ded579bdda07efc59d9ceb1d495964a912d
2026-09-17 12:03:15 +00:00
Jimmy Domagala-Tang 8ab6005580 feat: updating userId to nullish and explaining why for reject changes (#36803)
GitOrigin-RevId: 6f7a46893b9ab32d8f726ad6ba928eb4e0175813
2026-09-17 12:03:12 +00:00
Brian Gough 538d04ddb8 Merge pull request #36664 from overleaf/lg-wordcount-sync-resources
[clsi] sync project resources for word count

GitOrigin-RevId: fe96c936e7aba22ab94416a102dcb7e4835e1256
2026-09-17 12:03:09 +00:00
Anna Claire Fields 285bcd4315 [editor-core] accept the legacy source_project_display_name on project_file (#36837)
GitOrigin-RevId: bebbba328ecdd04ac4df06f66e6b0b1f8a41276f
2026-09-17 12:03:05 +00:00
Eric Mc Sween 6590b33321 Merge pull request #36792 from overleaf/em-zod-validation-cleanup
Stop rejecting unrecognized request fields in the Dropbox, GitHub-sync, git-bridge and reference-manager APIs

GitOrigin-RevId: 5efdee90830b2a48172f839da8e59ef938746f65
2026-09-17 12:03:02 +00:00
Eric Mc Sween c55411ee99 Merge pull request #36802 from overleaf/em-migrations-auxiliary-tags
[migrations] tag the auxiliary library migrations for saas

GitOrigin-RevId: cfe1169af24b36d6dcb352b4e0f79b250d74a1a0
2026-09-17 12:02:53 +00:00
f671c3dc59 [web] read a linked project file's source from history (#36817)
* [web] read a linked project file's source from history

Behind the `linked-file-from-history` flag, a linked `project_file` import reads
the source out of the source project's latest history snapshot rather than out of
its file tree in mongo, where docs and files are two kinds of thing.

- `ProjectFileAgent` gains `_importFromHistory` alongside the existing
  `_importFromProject`, and the flag picks between them. The snapshot only holds
  what has reached history, so the source project is flushed through
  document-updater and project-history first. A file that is editable is loaded
  eagerly, which applies the edit operations sitting on top of its blob; one that
  is not keeps its hash and is streamed. The file is promisified on the way.
- `History/HistoryOTContentManager.mjs` reads the latest chunk and rolls its
  changes into a snapshot, retrying the read. `History/HistoryBlobStore.mjs` is
  the blob store overleaf-editor-core asks for while loading a file.
- `LinkedFilesController` resolves the flag, including from the referer: the
  import endpoints have no query string of their own. Every agent takes the
  assignment so that the ones that do not read it still pass it along.
- `linked-file-from-history.spec.ts` drives the flag from the editor url, over
  three imports from another project: a file uploaded to the source project and
  then refreshed from it, an edit the source project has only made in its editor,
  and a binary file. Both sources produce the same file, so which one was read is
  asserted in the acceptance tests rather than here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [saas-e2e] add test coverage for tracked-delete in linked file source

* Apply suggestions from code review

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: 3907d6d399e4b5315e68bf833663c06bc72f4011
2026-09-17 12:02:47 +00:00
34d57a87f9 Add contextual pointers to the AI assistant in the editor gutter (#36643)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: jessdomanska <87976015+jessdomanska@users.noreply.github.com>
GitOrigin-RevId: 0df858dae9673868f02ca32cea024fe89794b544
2026-09-17 12:02:41 +00:00
Olzhas Askar bb7dfcd588 Merge pull request #36767 from overleaf/oa-express-types
[web] Express req/res/next types

GitOrigin-RevId: 30b4822e2ee55976331d049f8109f7098914973c
2026-09-17 12:02:38 +00:00
Olzhas Askar e9206d2b95 Merge pull request #36312 from overleaf/oa-unhardcode-homepage
[web] Unhardcode homepage

GitOrigin-RevId: 7ae0b66b5db57998ad1bd1988766d45e096620f7
2026-09-17 12:02:34 +00:00
Eric Mc Sween eb98741f03 Merge pull request #36693 from overleaf/em-node-24-version-metadata
[monorepo] bring stale Node version declarations up to 24.18.1

GitOrigin-RevId: 07d08e32920f05f02b817950eed25030d3b82ce4
2026-09-17 12:02:26 +00:00
Jimmy Domagala-Tang 1c0bc6c50a [web] Gate labs experiments behind premium compiles (#36339)
* feat: gating labs experiments behind premium compile feature

* feat: move filtering to the labs module, and only propogate experiments that user can opt into on the FE

GitOrigin-RevId: 984aa0339c7dc8d61f0f6830780a599508306d5e
2026-09-17 12:02:23 +00:00
Jessica Lawshe 2c8e2781c7 Merge pull request #35038 from overleaf/th-add-v1-blocked-domain
Add 'passmail' to the list of domains blocked in v1

GitOrigin-RevId: 6c12800d03af0ad838f60a13fcf7d77d321b03c6
2026-09-17 12:02:17 +00:00
Jessica Lawshe 49a00ef0e2 Merge pull request #36310 from overleaf/jel-confirm-existing-email
[web] Check if domain captured by group when showing confirm email UI

GitOrigin-RevId: bb6add82e083497c8036739d877610a47e35ecfd
2026-09-17 12:02:03 +00:00
Jakob Ackermann f62b8525f2 [web] gracefully handle empty relativePath in uppy uploads (#36773)
GitOrigin-RevId: 640094f69cf53ba9f33a5bdc3b6d532547431d01
2026-09-17 12:01:55 +00:00
Olzhas Askar c2cd5d686c Merge pull request #36711 from overleaf/oa-prices
[web] Prices, their versions and helper functions

GitOrigin-RevId: b42f16fe3b083a4872230f2e348b247650d08df8
2026-09-17 12:01:51 +00:00
Domagoj Kriskovic a301363ece Move Library BibTeX export to a server-side endpoint (#36604)
* Move bulk bibtex download to a server-side streaming endpoint

* Wrap download fetch errors in FetchError and parse the error body

* Refactor library references download to use GET request and stream response

* Remove MAX_DOWNLOAD_IDS limit from downloadIdsSchema and related test

* fix merge conflict

GitOrigin-RevId: dee2bae8bfb982cd1b79bbd21c9f1b90e8cff1a4
2026-09-17 12:01:40 +00:00
Tom Wells f0cd4b737f Merge pull request #36725 from overleaf/tw-add-library-docs-button
Add info button with link to docs on the Library page

GitOrigin-RevId: 7e7bc10272e0b5bcb7b1f98c07f493643f2ea92d
2026-09-17 12:01:32 +00:00
Jakob Ackermann 58e2396d91 [web] remove legacy body field for rootDoc_id (#36608)
The migration finished in 2014, there are no more clients using it.

REF: 175dfae085
GitOrigin-RevId: 6fc21e93c5ef34622e9cf6259e7105f3202d66ae
2026-09-17 12:01:26 +00:00
Jakob Ackermann 640a2e1c60 [web] remove unused link=false flag from /user/oauth-unlink (#36566)
GitOrigin-RevId: 5a8af17bf339df62a6cf54d6c372bff4cbe88eb3
2026-09-17 12:01:23 +00:00
Jakob Ackermann 4e80d65efc [ranges-tracker] add legacy orderedRejections field in tracked-changes (#36565)
* [docstore] use more realistic test stubs for ranges

* [monorepo] define ranges schemas in @overleaf/ranges-tracker

* [ranges-tracker] add legacy orderedRejections field in tracked-changes

* [monorepo] resolve @overleaf/ranges-tracker subpath imports in typescript

* [real-time] validate comment thread ids and tracked change id seeds

* [ranges-tracker] require ObjectId ids and tracked change metadata in ranges

* [monorepo] require ObjectId tracked change ids on the accept routes

* [web] use ObjectId comment thread ids in the DocumentController tests

GitOrigin-RevId: 58b20dec311d85b917ef63f9efbe745a265a2de6
2026-09-17 12:01:19 +00:00
Eric Mc Sween 833e44d142 Merge pull request #36733 from overleaf/dk-fetch-utils-unset-headers
[fetch-utils] Drop headers with unset values

GitOrigin-RevId: 284b8551dfc3b15315a873a9256fbf9f35e4004b
2026-09-17 12:01:03 +00:00
ilkin-overleaf 7cfdd0f75a Merge pull request #36230 from overleaf/ii-workspaces-projects-page
Empty states for the project list views

GitOrigin-RevId: 3ce8e327bb675f3c63763e7599396020a4252025
2026-09-17 12:00:54 +00:00
Jimmy Domagala-TangandTim Gibbon baa9cf78d9 Adding 2025 and 2026 incremental compiles options (#35850)
* feat: adding 2025 and 2026 incremental compiles options

* feat: remove dmtcp from compiler list and autoselect checkpointing version with labs inclusion

* feat: updating seccomp profile for checkpointing

* feat: disable non-premium dmtcp compiles, and align checkpoint version detection between clsi and web

* feat: adding in reminder of checkpoint experiment, and using session based split test retrieval

* Increase Packer build.sh timeout from 1h15m to 1h30m as build takes approx 1h

* fix: moving checkpointing check from tag name based to hasCheckpointing flag set as part of allowed_images, and removing uneeded seccomp rule

* feat: move variant/ image selection to CLSI, and have web control only what options to set for a compile. removing uneeded extra variants from webs list of allowed images

* feat: adding compile timer as a query string as well

---------

Co-authored-by: Tim Gibbon <tim.gibbon@overleaf.com>
GitOrigin-RevId: cdbdf76a62dcd5a585925f18d64b6cb9cd6ed373
2026-09-17 12:00:46 +00:00
John Lees-Miller 35271cb33c Merge pull request #36639 from overleaf/jlm-github-sync-distinguish-404s
Distinguish GitHub's repo-lookup 404 from its commit-comparison 404

GitOrigin-RevId: 67cd1d3af1fde553fc200371f5d2ec020c7245fa
2026-09-17 12:00:40 +00:00
David 28fd2f6605 Merge pull request #36675 from overleaf/dp-dark-library-modals
Theme the bibtex import and entry edit modals for dark mode

GitOrigin-RevId: 7aaf69f418844435de3421aa9b48e797b2ba93ef
2026-09-17 12:00:37 +00:00
Antoine ClausseandClaude Opus 5 e2e05b69f1 [web] Add pro-value-framing split test copy (#36628)
* feat(web): add pro-value-framing split test copy

Serve quantified, benefit-led plan differentiators on the plans and
interstitial pages to the `enabled` variant of the `pro-value-framing`
split test, in place of the vague "increased/max AI allowance" and
"10/unlimited collaborators" framing.

The variant changes the bullet set and order as well as the strings, so
it swaps in its own short feature list rather than remapping keys: Pro
leads with "Everything in Standard, plus:", Free gains Error Assist and
"Compile simple projects". The plans page comparison table mirrors the
card wording in its AI allowance row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(web): assert plan copy numbers against plan features

Check the quantified claims in the plan copy against the SaaS plan
features: the collaborator limits, and the "24x" compile timeout claims
as the ratio of the paid to the free compileTimeout. The "2x more AI
usage" claim is checked against the allowance tooltips, since the
allowances themselves come from AI_*_QUOTA_GRANT and are not available
in the unit test environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Remove overly contrasted color in card

* Remove redundant `makeUISettings2026`

* Simplify valueFramingOverrides

* Fix types

* Revert changes to the table, replace `2x_more_ai_usage` to `2x_the_ai_allowance`

* Remove oververbose PlansValueFraming.test.mjs

* Revert "Remove overly contrasted color in card"

This reverts commit 8e83a0c523c4ebfe8a40a04023f1801080d0e412.

* Apply suggestions from code review

GitOrigin-RevId: 25b0736af47a3a32f4e1dd108ab0eb14c1bc13e3
2026-09-17 12:00:26 +00:00
Antoine ClausseandClaude Fable 5 0e510226fa [web] Format paused reactivation date in UTC via shared helper (#36600)
The paused branch of getFormattedRenewalDate formatted the reactivation
date in the viewer's local timezone with a hand-rolled format literal,
while the surrounding dates are UTC. Compute the reactivation date in
UTC and format it with formatPaymentDate.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitOrigin-RevId: 1f1679c1988c696c16c78bd11152d6a39a20c4e1
2026-09-17 12:00:22 +00:00
Eric Mc Sween 82539b5842 Merge pull request #36684 from overleaf/em-user-settings-zotero-groups
Stop carrying an _id on reference manager groups in user settings

GitOrigin-RevId: 1ac74d201a3b81b2069215e05b9c84189c42aad6
2026-09-17 12:00:19 +00:00
Tim Down a84079e600 Merge pull request #36682 from overleaf/td-migration-readme
Update migrations README following switch to Yarn

GitOrigin-RevId: 718af0c66d173ecbfaa27209a41f525c9d1a4a8a
2026-09-17 11:59:50 +00:00
Lucie GermainandClaude Sonnet 5 57e536de79 [Security Upgrade]: cypress in root workspace (drops unpatched extract-zip) (#36689)
* [security] bump cypress 15.12.0 -> 15.20.1 to drop unpatched extract-zip

extract-zip <=2.0.1 has no patched release (GHSA-jmr9-qjv8-65gv). It was a
transitive dependency of cypress, used only to unpack Cypress's own binary.
Cypress 15.20.1 replaces extract-zip with yauzl directly, so upgrading the
parent removes the vulnerable package entirely.

Also bumps the pinned cypress/included Docker base image used by
test_frontend_ct / server-ce e2e / saas-e2e.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [security] fix cy.get<T> alias typing broken by cypress 15.20.1

Cypress 15.20.1 adds a dedicated Chainable.get overload for alias strings
(get<S>(alias: `@${string}`): Chainable<S>), which now takes priority over
the generic selector overload. cy.get<HTMLInputElement>('@input') therefore
resolved to Chainable<HTMLInputElement> instead of
Chainable<JQuery<HTMLInputElement>>, so .then(element => element.get(0))
no longer type-checked. Pass the JQuery-wrapped type explicitly to match
the new overload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GitOrigin-RevId: f4a56a2d5948b5aaf00a07ecb384fc75abf6de53
2026-09-17 11:59:44 +00:00
Lucie GermainandClaude Sonnet 5 a8e1198da8 bump ip-address to 10.3.1 in yarn.lock (#36452)
Fixes GHSA-mwp4-54f8-5fhr (high), GHSA-4xrf-jv44-h6hh (medium),
GHSA-22jq-vg5j-6vgg (medium) in ip-address, a transitive dependency
pinned via a resolutions entry in root package.json.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GitOrigin-RevId: 1557589d719801cbd0ed95d36a17583fdc215f90
2026-09-17 11:59:36 +00:00
Mathias Jakobsen 0dd792b146 Merge pull request #36677 from overleaf/mj-fix-tag-hsl
[web] Allow derived HSL values for tags

GitOrigin-RevId: c9617a8b289e361f3dd438c7d81d8b3c1d5cb645
2026-09-17 11:59:32 +00:00
Rebeka Dekany b5c161eeca [web] Cache Stripe prices and products in StripeClient (#36236)
* Cache Stripe prices and products in StripeClient

* Note the Stripe price cache in the price scripts and dev manual

* Add tests for the Stripe price and product cache

* Simplify the not-found guard in getProductsByIds

GitOrigin-RevId: eb2a270a3abe0e9bf53ca2ebee1f0ad02b1a4cc4
2026-09-17 11:59:29 +00:00
Jimmy Domagala-Tang 009668c224 Release TeX Live 2026.1 as the default image (#36382)
* feat: releasing 2026.1 texlive to general release and promoting to default image

* fix: update clsi image tests so their likeness calcs work locally on different archs

* feat: updating epstopdf fixture and adding note to texlive Dockerfile used for codespaces

* fix: use generated image-eps-convert instead of fixture version that drifts

GitOrigin-RevId: 13c4f6977dbf07ddd34c9a5ab3ea295cddcbe025
2026-09-17 11:59:26 +00:00
Olzhas Askar 6053781e57 Merge pull request #36496 from overleaf/oa-annual-events
[web] Subscription event segmentation

GitOrigin-RevId: fdc25ac7bc7320ed82617b7c27c12a17eb31d723
2026-09-17 11:59:20 +00:00
Andrew Rumble ba4e6ef8ed Merge pull request #35468 from overleaf/ar-fetch-utils-type-improvements
[fetch-utils] type improvements

GitOrigin-RevId: 4652054c715c4ddb19bac1f46cdf1fc3916c533d
2026-09-17 11:59:06 +00:00
Mathias Jakobsen 1f9873e391 Merge pull request #36575 from overleaf/mj-generic-modals-themes
[web] Add dark mode to ide modals

GitOrigin-RevId: cd8e95e2286df46510a377ff9a69c4ab646ceabd
2026-09-17 11:59:00 +00:00
Mathias Jakobsen 8139638d9d Merge pull request #36570 from overleaf/mj-themed-labs-modal
[web] Add dark mode support for labs modal

GitOrigin-RevId: 18f9aff407ee5324c835f915419819b00d49bfb0
2026-09-17 11:58:57 +00:00
John Lees-MillerandClaude Opus 5 1ecdd122d0 Add Stripe migration features for stage 7 testing (part 2) (#35890)
* upsert: migrate US/CA PayPal without a payment method when flagged

getPaymentMethod no longer throws for a PayPal billing agreement destined for
Stripe US/CA when the account is flagged ignore_payment_method; instead it
returns null so the customer is migrated without a default payment method
(the send_invoice paymentMethodPending flow). Unflagged accounts still throw.

* finalize: expect the plan-code remap for legacy-plan subscriptions

When a subscription carries legacy_remap=1 metadata, compare the Stripe product's planCode against the recorded target plan code instead of the raw Recurly plan code, so the intentional remap is not misread as drift (the price is preserved, so the amount comparison is unchanged). Clear the legacy_remap metadata during post-migration cleanup.

* finalize: expect the assistant add-on reprice

When a subscription carries assist_addon_repriced=1 metadata, compare the assistant item against the recorded target amount (Stripe minor units) instead of the raw Recurly amount, so the intended reprice is not misread as drift. Clear the reprice metadata during post-migration cleanup.

* create_coupons: create migration coupons by name with amount_off, currency and product scope

Create the pipeline's per-region coupon_specs_<region>.csv coupons by name (idempotent, auto-paginated), supporting amount_off + currency (fixed) or percent_off, duration, and applies_to.products, with no promotion code. Region comes from the existing --region flag. The existing name-based percent-off + promotion-code path is unchanged.

* finalize: schedule the pending change as a phase 2

For a subscription flagged pending_change_scheduled, compare drift against the current Recurly state (not the pending change), then in Step 7 release the toolkit schedule and create a fresh two-phase subscription schedule (current until renewal; pending state from renewal with proration disabled and end_behavior release), clearing the scheduling metadata once the schedule exists.

* finalize: flag address-pending migrations and push the needs_to_update_address trait

When a migrated subscription carries metadata.addressPending, append -address-pending to the finalization status and push needs_to_update_address to customer.io (alongside the existing tax-info-pending handling), so the customer can be driven to add a billing address. The flag stays on the subscription so the customer.updated webhook can enable automatic_tax and clear it once the address resolves.

* webhook: heal address-pending migrations when the tax location resolves

On customer.updated (address/tax change), for a customer whose location is no longer unrecognized_location, enable automatic_tax on their migrated subscriptions still flagged metadata.addressPending and clear the flag. Enabling automatic_tax is rejected by Stripe while the location is still invalid, so the update is a safe idempotent no-op until the address is fixed; the cancellation state is never touched.

* cron: cancel unresolved address-pending subscriptions before they renew untaxed

Backstop for the address-pending cohort: searches the address-pending subscriptions in a region and, for any about to renew (within a configurable lead window) whose customer location is still unrecognized_location, cancels it at period end so it never renews untaxed. Skips subscriptions that are already cancelling and those whose location has resolved (healed by the webhook). Dry-run unless --commit.

* upsert: set tax.validate_location=auto on customer update

Conform PAN-copied customers to the account's default tax-location validation. Uses 'auto' rather than 'immediately' (which throws for country-only addresses and would break the address-pending migration flow).

* finalize-migration: send payment method info to customer.io

Add payment_method (card/paypal/none) and needs_to_add_payment_method
traits to the stripe_migration customer.io payload so the migration
email campaign can personalise per payment method without enumerating
the full cross-join. The payment method is read from the already-fetched
customer/subscription (default_payment_method now expanded on that fetch),
so no extra Stripe request is made. Also report a -payment-method-pending
status suffix in the CSV output.

* upsert: flag customers whose address Stripe cannot resolve

After pushing the Recurly address, read customer.tax.automatic_tax; when it is 'unrecognized_location', record address_unrecognized in the output so the subscription is migrated untaxed (address-pending) instead of failing at the billing-migration toolkit. This is the authoritative, region-agnostic detector (covers US/CA country-only and broken non-US addresses, e.g. India with a postal but no state/city). Adds isCustomerAddressUnrecognized helper + unit tests.

* Close reactivation-without-valid-address loophole

* webhook: detect newly-set address/tax with !== undefined

Review follow-up: the address-pending heal guard used a truthy check, which
would miss an address newly set from empty. Harmonise with _billingChanges,
which already uses !== undefined.

* upsert: set tax.validate_location via customerParams

Review follow-up: move the tax.validate_location='auto' directive into the
assembled customerParams instead of overriding it inline at the update call,
so it also appears in the dry-run output.

* create_coupons: guard migration coupons against ambiguous discount specs

Review follow-up: throw a clear per-coupon error if a migration coupon spec
has neither or both of amount_off/percent_off, catching data-processing
issues instead of silently sending percent_off=NaN.

* Rely on presence of customer.tax

* webhook: cover the address-pending heal path in unit tests

The customer.updated fixtures carried no `tax` hash and the mocked region
client had no `stripe` property, so the one test that reached the heal branch
threw and was swallowed by the caller's error handler. The heal logic had no
real coverage as a result.

Add the missing fixture fields and tests covering the heal itself, the
still-unrecognized early return, the skip rules for unflagged and cancelled
subscriptions, the Stripe rejection path that leaves the flag set, and the
missing-tax guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
GitOrigin-RevId: 8162e3e0f01e136d3aa560c22e98f43e36da4eb5
2026-09-17 11:58:48 +00:00
Brian Gough 446c4f3f6a Merge pull request #35899 from overleaf/bg-custom-multer-workaround
add custom multer upload handler to prevent leaked files

GitOrigin-RevId: 7b390d326b4cf81fd82c532c53e9772cb4107a29
2026-09-17 11:58:42 +00:00
Andrew Rumble 44753a2a50 Merge pull request #36606 from overleaf/ar/log-relativePath
[web] log relativePath in uploadFiles endpoint

GitOrigin-RevId: d30cace4632c6c1b70aad2c84a2a6f470ef2ffa8
2026-09-17 11:58:39 +00:00
Domagoj Kriskovic ce194c9752 Use named <a> in feedback link Trans and themed-links mixin
GitOrigin-RevId: 812beaa01376c8ce0a423ba4cf1ae25581396fbb
2026-09-17 11:58:36 +00:00
Domagoj Kriskovic d7b4ef33ed Add "New" badge to library nav link and a feedback link on the library page
GitOrigin-RevId: 6c936ca8168dd718c4eaf806f426d5c00078add8
2026-09-17 11:58:32 +00:00
Brian Gough a4b9b18109 Merge pull request #36402 from overleaf/bg-zod-move-safe-path-tests
move safe path tests from zodHelpers to web

GitOrigin-RevId: 3bb25fa108a4a0da0bed1ae60fabfdd47ed76283
2026-09-17 11:58:29 +00:00
Lucie GermainandClaude Sonnet 5 31d766c4b9 Migrate services/web/scripts/{plan-prices,translations,ukamf} off npm (#36594)
* Migrate services/web/scripts/{plan-prices,translations,ukamf} off npm

Three more of the last standalone npm projects in the repo (own
package-lock.json, no automated Dependabot fix path):

- plan-prices and ukamf fold into the root Yarn workspace (own
  package.json, no lockfile), same pattern as jobs/wiki-algolia. ukamf
  was missing a "name" field (required for a workspace member) and
  relied on moment/lodash/xml2js being available as phantom
  dependencies via web's node_modules; under PnP that no longer works,
  so they're now declared explicitly, matching the versions already
  used by services/web.

- translations stays standalone (own yarn.lock, packageManager field,
  node-modules linker) because its Dockerfile builds an isolated
  "onesky-client-jail" sandbox image with deliberately restricted
  filesystem access — not a good fit for pulling in the whole
  monorepo's PnP install. Its Dockerfile now installs via corepack
  yarn instead of npm ci.

Updated the plan-prices README and the two dev-manual docs that
referenced running these scripts with plain `node`/`npm install`.
Also trimmed the now-stale "a few helpers still use npm" line from
security-triage's security-context.md (wiki-algolia was already
migrated; this covers the rest, translations still gets its own
yarn.lock note).

checkSanitizeOptions.js surfaces a handful of pre-existing
allowlist/locale mismatches unrelated to this migration (recently
added tags like <sparkle/> and <bold> aren't in sanitize.js's
allowedTags) — flagging but not fixing, out of scope here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix stale ukamf install/run instructions per Copilot review

metadata-processor.js/check-certs.js/check-idp-metadata.js's doc
comments and developer-manual's institutional-sso-configuration.md
still told people to `npm install`/`yarn install` inside
scripts/ukamf and run the scripts with plain `node`. Now that ukamf is
a root Yarn workspace member, that install step doesn't do anything
and plain `node` won't resolve PnP-managed deps. Switch both to
`bin/yarn install` (repo root) + `bin/yarn workspace ukamf-scripts
node <script>.js`.

--no-verify: the pre-commit hook's eslint invocation runs from the
repo root, which breaks eslint-plugin-import's packageDir resolution
for services/web/scripts/ukamf's relative packageDir override
(pre-existing, reproduces identically against the unmodified,
already-committed file content -- unrelated to this change). Verified
correctness instead via the actual web-scoped lint:
bin/yarn workspace @overleaf/web exec eslint scripts/ukamf, which
passes clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Align translations Dockerfile's corepack setup with the rest of the repo

Per Anna Claire's review on #36594: use the same corepack bootstrap as
every other service Dockerfile (services/chat, filestore, etc.) --
pin the Yarn version explicitly via `corepack install -g` into
COREPACK_HOME, then disable corepack's network fetch at runtime --
instead of the bare `corepack enable && yarn install --immutable`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GitOrigin-RevId: 9d05064de9191ab3eef39a672ca00f0f0f7066bb
2026-09-17 11:58:20 +00:00
KristinaandJimmy Domagala-Tang 9c005e4e54 [web/document-updater] display preview for accepted/rejected tracked changes (#36240)
* [web] add preview payload to track-change notification types

* [doc-updater] pass accepted/rejected change data through to web for previews

* [web] snapshot change preview onto accepted/rejected notifications

* [web] render resolved change previews instead of accepted/rejected count lines

* [web] add unit coverage for accepted/rejected change preview flow

* fix: notification carries multiple previews per accept/reject batch; extract shared group helper

* fix: one preview per accept/reject action, drop adjacency grouping

* fix: move buildResolvedChangePreviews out of the email builder class

* fix: type error, make tests more explicit

* fix: render resolver below preview

* fix: overflow text, alignment

* fix: test error

* fix: resolved count

* [document-updater/web] send sparse change preview instead of full doc lines

* fix: store sparse preview, hydrate at render, drop CE gate

* fix: advance slice cursor past tracked-delete text in sparse preview

* fix: inline buildChangeGroupPreview

* fix: changePreviewBuilder -> sparseChangePreview

* fix: rm change_contributors

* fix: copilot flagged testing issues

* fix: hydrate and truncate change preview at write time

* fix: avoid materializing full doc text when slicing preview window

* fix: update reject preview test to hydrated shape

* fix: accepted delete removing context

* [document-updater/web] split resolved change previews into one per change cluster

* fix: narrow cluster userIds to string[] for typecheck

* feat: updating to use zod schema for previews

---------

Co-authored-by: Jimmy Domagala-Tang <jimmy.domagala-tang@overleaf.com>
GitOrigin-RevId: 0740713c715a61a220180f353e497b27da6a5b0f
2026-09-17 11:58:14 +00:00
Brian Gough 3e0a54c917 Merge pull request #36568 from overleaf/bg-add-ranges-hash-to-add-file-operation-find-blob-hashes
include range hashes in findBlobHashes for AddFileOperation

GitOrigin-RevId: 5d31f7efbe409b8c0433607152f66262f99cb94c
2026-09-17 11:58:11 +00:00
Andrew Rumble d9b8aa1322 Merge pull request #36592 from overleaf/ar-remove-unused-output-zip-query-params
[web] remove unused query params from download all link

GitOrigin-RevId: e6c1ac8d8c7f369d7ea99c39ed0f0c3c596580b7
2026-09-17 11:58:00 +00:00
Andrew Rumble a84676c4f0 Merge pull request #36459 from overleaf/lg-security-postcss-root
[Security upgrade] remove unnecessary postcss pin (resolves to 8.5.26) in yarn.lock

GitOrigin-RevId: 90bed3a289b304d19ad539f516e32e1d8194da1c
2026-09-17 11:57:57 +00:00
Eric Mc Sween 93d96c2cfb Merge pull request #36551 from overleaf/em-github-sync-acceptance-vitest
[github-sync] Migrate acceptance tests to vitest

GitOrigin-RevId: cafa14a0cddbfde31495264b7b19c1a5a1b6e562
2026-09-17 11:57:45 +00:00
Anna Claire Fields fd0e0ac7df Log zipUrl value in req-validation schema failure (#36603)
GitOrigin-RevId: c53b1d54e356eb937c12a3d06f620bd515af0042
2026-09-17 11:57:42 +00:00
Lucie GermainandClaude Sonnet 5 efd342919a Migrate jobs/quay-logging from npm to Yarn workspace (#36593)
Folds jobs/quay-logging into the root Yarn workspace (same pattern as
jobs/wiki-algolia): removes its own package-lock.json, adds it to the
root workspaces array, and updates cloudbuild.yaml to install/run via
corepack yarn instead of npm.

This brings it under yarn-security-update's automated coverage for
Dependabot alerts, instead of needing a manual backup issue.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GitOrigin-RevId: 5790ed41001c6ea1830d72018ce9af92525097a1
2026-09-17 11:57:36 +00:00
Alf EatonandClaude Fable 5 44315375fa Improve the Dimensions search feature (#36386)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitOrigin-RevId: 3d1ca7883102955dac3bc4a2e56c72975d0a3d20
2026-09-17 11:57:33 +00:00
Alf Eaton 1f394b32dc Copy asset rules into dev webpack config (#36371)
GitOrigin-RevId: 0b77675cf17cf0f023d0ffa3b9568621774c2a7f
2026-09-17 11:57:27 +00:00
Alf EatonandMathias Jakobsen 8c869db734 Replace unload handlers with visibilitychange (#36387)
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: 3b52b09445597d93eb5fdfa5a343fbc2fac14d6c
2026-09-17 11:57:24 +00:00
Mathias Jakobsen aac9e669a3 Merge pull request #36548 from overleaf/jd-upload-file-delete-icon
[web] Use themed danger colour for figure modal's remove icon

GitOrigin-RevId: b881d3a08d22636c5c521a114499b650af6e6c82
2026-09-17 11:57:18 +00:00
ilkin-overleaf 0113c8ee9f Merge pull request #35936 from overleaf/ii-workspaces-subscription-admin-settings
Shared Workspace settings for subscription admins

GitOrigin-RevId: 61cc45a48aaf1709905e98b3cccf09b50e2afa84
2026-09-17 11:57:14 +00:00
Mathias Jakobsen 51ae580da9 Merge pull request #36539 from overleaf/mj-misc-dark-mode-modals-core1
[web] Add dark mode to misc editor modals

GitOrigin-RevId: 3d7bd45476b0f814abfc26fa3da3eecca54342e8
2026-09-17 11:57:06 +00:00
Mathias Jakobsen 034b9e35be Merge pull request #36573 from overleaf/mj-review-panel-modal-theme
[web] Add theme support to review panel modal

GitOrigin-RevId: e5bfc4a4affdb6352e81bbf835d64832ec249422
2026-09-17 11:57:02 +00:00
Jimmy Domagala-Tang 73ef3f6439 fix: remove webkit patch for safari input height jumping on persistent scrollbars since it causes a full on crash in firefox (#36589)
GitOrigin-RevId: a64fcae032ad8370c67b9e0a05b54c2e0759d40f
2026-09-17 11:56:56 +00:00
Alexandre Bourdin c861628684 Merge pull request #36208 from overleaf/ab-labs-description-headings-code
[web] Support headings and code in labs experiment descriptions

GitOrigin-RevId: 2a85f92fb5f0ce0696c1056087108ef2599547de
2026-09-17 11:56:43 +00:00
Alexandre Bourdin 329eca74c1 Merge pull request #36210 from overleaf/ab-fix-focus-mode-shortcut-linux
[web] Fix Focus mode shortcut showing Mac control glyph on non-Mac platforms

GitOrigin-RevId: d2f9f068ab9a2585813fbb777a6ab4fb41d386a2
2026-09-17 11:56:40 +00:00
Domagoj Kriskovic 0adf9ea45c [visual-markdown-editor] Support for images for in project files
GitOrigin-RevId: 488ebcfc6429cfa2ce4f919f8c736822c80fdca1
2026-09-17 11:56:36 +00:00
Jakob Ackermann cd7ac6f2fc [editor-core] gracefully handle verbose project_output_file details (#36552)
GitOrigin-RevId: 2c549db61dc05c02729bf80c8fbd3f31d52f1e9c
2026-09-17 11:56:28 +00:00
Lucie GermainandClaude Sonnet 5 dcb0f9963f bump js-yaml resolution to ^4.3.1 in yarn.lock (#36464)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GitOrigin-RevId: 78219c1f526b78fc7448394140cb22fc7cc73511
2026-09-17 11:56:22 +00:00
Copilotandthomas- 490c2141d2 Remove outdated humans.txt file (#36547)
* Remove outdated humans.txt file

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomas- <2176518+thomas-@users.noreply.github.com>
GitOrigin-RevId: cbeab28912ca8256c052d42a0ba2cf0b7790feef
2026-09-17 11:56:13 +00:00
Maria Florencia Besteiro Gonzalez 173081aa6f Add user affiliations to Library page to fix affiliation widget on Commons (#36522)
* load affiliations data when rendering library page

* address Copilot review feedback on PR #36522

- default userAffiliations to [] if getUserFullEmails fails, matching
  ProjectListController's error handling, so Library/Trash still render
  when the institutions service is unavailable
- stop mutating the affiliation object when attaching the email
- rename a misleading test title (affiliation is not necessarily confirmed)
- fix sidebar-ds-nav "hides the widget" tests: they were only waiting for
  the /api/project fetch to be called, not for the project list state to
  settle, so they passed even before the affiliations check ran

GitOrigin-RevId: 7d04ae7f7dc8d1526c2deb8959099f6e9fe9c4ee
2026-09-17 11:56:03 +00:00
Mathias Jakobsen 531718c874 Merge pull request #36521 from overleaf/mj-project-list-themed-modals
[web] Add theme support to most project list modals

GitOrigin-RevId: fd61f096c2a3f021795c30a490c7fe2d4dc13100
2026-09-17 11:55:59 +00:00
Antoine Clausse a6b7329e47 [web] Render address form above payment element on Stripe checkout (#36209)
* [web] render address form above payment element on Stripe checkout

Changing country across Stripe regions (US/CA vs rest) remounts the Elements
tree and clears any entered card details. Rendering the address form (which
contains the country selector) first lets users settle the country before
entering payment details.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [web] gate checkout address-first order behind checkout-address-order split test

Per PR review: the reorder needs testing, so it is now behind the
checkout-address-order split test (default keeps the current payment-first
order). With the enabled variant the heading becomes "Payment details", since
"Select a payment method" no longer sits above the payment element.

Part of #36429

* Rename `checkout-address-order` to `checkout-address-first`

GitOrigin-RevId: 9c861b1013a284b10757fc416bcdc5610790d582
2026-09-17 11:55:56 +00:00
Antoine Clausse b2139b170c [web] Remove SubscriptionFormatters, format subscription dates in the frontend (#36409) (#36476)
* Reapply "[web] Remove SubscriptionFormatters, format subscription dates in the…"

This reverts commit 2939e71e5b919529cbb8a602916d5f597d117792.

* Fixup after rename conflict: `nextPaymentDueDate`->`periodEnd`

GitOrigin-RevId: c024a3a2cd08c1f8f9d4c99f9ba6f018d334439e
2026-09-17 11:55:52 +00:00
Jakob Ackermann a6792af5b1 Merge pull request #36225 from overleaf/jpa-zod-web-final
[web] fully adopt strict zod schema and enforce in tests

GitOrigin-RevId: 1bb03ed3a819be6de38c8ca5758b3909cfa23318
2026-09-17 11:55:44 +00:00
Jakob Ackermann de0d1a7226 Merge pull request #36219 from overleaf/jpa-zod-web-platform
[web] platform: adopt strict zod schema and extend test coverage

GitOrigin-RevId: e3a0b7b4c52ac116f28be0d284284e3bd3994cda
2026-09-17 11:55:40 +00:00
Jakob Ackermann 55c23b6812 Merge pull request #36223 from overleaf/jpa-zod-web-core-2
[web] core-2: adopt strict zod schema and extend test coverage

GitOrigin-RevId: f26d349fefb6bf9e042cb39117395120c3f7c877
2026-09-17 11:55:37 +00:00
Jakob Ackermann e2564f356c Merge pull request #36222 from overleaf/jpa-zod-web-core-1
[web] core-1: adopt strict zod schema and extend test coverage

GitOrigin-RevId: 6dc74a100989e6eca1565480a9ecb5f24404ef96
2026-09-17 11:55:33 +00:00
Jakob Ackermann f26a48fc4c Merge pull request #36221 from overleaf/jpa-zod-web-b2c
[web] b2c: adopt strict zod schema and extend test coverage

GitOrigin-RevId: 5c612e3ce83b81cfff7b8f1c5bc4522d6feb665a
2026-09-17 11:55:30 +00:00
Jakob Ackermann 784b9c64ec Merge pull request #36220 from overleaf/jpa-zod-web-b2b
[web] b2b: adopt strict zod schema and extend test coverage

GitOrigin-RevId: ae9bb04cfe1034ff36dec33ba5e77aceecd71db7
2026-09-17 11:55:27 +00:00
Eric Mc Sween 2d3474fbdb Merge pull request #36494 from overleaf/em-remove-visual-options
Remove the Options object from the visual editor extensions

GitOrigin-RevId: f06e60b88ef77d3a670d7d956d1a86c116d71873
2026-09-17 11:55:18 +00:00
Mathias Jakobsen 33e2a1e282 Merge pull request #36526 from overleaf/mj-command-palette-network-stall-files
[web] Avoid searching files when network is stalled

GitOrigin-RevId: 08707b1c8a7e0088e8fee6ead3e74bdd807514c7
2026-09-17 11:55:12 +00:00
Mathias Jakobsen 1edb50de7b Merge pull request #36505 from overleaf/mj-themed-url-field
[web] Enable dark mode url fields

GitOrigin-RevId: 97e820e773e9d1528a5e852f10fa31fab84a185b
2026-09-17 11:55:09 +00:00
ilkin-overleaf 0ee4158abe Merge pull request #36392 from overleaf/ii-audit-logs-survey-removal
[web] Remove the feedback survey link from audit logs page

GitOrigin-RevId: f0b662146cec72e1a805387a0566154aab82c892
2026-09-17 11:55:00 +00:00
ilkin-overleaf b42b9adbd8 Merge pull request #36447 from overleaf/lg-security-undici-729
[Security upgrade] bump undici to 7.29.0 in yarn.lock

GitOrigin-RevId: a7f50ff0fd2428f281de425025ed6b9cf39116e0
2026-09-17 11:54:57 +00:00
Malik Glossop f534349991 Merge pull request #36490 from overleaf/copilot/fix-flaky-test-sends-invites
Stabilize flaky ShareProjectModal invite JSDOM test

GitOrigin-RevId: c91abb89f1d16c0347de11287ee26f1c885edba8
2026-09-17 11:54:46 +00:00
Andrew Rumble ff14d94c04 Merge pull request #33538 from overleaf/ar-remove-web-transform-vitest
[web] remove mocha-to-vitest transform script

GitOrigin-RevId: b33f1587846640a8823ff963e789cde4aaae081f
2026-09-17 11:54:28 +00:00
Brian Gough 3edb312a35 Merge pull request #36460 from overleaf/lg-security-brace-expansion-root
[Security upgrade] bump brace-expansion in yarn.lock

GitOrigin-RevId: 6ffec751406ab8f20a664c180c11ade4a0f96f67
2026-09-17 11:54:22 +00:00
Brian Gough 6a74e50ba1 Merge pull request #36388 from overleaf/lg-tar-profiler-upgrade
[Security upgrade] remove tar 6.2.1 and bump tar to 7.5.22 in root workspace

GitOrigin-RevId: 7471e82d64bf0a951a25586c19027300ecdeee98
2026-09-17 11:54:18 +00:00
Kristina 42f441b498 [web] disable editor tabs in review panel tests (#36504)
GitOrigin-RevId: baefd7028b89c1a157a7f279452c953843031f08
2026-09-17 11:54:12 +00:00
Kristina 3e11985e59 [web] prevent dangling comments by using the thread's doc id in operations (#36423)
GitOrigin-RevId: 9f3ddf282a058e92419e772b583169ac8bfe4af9
2026-09-17 11:54:06 +00:00
Mathias Jakobsen 2cb515ac2f Merge pull request #36463 from overleaf/mj-themed-history-modals
[web] Add theme support to history modals

GitOrigin-RevId: 10cc2176b91213342e066fad8ff0eba265d868ec
2026-09-17 11:54:00 +00:00
Mathias Jakobsen 6a800028be Merge pull request #36474 from overleaf/mj-themed-file-tree-modals
[web] Add dark mode file tree modals

GitOrigin-RevId: 8bfd548eedd16fd6848a9d6460cdd7f1be50d759
2026-09-17 11:53:55 +00:00
Mathias Jakobsen 99f0b2d562 Merge pull request #36446 from overleaf/mj-themed-keyboard-shortcuts
[web] Add theme support to keyboard shortcuts modal

GitOrigin-RevId: c92088cbe81a58431627b2343df5d90b68f2bc60
2026-09-17 11:53:51 +00:00
Mathias Jakobsen 67478b1b56 Merge pull request #36453 from overleaf/mj-themed-table-generator-modals
[web] Add dark mode support for table generator modals

GitOrigin-RevId: aeb0d0e8eae2b4257c287cc322d1eb7b8d9b9a35
2026-09-17 11:53:48 +00:00
Mathias Jakobsen dfda63f060 Merge pull request #36470 from overleaf/mj-themed-uppy-figure-modal
[web] Use themed uppy in figure modal

GitOrigin-RevId: 54e92a40516a947befdd4d1f758dae631703672f
2026-09-17 11:53:44 +00:00
Mathias Jakobsen a7c830ee23 Merge pull request #36438 from overleaf/mj-themed-figure-modal
[web] Support theming in figure modal

GitOrigin-RevId: 31fcc5b8585cb9f81ff073fbc07b7b36e04ba54d
2026-09-17 11:53:41 +00:00
Mathias Jakobsen e8ee19d17d Merge pull request #36408 from overleaf/mj-context-menu-flag-teardown
[web] Tear down editor-context-menu flag

GitOrigin-RevId: 80cc2ebaf62ec1bd6064b0f96b884d159a9d4b5f
2026-09-17 11:53:38 +00:00
Mathias Jakobsen 38eb3ddfa6 Merge pull request #36426 from overleaf/mj-wrong-variable-removed
[web] Remove typoed themed variable

GitOrigin-RevId: ec23ed1819f78cfcfb42c578ebe58373ddbef6b1
2026-09-17 11:53:34 +00:00
Mathias Jakobsen 06b4124aca Merge pull request #36407 from overleaf/mj-tabs-teardown
[web] Tear down tabs feature flag

GitOrigin-RevId: c25a3abe5c4b85f9802751bb1882385ed9c50a4a
2026-09-17 11:53:28 +00:00
Mathias Jakobsen 57697c6c56 Merge pull request #36405 from overleaf/mj-remove-context-menu-survey
[web] Remove context menu survey

GitOrigin-RevId: 841705a3874e870ee38968aa56e7db7eea1c3f01
2026-09-17 11:53:25 +00:00
Domagoj Kriskovic 2ecbe6fb85 Zotero sync: treat the paged pull as a single snapshot (#35638)
* Zotero sync: treat the paged pull as a single snapshot

* fix zod after rebasing

* if extraHeaders, build a new client on each request

GitOrigin-RevId: b73829ad9fddaeb91753b3f9b80c5c5ae0cbfa50
2026-09-17 11:53:06 +00:00
Miguel Serrano 194de0b457 [web] Update CE/SP footer with copyright notice (#36065)
* [web] Update CE/SP footer with copyright notice

GitOrigin-RevId: b1e650e9512d8497b03ab167b29792b32053e95c
2026-09-17 11:52:58 +00:00
Kristina 00b594a14f [web] block idle connection-restored events (#36425)
GitOrigin-RevId: 74b142f72f7982e6df9e0208965d8372e23afc74
2026-09-17 11:52:55 +00:00
Antoine Clausse 07835f92b3 Merge pull request #36475 from overleaf/revert-36409-ac-remove-subscription-formatters
Revert "[web] Remove SubscriptionFormatters, format subscription dates in the frontend"

GitOrigin-RevId: d4c6bb600f7315c27016a45a8250a61b46de0119
2026-09-17 11:52:51 +00:00
roo hutton c461297b13 Merge pull request #36461 from overleaf/lg-security-sanitize-html-root
[Security upgrade] bump sanitize-html to 2.17.6 in yarn.lock

GitOrigin-RevId: 2a4629fca9d0c95cad7b0e993433ffcdb0f2b99c
2026-09-17 11:52:48 +00:00
Antoine Clausse ba4f024f34 [web] Remove SubscriptionFormatters, format subscription dates in the frontend (#36409)
* [web] Remove SubscriptionFormatters, format subscription dates in the frontend

Replace the server-formatted nextPaymentDueAt/nextPaymentDueDate/
trialEndsAtFormatted fields with the raw periodEnd date and format in
the React dashboard (same strings as before, English + UTC). Also
consolidate hand-rolled trial checks onto isInFreeTrial /
SubscriptionHelper.isInTrial.

* [web] Remove the now-unused dateformat dependency

* [web] Address review: payment-dates unit test, UTC dates in acceptance test

Add a dedicated unit test asserting the exact formatted strings from
formatPaymentDate/formatPaymentDateTime, and use explicit UTC timestamps
in the SubscriptionDashboard acceptance fixtures instead of
local-timezone Date constructors.

GitOrigin-RevId: 16ac4f1098b14ef9f1478d22cf39c1357ead7dea
2026-09-17 11:52:44 +00:00
Lucie GermainandClaude Sonnet 5 d0399bd09f bump nanoid in yarn.lock (#36457)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
GitOrigin-RevId: 5270797e8c3a3037d39e22c8ff83abf0704bba00
2026-09-17 11:52:33 +00:00
Malik Glossop 15bc30b596 Merge pull request #36140 from overleaf/mg-ai-usage-limit-show
Add 'ai-usage-limit-show' event for ai limits

GitOrigin-RevId: 887ecc8b3fea52bcd45cf771ab0d22b8f4539308
2026-09-17 11:52:30 +00:00
Jakob Ackermann 2566455740 [web] mock the changes/users request in EditorProviders (#36435)
GitOrigin-RevId: 5f8f5b9317d089ca52ff17ad1307d58d625709d5
2026-09-17 11:52:22 +00:00
Eric Mc Sween 62e4164269 Merge pull request #36345 from overleaf/em-library-design-review
[web] Library design review fixes

GitOrigin-RevId: dfd1846d275eb975906a2fe8a8c4dee057c51d1f
2026-09-17 11:52:11 +00:00
Antoine Clausse 1daed4efe4 [web] Add cancel-loss-messaging split test (#36363)
* [web] Add cancel-loss-messaging split test

Populate the cancel confirmation step with plan-specific losses when the
cancel-loss-messaging split test is enabled: "Are you sure you want to
cancel?" heading, the subscription end date, and a card listing what the
user loses moving to the Free plan (compile timeout, AI, collaborators,
history), with the lost benefit struck through next to its Free-plan
replacement.

Shown only to non-trial Student/Standard/Pro individual subscribers with
no other retention offer (trial extension or downgrade upsell) — those
keep the existing confirmation.

Copy matches the mockups in the ticket. No new analytics events; the
existing subscription-page-cancel-button-click / subscription-cancelled
funnel is used.

Part of #35765

* [web] Consolidate ActiveSubscription stories, add cancel confirmation story

Replace the 13 fixture-per-story exports with a single Dashboard story
driven by controls (subscription fixture select, flexible licensing,
cancel-loss-messaging variant), using the CSF factories pattern. Add a
CancelConfirmation story that renders the cancel confirmation step
directly so the cancel-loss-messaging variant can be previewed without
clicking through, with a Professional fixture available to see the Pro
loss rows.

* [web] Use withSplitTests for the ActiveSubscription stories

Register cancel-loss-messaging in defaultSplitTestsArgTypes and drive the
stories' variant from the shared withSplitTests decorator (radio control,
SplitTestContext provided from args) instead of hand-rolled meta tags and
SplitTestProvider. Generalize withSplitTests' typing so it composes with
CSF factories (preview.meta) as well as legacy meta objects.

* [web] Fix Storybook indexing of the ActiveSubscription stories

Storybook's static CSF indexer requires preview.meta() to receive an
object literal, so spread withSplitTests' argTypes/decorators into the
meta literal instead of wrapping the whole config in the helper call.

* [web] Compose ActiveSubscription stories from orthogonal controls

- Wrap the stories in #subscription-dashboard-root so the dashboard's
  scoped styles (e.g. the cancel-loss strikethrough) apply in Storybook
- Replace the single fixture dropdown with independent controls (group,
  plan, period, currency, trial, status, pending plan change, add-ons,
  coupons, pending license change), building the subscription from the
  fixtures' base + patches
- Guard impossible mixes with argTypes `if` conditions: individual-only
  controls hide when group is on, and pendingLicenseChange only shows
  for groups

* [web] Add student plan to the ActiveSubscription story controls

The cancel-loss-messaging variant has student-specific rows (AI
Assistant / 10 collaborators), so the plan control needs a student
option. Groups have no student plans, so for group subscriptions the
student choice maps to the Standard group fixture.

* [web] Provide ol-plans in the ActiveSubscription stories

The cancel confirmation's downgrade branch (monthly Standard is
isEligibleForDowngradeUpsell) looks up paid-personal in ol-plans and
spins forever when the meta tag is missing. Provide ol-plans and
ol-groupPlans from the fixtures, matching the test render helper.

GitOrigin-RevId: e8491c8d39ed059e42ea3442e8d70c3ef304b009
2026-09-17 11:52:02 +00:00
roo hutton dac6254831 Merge pull request #36320 from overleaf/rh-cio-split-tests
[web] require admin opt-in for split test assignments sent to customer.io

GitOrigin-RevId: 5a6f5c3726f3b65c2f432ab20720e6f8f5c53eb1
2026-09-17 11:51:56 +00:00
Brian Gough 3e8f2ebe31 update zod safePath validation to match existing behaviour in web (#36357)
* update zod safePath validation to match existing behaviour in web

* test SafePath from web against zod validation

GitOrigin-RevId: a159f5078b5bf75eac4365c97e34e21e716290eb
2026-09-17 11:51:30 +00:00
Lucie GermainandClaude Opus 5 9cf4fa8bf5 [web] gracefully handle failed oauth token exchange for reference managers (#36377)
* [web] gracefully handle failed oauth token exchange for reference managers

Mendeley/ReadCube redirect back to /:ref_provider/oauth/token-exchange with
`error` and `error_description` instead of `code` when a user declines consent.
completeAuth forwarded the whole query string on to third-party-references,
whose strict zod schema (d8c1679bb08) accepts only oauth_token, oauth_verifier
and code, so the exchange failed and the user got a 500.

The same forwarding also sent `state` on every callback, success or failure.
`state` is web's own CSRF token and is never read by third-party-references, so
it tripped the log-only schema on every single token exchange, and would have
broken linking outright under REQ_VALIDATION_MODE=enforce.

Forward only the credentials the exchange needs, and show a translated
notification in the Reference managers section rather than erroring. Validate
ref_provider against the known providers so the provider name can be
interpolated into the message without reflecting a URL segment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [web] fix providerName argument and validate before deriving refProvider

Address Copilot review on #36377:

- The token-exchange failure handler passed `false` positionally into the
  `providerName` slot of _redirectWithLinkingError, so a failed exchange would
  have rendered "your false account". Pass providerName and an explicit false.
- _getRefProviderBackendKey(req) ran before parseReq, so refProvider was derived
  from raw, unvalidated req.params. Parse first and derive from the validated
  params via _toRefProviderBackendKey; _getRefProviderBackendKey now delegates
  to it so the other handlers are unchanged.

The completeAuth tests kept their assertions inside ctx.res.callback without
awaiting the controller, so the callback fired after the test had already
returned and the assertions were never enforced — which is why the suite was
green with the providerName bug in it. Await the controller and assert inline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [web] add logOnly to the completeAuth schema and guard the provider lookup

logOnly returns the raw input on a schema failure, so ref_provider can be an
arbitrary URL segment. Check it against the display-name table rather than
trusting the schema: unknown providers 404 in both validation modes, and
providerName is guaranteed non-null for the rest of the handler.

Also covers the schema half under REQ_VALIDATION_MODE=enforce, so dropping
ref_provider from completeAuthSchema fails a test rather than passing unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: b98eb5414fe8b672f44aab8c3690f7637d434cd2
2026-09-17 11:51:27 +00:00
Rebeka Dekany 6e57b24811 Record a metric for Stripe API request statuses to monitor rate limiting (#35779)
* Record a metric for Stripe API request statuses to monitor rate limiting

* Add on() stub to Stripe mock in StripeClient unit tests

* Record the Stripe rate-limit reason for 429 responses

* Add acceptance test for Stripe rate-limit reason metric

GitOrigin-RevId: 1674b389f5a7fee403f980b6e813a3e2df81063c
2026-09-17 11:51:23 +00:00
Olzhas Askar b4880f691e Merge pull request #36346 from overleaf/oa-fix-counts
[web] Fix counts

GitOrigin-RevId: b1ec87eb19d41c897b205d2ff178ab2b600fb987
2026-09-17 11:51:17 +00:00
Andrew Rumble 66355e4372 Merge pull request #36361 from overleaf/ar-fix-postdoc-validation-errors
[docstore][document-updater] fix POST doc validation errors

GitOrigin-RevId: 9b5fbc667a3b56b30077ea8de9d81c17253db273
2026-09-17 11:51:12 +00:00
Alf EatonandClaude Opus 4.8 98842d4659 Add Draw mode to the symbol palette (#34868)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: 4ea4f97023e65fdb8b018f610f77d7dd107c8e76
2026-09-17 11:51:03 +00:00
Alf EatonandClaude Fable 5 10f10ca95f Only show "Jump to command definition" for right-click on command name (#36191)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitOrigin-RevId: 05811783a327fb641f6c677eaa1efeb8a87668fc
2026-09-17 11:50:57 +00:00
roo hutton 1681343782 Merge pull request #36024 from overleaf/rh-cio-group-support
Enable customer.io in-app messages on group management pages

GitOrigin-RevId: 0c30c4ccf219cdbc4dd48ea6940cc46a4c6964c3
2026-09-17 11:50:54 +00:00
Kristina c628d82f07 [web] rm 30s filter for connection-restored events (#36315)
* rm 30s filter for `connection-restored` events
* rm unnecessary lastOutageRef
* stop sending idle events

GitOrigin-RevId: 6ff228c878496ad25bc8cc4c1acb4da5a88dce15
2026-09-17 11:50:51 +00:00
Jakob Ackermann 975cf90357 [editor-core] gracefully handle any legacy v1 import agent (#36354)
GitOrigin-RevId: 186afa70c03e69b3288af66d85a0a3e431780d14
2026-09-17 11:50:47 +00:00
Mathias Jakobsen f7de42f1d3 Merge pull request #36181 from overleaf/mj-offline-mode-feature
[web] Add offline mode to feature set

GitOrigin-RevId: 93d2ed373af5b46f5ead924c14605775826c46e3
2026-09-17 11:50:44 +00:00
Malik Glossop 42feabe643 Merge pull request #35907 from overleaf/mg-ai-usage-event
Fire a 'ai-usage-limit-reached' event in backend

GitOrigin-RevId: b43babdd2a0146086cee5ebd2bde83f8daaf1bb5
2026-09-17 11:50:38 +00:00
Malik Glossop e567abe71a Merge pull request #36105 from overleaf/mg-focus-floating-menu
Hide floating menu and comment context menu item in focus mode

GitOrigin-RevId: 4cc55f5cebae5e88ea1d1c03a9ac270e43c3073e
2026-09-17 11:50:35 +00:00
Olzhas Askar f513b1aa52 Merge pull request #36302 from overleaf/oa-teardown-trial-ineligible-cta
[web] Teardown trial ineligible cta

GitOrigin-RevId: 4d114943bc2839b29b6eae3a133d6c235f98ffcc
2026-09-17 11:50:24 +00:00
Olzhas Askar 58d76ff1a0 Merge pull request #36247 from overleaf/oa-wider-panel
[web] Wider checkout panel

GitOrigin-RevId: ea9edafa4e11b0b04d5a74c664781c3dd0cc5e87
2026-09-17 11:50:20 +00:00
Maria Florencia Besteiro Gonzalez 6b7a77b4b2 Merge pull request #36239 from overleaf/mfb-show-duplicate-in-visual-bib-editor
Show duplicate entries in visual bib editor

GitOrigin-RevId: 32e4e41a751624a2142c7fd2c5e5de5a5d3905f5
2026-09-17 11:50:14 +00:00
Andrew Rumble d43b9d12ba Merge pull request #36071 from overleaf/ar-node-24.18.1
[monorepo] upgrade node to 24.18.1

GitOrigin-RevId: f6857b443d3e7882567d3cb62fa884dce2368191
2026-09-17 11:50:11 +00:00
roo hutton ba589211ea Merge pull request #36314 from overleaf/revert-36194-rh-cio-split-tests
Revert "[web] send active split test assignments to customer.io"

GitOrigin-RevId: c3acb504be2ef003d6a103568ece3175f787e6d5
2026-09-17 11:50:08 +00:00
roo hutton 6b69ea7dc1 Merge pull request #36194 from overleaf/rh-cio-split-tests
[web] send active split test assignments to customer.io

GitOrigin-RevId: 3036d6442e8419326f4b641482d65bd5114fe3da
2026-09-17 11:49:59 +00:00
Brian Gough df7a9159c2 normalise image counts in compile-with-optimizable-pngs analytics events (#36308)
GitOrigin-RevId: 6049161196fab1b2e4c3d9aa811b97e3e529aca2
2026-09-17 11:49:54 +00:00
Tom Wells 88fb4c48b2 Merge pull request #36290 from overleaf/copilot/update-library-empty-state-illustration
Update library items for better guidance

GitOrigin-RevId: 15f60589f721a8243110dda58ef7b1134e2c5275
2026-09-17 11:49:50 +00:00
Antoine ClausseandMathias Jakobsen 87fd22cefc [web] Fix negative AI quota countdown after reset period elapses (#35690)
* Fix negative AI quota countdown after reset period elapses

Backend (`FeatureUsageRateLimiter`, `TokenUsageRateLimiter`): once the
24h period had lapsed, `remainingUsage`/`remainingTokens` reset to the
full allowance but `resetDate` kept pointing at the old `periodStart +
24h` (i.e. in the past). Advance to `now + 24h` in that case.

Frontend (`NewPlanPaywall`, `ErrorGetPremium`): the "wait X hours and Y
minutes" copy computed `restoreTime - now` without clamping, so a past
`restoreTime` produced negative hours/minutes (users saw "wait -118
hours and -35 minutes"). Clamp to 0.

* Address PR review on quota reset fix

- Keep `resetDate` tied to the stored `periodStart + PERIOD` rather
  than jumping to `now + PERIOD` when the period has lapsed. The
  moving-target variant unnecessarily reinterpreted a stable field
  (existing consumers such as `use-ai-access-gate` check whether the
  reset date is in the past).
- Clamp `remainingUsage` / `remainingTokens` at 0 so a concurrent
  over-usage never leaks a negative count.
- Align the boundary check with `_checkRateLimit` / `checkUsage`
  (inclusive `<=`).
- Cover the period boundary and the clamp in unit tests for both
  `FeatureUsageRateLimiter` and `WorkbenchRateLimiter`.

* Truncate test fixtures to whole seconds

`resetDate` is serialized with `Date.toString()`, which has second
resolution, so fixtures with a millisecond component failed the
boundary assertions.

* Revert code changes; keep tests that document the bug

The fixes masked the symptom (negative countdown) without addressing
the underlying issue, and it is not fully understood yet. Revert the
rate limiters and the writefull paywall components to their original
behaviour, and instead pin the current behaviour in tests:

- `it.fails` tests document that `remainingUsage`/`remainingTokens`
  go negative when usage exceeds the allowance — these flip red when
  the bug is fixed, at which point they should become plain `it`.
- The lapsed-period tests assert (with explanatory comments) that
  `resetDate` keeps pointing at the end of the lapsed period, which is
  the server-side ingredient of the negative-countdown support ticket.

* Reapply quota fixes

Restores the post-review state of the rate limiters (clamp remaining
counts at 0, inclusive period boundary, resetDate tied to periodStart)
and the writefull paywall components (clamp countdown at 0).

Tests intentionally untouched in this commit; the two `it.fails` tests
now need flipping to plain `it` in a follow-up.

* Flip bug-documenting tests to plain assertions

The clamp fixes are reapplied, so the `it.fails` tests now assert the
fixed behaviour directly. Trim the explanatory comments.

* Set resetDate to `Date.now() + PERIOD_IN_MILLISECONDS` instead of the expired/stale date.

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>

* Update lapsed-period tests for the fresh resetDate

`resetDate` is now `Date.now() + PERIOD` once the period has lapsed, so
assert an approximately-fresh date instead of the stale period end.

---------

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: ad39627ab9e5ceccb2a61443b2b7d39c68600594
2026-09-17 11:49:47 +00:00
Brian Gough 6b3d95cea4 add optimisable png count and compile stats to compile-with-optimizable-pngs event (#36279)
* add count of optimisable pngs to HistoryResourceWriter

* add optimisable png count and compile stats to compile-with-optimizable-pngs event

GitOrigin-RevId: 5c528d727473ec2d3082845d8d5b60844f3434f6
2026-09-17 11:49:41 +00:00
Anna Claire Fields b8339c5b4a history-v1: handle ERR_STREAM_PREMATURE_CLOSE in streamZip (#36135)
GitOrigin-RevId: 963b65583da58034afe304206f6e0c66e9186b06
2026-09-17 11:49:33 +00:00
Andrew Rumble d9e7900246 Merge pull request #35553 from overleaf/copilot/fix-split-test-calculator-event-suggestions
Fix split-test calculator event suggestions by scoping event queries and deduping in-flight lookups

GitOrigin-RevId: 2afebbd645218770cb3a8fbd615038df215dd9ed
2026-09-17 11:49:27 +00:00
Mathias Jakobsen 77f208633b Merge pull request #36233 from overleaf/mj-command-palette-events
[web] Add events to command palette usage

GitOrigin-RevId: 1bb40c20f3228027ee4470dc84fa2983bb910cbf
2026-09-17 11:49:19 +00:00
David 3325724a9e Merge pull request #36258 from overleaf/dp-fix-library-badge
Use feature flag config for library badge rather than hard-coding

GitOrigin-RevId: a53bd7c739d2d8e6c5dbd5bda1fbc3a87f87b280
2026-09-17 11:49:12 +00:00
Jakob Ackermann 49969f6e0d [history-v1] adopt strict zod types and extend test coverage (#36089)
GitOrigin-RevId: 7bdffe2c05c7c17be6eb3e4296d8df05d650c53e
2026-09-17 11:49:08 +00:00
roo hutton b886c0b266 Merge pull request #35836 from overleaf/rh-prices-split
[web] Update monthly plan prices for new subscriptions with split test

GitOrigin-RevId: 42d0adaf68647adc93f4c05d5ddeefb356753db2
2026-09-17 11:49:04 +00:00
Kristina 2e1a313ccd [web] resize mentions dropdown styles (#36172)
GitOrigin-RevId: e0f4ff0d51f168569ad54fa896ab25c399278ebb
2026-09-17 11:48:53 +00:00
Kristina c5f3726eda [web] update comment placeholder for mentions (#36167)
GitOrigin-RevId: c702e00004d8dfcf243425503459f558973a657b
2026-09-17 11:48:50 +00:00
Jakob Ackermann 95e3444a6e [editor-core] refine zod schemas for rawTextOperation and rawFileMetadata (#36253)
* [editor-core] gracefully handle no-op TextOperation

* [editor-core] add support for legacy url imports

* [editor-core] reorder rawFileMetadata entries

GitOrigin-RevId: f5c3158def00183d4a60a5b934abd921716e2339
2026-09-17 11:48:47 +00:00
Jakob Ackermann b4937905bb [linked-url-proxy] adopt strict zod types and extend test coverage (#36166)
GitOrigin-RevId: 1edbd4ea3240cc8973d0a7e6488a8b882422a42b
2026-09-17 11:48:43 +00:00
Jakob Ackermann 2e8f6f0d03 [chat] adopt strict zod types (#36102)
GitOrigin-RevId: eb3e245599f5db2d29366ce5d8e0993ec6b20338
2026-09-17 11:48:40 +00:00
Jakob Ackermann 4e79741e78 [latexqc] integrate latexqc closer into build scripts (#36252)
* [monorepo] drop unused --docker-repos build script flag

* [latexqc] integrate latexqc closer into build scripts

- Generate Dockerfile
- Drop now unused --only-jenkins-include-file

* [monorepo] update header for custom Dockerfiles in build scripts

GitOrigin-RevId: 84d0b100e731ae41db132f676e7837e17c17542e
2026-09-17 11:48:37 +00:00
Jakob Ackermann 4e6e98941a [latexqc] adopt strict zod types and extend test coverage (#36165)
GitOrigin-RevId: 339aa5c5b51dc2ab97178e482142f64d7324a57b
2026-09-17 11:48:33 +00:00
Jakob Ackermann a80f9d4803 [github-sync] adopt strict zod types and extend test coverage (#36050)
GitOrigin-RevId: cd4060778455e9bc252588e449d5588852648592
2026-09-17 11:48:27 +00:00
Jakob Ackermann 6ca58edf72 [web] add infrastructure level zod change (#36190)
* [web] add infrastructure level zod change

* [web] set req validation mode for tests to 'enforce'

GitOrigin-RevId: 6dbc8bff7ed9f89045c398df93d84bca589de4e9
2026-09-17 11:48:19 +00:00
Antoine Clausse 86bab9dc99 Merge pull request #35724 from overleaf/ac-wallet-payment-method-analytics
[web] Add Stripe wallet payments (Apple Pay / Google Pay) in analytics and remove conversion to "card"

GitOrigin-RevId: f6d404d35eb108d7ddaf8ca1b6c2164cb5100c9c
2026-09-17 11:48:15 +00:00
Jakob Ackermann 731ed50f78 [clsi] address zod violations (#36199)
* [editor-core] gracefully handle no-op TextOperation

* [web] avoid sending sync to pdf requests without a ?file

* [validation-tools] log deeper nested violations

* [editor-core] fix round trip of EditNoOperation

* [web] use custom error for sync to pdf w/o file, fix tests

* [validation-tools] include request params in log msg for violation

* Revert "[editor-core] gracefully handle no-op TextOperation"

This reverts commit c1d37261d5db72b37dbf39428c54edcfb5423f41.

* [web] gracefully handle rejected sync to pdf from context menu

GitOrigin-RevId: 21de7dc96b23575b0d67b6eb0c68ed9c2f86292b
2026-09-17 11:48:12 +00:00
David 761d4efc1f Merge pull request #36114 from overleaf/dp-merge-library-bibtex-flags
Merge bibtex-visual-editor into overleaf-library feature flags

GitOrigin-RevId: 26f8325d68367d0e33236e9f92ab6dc76f76987d
2026-09-17 11:48:08 +00:00
Davinder Singh a0c923aea8 fix download filename in unable-to-sync modal error message (#36113)
GitOrigin-RevId: fdc891962b24a096a1d54a73fcb0d7af6d504729
2026-09-17 11:48:02 +00:00
Davinder SinghandMathias Jakobsen b04bdd2944 [WEB] Only show affected lines plus one above/below in the diff viewer (#36052)
* adding trimmed diff view with context lines in unable-to-sync modal

* addressing co pilot comments

* Update services/web/frontend/js/features/ide-react/components/modals/diff-viewer.tsx

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>

---------

Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: de5a01048b0609288acf01196ff0a0f069e8bed3
2026-09-17 11:47:59 +00:00
Davinder Singh 98b79d9532 [WEB] Simplify "New project" dropdown (#34290)
* Simplify the new project dropdown

* Simplify the New project dropdown

* removing artefact of rebase

GitOrigin-RevId: e7e4dc48c10f2297abcd7c5b489ff2308bf9e18b
2026-09-17 11:47:56 +00:00
Jakob Ackermann deec57e54e [chat] gracefully handle projects without threads/messages when cloning (#36101)
GitOrigin-RevId: 0623a4b801f469b7d55412f2be8aac782cb6ab19
2026-09-17 11:47:52 +00:00
Jimmy Domagala-Tang 560b7a209a Transition optimized images to project-level toggle (#36093)
* feat: transitioning optimized images from a compile mode to a project level toggle. We still persist it for the proect and between users, and ensure that draft mode does not interact with it (silently disabled while draft mode is on)

* feat: cleaning up unused var, tests, and locales

* feat: updating copy

GitOrigin-RevId: 8010edfab164980f14125739e481d8b512f6b3df
2026-09-17 11:47:49 +00:00
Olzhas Askar 49747e61c9 Merge pull request #36163 from overleaf/oa-cookie-banner
[web] Rephrase the cookie banner

GitOrigin-RevId: 229cf5ead3d6a9f7c8fa1b8dda45508b06b23c42
2026-09-17 11:47:40 +00:00
Brian GoughandCopilot Autofix powered by AI a9a36237aa Use logger.exit in expire-deleted user/project scripts (#36169)
* add logger.exit method to avoid logs being lost by process.exit

* use logger.exit in expire deleted projects and users scripts

* improve comment for EXIT_DELAY

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* convert to async

* await logger.exit in expire deleted projects and users scripts

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 020d5cc2755871e5343130029706fe3b1c8e15ee
2026-09-17 11:47:35 +00:00
Mathias Jakobsen 43ea352733 Merge pull request #36185 from overleaf/mj-cmd-plt-fixes
[web] Tweak command palette

GitOrigin-RevId: f69cf1f24dadecd81b4c563d289cbffc5ef4788f
2026-09-17 11:47:29 +00:00
Jakob Ackermann 4ca028cb4c [validation-tools] eventName: accept upper case letters (#36188)
GitOrigin-RevId: 979ec5c744d5304232d447ff5fff076ee081721f
2026-09-17 11:47:25 +00:00
Kristina f179e89c0c [web] add analytics for @mentions (#36144)
GitOrigin-RevId: 9fb1447166eb5ee82867466abc9af2207c886f8b
2026-09-17 11:47:22 +00:00
Kristina 20a4144cf9 [web] add events for out of sync modals (#36132)
GitOrigin-RevId: f9673defab0d06d8e814bf2d43cbb7e19eb249a7
2026-09-17 11:47:19 +00:00
Jakob Ackermann 591b9f5d39 [clsi] adopt strict zod types and extend test coverage (#36090)
GitOrigin-RevId: ce11933ccc10ac74baf5c78cec194b5532a357a2
2026-09-17 11:47:13 +00:00
Alf EatonandClaude Opus 4.8 f0b8e4af11 Add "Jump to command definition" to the editor context menu (#35116)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: 5fcaef2004495a5e71bc083cf8fdc2bffce579d5
2026-09-17 11:47:09 +00:00
Miguel Serrano d20052bf3e [web] Read subscription.features on Group Management rendering (#36168)
* [web] Read subscription.features on Group Management rendering

Fixes 'ai-toggling' not properly read on Group Management rendering, causing the Feature Controls option not being displayed in certain scenarios.

GitOrigin-RevId: 4465fea15279d650d9d231e85072f1d85f2358be
2026-09-17 11:47:01 +00:00
Mathias Jakobsen 588979097d Merge pull request #36139 from overleaf/jd-command-palette-ui-tweaks
UI tweaks to the command palette & adding a "No results" state

GitOrigin-RevId: 4e3c7aa1498ef03f3a1d9948c3f3df8fa38670c3
2026-09-17 11:46:57 +00:00
Mathias Jakobsen 4e088c54fd Merge pull request #36092 from overleaf/mj-command-palette-pre-release
[web] Pre-labs-release changes for command palette

GitOrigin-RevId: 578cf56f1ef51996ce928466edf50e360fa73b42
2026-09-17 11:46:54 +00:00
Tom Wells e3a75e2396 Merge pull request #36125 from overleaf/tw-bibtex-project-drawer
Add side drawer to the project-level Visual BibTeX Editor

GitOrigin-RevId: b9792dd1162e459511f70e2ac325ccdfc7d5b26f
2026-09-17 11:46:46 +00:00
Jakob Ackermann 0776357d1a [document-updater] address zod violations (#36147)
* [editor-core] address zod violation for group_id

* [monorepo] use discriminated union in zod schemas where possible

* [editor-core] v1_source_doc_id is a number

GitOrigin-RevId: 38f591b07cca4e22954bfe30bf855545a6af675d
2026-09-17 11:46:42 +00:00
Tim Down 7895541382 Merge pull request #36141 from overleaf/oa-teardown-editor-upgrade-button-relocation
[web] Teardown `editor-upgrade-button-relocation`

GitOrigin-RevId: b6c53eef240ac2386907e0d0a942c674cfc9133b
2026-09-17 11:46:39 +00:00
Tim Down e8fca0a7aa Merge pull request #36129 from overleaf/td-onboarding-split-test-teardown
Remove onboarding-plan-comparison from code

GitOrigin-RevId: 7fc63e777c0924c3844f144f54623cbc30278afa
2026-09-17 11:46:33 +00:00
Jakob Ackermann b1dece55c8 [docstore] adopt strict zod types and extend test coverage (#36076)
GitOrigin-RevId: b9f1b965dbdb763ce541dabbe89300ce6d044217
2026-09-17 11:46:14 +00:00
Jakob Ackermann 0a08c78987 [clsi-cache] adopt strict zod types and extend test coverage (#36078)
GitOrigin-RevId: 92c01afb3bfb22ab7c14447dda510ca6361c9879
2026-09-17 11:46:11 +00:00
Jakob Ackermann 4e80098424 [document-updater] adopt strict zod types and extend test coverage (#36077)
* [document-updater] adopt strict zod types and extend test coverage

* [saas-e2e] add test for project restore with binary and linked file

GitOrigin-RevId: a8e45bd1e46b6ac8a293da24c8b8ad7b236fd9bd
2026-09-17 11:46:05 +00:00
Jakob Ackermann a57d8449ef [third-party-datastore] adopt strict zod types and extend test coverage (#36082)
GitOrigin-RevId: 30b8dbcc72844ab421d73991701234f8264e8569
2026-09-17 11:46:02 +00:00
Jakob Ackermann f48132a70e [notifications] adopt strict zod types and extend test coverage (#36049)
* [notifications] adopt strict zod types and extend test coverage

* [notifications] validate expires date via schema

GitOrigin-RevId: 25dee0578ec443447e4c38a50a7e6c8b44831fe7
2026-09-17 11:45:58 +00:00
Jakob Ackermann eb36edac6d [project-history] report a resync that is still pending after a flush (#36122)
GitOrigin-RevId: ccd4390048c09f732531d2e02a40336fce88a952
2026-09-17 11:45:55 +00:00
Jakob Ackermann 9be1b4edb4 [monorepo] refine prettier setup (#36142)
- Trigger monorepo-check on Jenkins init scripts and fix violations
- Ignore all the json-dashboards in terraform folders

GitOrigin-RevId: f27b46d6fb81f5ac731f56b065951bede7d1a9e4
2026-09-17 11:45:52 +00:00
Jakob Ackermann d62f8fb716 [filestore] adopt strict zod types and extend test coverage (#36112)
GitOrigin-RevId: a761477f2b412133da2d71748233c712c5728fae
2026-09-17 11:45:41 +00:00
Maria Florencia Besteiro Gonzalez 139ebafdca Merge pull request #36003 from overleaf/mfb-truncate-long-fields-in-cards
Truncate long title/author fields in bibtex entry cards

GitOrigin-RevId: 588798c4307958fcd60fce258d46419fbb29dab5
2026-09-17 11:45:32 +00:00
Anna Claire Fields a63fd554c3 Replace MOCHA_GREP with TEST_NAME_PATTERN (#34404)
GitOrigin-RevId: b1b2d1be3d0725f930d83ef035836482ffb72dcd
2026-09-17 11:45:29 +00:00
Jakob Ackermann daf4a0e412 [third-party-references] adopt strict zod types and extend test coverage (#36083)
GitOrigin-RevId: d8c1679bb088feb2c0827d94a71e2e006ab71ba1
2026-09-17 11:45:23 +00:00
Jakob Ackermann 52e5fb3498 [project-history] adopt strict zod types and extend test coverage (#36087)
GitOrigin-RevId: ce1396e87926fc8816afa424c9db2a052c7da5e6
2026-09-17 11:45:20 +00:00
Jakob Ackermann 6a0f70bb23 [idp] adopt strict zod types and extend test coverage (#36038)
GitOrigin-RevId: d9887f877b44e03b501072f738387e4646a1431a
2026-09-17 11:45:16 +00:00
Alf Eaton 6b349ee3be Add start_url to web manifest (#35015)
GitOrigin-RevId: 81c723df6e6134d9bce32fd3a0dd632769a595e1
2026-09-17 11:45:13 +00:00
Alf Eaton 2010622ae3 Handle missing state field in line wrapping indentation extension (#35442)
GitOrigin-RevId: 7cc03042c42fbc4afe14438cd9347da7682815c6
2026-09-17 11:45:07 +00:00
Alf Eaton fd52dca3eb Fix the CodeMirror dev tools button (#35757)
GitOrigin-RevId: af14fd8a637b9b71f1940c71c32bf57371bb2267
2026-09-17 11:45:04 +00:00
Alf Eaton c302bbaaaa Ignore more failure states of startViewTransition (#35951)
GitOrigin-RevId: e5aac40612550ea42abdc4d93e80045018670137
2026-09-17 11:44:55 +00:00
Maria Florencia Besteiro Gonzalez 84aa1900f1 Merge pull request #36086 from overleaf/mfb-hide-reference-format
Hide reference format setting in References tab

GitOrigin-RevId: 4df81638c788e4b2908dd0ae8966bf0f9989ee9e
2026-09-17 11:44:44 +00:00
Tom Wells 90931a6c41 Merge pull request #36007 from overleaf/tw-library-validation-side-drawer
Show reference validation errors in the Library preview drawer

GitOrigin-RevId: 68a6a8879642cd32466240a6d25956d1fe8b2c78
2026-09-17 11:44:36 +00:00
Mathias Jakobsen d1691345c5 Merge pull request #36074 from overleaf/mj-reload-after-failed-catch-up
[web] Reload after failed intermittent-connection catchup

GitOrigin-RevId: c9a6315ecb6ebe957ad85c05ebc84bb8165881ab
2026-09-17 11:44:33 +00:00
Jimmy Domagala-TangandBrian Gough 35ba44f22b Adding project PNG optimization check to compile analytics (#35925)
* feat: adding in check for if the project could have had a png optimized as part of the compile-result-backend event

* feat: updating using new compile metrics to filter for only eligible project compiles

* move projectHasUnconvertedPngs to stats

* add e2e test for projectHasUnconvertedPngs

* add unit tests for compile-with-optimizable-pngs analytics event

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 97e9d184420c34fc1d3e9f6cfe63f6b916f0202f
2026-09-17 11:44:29 +00:00
Jakob Ackermann 405b675740 [freegeoip] adopt strict zod types and extend test coverage (#36039)
GitOrigin-RevId: 889ea0fa67cfdf2ad2469936e0fd737169868ff1
2026-09-17 11:44:26 +00:00
Brian Gough fd87cf1ddd Merge pull request #35966 from overleaf/bg-return-optimised-image-stats-and-timings
return image stats and timings in compile response

GitOrigin-RevId: ec700b8cb6680bfd8f28ad0f1f2bbdb0479398bc
2026-09-17 11:44:22 +00:00
Kristina b53bfbe8a4 [web] analytics for flaky connections (#35916)
* emit connection-restored events
* emit post-offline sync success/failure events

GitOrigin-RevId: 3fce5f328d726aeb5edf39f14239ef6e5bda2f99
2026-09-17 11:44:19 +00:00
Simon Gardner f4eb0102ea [web] Hide "Request edit access" button while the network is stalled (#36069)
GitOrigin-RevId: 5bbfe2484a50fdca055e55f6e900346af1e4c1b2
2026-09-17 11:44:16 +00:00
Jakob Ackermann 2f69caef63 [editor-core] allow empty ranges on rawAddCommentOperation (#36073)
GitOrigin-RevId: 8674a2b458770439925047aab9a129242eb984d4
2026-09-17 11:44:12 +00:00
Tom Wells 074c9a882a Merge pull request #36045 from overleaf/dp-dark-theme-textareas
Apply themed light/dark styling to textarea form controls

GitOrigin-RevId: 304b73e983d0e20fa5d691a513ca6b16b8ef5d54
2026-09-17 11:44:09 +00:00
Copilot 4234c9af29 Merge pull request #36023 from overleaf/copilot/update-simple-search-description
Update Simple Search description to mention Library when overleaf-library is enabled

GitOrigin-RevId: f694d38b830bc3699c5b05a25dd0edc3b58dcd89
2026-09-17 11:44:03 +00:00
Tom Wells 1a56eeaf00 Merge pull request #36051 from overleaf/tw-library-bulk-delete
Add bulk delete and restore actions to the Library

GitOrigin-RevId: 72f56eb78c44f496ce6edeb9fdddbd57074125a1
2026-09-17 11:44:00 +00:00
Kristina 68dafe202c [web] remove beta icon from notifications settings & survey links (#34989)
GitOrigin-RevId: bdd19ccf117be8aa051c02dfa50665fdbc874da2
2026-09-17 11:43:56 +00:00
Copilot cb3fbaba84 [web] update notification settings copy for mentions (#35953)
GitOrigin-RevId: cc86f0931f6740f1d509118ee558719bc3559816
2026-09-17 11:43:53 +00:00
Davinder Singh 95b373ed52 Merge pull request #36035 from overleaf/ds-unable-sync-modal-error
[WEB] Adding error message in the unable to sync modal

GitOrigin-RevId: b9d2cdc0db8c87e8f90b77d812dbb6d0c6695622
2026-09-17 11:43:49 +00:00
Malik Glossop 302b6f28d6 Merge pull request #35989 from overleaf/worktree-mg-ai-limit-countdown-sub-minute
Round AI usage limit countdown up so it never renders empty

GitOrigin-RevId: f277de79c4962f534603497201a8f4a43fb01125
2026-09-17 11:43:44 +00:00
Malik Glossop 6c63002cd1 Merge pull request #36041 from overleaf/mg-offline-focus-mode
Add offline indicator for focus mode

GitOrigin-RevId: 3764577500d67b139c31a356f4f5e82888c44209
2026-09-17 11:43:40 +00:00
Chris McIsaac 8526dec62d Add tab as a method to accept mentions autocomplete (#35933)
GitOrigin-RevId: 7cbb459294175817a63d0942f882fd29b3c813a4
2026-09-17 11:43:37 +00:00
Mathias Jakobsen 7a5b1da06d Merge pull request #36034 from overleaf/mj-free-user-intermittent
[web] differentiate free and paid users for intermittent connection timeouts

GitOrigin-RevId: 9c1847ff3dae7290b7c8d1093ef6dc05ae3b76aa
2026-09-17 11:43:34 +00:00
Mathias Jakobsen 774f2c8f06 Merge pull request #36028 from overleaf/mj-fatal-op-long
[web] Stop arming FATAL_OP_TIMEOUT timer

GitOrigin-RevId: f0b98d6fb5d2312d3b5137ebba0775a88cb01b72
2026-09-17 11:43:30 +00:00
Mathias Jakobsen 0c5e55a8be Merge pull request #36002 from overleaf/mj-save-backup-early
[web] Start saving offline backups after 10 seconds

GitOrigin-RevId: 4101598f46b6f71dc8bf10a31cbec9110216ed7a
2026-09-17 11:43:27 +00:00
Davinder Singh ffeb3ef5de Merge pull request #36000 from overleaf/ds-offline-mode-disable-buttons-detached-mode
[WEB] Read network stall state from compile context instead of hook directly for log error buttons in offline mode

GitOrigin-RevId: ca898f17960784690a27cea3f234568e4532c870
2026-09-17 11:43:24 +00:00
Davinder Singh 36d0a5c227 Merge pull request #35855 from overleaf/ds-unable-to-sync-modal-improved
[WEB] Adding improved version of the unable to sync modal

GitOrigin-RevId: 1cfe2b3a2e8417028339efddb18385a5224e50c3
2026-09-17 11:43:20 +00:00
Jakob Ackermann 469f5629d2 [saas-e2e] run all the spec files (#35993)
* [saas-e2e] run all the spec files

* [saas-e2e] add addComment helper

* [saas-e2e] add replyToComment helper

* [saas-e2e] check latexqc report for errors

* [saas-e2e] fail fast when the login request failed

* [monorepo] upsert the minimal v1 db entries in all environments

* [monorepo] serve the CDN assets from nginx to avoid session mix

* [monorepo] avoid loading fonts from Cypress UI via web

* [monorepo] effectively disable login + loadEditor rate-limit in dev-env

* [saas-e2e] project-list: generate unique project name on re-run

* [saas-e2e] add debug logs for latexqc

GitOrigin-RevId: 778003759414a3c993578062a631d083ecb1d2e6
2026-09-17 11:43:07 +00:00
Jakob Ackermann 05885f9943 [templates] adopt strict zod types and extend test coverage (#36030)
GitOrigin-RevId: d99894d42fac538f7acf799a05c57fd8630ce22b
2026-09-17 11:43:01 +00:00
Jakob Ackermann f6bb5ce1bd [real-time] adopt strict zod types and extend test coverage (#36031)
* [editor-core] extend coverage of zod schemas

* [real-time] adopt strict zod types and extend test coverage

* [project-history] make type check happy

* [web] make type check happy

GitOrigin-RevId: e1a65b9b3560b31ad2af817067a4b549865a5bb1
2026-09-17 11:42:50 +00:00
Jakob Ackermann 81df324bcc [monorepo] prepare for zod everywhere epic (#36027)
GitOrigin-RevId: f082ae7fc7857fa72b2c1823645050bd4f6df305
2026-09-17 11:42:46 +00:00
Jessica Lawshe bff27fc3cc Merge pull request #35922 from overleaf/jel-domain-notification
[web] Update message on group domains table when no verification created yet

GitOrigin-RevId: 0692f6a976f2ef11cd0b39577f058d171583a89e
2026-09-17 11:42:33 +00:00
Jessica Lawshe 91331a3aff Merge pull request #35482 from overleaf/jel-domain-capture-not-enabled
[web] Update domain capture header and not enabled paragraph

GitOrigin-RevId: 0827d0be44592c71347ece0fb43cd100e8f082b1
2026-09-17 11:42:30 +00:00
Jessica Lawshe 6211c1c49c Merge pull request #35101 from overleaf/jel-cleanup-domain-captured-by-group-getUserEmailsDataWithDomainCaptureEnabled
[web] Remove rollout flag check in `getUserEmailsDataWithDomainCaptureEnabled`

GitOrigin-RevId: 0c58b5752217cea8233e95b22ba7b236fd9d25ab
2026-09-17 11:42:24 +00:00
Jessica Lawshe 16a5c45b5f Merge pull request #35861 from overleaf/jel-email-notifications-domain-capture
[web] Use name in greeting for email notifications for domain capture

GitOrigin-RevId: 56e46bc6de031656e4e8c0a9332f6b94aa517506
2026-09-17 11:42:20 +00:00
Jessica Lawshe 12e02cc69a Merge pull request #35830 from overleaf/jel-domain-verification-text
[web] Update content in domain verification notification

GitOrigin-RevId: eaa58e4c7b53d55ef51a5d2fa318473205e82937
2026-09-17 11:42:17 +00:00
Jessica Lawshe a47fec5a38 Merge pull request #35068 from overleaf/jel-cleanup-domain-captured-by-group-getInstitutionViaDomain
[web] Remove `domain-captured-by-group` rollout flag for `getInstitutionViaDomain`

GitOrigin-RevId: 54e63f0e13ddda97e0554ff3e142bbf4502c3b61
2026-09-17 11:42:11 +00:00
Tom Wells 284c9d9d14 Merge pull request #35999 from overleaf/tw-library-bibtex-bulk-download
Add BibTeX bulk download to Library

GitOrigin-RevId: 30e83a41ae71b397b5388fb7ebdae7b99dd062a8
2026-09-17 11:42:00 +00:00
David f4cdfaa607 Merge pull request #36037 from overleaf/dp-library-trash-navigation-2
Add trash navigation with Projects/References tabs- #36001

GitOrigin-RevId: 675ad078f117c5acd8744024c751209c615a7c2e
2026-09-17 11:41:52 +00:00
Kate Crichton 281b5fe25c Merge pull request #34835 from overleaf/kc-email-member-limit-reached
[web] Email group admin when group reaches member limit

GitOrigin-RevId: 6b7b12a2c309e7d9b4314e8db51969d73864b7ed
2026-09-17 11:41:49 +00:00
David ea989a4788 Merge pull request #35906 from overleaf/dk-library-references-trashed
Soft-delete library references (move to trash)

GitOrigin-RevId: 908a4d41724300c70d56e850cdb519b7a274ad5a
2026-09-17 11:41:40 +00:00
Jakob Ackermann 03cbfbe90f [web] fix open redirect via utm tags (#36019)
GitOrigin-RevId: 1304c1578adf1d3248abcaa01902467422e8d8f6
2026-09-17 11:41:34 +00:00
David 7ff96b31ad Merge pull request #35414 from overleaf/dp-library-in-admin
[web] Add read-only library view to the admin user page

GitOrigin-RevId: 892a40a7ef1096570412c68dac79c09f5b70ea53
2026-09-17 11:41:31 +00:00
David 0fe77a8068 Merge pull request #35964 from overleaf/dp-dark-library-autocomplete
Add dark theme support to the library auto-complete dropdown

GitOrigin-RevId: be975d7c379e689ed61706bf027e488b7177eb73
2026-09-17 11:41:28 +00:00
Kristina 20027166fb [web] recover tracked changes after going offline (#35961)
GitOrigin-RevId: 8d34eb879de91a3b348fc1230eb3ce4d532194e0
2026-09-17 11:41:17 +00:00
Kristina 1f97dbc789 [web] do not show the cannot sync modal if still offline (#35939)
GitOrigin-RevId: 20c6f138e05fb8459b27f8ea9b376c21c7820799
2026-09-17 11:41:13 +00:00
Jimmy Domagala-Tang ba76fda37b Cleanup AiErrorAssistant todos post plans changes (#35067)
* feat: cleaning up ai quota leftovers

fix: removing more straggler invos

* fix: featureUpdater still need analyticsId in the user projection

* feat: updating cypress tests to also check for fair usage text

GitOrigin-RevId: 09c1167106c144084c111beea56867f6aa4a0906
2026-09-17 11:41:10 +00:00
Jakob Ackermann 275d381656 [monorepo] remove some dead code (#35901)
* [history-v1] remove unused duplicate file from swagger migration

* [web] drop unused ?scribtex_path

* [github] update path to security middleware in history-v1

* [dependency-management] update .jenkinsIncludeFile

GitOrigin-RevId: 8c59d0d1e3e9108a8498e33e4768992ecd342f00
2026-09-17 11:41:07 +00:00
Simon Gardner c40952813c [web] fix: Always unlink group SSO when a linked user leaves a group (#35895)
GitOrigin-RevId: f3a9cc5fec2b8950826aef02bdfe7ed72607f56b
2026-09-17 11:41:03 +00:00
Jakob Ackermann be9ab57d18 [web] speed up CI: bin-pack module acceptance tests (#35965)
GitOrigin-RevId: 92a33b4531d541c676018b3152c7abf74edd2eec
2026-09-17 11:40:52 +00:00
ilkin-overleaf fc7ad54116 Merge pull request #35488 from overleaf/ii-workspaces-admin-feature-toggle
[web] Admin toggle for shared workspace feature

GitOrigin-RevId: d6da0abd92f309cee55299a5f25677cf10c47a7c
2026-09-17 11:40:49 +00:00
Davinder Singh 1b95b31049 [WEB] Disable share and upgrade buttons when offline (#35923)
* disabling buttons in offline mode

* setting href as undefined when button is disabled

* Fix disabled state for premium button by removing gradient override

* Add disabled class to premium button for correct disabled styling

GitOrigin-RevId: b7dd59e05d8c818d58399e31d961e93bb0f188a9
2026-09-17 11:40:41 +00:00
Davinder Singh 0458293e9e Merge pull request #35941 from overleaf/ds-suggest-fix-disable-offline-mode
[WEB] Disabling buttons in error logs in offline mode

GitOrigin-RevId: b19daaf1cf98ecf626568690ff677a491b8dad63
2026-09-17 11:40:38 +00:00
Miguel Serrano 52350353cb [Security upgrade] bump postcss to 8.5.20 (#35853)
GitOrigin-RevId: b423aa572cb48a2a0adb8ea45b36c2a7923bfd1a
2026-09-17 11:40:34 +00:00
Copybot 3386101cb9 Merge pull request #1499 from ayaka-notes:fix/invite-token-secret-ignored
GitOrigin-RevId: 0b92bf6c5ee724880650f6db5d08cdd22074dd77
2026-09-17 11:40:26 +00:00
Maria Florencia Besteiro Gonzalez c0cb6710ba Merge pull request #35948 from overleaf/dk-git-bridge-health-check-race
[git-bridge] Fix health check race on shared probe file

GitOrigin-RevId: 1463880cb4770a730e3e164bf9a5ff89366434b5
2026-09-17 11:40:21 +00:00
Maria Florencia Besteiro Gonzalez 803b41bfdd Merge pull request #35804 from overleaf/lg-jetty-netty-security-upgrade
[Security upgrade] bump jetty and netty in git-bridge

GitOrigin-RevId: 650f017732f67be90048e41e314cd6646f182d08
2026-09-17 11:40:17 +00:00
Jakob Ackermann e947f3c4aa [web] improve logging for failed requests on MockApis (#35958)
- Prepare for zod usage in mocks
- Fix logging of the mock name

GitOrigin-RevId: 3c4d2981010992c293a9ece45935ce13cadd8dde
2026-09-17 11:40:09 +00:00
Jakob Ackermann e8070def26 [document-updater] fix typo in test stub (#35950)
tid does not exist anywhere else in document-updater at
sha 3d76f4b9bf when
the typo was added. The comment id was read from .t only.
Doc-snapshots are copied the ranges literally without any
normalizing of nested structures.

GitOrigin-RevId: f76fbc930fdf02d9367603b5b1d6414de8d9f559
2026-09-17 11:40:01 +00:00
Lucie Germain 8a37955106 [Security upgrade] bump @opentelemetry/sdk-node family in libraries/metrics
GitOrigin-RevId: f462289e88c3c021fa32561a96344d76939da4a0
2026-09-17 11:39:57 +00:00
Lucie Germain c3eac2dd35 Bump brace-expansion to 2.1.2 via lockfile (#35814)
GitOrigin-RevId: a6378e95920c321f69c313289dc64c9629a3d7bb
2026-09-17 11:39:54 +00:00
David b5fe972acb Merge pull request #35940 from overleaf/dp-visual-markdown-beta-icon
Add beta badge for markdown files in the file tree

GitOrigin-RevId: 304f426de9232a6d2a5fe768ae7e8b8f48fcd4d8
2026-09-17 11:39:48 +00:00
David b77187404b Merge pull request #35898 from overleaf/dp-visual-markdown-editor-setting
[web] Track markdown editor mode separately and default to visual

GitOrigin-RevId: 7195189c683618b3e0c735f91193fb662078b558
2026-09-17 11:39:45 +00:00
ilkin-overleaf f0b9ab322f Merge pull request #35915 from overleaf/ii-project-sharing-admin-page-hide
[web] Gate sharing permissions behind a new feature flag

GitOrigin-RevId: 8c6657aca11911e9c2ddcba000a501307184bcaf
2026-09-17 11:39:41 +00:00
Tom Wells 3588215c54 Add Abstract tab to the reference preview drawer (#35902)
* Add Abstract tab to the reference preview drawer

* Correct active border colour

* Swap mobile navigation items around

* Fix co-pilot suggestions

* fix: stop auto complete grabbing focus on mount

* fix: focus issues

* Fix failing test

GitOrigin-RevId: a9e808cf40ab0626ed9e2cf8dc732a224aab31d3
2026-09-17 11:39:36 +00:00
Jakob Ackermann 8fa62d2864 [document-updater] drop support for tc in briefly (#35864)
GitOrigin-RevId: 00d04d911249b81abc5eb4f9d9dfde4c7f2316b3
2026-09-17 11:39:22 +00:00
Jakob Ackermann 7a0a9c3ce5 [document-updater] implement HTTP endpoints for comments (#35833)
GitOrigin-RevId: 06b4f222a55e21b3def432d6e84d9ab4bf65b64d
2026-09-17 11:39:19 +00:00
Jakob Ackermann 8be65f1f93 [document-updater] add support for flush/restore of tc with history-ot (#35823)
GitOrigin-RevId: a79b0ca97c930ca2e2f3ffeea20defbc6973468c
2026-09-17 11:39:15 +00:00
Jakob Ackermann 00a2765d01 [document-updater] add track-changes support to setDoc (#35820)
GitOrigin-RevId: 5d39dd1945e710ec3f7068830fb2476e59f0f2a5
2026-09-17 11:39:12 +00:00
Jakob Ackermann a77a4782cc [history-v1] add setContent endpoint (#35799)
* [history-v1] add setContent endpoint

* [history-v1] refactor setContent to return a change+v

* [history-v1] make typescript happy

GitOrigin-RevId: e2cb0281367c01aa9cd842f250f2016d591ec0fc
2026-09-17 11:39:09 +00:00
Kristina b624adf885 [web] trigger post-offline sync result treatments (#35854)
* trigger `UnableToSyncModal` on refresh
* trigger `UnableToSyncModal` without refresh
* show success toast when offline edits sync
* always show out of sync modal as the final backup

GitOrigin-RevId: 3ed300fef9e6d2ff81e17868309f80ffa40e0576
2026-09-17 11:39:03 +00:00
Domagoj KriskovicandCopilot Autofix powered by AI 1ef6360fb9 comment typo
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: abb0bfda774dc278ae3256ee5a461240f5511461
2026-09-17 11:38:59 +00:00
Domagoj Kriskovic a71c8c24d9 Add migration for libraryReferences trashedAt indexes
GitOrigin-RevId: 4a850c11560df3ae3053fbc66048b083e750b8d3
2026-09-17 11:38:56 +00:00
Lucie Germain e814176bba Bump protobufjs resolution to 7.6.5 (covers GHSA-j3f2-48v5-ccww) (#35807)
GitOrigin-RevId: 76373e5c7afcad21fc44c058e5eaaa3633e992a4
2026-09-17 11:38:48 +00:00
Jakob Ackermann 5425202247 [e2e] cut down flakeyness (#35896)
* [saas-e2e] conversion: wait for initial compile before looking at PDF

* [eslint-plugin] s/before/beforeWithReRunOnTestRetry/ in e2e tests

* [server-ce] remove filestore migration scripts and E2E tests

* [server-ce] test: trim down upgrading tests to 5.x -> latest

GitOrigin-RevId: 2426ec4a4f3d67a5e0e7850ea84a942ae4619f94
2026-09-17 11:38:44 +00:00
Brian Gough 1b519d59a5 Merge pull request #35806 from overleaf/bg-fix-retry-failed-backups-script
fix extraction of project ids in retry_failed_backups.sh

GitOrigin-RevId: b1d7af8d9de84b0b28d14097f54a93893b93dd46
2026-09-17 11:38:41 +00:00
Brian Gough a46d987a51 Merge pull request #35695 from overleaf/copilot/close-graceful-shutdown-issue-35694
Handle graceful shutdown in project and user deletion batch expiry

GitOrigin-RevId: 43182850ad8e55b480ea0073c393222366d7e00a
2026-09-17 11:38:25 +00:00
Brian Gough 793e9e6796 Merge pull request #35819 from overleaf/lg-fast-uri-security-upgrade
[Security upgrade] bump fast-uri to 3.1.4 in root workspace

GitOrigin-RevId: b4d92b73162c848b1289d543763970af5ce89a50
2026-09-17 11:38:22 +00:00
Brian Gough 21ec38d0fa Merge pull request #35813 from overleaf/lg-svgo-security-upgrade
[Security upgrade] bump svgo to 4.0.2 in root workspace

GitOrigin-RevId: 962a8c79ccf50119094de970be32599353064482
2026-09-17 11:38:18 +00:00
Maria Florencia Besteiro Gonzalez 297b9bf443 Merge pull request #35860 from overleaf/tw-35479-navigate-prev-next-drawer
Add previous/next buttons to side drawer

GitOrigin-RevId: 720228c8686bcbf3871d7ea338fd76a3ae6d667c
2026-09-17 11:38:15 +00:00
Andrew Rumble e72fc09295 Merge pull request #35852 from overleaf/lg-fix-cloud-function-pug-lockfile-drift
Regenerate cloud-function lockfiles after pnp redux

GitOrigin-RevId: dd278f0912c0ff97c71282b82a7bf7f0b4b4fd98
2026-09-17 11:38:07 +00:00
David 691452580b Merge pull request #35735 from overleaf/dp-mobile-projects-dropdown
Update mobile projects dropdown to merge top and submenu views

GitOrigin-RevId: 33650de2018b2536101edb7071526b65393e90d2
2026-09-17 11:38:01 +00:00
Chris Dryden f81a7b2ff2 Merge pull request #35534 from overleaf/cd-share-sidenav-bar-with-library
[web] Share the dashboard sidebar with the library page

GitOrigin-RevId: 5595746fef6698bee6a35ee7fc8be15743e2c620
2026-09-17 11:37:57 +00:00
Kristina 6af1d79cd6 [web] reset all css variables impacting the comment field (#35681)
GitOrigin-RevId: b66f612e186434208a658d64ccc6346c37f23182
2026-09-17 11:37:54 +00:00
Lucie Germain ebf72a6a93 Bump js-yaml to 4.3.0 via lockfile (#35811)
GitOrigin-RevId: 1d938fecb596dad40ac523daee0aef6cc1129dc0
2026-09-17 11:37:48 +00:00
Tom Wells 4430252b77 Add reference preview/edit drawer to the library (#35763)
* Add reference preview/edit drawer to the library, with mobile bottom-sheet layout

* Fix stylelint comment spacing and update library Cypress spec for the drawer flow

* Fix desktop drawer focus/escape handling and autosave race in the library

Replace the desktop preview panel's hand-rolled Escape listener with the
shared FocusTrap component (already used for the mobile sheet), fixing
missing focus trap/restore and reusing existing infrastructure instead of
duplicating it. Track whether the kebab actions menu is open via a ref so
Escape closes just the menu first, not the whole panel.

Guard the autosave-on-blur form against redundant saves (skip when nothing
changed since the last save) and out-of-order responses (serialize saves
through a queue so a later edit can't be clobbered by a stale response).

Remove dead commented-out JSX for unimplemented tabs/actions.

* Reinstate library reference analytics events for the new preview drawer

* Surface an error notification when deleting a reference from the drawer fails

* Fix stuck kebab-menu state and blocked outside clicks in the reference drawer

* Share error-message extraction and add a test for the delete-failure path

* Fix formatting

* Fix the translation

GitOrigin-RevId: a0082377c6e3f6712fba80c485b9cde4d431c90f
2026-09-17 11:37:40 +00:00
45b4237fd5 fix: always render @[userId] mention tokens regardless of split test (#35564)
* Initial plan

* fix: always render @[userId] mention tokens regardless of split test

Readers outside the comment-mentions/email-notifications split tests
were seeing raw @[<userId>] tokens in chat messages and review-panel
comments, leaking internal Mongo ObjectIds.

Fix: always parse and render @[userId] tokens using parseMentions() +
MentionBadge regardless of split-test membership. MentionBadge already
safely falls back to "@Unknown" when a userId can't be resolved to a
project member, so no ID is ever emitted.

- message-content.tsx: remove mentionsFeatureEnabled gate; always call
  renderMentionedContent() instead of falling back to raw <Linkify>
- review-panel-message.tsx: remove mentionsFeatureEnabled gate; always
  pass displayMentions to ExpandableContent

The mentions-input autocomplete (typing new @mentions) remains gated
behind the split test as before — this fix is rendering-only.

Co-authored-by: jdomaga <25135258+jdomaga@users.noreply.github.com>

* feat: always allow for mentions to display

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jdomaga <25135258+jdomaga@users.noreply.github.com>
Co-authored-by: Jimmy Domagala-Tang <jimmy.domagala-tang@overleaf.com>
GitOrigin-RevId: c7cedbfe6966bb2ebce58a5f4e03b1627198dab7
2026-09-17 11:37:32 +00:00
Jimmy Domagala-Tang 4840bfc23e feat: allow for split tests in dev panel to be filtered to make it easier to test locally (#35653)
GitOrigin-RevId: 0e3600c290877136464765cafccd98294e12d9bb
2026-09-17 11:37:26 +00:00
Malik Glossop 7e69642c01 Merge pull request #35731 from overleaf/mg-historyot-delete-at-end
Fix History OT base length when a tracked delete is in the document

GitOrigin-RevId: 414349aedd498c2635bb3067388dcc7291e32ee6
2026-09-17 11:37:22 +00:00
Andrew Rumble e4bbf74476 Merge pull request #35812 from overleaf/lg-shell-quote-security-upgrade
[Security upgrade] bump shell-quote to 1.10.0 in root workspace

GitOrigin-RevId: 935aa24b352d5f5705609c83f5ea8997df7be425
2026-09-17 11:37:19 +00:00
Lucie Germain 2320967c1b Bump immutable to 4.3.9 via lockfile (#35810)
GitOrigin-RevId: be6149988462a97860074bc8766fe37b0c0ad19e
2026-09-17 11:37:13 +00:00
Rebeka Dekany 1d8c0fb23a Suppress navbar and footer on account security pages (#35744)
* Suppress skip-to-content link on pages without a navbar

* Add main content as target area for the skip link on the library page

* Suppress navbar, footer and skip-to-content on account security pages

GitOrigin-RevId: a85fac5667d84c6f60577cd0ea469cd215554cc4
2026-09-17 11:37:10 +00:00
Rebeka DekanyandCopilot Autofix powered by AI 0b574d2ccd Rename user-facing Papers reference manager to ReadCube (#35701)
* Rename user-facing Papers reference manager text to ReadCube

* Rename Papers component and files name to Readcube

* Update locales from Papers to ReadCube

* Rename ID name from papers to readcube

* Fix a chinese locale to use ReadCube

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: ee89e3f5da5bcf9987c0ee3e558b9ae1dab9372c
2026-09-17 11:37:06 +00:00
Domagoj Kriskovic 0a913bba5e simplify health check comments
GitOrigin-RevId: cc31a6fd98a2c21b8163912e25542e5584d1794f
2026-09-17 11:37:03 +00:00
Domagoj Kriskovic 655034fb8e [git-bridge] Make liveness detect a dead service
GitOrigin-RevId: 266f6db044eec8b362b07bdfed45f892dd272413
2026-09-17 11:36:59 +00:00
Davinder Singh e0a2af4bf6 Merge pull request #35725 from overleaf/ds-unable-to-sync-modal
[WEB] adds unable-to-sync modal for intermittent connection improvements

GitOrigin-RevId: 4cd85cf742cb3f36cc0c97f83bdeab09bb6f7c53
2026-09-17 11:36:54 +00:00
Maria Florencia Besteiro Gonzalez 02d36687cd Merge pull request #35739 from overleaf/mfb-bring-main-bibliography-to-top-level
Bring main bibliography file to the top level settings

GitOrigin-RevId: 4c4d4927067995d947c6d0656425760ce56f9435
2026-09-17 11:36:48 +00:00
Kristina 6f8a1be171 [web] emit mention notifications (#35611)
GitOrigin-RevId: 4852c217a8bf7f1fe71696604acbb0f7434d3f2a
2026-09-17 11:36:42 +00:00
Kristina 4e2840e247 [web] remove mentions from chat message editing (#35761)
GitOrigin-RevId: 6cd27490485196b82a9c4d9ffa19ba5d8e10922d
2026-09-17 11:36:38 +00:00
Jakob Ackermann 94cad14caf [chat] migrate to express with strict zod schema (#35742)
GitOrigin-RevId: edffacde0c069768def849f45364d519d9f76f1e
2026-09-17 11:36:35 +00:00
Rebeka Dekany 5ccbd1eb5c [web] Suppress skip-to-content link on pages without a navbar (#35670)
* Suppress skip-to-content link on pages without a navbar

* Add main content as target area for the skip link on the library page

GitOrigin-RevId: a47d7bbc283b648e47db93648d8fcae6ae12ea5a
2026-09-17 11:36:29 +00:00
Maria Florencia Besteiro Gonzalez 4132731016 Merge pull request #35680 from overleaf/mfb-migrate-bibtex-parser-to-core-feature
Migrate BibTeX parser to core frontend feature

GitOrigin-RevId: 274f64d31c1027c5eea9ec569e54dece57307220
2026-09-17 11:36:26 +00:00
Antoine Clausse f9a2a0ad6c Merge pull request #35733 from overleaf/lg-axios-security-upgrade
[Security upgrade] bump axios from 1.16.0 to 1.18.1 in services/web

GitOrigin-RevId: 9c413da00f137873b964b956364b742d366869eb
2026-09-17 11:36:20 +00:00
Antoine ClausseandClaude Fable 5 59b4a67c2d [web] Remove redundant annualDividedByTwelve from pricing tables (#35507)
* [web] Add snapshot test for all annualDividedByTwelve values

Captures the 85 stored values into a JSON fixture and asserts the
effective value (stored field, or PlansHelper.roundedTwelfth(annual)
once the field is derived at runtime) matches. Verification harness for
the annualDividedByTwelve refactor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [web] Compute annualDividedByTwelve at runtime instead of hardcoding it

* [web] Update annualDividedByTwelve snapshot for the rounding rule

The runtime derivation reproduces 84 of the 85 historical values exactly.
The one difference is PEN assistant, which was historically stored
unrounded (350 / 12 = 29.1667) and now follows the round-up-to-0.05 rule
(29.20). The assistant add-on is unavailable, so this is not user-visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove `localizedAddOnsPricing` from test

* Add more sanity tests

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitOrigin-RevId: e60e8b3e90360817d4be3732243d242dc0cbb69e
2026-09-17 11:36:17 +00:00
Mathias Jakobsen 33e0a5e1c2 [web] Remove toasts and NEW indicators from conversions (#35591)
* [web] Remove toasts for document conversions

* [web] Remove NEW indicators from document conversions

GitOrigin-RevId: 608b344d9be596ecb3cf4573ab1152f78d7d9d43
2026-09-17 11:36:11 +00:00
Mathias Jakobsen 17363fcc59 [web] Recover offline doc backup (#35647)
GitOrigin-RevId: 999ecc534821ab6f1bdc1bc4f7757316dab1658d
2026-09-17 11:36:07 +00:00
Andrew Rumble 64b6b28f69 Merge pull request #35800 from overleaf/ar-convert-project-notifications-script-to-esm
[document-updater] convert project notifications script to ESM

GitOrigin-RevId: df7a617e1e77ee31e4129b5e55d497005ac0acd4
2026-09-17 11:36:04 +00:00
Jakob Ackermann a1d1145df1 [web] fix settings override in acceptance tests (#35798)
GitOrigin-RevId: ee64407a47ef2dac28ca82bd46659c4735e02b6f
2026-09-17 11:36:01 +00:00
Brian Gough 8f5f86bce1 Merge pull request #35257 from overleaf/bg-png2pdf-premium
limit png2pdf to priority compiles

GitOrigin-RevId: a2b394efe6056ccbc6963d922f3e8b385645167b
2026-09-17 11:35:57 +00:00
Malik Glossop 73068f10ee Merge pull request #35571 from overleaf/mg-increase-max-string-size
Bump backend OT/history processing limit (MAX_STRING_LENGTH) to 3MB

GitOrigin-RevId: ed64fe5522c45b1ace221a01daa2ae70939816f9
2026-09-17 11:35:54 +00:00
Andrew Rumble cca0f9b518 Merge pull request #35788 from overleaf/dk-nonblocking-libreferences
Add 'nonblocking' tag to library reference migration

GitOrigin-RevId: 6a39eff80b41b1b0fa07d6cf78baf959b74346f1
2026-09-17 11:35:41 +00:00
Jakob Ackermann c6d1cea177 [monorepo] make format_fix happy (#35660)
GitOrigin-RevId: 6f070a87c5240317960c7227dfa8f868e781d2a0
2026-09-17 11:35:30 +00:00
Andrew Rumble da3adace36 Merge pull request #34850 from overleaf/acf-ar-yarn-pnp-redux
[monorepo] yarn pnp redux

GitOrigin-RevId: fb04cab879b04ead2665b5e985a44053ceea33f3
2026-09-17 11:35:26 +00:00
Miguel Serrano 44f053129f [web] Update @aws-sdk/client-ses to v2 (CE/SP only) (#35531)
This new version is compatible with the current nodemailer version.

GitOrigin-RevId: 8244fa8fc42edbc6aa7b702ec7975e9eee7f9879
2026-09-17 11:35:20 +00:00
Brian Gough 6c756eab73 Validate id types in LinkedFiles getSourceProject (#35748)
* Validate id types in LinkedFiles getSourceProject

Require source_project_id and v1_source_doc_id to be primitives before
querying Mongo, rejecting anything else with BadDataError.

* Validate refreshLinkedFile request params and body

Route refreshLinkedFile through parseReq with a schema requiring
project_id and file_id to be ObjectIds and typing the request body.

* Use well-formed ObjectIds in LinkedFilesController tests

Give the shared test setup valid ObjectId project_id/parent_folder_id
so parseReq passes and the controller logic is actually exercised, and
fail the tests if next is called unexpectedly.

* Use ObjectId file ids in v1 linked-file acceptance fixtures

The refresh route now validates file_id as an ObjectId, so the v1
not-imported fixtures need well-formed ids to reach the refuse-to-refresh
path (409) instead of failing id validation (404).

GitOrigin-RevId: d78b3664e825c6bb25ab2e363e5094b9212db307
2026-09-17 11:35:02 +00:00
Brian Gough 158d8a5839 Merge pull request #35646 from overleaf/jpa-fix-input-selection
[web] fix race condition of input selection and user edit

GitOrigin-RevId: 2180fba1f77da2d13f216d33d16f49ee2dd58442
2026-09-17 11:34:59 +00:00
Thomas 3fb3baf03a Merge pull request #34605 from overleaf/tm-request-access-redesign
[WEB] Request edit access in the new share modal

GitOrigin-RevId: d01cf4d038d5ce07c34deac0714b0cdad02136be
2026-09-17 11:34:55 +00:00
Thomas fc40c1f8a4 Merge pull request #35636 from overleaf/tm-fix-export-error-forwarding
[web] Forward v1 export errors to the publish modal instead of a 500

GitOrigin-RevId: 7cf69c4d6a954cd311ac64fff175156b300afabb
2026-09-17 11:34:52 +00:00
Brian Gough 41a1b639f6 Merge pull request #35734 from overleaf/bg-issue35720
add zod validation to ExportsController and TemplatesController

GitOrigin-RevId: 8f283999665d730d491160aabe75b2643b8c2178
2026-09-17 11:34:46 +00:00
Tom WellsandCopilot Autofix powered by AI c88544c290 Autogenerate citation key from author/year (#35577)
* Autogenerate citation key from author/year

* Fix abort handling and error fallback in citation key suggestions

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Always fall back to a generated key on suggestion failure

* Make citation-key suggestion location-aware (project vs library), fix validation timing and duplicate-key edge cases, and consolidate the resolution logic

* Update citation key helper/error copy and hide the autogenerate hint while its error is showing

* Refactor comments: significantly shorten and remove unnecessary

* Fix error logic: if empty show error on save. If invalid entry, show onBlur

* Error refactoring

GitOrigin-RevId: 0cd919c9224731f1d5694204e8cdb03288ade04b
2026-09-17 11:34:40 +00:00
Kristina 4d1e9677e1 [web] set up types for mentions (#35609)
GitOrigin-RevId: 70ebe11659072a7df09be916d7a01cfd87b73c81
2026-09-17 11:34:37 +00:00
Domagoj Kriskovic d8de261370 Add request validation to HistoryController endpoints (#35730)
* Add schemas for file restoration and label management in HistoryController

* Add request schemas for proxying to history API and resyncing project history

* prevent transforming version:null into version:0 with coerce

* move jsDoc comments above the handler

* check for undefined resyncProjectStructureOnly

* Add tests for restoreFileFromV2 and revertFile

* Set default value for resyncProjectStructureOnly in resyncProjectHistorySchema

GitOrigin-RevId: 7b8b8a4c3834b8c8a83f93052ab1f59e86c529a6
2026-09-17 11:34:33 +00:00
Jakob Ackermann c821cc695c [real-time] delete PubSubRace test (#35746)
With the removal of per-doc rooms, there is nothing to race anymore.
The next lower timeout is 0, i.e. useless check.

GitOrigin-RevId: effb6cc0525f5fd785c7dce8df4e279de1f1b13f
2026-09-17 11:34:30 +00:00
Malik Glossop 03ddb0de69 Merge pull request #35669 from overleaf/mg-fix-comment-scroll
Fix scroll for adding comments in editor

GitOrigin-RevId: fa9ecb07008bfca1d2e233b4da97817cd77f9105
2026-09-17 11:34:27 +00:00
Alf Eaton b4d2d11c7b Add search_project tool to the AI assistant (#31879)
GitOrigin-RevId: 257b59bea43bd8ee3ff81b2f5439455c4a6ea403
2026-09-17 11:34:23 +00:00
Alf Eaton 5c8a4f3210 Add read_project_outline tool to the AI assistant (#31875)
GitOrigin-RevId: 37e7d9bebdc6291a0ea7d89b73494d24f5cd5908
2026-09-17 11:34:20 +00:00
Jakob Ackermann aab08b4c28 [document-updater] remove doc lock from per-doc operations (#35696)
GitOrigin-RevId: c45c1dbf11049c0fab8a7d788006a698ede0d510
2026-09-17 11:34:14 +00:00
Jakob Ackermann e714810632 [real-time] remove per-doc rooms (#35691)
* [real-time] remove applied-ops pub/sub

* [real-time] remove per-doc rooms

GitOrigin-RevId: ec3b76b37a029df15bf20d6f2832fb7c089c0042
2026-09-17 11:34:11 +00:00
Lucie Germain 2dee2b31d9 [Security upgrade] bump adm-zip to 0.6.0 in tools/saas-e2e and server-ce/test (#35608)
Fixes GHSA-xcpc-8h2w-3j85 / CVE-2026-39244 (high): a crafted ZIP archive
declaring a huge uncompressed size could force an unbounded Buffer.alloc
and OOM the process.

adm-zip 0.6.0 ships built-in TypeScript types, so @types/adm-zip is
dropped from both workspaces' devDependencies to avoid duplicate/
conflicting type declarations.

GitOrigin-RevId: dffc1c38a10f42d09c799ae768f1ffcfc0e09328
2026-09-17 11:34:07 +00:00
Lucie Germain a22f4ad1c5 [Security upgrade] bump websocket-driver to 0.7.5 (#35559)
Fixes GHSA-xv26-6w52-cph6 (critical): message corruption via abuse of
protocol length headers in the WebSocket frame parser.

GitOrigin-RevId: 678d85f1132e55cfeeef266a65c2deb869fe41ae
2026-09-17 11:33:54 +00:00
Jakob Ackermann bc9d25d385 [monorepo] clean up the per-project pub/sub migration (#35686)
GitOrigin-RevId: 4bbb97d1d2750f21751ef54e206e3e720b88615a
2026-09-17 11:33:51 +00:00
Jessica Lawshe 10adc3c477 Merge pull request #35477 from overleaf/jel-domains-table-icons
[web] Remove icon position CSS in domains table

GitOrigin-RevId: ca4ed97528577866a7e45538d0820baabfbe8f0b
2026-09-17 11:33:47 +00:00
Jessica Lawshe dde570ad9b Merge pull request #35065 from overleaf/jel-cleanup-domain-captured-by-group
[web] Remove `domain-captured-by-group` rollout flag for user settings

GitOrigin-RevId: f7fe0e66649e4cd4b94124f8f2eefa150758203a
2026-09-17 11:33:44 +00:00
Jessica Lawshe 66671f9bf2 Merge pull request #34936 from overleaf/jel-scroll-to-notification
[web] Scroll to new notification added above domains table

GitOrigin-RevId: f62851398de3a54ef07c0e125c65a29dd84af6f8
2026-09-17 11:33:41 +00:00
f950d8ffde Replace the bibtex entries table with a card layout (#35565)
* Replace the bibtex entries table with a card layout

Render each reference as a card (citation key, title, author, year and an
error indicator) instead of a table row. Drop @tanstack/react-table, which was
the last consumer of the dependency, and keep @tanstack/react-virtual for
virtualization.

- Add a compact card variant (via a `compact` prop) for the bibtex editor:
  tighter spacing, no border-radius, and cards separated by a single collapsing
  border rather than a gap.
- Add hover and green selected/selected+hover states.
- Move the select-all checkbox out of the scrolling list into the editor
  toolbar so it stays fixed while the list scrolls.
- Lay out title/author/year with nested flex so author and year wrap together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* In progress dark mode for review

* Slight design changes & updated dark mode

* Small tweaks to dark mode secondary text, and light mode border

* Truncate long authors, also use .summarize() on authors

* Refactor comments: reduce verbose, remove unnecessary

* Fix empty line

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Tom Wells <wellsthomas@hotmail.co.uk>
GitOrigin-RevId: feaa6718484eefb9e9efb44b99f456910697cb17
2026-09-17 11:33:35 +00:00
Rebeka Dekany 607e0b0343 [web] Add paymentMethod segment to payment-page-form events (#35583)
* Add paymentMethod segment to `payment-page-form-submit` event

* Add paymentMethod segment to `payment-page-form-success` event

GitOrigin-RevId: 7ad1d18d3f4ed38d39f4336790581604ddae749e
2026-09-17 11:33:31 +00:00
Rebeka Dekany 94ea8f75a0 Remove Stripe customer.created webhook handler (#35547)
GitOrigin-RevId: 9dd1bf5b41189a986ec6dc1c838973dd4cb051b6
2026-09-17 11:33:28 +00:00
Andrew Rumble 15fb07da65 Merge pull request #35570 from overleaf/ar-remove-security-pins
[monorepo] remove pins responsible for security alerts

GitOrigin-RevId: f009618c05accc95b20a4bf4e32ebf26cfc6853b
2026-09-17 11:33:20 +00:00
Tom Wells 87d538dc71 Merge pull request #35642 from overleaf/tw-new-tag-button-modal
Fix New tag modal

GitOrigin-RevId: 6c0f227db27b87bc1affdf932de16c60d3571413
2026-09-17 11:33:14 +00:00
Kristina 5cde47d3d5 [web] revert adding mentions to chat (#35592)
GitOrigin-RevId: 9765993318e0467a97b7ced695fab55df29146a2
2026-09-17 11:33:08 +00:00
Kristina 74d7a649c8 [web] fix comment input in visual mode (#35665)
GitOrigin-RevId: 7aced47d90a856fe9ebcb07f8148c4987b3583c8
2026-09-17 11:33:05 +00:00
Jakob Ackermann 3565f51a84 [monorepo] prepare for per-project pub/sub for applied ops (#35663)
GitOrigin-RevId: 11d45addf366c3449cad432272ed03d4c8dae49d
2026-09-17 11:33:01 +00:00
Malik Glossop 1b2da240d5 Merge pull request #35558 from overleaf/mg-floating-menu-blank-line
Calculate floating menu position using line block

GitOrigin-RevId: aaf86d7cbdbddf6e77cb58563b16708888d03d7b
2026-09-17 11:32:55 +00:00
Simon Gardner a4c55af2e7 [web] Migrate publisher-managers and institution-managers pages to TypeScript (#35630)
GitOrigin-RevId: 61c14988a24b7b9050a8383e101f354253d09fc5
2026-09-17 11:32:20 +00:00
Jakob Ackermann e1358c4507 [document-updater] remove unused script (#35614)
GitOrigin-RevId: 95717884ce4b5207ef7289108eb64a84d4ed5dd4
2026-09-17 11:32:11 +00:00
Miguel Serrano b0015ef166 [CE/SP] Hotfix 6.2.2 (#35546)
* [CE/SP] Hotfix 6.2.2

GitOrigin-RevId: 5ee457a611fe7d6f73d8ce4bcf55378bd8c7fc69
2026-09-17 11:32:08 +00:00
ilkin-overleaf f27b19f86a Merge pull request #35607 from overleaf/ii-share-modal-invited-people-count
Exclude project owner from "X people invited" count in share modal

GitOrigin-RevId: 7f914a09401691b6e6f7299637ee695ed37d05c0
2026-09-17 11:32:05 +00:00
David 873bf5923d Merge pull request #35420 from overleaf/lo-stop-silently-dropping-large-bib-files
Lo stop silently dropping large bib files

GitOrigin-RevId: cba919227eb6d5c1f8100c6afba3076bd62a5797
2026-09-17 11:32:01 +00:00
Jakob Ackermann 324f011d80 [document-updater] add script for flushing/discarding stuck docs (#35603)
GitOrigin-RevId: 099e1e0a38515b35decbdc608acd75f59de0ec6f
2026-09-17 11:31:56 +00:00
Jakob Ackermann 68352783b0 [monorepo] clean up per-project queue migration (#35573)
GitOrigin-RevId: 988df4cdc7d929efc921e7e99d803c6ce6e52c57
2026-09-17 11:31:52 +00:00
Mathias Jakobsen d8f167dec2 Merge pull request #35293 from overleaf/mj-zod-history-ot
[real-time] Allow history-ot operations in real-time

GitOrigin-RevId: 4b1b9e4c1a214edf49c5eface46a4d309ba41b0d
2026-09-17 11:31:49 +00:00
Brian Gough 9d57689112 Merge pull request #35549 from overleaf/copilot/fix-prettier-caching-issue
Work around Prettier stale-cache parse-error behavior in root format commands

GitOrigin-RevId: 1669047b3f52c15fcbff8bff41fcb1ca1257418d
2026-09-17 11:31:43 +00:00
Miguel Serrano d07cc5a927 [web] Add features.aiToggling to Subscriptions (#35494)
* [web] Add `features.aiToggling` to Subscriptions

This new flag controls whether the subscription admin
can toggle AI Features for their group.

When the feature is unchecked, group admins only
see a dialog explaining AI Features are enabled/disabled,
but never the controls.

GitOrigin-RevId: 94132f5569102b91113bf15075e66c02aaed277f
2026-09-17 11:31:39 +00:00
Mathias Jakobsen b526135232 Merge pull request #35569 from overleaf/mj-network-stalled-on-disconnect
[web] Check websocket status in stall test

GitOrigin-RevId: c62f2d3641d444d8bbe2eed84a6ed6a0ad241b2a
2026-09-17 11:31:34 +00:00
Mathias Jakobsen bbe1c2126a Merge pull request #35579 from overleaf/mj-theme-race
[web] Avoid race condition in theme selection

GitOrigin-RevId: 2252e316d267ddd1e9c5f8e09a1c0a2c4e35b706
2026-09-17 11:31:30 +00:00
Miguel Serrano 4f9ada1ecb [web] Add env variable to control GceMetadataLogLevelChecker (#35273)
The feature should be disabled in Server Pro, since the container is expected not to make any external request.

GitOrigin-RevId: 38b59de61e498c58a5fbacf48d2244481d099187
2026-09-17 11:31:27 +00:00
Lucie GermainandClaude Fable 5 975cd99871 [github-sync] Surface "project too large" errors instead of hanging (#35183)
* [github-sync] Surface "project too large" errors instead of hanging

A GitHub sync of a project whose total editable text exceeds github-sync's
7MB request body limit would hang silently: the modal stayed on "Importing
and merging changes in GitHub" and then closed with no error.

Root cause was a chain of three issues:

1. github-sync's ErrorMiddleware did not recognise the body-parser
   PayloadTooLargeError, so it fell through to a generic 500.

2. web's _reportError rethrew any non-4xx error instead of responding, and
   several github-sync controller handlers were not wrapped in expressify.
   On Express 4 the rethrow became an unhandled rejection and no HTTP
   response was ever sent, so the browser request hung until the socket died.

3. the frontend read error.data.error without a null check, which threw a
   render-time TypeError when the fetch failed at the network level (as it
   did once the hung request was aborted).

Fixes:
- github-sync: add ProjectTooLargeError and map the body-parser 413 to it.
- web: _reportError always responds (500 for unexpected errors) and every
  controller handler is expressified as a backstop.
- frontend: null-safe error.data?.error, a friendly message for the
  too-large case, and a generic message for server errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [github-sync] Refine project-too-large error copy per content guidance

Apply the content-design system's error-message guidance to the new
github_project_too_large_error string: use a contraction, drop the
redundant "reduce the size of your project" restatement and the formal
"Please", keeping the problem/reason/next-step structure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [github-sync] Use plain string matcher in project-too-large CT test

Address review feedback: prefer a plain string matcher with exact:false
over a regex, per the repo's testing guidance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [github-sync] Link to the file-limits docs from the too-large error

Add a "Learn about GitHub sync file limits" link below the project-too-large
error, pointing to the GitHub synchronization file-limits documentation, so
users can find out what the limits are and how to stay under them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [github-sync] Use rel="noopener noreferrer" on the file-limits docs link

The eslint rule for external docs.overleaf.com links requires
rel="noopener noreferrer" (not just noreferrer) with target="_blank" to
prevent reverse-tabnabbing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitOrigin-RevId: 16f701a85fb4ae049c208e03e328ad80049e8579
2026-09-17 11:31:13 +00:00
Domagoj Kriskovic 7f74e5b1d2 Add support for auxiliary MongoDB sessions in startSession function
GitOrigin-RevId: 3ea0acd9c78e837f8aaf7c1e5ac06f7befb80805
2026-09-17 11:31:05 +00:00
Domagoj Kriskovic 5df372186e Add auxiliary tag to library reference migration
GitOrigin-RevId: ecb9bf2101dc5e633ec702041b02698d9c912317
2026-09-17 11:31:02 +00:00
Tom Wells e20d9f3ce8 Group tags into submenu (#35528)
GitOrigin-RevId: e6689ad3b73189444989f5f401df59f7ff0bbcd5
2026-09-17 11:30:54 +00:00
Olzhas Askar d29eefaa64 Merge pull request #35478 from overleaf/oa-paywall-ctas
[web] Paywall CTAs

GitOrigin-RevId: 26703fe008926db67473a43ef64addca0e23accd
2026-09-17 11:30:50 +00:00
Kristina 76e004b3e2 [web] save doc to sessionStorage while editing offline (#35519)
* save doc to session storage when offline
* scope offline doc backup key to user id
* clear offline doc backups on logout
* handle anonymous users
* include trackChanges

GitOrigin-RevId: 3fe6a1523cb10cf76a1fcf4ee46790f6673bd84f
2026-09-17 11:30:42 +00:00
Liam O'Brien bbac293414 using mongo transaction to make sync atomatic
GitOrigin-RevId: 4261e14ab8cf6afd418f11492395038ecb4e3be6
2026-09-17 11:30:26 +00:00
Liam O'Brien adc89fbf67 frontend handling zotero deletions
GitOrigin-RevId: 7e824ce8d86dd4f8185ba9d6c35ccb8f003e303e
2026-09-17 11:30:18 +00:00
Liam O'Brien ae28403f9b New index for querying items in a users synced collection
GitOrigin-RevId: aa81ca46cdd238a46a8002d1c991da787c1b9b0f
2026-09-17 11:29:59 +00:00
Jakob Ackermann da808cf653 [k8s] document-updater: advance PENDING_UPDATES_MIGRATION_PHASE=3 (#35551)
* [k8s] document-updater: advance PENDING_UPDATES_MIGRATION_PHASE=3

* [document-updater] add logging to check script

GitOrigin-RevId: e74de189e7bb2036fb0fbfe9e8ba44f8f87af110
2026-09-17 11:29:48 +00:00
Antoine Clausse 8f501c5eb7 [web] Fix lint: remove unused import (#35567)
GitOrigin-RevId: 77a6c89183768324058317c66e29f8dc31c9e277
2026-09-17 11:29:45 +00:00
Antoine ClausseandClaude Opus 4.8 a4da7a935c [web] Remove redundant monthlyTimesTwelve from pricing tables (#35506)
* [web] Remove redundant monthlyTimesTwelve from pricing tables

monthlyTimesTwelve was always exactly monthly * 12 in every pricing row,
and the value was only ever used to render the "billed monthly for a year"
strikethrough price in the compile-timeout paywall.

- Drop the stored field from the pricing tables
- Send raw monthly/annual numbers in ol-standardPlanPricing instead of
  pre-formatted strings
- Format the prices (and derive the strikethrough as monthly * 12) in the
  paywall modal via the frontend currency helper

No user-facing change: the displayed prices are identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Remove `monthlyTimesTwelve` from script

* Remove comments

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: b637208540b3b96180b1a3cc4417b09944c5d158
2026-09-17 11:29:42 +00:00
Antoine ClausseandClaude Opus 4.8 bf50bf56b4 [web] Widen auth-aux container to fit page headers on one line (#35513)
* [web] Widen auth-aux container to fit page headers on one line

Bump .auth-aux-container max-width from 320px to 400px, matching
.login-register-container. Fixes the "Keep your account safe" header
wrapping to two lines on primaryEmailCheck, and keeps the auth pages
(reconfirm, passwordReset, setPassword) consistent with login/register.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add x-padding to `.auth-aux-container`

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: 3256447bd7f6a1b13fbe09d8d359f037c0e50ac2
2026-09-17 11:29:38 +00:00
Antoine Clausse 82f92933a2 [web] Remove some unused code related to AI assist (#35533)
* Remove unused `ol-addonPrices`

* Remove unused `showAiAssistAnnualDiscount`

* Remove unused `buyAddOnUrl` and related code

GitOrigin-RevId: d4f47d8c6cd8e2b304ad5c0a2c78cbef28a6cb97
2026-09-17 11:29:35 +00:00
Jakob Ackermann 04c9e583a3 [real-time] fix gradual draining of connected clients (#35542)
* [real-time] add regression tests for gradual draining

* [real-time] fix gradual draining of connected clients

GitOrigin-RevId: d9985d2ec72176b567c17f066551cd4f205b0219
2026-09-17 11:29:29 +00:00
Jimmy Domagala-TangandKristina 7e550a9475 Chat and Review-panel mentions (#34564)
* feat: changing chat to use cm6 editor instead to allow for mentions display and autocomplete

* feat: changing review panel comment, edit, and reply to also use cm6 editor to enable mentions

* feat: updating tests, and moving from jsdom to cypress tests as jsdom cant handle cm6

* feat: centralizing mentions component and applying to both chat and review-panel

* feat: centralizing mentions input, and using changesUsers in chat mentions

* fixing dark mode styles of chat

* feat: updating mentions dropdown style and adding custom icon generation for mentions cm6 autocomplete tooltip

* feat: making mentions look like links, and fixing border radius on tooltip

* fix: naming and using correct color util

* feat: fixing hover on highlight

* update scroll bar dance in review panel

when a system had persistent scroll bars, we would run into issues calculating the height of the review panel

Co-authored-by: Kristina <7614497+khjrtbrg@users.noreply.github.com>

---------

Co-authored-by: Kristina <7614497+khjrtbrg@users.noreply.github.com>
GitOrigin-RevId: 96441ad55f9614427506edfecf3d87bae82d1607
2026-09-17 11:29:26 +00:00
Brian Gough 9ff7e66b99 Merge pull request #35504 from overleaf/bg-png2pdf-on-slow-images-only
run png2pdf on slow images only

GitOrigin-RevId: 2d0ecd35bfcfc308773502c28eeb5a722055a5a9
2026-09-17 11:29:12 +00:00
Brian Gough aad50452af Merge pull request #35333 from overleaf/bg-png2pdf-minimum-size-threshold
enforce a minimum size threshold on png2pdf conversions

GitOrigin-RevId: fa558afb5455ff5d0373e3d50e57f7207a923fcb
2026-09-17 11:29:09 +00:00
Mathias Jakobsen c3fa2bf304 Merge pull request #35543 from overleaf/mj-survey-removal
[web] Remove tabs and focus mode surveys

GitOrigin-RevId: 6ab783b6db0089a61b9620ba86fa1df36e30b79a
2026-09-17 11:29:05 +00:00
roo hutton 90ab683992 Merge pull request #35516 from overleaf/rh-cio-commons-ai
[web] Send Commons AI attribute to customer.io

GitOrigin-RevId: de010cc34f4e1a854a730ec39212845e1c05d4b1
2026-09-17 11:28:57 +00:00
Jakob Ackermann 8270641b42 [web] optimize project lookup for tpds operations (#35466)
* [web] store ObjectIds in addUserToProject acceptance test helper

* [web] optimize project lookup for tpds operations

GitOrigin-RevId: 03db9fc450556072f177d56f640fcc6114b295d4
2026-09-17 11:28:44 +00:00
Kristina dfd816801d [web] add disconnected state to offline indicator (#35378)
GitOrigin-RevId: 692dc16cee738a8b05353717044a4063c3dd8e8a
2026-09-17 11:28:30 +00:00
Mathias Jakobsen e562fc97f1 Merge pull request #35393 from overleaf/ds-adding-paused-banner
[WEB] Add "Editing Paused" banner when we lock the editor due to network stall

GitOrigin-RevId: 6e8d984f03afb6a29ed900cd342a8a8a655e20d0
2026-09-17 11:28:27 +00:00
Mathias Jakobsen 6f06df61a6 Merge pull request #35457 from overleaf/mj-more-disabled-buttons
[web] Disable compilation buttons during network stall

GitOrigin-RevId: c94a77fea7171c6e9ff4fa9f30e9475474900845
2026-09-17 11:28:24 +00:00
Mathias Jakobsen f86334750e Merge pull request #35465 from overleaf/mj-download-file-button
[web] Add download file button during network stall

GitOrigin-RevId: f85c69c5ce5bc2a0c28c3b5205f108f983fe4c8b
2026-09-17 11:28:20 +00:00
John Lees-Miller b263fd2144 Merge pull request #35464 from overleaf/jlm-recurly-stripe-axis-c-review
Add Stripe migration features for stage 7 testing (part 1)

GitOrigin-RevId: 2750aea7cf611da8e53a2748a11601d617d260e0
2026-09-17 11:28:09 +00:00
Eric Mc Sween 9b79ccea5b Merge pull request #35435 from overleaf/em-library-stop-main-cluster-writes
Stop writes to the main Mongo cluster for library collections

GitOrigin-RevId: 39c9ccc2a02e291befcf197fa806ee5aab900a8d
2026-09-17 11:27:53 +00:00
Tim Down 61ef838424 Merge pull request #35470 from overleaf/td-stripe-display-past-due
Display subscription for past-due Stripe subscription

GitOrigin-RevId: eb37e56676d15596ae23c0ff3261db89d93d41f3
2026-09-17 11:27:45 +00:00
Tom Wells 0ec741ad76 Separate Trash into its own sidebar nav item (#35505)
* Move Trash out of project filters into its own sidebar nav item
* Fix regression in Trashed projects item
* Co-pilot suggestion fixes
* Update mobile menu

GitOrigin-RevId: 5e2316578af201b165ccb0c4f45bd05aaa84eba0
2026-09-17 11:27:42 +00:00
Malik Glossop bbbbc49bc0 Merge pull request #35514 from overleaf/worktree-mg-comment-fix
Split add-comment to own extension and always register command

GitOrigin-RevId: 2e25643b7fe4ff30693479975945d94cc586bd61
2026-09-17 11:27:36 +00:00
Eric Mc Sween c65a630438 Merge pull request #35394 from overleaf/em-35370-library-remove-navbar
Remove the page header from the library page

GitOrigin-RevId: 2bb13c97426215bb1314d37a8702a8108a8c1219
2026-09-17 11:27:30 +00:00
Eric Mc Sween b0b2c8c953 Merge pull request #35395 from overleaf/em-35371-account-help-full-width-nav-items
Make Account and Help full-width nav items in the sidebar

GitOrigin-RevId: 625df4622beb037acd76c6028e19b3803a8fe668
2026-09-17 11:27:27 +00:00
Eric Mc Sween 5dd86068c8 Merge pull request #35434 from overleaf/em-change-library-mongo-cluster-switch-reads
Switch library collection reads and authoritative writes to the auxiliary Mongo cluster

GitOrigin-RevId: 5bbaa30b50c4d98d70d5d310dcd1209837e3f921
2026-09-17 11:27:23 +00:00
Antoine Clausse b8c0a4a201 [web] In the Subscription page, point free-users to the choose-your-plan page instead of the plans page (#34721)
GitOrigin-RevId: 0e589aabfd1bd768fa6a473d1c1bda387958998a
2026-09-17 11:27:15 +00:00
b8b78b6932 [web] Gate new reusable sharing links behind sharing-updates-new-link (#35412)
Ship the new share-project modal (behind sharing-updates) wired to the
legacy token-based link sharing, and hide the new reusable-link
functionality (anyone with the link / group links) behind a new
sharing-updates-new-link split test.

- With the flag off, the modal offers two options, "Only invited people"
  and "Via sharing links", both driven by publicAccessLevel; the reusable
  -link options, privileges dropdown and copy-sharing-link button are hidden.
- Enable the flag in the acceptance tests that exercise those routes: a
  seeded split test in saas, splitTestOverrides in non-saas.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomas- <2176518+thomas-@users.noreply.github.com>
GitOrigin-RevId: 3f34a2da5dbdc6f27ed389f9c25eea66143b8160
2026-09-17 11:27:06 +00:00
Thomas e0d77271c2 Merge pull request #35452 from overleaf/tm-commons-ai-label
[WEB] Show "Commons AI" label when the AI assist bundle is active

GitOrigin-RevId: f49722b94be9b12c7ed5f031dc98182e3a761273
2026-09-17 11:27:03 +00:00
Mathias Jakobsen 68b36a8cc6 [web] Fix flaky test around file creation (#35476)
GitOrigin-RevId: de41c045e5d3cdaadca5741bfbd86639916cf2b4
2026-09-17 11:26:57 +00:00
Maria Florencia Besteiro Gonzalez 99e7429b6d Merge pull request #35288 from overleaf/mfb-item-selection-import-preview-screen
[library] Add item selection to import preview screen

GitOrigin-RevId: b237e27726a37d930d998968acc6b2547d74f8ef
2026-09-17 11:26:54 +00:00
Jakob Ackermann cdba372dfc [monorepo] prepare per-project queues (#35386)
GitOrigin-RevId: ab020603ec04c705d08be466563adac0ce30bc04
2026-09-17 11:26:50 +00:00
Kristina 3700cf5968 [web] fix lint error in unsaved-docs.spec.tsx (#35460)
GitOrigin-RevId: 4c12b5906acff9b6d81a06726d97553d2efcda0a
2026-09-17 11:26:39 +00:00
Kristina 0b06fda437 [web] adjust global notifications link in settings modal (#35455)
GitOrigin-RevId: 49e91736fc6cb69329ad19421f208cc220d582ac
2026-09-17 11:26:26 +00:00
Kristina 26f12a6366 [web] hide all toasts when offline (#35400)
GitOrigin-RevId: cf179bf7a761ddaae56f3e5bab6327b0f3625fb5
2026-09-17 11:26:23 +00:00
Kristina 6f55a97262 [web] hide online collaborators when offline (#35403)
GitOrigin-RevId: bea77d919c63740e0049467327720036876f64ee
2026-09-17 11:26:19 +00:00
Eric Mc Sween 235685aa61 Merge pull request #35432 from overleaf/em-change-library-mongo-cluster-migration-script
Copy library collections to the auxiliary Mongo cluster

GitOrigin-RevId: 87c0433b4345fdaa3fe3c141af888eaa0106ea45
2026-09-17 11:26:16 +00:00
Jimmy Domagala-Tang dd53057708 feat: adding gating based on entitlement when calculating institutional features, and adding tests to ensure (#35351)
GitOrigin-RevId: 897ce7610cd08c77be6a90aad8b953d38213573a
2026-09-17 11:26:12 +00:00
Olzhas Askar f9a1952228 Merge pull request #35300 from overleaf/oa-fix-locale-issues
[web] Fix locale issues

GitOrigin-RevId: 393f362bc57739af9facfccf04cab7c2509a9002
2026-09-17 11:26:06 +00:00
Jakob Ackermann b68a013751 [web] add endpoint for resolving a project for writes from tpds (#35408)
* [web] add endpoint for resolving a project for writes from tpds

* [web] return historyId from new endpoint for resolving projects

* [web] add zod validation to new endpoint for resolving projects

GitOrigin-RevId: 0e607e56442117a4150a08373169c44b53dd4fdf
2026-09-17 11:26:03 +00:00
Eric Mc Sween 528e587038 Merge pull request #35377 from overleaf/em-change-library-mongo-cluster
Add auxiliary Mongo cluster support for library collections

GitOrigin-RevId: 1cfdbe4bbeb6c95f1158834ae60a4e34bef7bb87
2026-09-17 11:26:00 +00:00
Mathias JakobsenandDavinder Singh 1acb00407e [web] Disable various editor buttons during network stall (#34247)
Co-authored-by: Davinder Singh <davibaweja@gmail.com>
GitOrigin-RevId: 9e169f3591cfff2fdae63b461b946fde4611f2d7
2026-09-17 11:25:49 +00:00
Kristina 8741846df8 [doc-updater] fix unbounded Redis key growth in project notifications cron (#35425)
* delete timestamp notification keys for solo projects
* add fallback 7d TTL for keys

GitOrigin-RevId: e0493e74b14b861863afc1d4ef5fa181432ad450
2026-09-17 11:25:41 +00:00
Kristina 7fe05140b0 [doc-updater] add mongo logging to project notifications cron (#35421)
GitOrigin-RevId: db8a84f2079c57b5d5f1ae5447f8526dfb0a6d67
2026-09-17 11:25:37 +00:00
ilkin-overleaf 6fd593ff86 Merge pull request #35312 from overleaf/ii-anonymous-project-access
[web] Restore anonymous read-only access for "Anyone with the link" sharing

GitOrigin-RevId: b17e30259f73d43f0febffeea8908eb47c98b7ee
2026-09-17 11:25:29 +00:00
Mathias Jakobsen 912fb2dcd8 Merge pull request #35056 from overleaf/mj-command-palette-improvements
[web] Command palette improvements

GitOrigin-RevId: 3d6e11697ed4726b7ecada1744d24a15291ad646
2026-09-17 11:25:26 +00:00
Copilot 4ef7324263 Merge pull request #35285 from overleaf/copilot/rename-ide-open-left-menu-once-event
Rename IDE settings open telemetry event

GitOrigin-RevId: 6240798d2d5309792471386ffd1612ebadd559ab
2026-09-17 11:25:23 +00:00
Mathias Jakobsen 3f38d1fefc Merge pull request #35404 from overleaf/mj-remove-nonblocking
[migrations] Remove nonblocking tag from breadcrumbs migration

GitOrigin-RevId: eb65c1b0fb3386972135a89eafdfb49d67306d28
2026-09-17 11:25:19 +00:00
Copybot 96fe5fba0a Merge pull request #1495 from MarkQuach12:bugfix/tag-star-linter-warning
GitOrigin-RevId: 3d66ca34c16e2820e3051ce81ab67abba54cde13
2026-09-17 11:25:09 +00:00
Copybot f72e36a492 Merge pull request #1494 from MarkQuach12:bugfix/proof
GitOrigin-RevId: 4d75435011f33ac9e56b7b2443ea359b2c8ced39
2026-09-17 11:25:01 +00:00
Domagoj Kriskovic 0ecf928249 Zotero sync: error handling & error UX (#35340)
* feat(zotero-sync): implement persistent sync state management and user notifications

* fix tests

* fix(sync): add accessOk flag to sync status recording

GitOrigin-RevId: 2da17038ad21691e7b184bb1494cf97fff8234f9
2026-09-17 11:24:50 +00:00
Jakob AckermannandAndrew Rumble 9519dc5e66 [fetch-utils] use default agent with a 1s timeout and 3 attempts in connect phase (#35406)
Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>
GitOrigin-RevId: cb1e854d61ce623fa557ae003dc7159b096e1f86
2026-09-17 11:24:37 +00:00
Jakob Ackermann 89f12e6b1f [document-updater] wrap doc locks in project lock (#35416)
* [document-updater] wrap doc locks in project lock

* [document-updater] use BlockingProject as redis key prefix

* [redis-wrapper] update comment

GitOrigin-RevId: 0ce50e76386871e6c29481494febc8d6b37e7df5
2026-09-17 11:24:34 +00:00
ff72ccf5ba [web] set a few more explicit timeouts on fetch requests (#35402)
* [web] set a few more explicit timeouts on fetch requests

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>
Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [web] update test mocks to expect signal

---------

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>
Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 356b97207f6e9a2628e54ff15b034a8e694a4524
2026-09-17 11:24:31 +00:00
Andrew Rumble 05423f1a1d Merge pull request #34344 from overleaf/copilot/convert-fetch-utils-to-es-modules
Convert @overleaf/fetch-utils to TypeScript ESM with Vitest, enable noImplicitAny, fix downstream regressions, and align buildscript generation

GitOrigin-RevId: e060152f056cbae31da0650fb22471525835bd5e
2026-09-17 11:24:05 +00:00
Andrew Rumble 152004a600 Merge pull request #35095 from overleaf/lg-node-24-17-platform-compat
[Node 24.17.0][Platform packages] Fix node-fetch keep-alive false positive and bump clsi yauzl for Node 24.17

GitOrigin-RevId: 9e0382533d45e1e3b3ed88d4ccfc577591666e10
2026-09-17 11:24:02 +00:00
David f1e3093ae3 Merge pull request #35268 from overleaf/dp-frontend-routing
[web] store project dashboard navigation state in the URL

GitOrigin-RevId: 76d95c5706fc196dac7c362e186c66c5334f35ac
2026-09-17 11:23:59 +00:00
Jakob Ackermann bccef96581 [editor-core] fix TextOperation.isNoop() for tracked-changes (#35389)
* [editor-core] add regression test for composing no-op tracked change

* [editor-core] fix TextOperation.isNoop() for tracked-changes

GitOrigin-RevId: 345ee20275db9bf32ad32037013dde2bda507027
2026-09-17 11:23:53 +00:00
Rebeka Dekany b384c06b99 Fix keyboard shortcut handlers to use event.key instead of event.code (#35187)
* Use event.key instead of event.code in keyboard shortcut handlers

* Fix remaining event.code usages in modules/ keyboard handlers

GitOrigin-RevId: 51ad1f3442f9ecfe900b75a3e2631abb4199bb8f
2026-09-17 11:23:49 +00:00
Rebeka Dekany bfd7bc7da5 capture IP address country in Stripe intent metadata (#34779)
GitOrigin-RevId: 6b22129818c17d209ade38093bdc304990e43124
2026-09-17 11:23:46 +00:00
Rebeka Dekany 1ee87a823d Unify component wrappers and OL-prefix dropdown components (#35152)
* Rename dropdown primitives to OL-prefixed components

* Migrate all consumers to OL-prefixed dropdown components

* Migrate subscription dropdown shells to OL components

* Update pdf-preview story to OL-prefixed dropdown components

* Migrate navbar and spelling dropdowns to OL components

* Move dropdown primitives to ol/ol-dropdown-menu, remove BS5 import aliases

* Rename dropdown prop types to OL-prefixed, clean up BS5 import aliases

* Widen OLDropdownItem/Toggle/Menu props to include AriaAttributes and data-*

GitOrigin-RevId: 0babce46867849b3f176bac8cf1652dc76fd34da
2026-09-17 11:23:43 +00:00
Antoine Clausse 882ca2f981 [web] Cleanup plans-2026-phase-1 - phase 4: rest of backend (#34771)
* Remove `plans-2026-phase-1` from subscription module

* Remove code related to `plans-2026-phase-1`

* Remove `SplitTestUserGetter.getProjection`

* Update tests

* Remove last mentions of `plans-2026-phase-1`

* In test, wait for `_updateCustomerIoSubscriptionProperties` to be done

* Remove `purchaseAddon` test

* Remove AI-addon tests

* Fix `_quotaTierToAllowance` throwing on zero-grant tiers

`!quota` treated `0` as invalid, so `basic` (grant 0) threw. Check `typeof quota !== 'number'` alone. Unmasked once branch removed the split-test branch that skipped this path.

* Extract `waitForCustomerIoSync` helper in FeaturesUpdater tests

* https://github.com/overleaf/internal/pull/34771#discussion_r3552882875

GitOrigin-RevId: 0fe0e41b7a4105a3c4739214e2fffb77e91dbc42
2026-09-17 11:23:39 +00:00
Malik Glossop 414027160d Merge pull request #35173 from overleaf/worktree-mg-ai-actions-position
Move floating menu and writefull AI actions to bottom of selection

GitOrigin-RevId: 2d94c2fc9513b8fd42b1904433382eca898d8d99
2026-09-17 11:23:36 +00:00
Mathias Jakobsen 63968ad775 Merge pull request #35381 from overleaf/mj-disable-ft-overlay
[web] Disable disconnected-overlay for flaky connections

GitOrigin-RevId: 1c49b89084d54945dcca86a8861ba89f5728e77f
2026-09-17 11:23:30 +00:00
Mathias Jakobsen fa2a646cc2 Merge pull request #34443 from overleaf/mj-disable-breadcrumbs
[web] Disable breadcrumbs for all users

GitOrigin-RevId: 31482279c86fa4812b5ca351aabdf29b3a635c05
2026-09-17 11:23:27 +00:00
John Lees-Miller e4dd97936a Remove mirror-documentation job (#35399)
* Remove mirror-documentation job

* Update yarn.lock after removing mirror-documentation

GitOrigin-RevId: b6be94f44ff4d2aaeda14798f7232cf89a54eeee
2026-09-17 11:23:23 +00:00
Chris Dryden 185611556f Merge pull request #35315 from overleaf/cd-library-reference-search
Search the account Library by cite key in simple \cite{} autocomplete

GitOrigin-RevId: 1ee7512869316220c42a0011aeafa2520b2d767a
2026-09-17 11:23:20 +00:00
Antoine ClausseandClaude Opus 4.8 894a652b39 [web] Add compile-timeout-interstitial split test (#35286)
* [web] Add compile-timeout-interstitial split test

Show targeted interstitial copy to users arriving from the compile
timeout paywall (paywall-type=compile-timeout). Mirrors the existing
ai-limit-upgrade-copy pattern.

When enabled:
- H1 becomes "Upgrade for 24x more compile time"
- Standard/Pro card descriptions become compile-time targeted
- "24x Basic compile timeout" feature is bolded and hoisted to the top
  of the includes list (all plan types)

Part of #34982

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Don't exclude trial users from the compile-timeout copy

* Add missing dot to translation

* Add "AI assistant" to student card

* Refactor how feature list is defined and ordered in pug file

* Replace `aiLimitUpgradeCopy` and `compileTimeoutInterstitial` boolean by `upgradeCopy` string

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: 6c7c7e0457f93284d6798be229da5513f92ca77c
2026-09-17 11:23:17 +00:00
Andrew Rumble 81b3731cde Merge pull request #35299 from overleaf/ar-map-resource_type_template_to_v1_template
[web/metrics] map resource type "template" to "v1_template"

GitOrigin-RevId: 58468f7629b7a327b24dedc1eb09b095a1e38838
2026-09-17 11:23:11 +00:00
Kristina 77ce277a8b [web] Add offline indicator to toolbar (#35368)
GitOrigin-RevId: 81127bff21afb6aadcf175ba1767a2629f01d747
2026-09-17 11:23:02 +00:00
Kristina d3920e76d3 [document-updater] log duration ms for project-notifications job (#35238)
GitOrigin-RevId: 6a654e4140a6640298d97fdff4f260d6a1be0d99
2026-09-17 11:22:59 +00:00
Domagoj Kriskovic e88cb2fd66 [web] add sync now button to library for syncing with zotero
GitOrigin-RevId: 6e848c148eead7a021a714cc90b2b023f64f15a7
2026-09-17 11:22:41 +00:00
Mathias Jakobsen 843aae01c9 Merge pull request #35350 from overleaf/mj-up-timeouts
[web] Increase reconnection window in editor

GitOrigin-RevId: e6ffb4ecda170eb171455a9c692900ce71309c88
2026-09-17 11:22:37 +00:00
ilkin-overleaf 6f8ecc7678 Merge pull request #35182 from overleaf/ii-use-event-listener
[web] Replace useScopeEventEmitter and useScopeEventListener with useEventListener

GitOrigin-RevId: 8348c8c980b01e172fee3371980176d2b58ceec6
2026-09-17 11:22:34 +00:00
Davinder Singh df4e336399 Merge pull request #34245 from overleaf/mj-unsaved-docs-context
[web] Move unsaved docs monitoring to context

GitOrigin-RevId: 6d2c778efb7a52616ba1a1d52c617d8170a7f798
2026-09-17 11:22:28 +00:00
Eric Mc Sween c4104f3bd7 Merge pull request #35235 from overleaf/em-library-size-limits
Add size and count limits to the reference library

GitOrigin-RevId: ec4fb333bbd02043a929f91a702bdfd3fd285785
2026-09-17 11:22:20 +00:00
Eric Mc Sween 5388f548ac Merge pull request #35294 from overleaf/em-drop-old-index
Drop obsolete libraryReferences searchValue index

GitOrigin-RevId: 9e6192aae888cd873a643b54c89a1db3398694cc
2026-09-17 11:22:16 +00:00
Liam O'Brien 1edff0e090 Fixing local build issues from stale branch merged to main (#35354)
GitOrigin-RevId: 09917d1ffceedcedb74b24ad1a8ab4ff5ded24b5
2026-09-17 11:22:08 +00:00
Liam O'BrienandClaude Sonnet 4.6 77970c98c9 Fix GitHub Sync unlink robustness and detect expired credentials (#35147)
* github-sync: only swallow permanent revocation failures during unlink

404 (token gone) and 422 (token issued under a mismatched client_id,
the confirmed cause of #35108) are treated as safe to proceed past;
any other revocation error now aborts the unlink instead of silently
deleting the local credential row, so a token that might still be
live isn't orphaned by a transient GitHub failure.

* web: surface genuine github-sync unlink failures instead of a 500

When GithubSyncApiHandler.unlink rejects for a reason other than the
permanent 404/422 cases now handled by github-sync, show an error
notification on the settings page instead of crashing the request,
mirroring the existing projectSyncSuccessMessage session-flash pattern.

* github-sync: detect expired credentials in user status check

getStatus now validates a stored token against GitHub (GET /user)
before reporting it as linked. A GithubInvalidCredentialsError (401)
now surfaces as expired: true alongside enabled: true, distinguishing
a dead token from a genuinely working link. Any other validation
failure fails open and reports enabled: true as before.

* web: propagate github-sync expired status through to the client

Thread the new expired field from github-sync's status endpoint
through GithubSyncController.getUserStatus and the settings-page
injectUserSettings middleware, and extend the corresponding
TypeScript types on both the app and frontend side.

* web: show a re-link path on the settings widget when github credentials expire

The widget now checks expired status: it shows an error indicator
with a "connection expired" message and renders the Link action
instead of a dead-end Unlink button, since relinking upserts a fresh
token regardless of the stale row.

* web: keep unlink available for expired github credentials

Some users just want to remove an expired connection rather than
relink it. Keep Unlink as the primary action regardless of expired
status, and surface the relink path as an inline link next to the
expired message instead of replacing the primary action.

* web: correct the github unlink confirmation warning

Unlinking never touches a project's githubSyncProjectStates row,
regardless of whether the credential was healthy or expired -
verified by tracing unlinkAccount/deleteUser and confirming no other
code path deletes project sync state on account unlink. Sync pauses
until relink, but no project loses its GitHub connection, so the
warning text was simply wrong; corrected rather than adding a
separate expired-specific message.

* web: add Storybook story for expired github-sync credentials

Covers the new expired state alongside the existing NoFeature,
Unlinked, Linked and Error stories for the settings widget.

* simplify: collapse duplicated branches and extract session-flash helper

- Merge the near-identical 404/422 checks in _revokeToken into one
  status-set check.
- Use optional chaining instead of null-guard ternaries in
  GithubSyncMiddleware.
- Extract popSessionValue() in UserPagesController to de-duplicate the
  read-then-delete session flash-message pattern, now used 3 times.

* review-self: close test coverage gaps found in self-review

- AuthenticationController: cover 401/403 revocation responses and a
  plain network error with no .response, alongside existing 404/422/500.
- GithubSyncController: assert projectSyncErrorMessage stays unset on
  the unlink success path.
- UserPagesController: cover projectSyncErrorMessage set-then-cleared
  and absent-stays-undefined cases.
- Add GithubSyncMiddleware.test.mjs (previously untested): not-logged-in,
  successful status fetch, and callback-error branches.
- Cypress: cover cancelling the unlink confirmation modal while expired.
- Storybook: add ErrorWhileUnlinked story for the disabled-Link +
  error-indicator combination.

* review-self: fix audit-log ordering, cache getStatus validation safely

- GithubSyncApiHandler: write the link-github/unlink-github audit log
  entry only after the corresponding github-sync API call succeeds,
  instead of before (resolves the discrepancy tracked in issue 3354).
- UserController/UserManager: cache getStatus's live token-validation
  result for 5 minutes to cut down on redundant GitHub API calls,
  with an optimistic-lock write (matched against the encrypted token
  that was actually validated) so a relink can never be overwritten by
  a stale in-flight validation of the old token.
- github-integration-linking-widget: dedupe the relink href against
  the linkPath already passed to IntegrationLinkingWidget.

* web: surface expired github credentials proactively in the sync modal

The modal previously only showed the reauthorize prompt after a real
GitHub API call failed with 401 mid-action. It now also checks the
expired field already returned by the status endpoint and shows the
same prompt immediately on open, before ever rendering the export
form against a token known to be dead. Extracted the existing
reauthorize markup into GithubCredentialsExpired so both call sites
share it.

* github-sync: avoid double-fetching the credential doc in getStatus

getStatus called UserManager.getUser, then getAuthToken which
internally fetched the same document again. Add getAuthTokenFromUser
to decrypt from an already-fetched doc and use it in getStatus,
removing the redundant Mongo query and the now-unreachable
GithubUserNotLinkedError handling (the user doc's existence was
already confirmed by the first fetch).

* github-sync: make expired-token handling reactive, drop proactive getStatus validation

Reverts getStatus in the github-sync service to a pure DB check — no live
GitHub API call, no 5-minute cache, no token_expired/token_validated_at
fields. The expired state is already surfaced reactively: the unmerged-commits
fetch on modal open returns 401 when the token is dead (handled by the existing
case 401 branch), and ExportToGithub handles 401 from the orgs call. The
proactive check added complexity (caching, optimistic-lock writes) without a
meaningful UX gain over the reactive paths.

Removes the expired indicator from the settings widget and the proactive
GithubCredentialsExpired screen from the modal's pre-flight check. The
github_sync_expired locale key is removed as it is no longer referenced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* github-sync: show reauthorize prompt when push/pull fails with expired credentials

When a merge (push or pull) returns 401, GithubSyncMergeAction now renders
GithubCredentialsExpired with the Reauthorize button instead of the raw
"GithubInvalidCredentialsError" string that was previously falling through
to the default error branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* github-sync: wrap all async controller handlers with expressify

Without expressify, an unhandled throw from an async route handler
(e.g. a token decryption failure in the github-sync service producing a
non-4xx error that _reportError re-throws) becomes an unhandled rejection.
Express never sends a response and the modal is left stuck on loading.
expressify catches the error and passes it to next(), so Express's error
handler sends a 500 and the frontend can surface it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* removing redundant tests

* removing getAuthTokenFromUser function

* removing redundant key

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: 0d9a314bf70bea9e6037593f2a6f8b8d1f2c6e71
2026-09-17 11:22:04 +00:00
Anna Claire Fields da7a2f0e72 Add reCAPTCHA enterprise support and refactor invite flow (#35134)
GitOrigin-RevId: 4a4ebbd9e5ec55c3532a0cd3fa6c0feda3e2733c
2026-09-17 11:22:01 +00:00
Andrew Rumble 5a602e1b1d Merge pull request #35290 from overleaf/ar-stop-using-unix-formatter-locally
[monorepo] switch from unix to stylish formatter for lint

GitOrigin-RevId: e3c84e556941b4747e469a123a99d5731697976d
2026-09-17 11:21:32 +00:00
Liam O'BrienandClaude Sonnet 4.6 6c187074e5 Exclude Python venv directories from Dropbox sync (#35029)
* filtering out venv directories

* also exclude .venv directories and sync all pattern copies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: 6634d9780da44f90c6777347d52def9a854bd29f
2026-09-17 11:21:23 +00:00
Liam O'BrienandClaude Sonnet 4.6 c230dab083 Add provider-level rate limit backoff for third-party references (#35035)
* Throwing 429 errors when ref manager responds with 429

* respecting the APIs retry-after first

* Handle 429 on linked file refresh path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Show rate-limited message on linked file refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Consolidate backoff helpers into ProviderBackoffStore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix EX 0 crash, null guard, BackoffProvider type, test fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add unit tests for rate limiting implementation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* update ZoteroProviderTests for new signedGet return shape

getCollections now returns { collections, backoff, retryAfter } instead
of a bare array. getUsersItemsChanges reads backoff/retryAfter from the
signedGet return tuple, not from response.headers (extraction moved to
the transport layer in OAuth1Transport).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix tpr-webmodule unit test regressions

ZoteroSyncClient: collectionsResponseSchema now requires backoff/retryAfter
as number|null; stub responses were missing these fields causing ZodError.
Assertions updated since getCollections returns CollectionsResult (object)
not a bare array.

ReferencesApiController: vi.doMock for @overleaf/fetch-utils was missing
RequestFailedError; the three 429 propagation tests inline-import it from
the mocked module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* address PR review comments

- ReferencesApiController: add fallback 500 status and optional-chain
  response in all catch blocks where err.response may be absent
- OAuth1Transport: guard Number(header) with Number.isFinite to prevent
  NaN propagating as backoff/retryAfter when header value is non-numeric
- ZoteroProvider: same Number.isFinite guard for lastModifiedVersion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix prettier formatting in ZoteroSyncManager.test.mjs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* show rate-limited message in groups modal and Write & Cite

use-provider-groups: detect 429/rateLimited response and set error to
'rate_limited' instead of generic 'loading_groups'

file-tree-import-bibliography: add rate_limited key to all three
providers' translation maps so the import modal renders the rate-limited
message when groups loading is blocked

reference-manager-source: add RateLimitedError, throw it on 429 from
search, block further searches and show a toast notification

toasts: add write-and-cite:rate-limited toast and showRateLimitedToast()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fixup! fix tpr-webmodule unit test regressions

* fixing CI failures

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: 158c2b92c557caacef337b89f68c91c47569151f
2026-09-17 11:21:20 +00:00
Miguel Serrano 823d736e94 [web] drop Writefull name from AI Control subtext (#35272)
GitOrigin-RevId: 923d5ab23bb366e5ddb3df9569fa1ae4cca6564e
2026-09-17 11:21:16 +00:00
Domagoj Kriskovic 6a736ecc62 refactor(third-party-references): replace simple-oauth2 with a custom OAuth2 client (#34917)
* [3rd-party-references] replace simple-oauth2 with openid-client

* Refactor OAuth2 handling: replace openid-client with custom OAuth2Client implementation

* Guard unconfigured OAuth2 providers with a controlled error

Providers whose client id/secret env vars are unset now stay registered
but without an OAuth2 client. Using one throws a controlled OError
("OAuth2 provider is not configured") from getOAuth2Client rather than a
TypeError from calling a method on undefined or sending "undefined"
credentials.

* Guard unconfigured OAuth2 providers with a controlled error

* Add clarification comments on OAuth 2.0 client ID and secret encoding requirements

* Add logging for invalid token expiration dates in parseToken function

GitOrigin-RevId: b17884ccba171ff4db8e6a698b3d4dcdca24e8ae
2026-09-17 11:21:13 +00:00
David cb5e36340b Merge pull request #35246 from overleaf/dp-34792-no-references-empty-state
[web] Show "No references found" empty state in import preview

GitOrigin-RevId: 5f8380863eb35235b0ef13d2138c8769e241d53f
2026-09-17 11:21:02 +00:00
Eric Mc Sween 6cea8580aa Merge pull request #34992 from overleaf/em-dropbox-rate-limits
[third-party-datastore] Handle Dropbox rate limits and per-project file-count limits during sync

GitOrigin-RevId: 0a3c4a06b6444a70394c35cb404c566ef7e25cef
2026-09-17 11:20:59 +00:00
Jakob Ackermann 926b7c534b [agents] allow running all the test-runner skill commands (#35247)
* [agents] allow running all the test-runner skill commands

* [agents] move linting into AGENTS.md

* [agents] double down on permissions and cwd

* [saas-e2e] add support for MOCHA_GREP

* [saas-e2e] fail fast by default outside CI

* [agents] avoid need for Codespace detection

* [agents] work around missing trust prompt in Codespaces

* [saas-e2e] e2e_test_setup: merge split-tests in dev-env

GitOrigin-RevId: f1c1cd2a732e98b2e6125ef86a2b2cda5d3aabb6
2026-09-17 11:20:53 +00:00
Mathias Jakobsen 612a6302b4 Merge pull request #35219 from overleaf/mj-themed-select
[web] Add dark mode for Select and OLFormSelect

GitOrigin-RevId: f71b95ff31720e898b33b508c5948b3219b28046
2026-09-17 11:20:44 +00:00
Kristina 1765f6be51 Merge pull request #35223 from overleaf/lg-linkify-it-security-upgrade
[Security upgrade] bump linkify-it to patched version via resolutions

GitOrigin-RevId: a3e689de7d4842c00b2d6d5ac7631224ce76ad1a
2026-09-17 11:20:41 +00:00
Tim Down 6ed39f18b7 Merge pull request #17214 from overleaf/td-latexqc-remove-babel-polyfill
Remove @babel/polyfill from latexqc service

GitOrigin-RevId: 5d649b14ade41fc2fce350419443f6db71e9d161
2026-09-17 11:20:38 +00:00
Brian Gough 0dd69cb143 add clsi metrics for png2pdf (#35239)
* add clsi metrics for png2pdf

* update metrics to use png2pdf boolean

GitOrigin-RevId: 0d96020b659b31cdf16f5d26c128928dae45f8f9
2026-09-17 11:20:34 +00:00
Olzhas Askar 18b3bb7503 Merge pull request #35043 from overleaf/oa-babelfish-2
[web] Babelfish all language files

GitOrigin-RevId: 3889f38b80c511f3cda1f47b9ecf4d8b7d2cdfb6
2026-09-17 11:20:29 +00:00
Tim Down 6a7c91c8fa Merge pull request #35199 from overleaf/td-pricing-features-column-truncation-fix
Fix for pricing page features table column heading being truncated

GitOrigin-RevId: 9e1ffaa9b21add17cc2566e26ab92f526dffa9b5
2026-09-17 11:20:23 +00:00
Malik Glossop 794e813158 Merge pull request #35180 from overleaf/mg-remove-ai-setting
Remove floating menu setting from editor settings

GitOrigin-RevId: dd17a7fed607b65590a919f69b3c22e48f4cc0ec
2026-09-17 11:20:12 +00:00
Malik Glossop 11b9ff5ac8 Merge pull request #34772 from overleaf/mg-writefull-citations-button
Add a `Check citations` option to writefull ai menu

GitOrigin-RevId: c2204c663474a016085668fa708460b3db9517cb
2026-09-17 11:20:09 +00:00
Jakob AckermannandBrian Gough 84c6a68eae [web] enforce size limit on individual files when cloning project (#35211)
* [web] enforce size limit on individual files when cloning project

Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [web] improve test coverage

* [agents] speed up running frontend tests

* [web] gracefully handle project too large error

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: c7b35a3a9688e15a9bb51c61816da82d39e0d29d
2026-09-17 11:20:03 +00:00
Jakob Ackermann f62bd990d4 [clsi-cache] add support for png2pdf (#35200)
GitOrigin-RevId: ebc505528f2a001923acf2d39e6dd43107f5f35e
2026-09-17 11:20:00 +00:00
Lucie GermainandClaude Opus 4.8 cc741ec2f1 [Security upgrade] bump multer 2.1.1 -> 2.2.0 (web + clsi) (#34916)
GHSA-72gw-mp4g-v24j / CVE-2026-5079 — DoS via deeply nested field names,
patched in 2.2.0. multer is a direct dep of web and clsi (file uploads).

- bump multer to 2.2.0 in services/web and services/clsi
- re-target the Overleaf local patch (attach files with no filename as
  'undefined' instead of skipping) from 2.1.1 to 2.2.0, kept as a scoped
  root resolution to match the repo's patch convention

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 0d10137b521ebaacee71fc41adba32c4530a4041
2026-09-17 11:19:49 +00:00
Lucie GermainandClaude Sonnet 4.6 acdec8dd6b [Node 24.17.0][Core packages] Bump yauzl to ^3.4.0 in history-v1 and web for Node 24.17 (#35093)
* Bump yauzl to ^3.4.0 in history-v1 and web for Node 24.17

yauzl 3.x hangs extracting a large zip entry under Node 24.17 due to
stream backpressure changes. Fixes zip/project uploads, open-in-overleaf
template tests, and the SaaS-E2E "templates" test.

Also fix the open-in-overleaf acceptance test mock to serve the "too large"
fixture with a single res.end (Content-Length) instead of a backpressure-
ignoring res.write loop, which Node 24.17 surfaces as ECONNRESET (404)
instead of the expected 422.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Use plainTextResponse for large-file test fixture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: 55627b7c2c91959f0d1a7f3419467455edb06c9f
2026-09-17 11:19:46 +00:00
Miguel Serrano a84257ff81 [web] AI Feature controls for managed groups (#35193)
* [web] AI Feature controls for managed groups

Adds a Feature Controls section to the group settings with a switch to control whether AI features are enabled or disabled

GitOrigin-RevId: c5d5ed80c0cc3435075bb2c58b43fc08192dd46b
2026-09-17 11:19:42 +00:00
Rebeka Dekany e2a8bf768b Merge intermediate component wrappers into OL-prefixed components (#34996)
* Merge intermediate component wrappers into OL-prefixed components

* Replace all intermediate wrapper consumers with OL-prefixed components directly

* Fix jsx/js files still importing deleted shim components

* Remove duplicate OLNotification imports left by first pass

* Rename OLTagIcon to TagIcon, remove OL prefix

* Rename OLDropdownMenuItem to DropdownMenuItem, remove OL prefix

* Remove OLNotification, use Notification directly

* Move Notification to the main shared component folder

* Update tag-icon import path out of ol/

* Move TagIcon to the main shared component folder

* Update dropdown-menu-item import path out of ol/

* Move DropdownMenuItem to the main shared component folder

* Fix dropdown-menu-item import path to dropdown/ subfolder

* Remove BS5 alias prefixes from shared components

* Restore notification-list wrapper in Notification component

* Add translation for loading button

* Remove redundant import aliasing

* Resolve conflicts

* Restore notification container `notification-list`

GitOrigin-RevId: 24a5af18599a3e02ac6fd61c09cf5a49ef6e0020
2026-09-17 11:19:34 +00:00
Alf Eaton 39e1447258 Use custom MathJax tag factory to avoid multiple label error (#34934)
GitOrigin-RevId: fcfae0d48f4f360f04a55d081b53bef9a4a5420e
2026-09-17 11:19:23 +00:00
Alf Eaton 5fec504ba9 Pass displayMode when rendering math preview (#35165)
GitOrigin-RevId: ea80dcdd6620ffbc3066aaa3f27b751b81d87ba6
2026-09-17 11:19:20 +00:00
Andrew Rumble e0d91517e8 Merge pull request #35225 from overleaf/copilot/set-globals-false-vitest
Set `globals: false` in web vitest configuration

GitOrigin-RevId: 3b809d0b5c47910f56ce81fa229292065b4527f0
2026-09-17 11:19:16 +00:00
Kate Crichton 3f8270e4f6 Merge pull request #35162 from overleaf/jel-domain-uncaptured
[web] Change `it` to `domain capture` in email notification to group admin

GitOrigin-RevId: 17ed6de60c3c7cd70a8c574185c3b328e5ca879c
2026-09-17 11:19:11 +00:00
Kate Crichton 2accb05426 Merge pull request #35063 from overleaf/jel-btn-txt-generate-token
[web] Update text for generate domain verification token button

GitOrigin-RevId: 27345f437d68d0fce7538869083a704ed1a22841
2026-09-17 11:19:07 +00:00
Jakob Ackermann 0354f03612 [eslint-plugin] integrate with build scripts (#35176)
GitOrigin-RevId: cc042f70981da3862f2660c695193190b0c62000
2026-09-17 11:19:04 +00:00
Maria Florencia Besteiro Gonzalez b93d180ee9 Merge pull request #34681 from overleaf/mfb-34187-move-editor-switch-to-right-hand
Introduce shared ToolbarLayout component and move editor switch to right in bibtex visual toolbar

GitOrigin-RevId: 5f61bf8144dc68dfb4adce8a03d43c2dd0de3369
2026-09-17 11:18:57 +00:00
Rebeka DekanyandCopilot Autofix powered by AI f30c23cacc Update in-product doc links to point to docs.overleaf.com (#35184)
* update doc links to point to docs.overleaf.com instead of the old learn wiki

* fix broken link to LaTeX positioning wiki article in figure modal help

* point figure positioning help link to the docs.overleaf.com page

* point figure/table label help links to specific anchors on the captioning-and-referencing-figures doc page

* point figure modal error-help link to the graphicx-loading doc page

* normalize target/rel on docs.overleaf.com links opened in a new tab

* open figure modal and SSO help links in a new tab now that they point off-site

* add eslint rule enforcing rel=noopener noreferrer on docs.overleaf.com links

* extend require-rel-for-docs-links to also check OLButton

* add lint script catching missing rel on docs.overleaf.com links in pug templates

* fix require-rel-for-docs-links autofix crash on valueless rel attribute

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* add regression test for require-rel-for-docs-links valueless-rel autofix

* revert `target=_blank` on figure modal and SSO help links

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: b6111341e1080fd7e4b42f6a1a18c27ea0fd7d8c
2026-09-17 11:18:47 +00:00
Antoine Clausse 5698ef16b3 [web] Fix pricing page table overflow (#35113)
Adding position:relative prevents the visually hidden elements from getting absolutely positioned outside of the scroll area

GitOrigin-RevId: abe9e0ff91f769be39ed39223ebbbe17420af111
2026-09-17 11:18:41 +00:00
Mathias Jakobsen 362b5b982e Merge pull request #35204 from overleaf/mj-command-palette-themed
[web] Make command palette explicitly themed

GitOrigin-RevId: 4fa7221698d46fc346ae59442ca70f857d9b8189
2026-09-17 11:18:27 +00:00
Mathias Jakobsen 1db2898241 Merge pull request #35192 from overleaf/mj-themed-variables-modals
[web] support themed variables in modals

GitOrigin-RevId: dd841f18b796226c2177dce3cdabf85b19d7f1c3
2026-09-17 11:18:24 +00:00
Mathias Jakobsen 8ae794cce3 Merge pull request #34940 from overleaf/mj-submit-button-cleanup
[web] Clean up submit buttons post-editor-redesign

GitOrigin-RevId: 3ad6d02038503294be28bcd4d8926261e12849ab
2026-09-17 11:18:18 +00:00
Rebeka Dekany e8e6bca16c Consistently use cursor: not-allowed for disabled buttons and dropdown items (#35171)
* consistently use cursor: not-allowed for disabled buttons and dropdown items

* fix: exclude anchor .btn.disabled from pointer-events override

* also handle li.dropdown-item and narrow fieldset selector to button.btn

GitOrigin-RevId: d94974370952bbefe74d971d39fa99802a6c3130
2026-09-17 11:18:02 +00:00
Jakob Ackermann 70a9069015 [web] add retries to history calls in compile from history (#35191)
* [web] add retries to history calls in compile from history

* [web] address review feedback

GitOrigin-RevId: f22e0d1515dd22a0a43a0faf28c370e4f196e1da
2026-09-17 11:17:58 +00:00
Lucie GermainandClaude Opus 4.8 d2c848320e [Security upgrade] bump js-yaml to 4.2.0 in root workspace (#34925)
* [Security upgrade] bump js-yaml 4.1.1 -> 4.2.0 in root workspace

GHSA-h67p-54hq-rp68, patched in 4.2.0. js-yaml 4.x is pulled at runtime via
the patched referer-parser (web) plus many dev/build consumers.

Bumped the existing scoped resolution referer-parser/js-yaml ^4.1.1 -> ^4.2.0
(security floor on the patched-parent path) and re-resolved all 4.x to 4.2.0
(yarn up -R). The js-yaml 3.x line (3.14.2) is a different major, unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Bump js-yaml range in referer-parser patch to ^4.2.0 for consistency

The referer-parser/js-yaml resolution override forces js-yaml to 4.2.0,
but the local patch still declared ^4.1.0. Align the patch's declared
range with the resolution. Addresses Jakob's review comment on #34695
(discussion_r3440549168).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make patched-parent realignment a mandatory Type B step in yarn-security-update skill

The patch-realignment instruction was a soft 'for consistency' parenthetical;
a session reading the rule acted only on 'use a resolution' and dropped the
patch edit, leaving the resolution at ^4.2.0 while the referer-parser patch
still declared ^4.1.0 (PR #34925). Promote it to an explicit mandatory step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fully resolve js-yaml to 4.2.0 and stop patching its range in referer-parser

Add a scoped resolution for @istanbuljs/load-nyc-config/js-yaml so the last
sub-4.2.0 instance (3.14.2, GHSA-h67p-54hq-rp68) is forced to 4.2.0 too;
load-nyc-config uses js-yaml.load(), which is 4.x-compatible.

Drop the js-yaml and mocha dependency edits from the referer-parser patch:
editing a patched package's dependencies has no effect on resolution, so the
scoped resolution is the entire fix. Update the yarn-security-update skill to
match (patched parents route to Type B; do not edit the patch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Combine js-yaml resolutions into a single global override

Replace the two scoped js-yaml resolutions (referer-parser/js-yaml and
@istanbuljs/load-nyc-config/js-yaml) with one global "js-yaml": "^4.2.0". Every
js-yaml consumer should reach the security floor and none needs an older major,
so the global override is equivalent and tidier (reviewer request).

Note the scoped-vs-global tradeoff in the yarn-security-update skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 1738cbc04f4cacbfb6880ba6f176a192aa4a03f7
2026-09-17 11:17:55 +00:00
Domagoj Kriskovic a2712ce523 [web] Clean up boolean handling of track changes state in the frontend
GitOrigin-RevId: 640380219274e156d5bbac704d82e7ea75e1fee2
2026-09-17 11:17:51 +00:00
Miguel Serrano fa6eccf939 [CE/SP] Hotfix 6.2.1 (#35140)
GitOrigin-RevId: 015db4b74ac04398e0227775daf8aa8782195489
2026-09-17 11:17:47 +00:00
Miguel Serrano 9394f64c54 [web] Minor fixes for AI Disabled state (#34504)
* [web] Minor fixes for AI Disabled state

- Applies opacity to disabled spark icons
- Forces AI controls to be hidden on View state

* Removed redundant check

GitOrigin-RevId: a1389bec3e80a396c77a22294b1d921b4116a75c
2026-09-17 11:17:38 +00:00
Jakob AckermannandBrian Gough 69ccb3c316 [clsi] integrate png2pdf (#34796)
* Add png to pdf converter for optimising image loading in pdftex

* refactor

* move fixtures

* refactor and add unit tests

* Remove gamma correction from PNG processing and update related tests

* Add crc32fast dependency and enhance PNG processing tests

* Add fixture test cases for PNG processing

* fix alpha channel to match pdftex

pdftex always uses an 8 bit alpha channel in the output even for 16 bit images

* Refactor PNG processing to use Option for alpha stream and update related logic

* Enhance PDF writing by adding DecodeParms for alpha stream and optimize PNG encoding process

* Add pixel dimensions support in PNG processing and PDF writing

* [copybara] move clsi excludes

* [clsi] move gitignore rules

* [png2pdf] move into dockerfiles folder

* [png2pdf] use atomic writes

* [clsi] integrate png2pdf

* [saas-e2e] add coverage for png2pdf

* [png2pdf] make compatible with seccomp profile in clsi

* [copybara] remove exclude following move of png2pdf

* [copybara] remove spurious excludes

* fix broken detection of malformed chunk headers

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* [project-history] add build script flag for depending on doc-updater types

* [png2pdf] run tests as part of clsi pipeline

* [png2pdf] fix calculation of PDF page size

* [web] put png2pdf behind feature flag and make it opt-in

* [web] fix ClsiManager.promises.sendRequest invocation after merge

* extend is_copy_safe function to follow pdftex

see the conditions in pdftex's writepng.c
https://svn.tug.org:8369/texlive/trunk/Build/source/texk/web2c/pdftexdir/writepng.c?revision=78081&view=markup#l575

the criteria are not an exact match because the png crate does not support all the
png chunk types
https://github.com/image-rs/image-png/issues/116

if the criteria result in a png being converted or skipped the final result should be the same.

in one case the conversion will be done by pdftex (at runtime)
in the other case the conversion will be done by png2pdf -

both conversions should give the same result.

* include tests in png2pdf Dockerfile

* [clsi] explicitly disable png2pdf conversion for project conversion

* [clsi] png2pdf: count converted files

* [png2pdf] bump date in docker image tag

* [web] make draft-setting option wider when png2pdf is available

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: d0d952ee009bf93c72bba2ffe763ae0160d396e4
2026-09-17 11:17:28 +00:00
Miguel Serrano 2b05a3c9df Revert "[git-bridge] Setup ArtifactRegistry for maven central proxy (#35167)" (#35178)
This reverts commit ae949219b2e19d969f74e103b502177e1a3743a2.

GitOrigin-RevId: 81be00d03c931f22993df2eb79640ce52205464a
2026-09-17 11:17:19 +00:00
Miguel Serrano ae47a39cdc [git-bridge] Setup ArtifactRegistry for maven central proxy (#35167)
* [git-bridge] Setup ArtifactRegistry for maven central proxy

GitOrigin-RevId: ae949219b2e19d969f74e103b502177e1a3743a2
2026-09-17 11:17:07 +00:00
Kristina c2ba7955dc [web] adjust styles in notifications settings (#35143)
GitOrigin-RevId: 5867072acd6e38c1a00a252b13e4ef4169ef7b79
2026-09-17 11:17:01 +00:00
Domagoj Kriskovic 74609f74ed Add Cmd+Shift+X strikeout shortcut to the visual markdown editor
GitOrigin-RevId: d76fce965c0bffd393f5ca5176d3b61453ac2f93
2026-09-17 11:16:55 +00:00
Domagoj Kriskovic a8d888ea0c Toggle bold/italic off on Cmd+B / Cmd+I when already applied
In the visual markdown editor, Cmd+B (bold) and Cmd+I (italic) now remove
the markdown delimiters when the cursor or selection is already inside a
bold/italic span, instead of always adding another wrapper. Mirrors the
toggle behavior of the visual LaTeX editor.

Issue #34826

GitOrigin-RevId: d9ced8493dfaa28b61f3f0d51479d95f62e0bc4a
2026-09-17 11:16:51 +00:00
Jakob Ackermann 966728d5a2 [clsi-lb] add health check and alerting for stalled poll loop (#35117)
* [clsi-lb] add settings entry for pollInterval

* [clsi-lb] add health check and alerting for stalled poll loop

* [k8s] clsi-lb: switch livenessProbe to config writer

* Revert "[k8s] clsi-lb: switch livenessProbe to config writer"

This reverts commit ae0c8624528db4bdf35a27fac836ffeecfc1682c.

* [clsi-lb] fix docker build

* [clsi-lb] throw when app.listen fails

* [clsi-lb] expose gc

* [clsi-lb] refactor lastPolledAt to Date

GitOrigin-RevId: 88542d9c10d5f63a4283ffab32badcdea1d3a95a
2026-09-17 11:16:48 +00:00
Olzhas Askar dd48dfa093 Merge pull request #35158 from overleaf/oa-compile-timeout-final
[web] Compile timeout CTA

GitOrigin-RevId: f64c651332a32c293da1f3256cf9443413878d63
2026-09-17 11:16:42 +00:00
Lucie Germain a897780ca7 [Security upgrade] bump tmp to 0.2.7 in root yarn.lock (#34641)
GitOrigin-RevId: cc473f209393f9f44a1ef5ed6be1f840f22d6f07
2026-09-17 11:16:38 +00:00
Copilot c15352a9d9 Silence "Request aborted" errors from multer when client disconnects mid-upload (#33662)
GitOrigin-RevId: 378358db1ef45746c1c7bfc08277e01d4a0720c6
2026-09-17 11:16:35 +00:00
Anna Claire Fields bdcb79c6fe third party code inventory changes (#34732)
GitOrigin-RevId: dc691ec60ade749b2fa9a14fac857840589d34ea
2026-09-17 11:16:31 +00:00
Mathias Jakobsen 68d540c2fa Merge pull request #35118 from overleaf/mj-survey-beta-options
[web] Add beta filtering to survey configuration

GitOrigin-RevId: b1d8f5266b48278ee775208bcaa68bb960be3250
2026-09-17 11:16:25 +00:00
Chris Dryden 5778239fe2 Merge pull request #34953 from overleaf/cd-migrate-tpds-to-esm
Migrate `third-party-datastore` service from CommonJS to ESM syntax

GitOrigin-RevId: 5f1bbe827a0fd21c5ddb0be0e2cddce7d4f59a83
2026-09-17 11:16:19 +00:00
John Lees-Miller aa97f5794c [git-bridge] fix symlink handling (#35133)
GitOrigin-RevId: 282823db63f64bfae950b00e77a6cdc2a048b0ce
2026-09-17 11:16:15 +00:00
David de429e18d8 Merge pull request #35115 from overleaf/dp-library-no-results-state
Library: "No results" state
GitOrigin-RevId: 9c6e8d88abfa7372ca70826101ecd4e7b8fb0ae5
2026-09-17 11:16:09 +00:00
Tim Down 3f26afcb8b Merge pull request #35124 from overleaf/td-dropbox-logo-rename
Rename Dropbox icon file to refresh cache in staging and production

GitOrigin-RevId: 4538892d8db227af3861e88da8e021936db0447d
2026-09-17 11:15:56 +00:00
Tim Down f7ab3d2b1f Merge pull request #34888 from overleaf/td-onboarding-split-test
Implement onboarding split test

GitOrigin-RevId: bf3675437d7fbbffc9374ffb7c0ba95bbb5ec3b8
2026-09-17 11:15:48 +00:00
5dccf3b62c [Security upgrade] bump socket.io-client fork to ws@^8.21.0 (#35060)
* bump socket.io-client to overleaf fork 0.9.17-overleaf-6

Fixes GHSA-96hv-2xvq-fx4p (ws memory exhaustion DoS) by bumping the
overleaf/socket.io-client fork to a version that declares ws@^8.21.0
instead of ws@^1.1.5. The existing ws@npm:^8 resolution pins it to
8.21.0, removing ws@1.1.5 from yarn.lock entirely.

Depends on overleaf/socket.io-client#6.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* use 0.9.17-overleaf-6 tag for socket.io-client

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* use the socket.io-client-0.9.17-overleaf-6 release

* provide ws module for real-time acceptance tests

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Brian Gough <briangough@users.noreply.github.com>
Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 2d9a42f90bcf95a3a8f053df4c0092c4fbc26422
2026-09-17 11:15:42 +00:00
Jakob Ackermann b6208ee52f [real-time] gracefully handle undefined meta.tc (#35049)
GitOrigin-RevId: c375373585e4cb2405eac778bbe2dd987c342a19
2026-09-17 11:15:38 +00:00
Jakob Ackermann 9f1a2e058a [web] avoid sending empty sharejs op (#35066)
* [web] add frontend tests for sharejs transform

* [web] extend tests for sharejs transform to flush eventually

* [web] full coverage for sharejs transform

* [web] add tests for sharejs events

* [web] add tests for sharejs callbacks

* [web] avoid sending empty sharejs op

* [web] add tests for resending after reconnecting

* [web] avoid resending of empty sharejs op after reconnecting

GitOrigin-RevId: 226fe47a7069cb877fd5a60454f86cc5855d9a0e
2026-09-17 11:15:33 +00:00
Mathias Jakobsen 3c3dd0fb37 Merge pull request #35037 from overleaf/mj-themed-modals-again
[web] Add support for dark mode modals

GitOrigin-RevId: 34770a6f91d6a331cfaccf86c7dc1a5d151ad25b
2026-09-17 11:15:24 +00:00
Mathias Jakobsen ef8fce67f1 Merge pull request #35033 from overleaf/mj-drop-dark-mode-components-opt-in
[web] Remove explicit dark mode component opt-in

GitOrigin-RevId: b28603a9989bd046039c247ee2cf9f740c9feac0
2026-09-17 11:15:18 +00:00
Mathias Jakobsen e9127933e6 Merge pull request #35079 from overleaf/mj-select-portal-container
[web] Pass container ref for writefull model select component

GitOrigin-RevId: 51a685a0f53df5b1be51dfcbebb144f413dee15e
2026-09-17 11:15:15 +00:00
Lucie Germain 210ced0d31 Merge pull request #33361 from overleaf/copilot/remove-direct-use-of-body-parser
Remove direct use of body-parser in favour of express built-ins

GitOrigin-RevId: 656c5f216daf68e86d53ae07d272e080bd5ab27f
2026-09-17 11:14:46 +00:00
Andrew Rumble 6be5db7667 Merge pull request #34926 from overleaf/lg-otel-root-security
[Security upgrade] bump @opentelemetry/core to 2.8.0 (OTEL family)

GitOrigin-RevId: dd45fba9999de03b44ef6bd38ed4dedba0900a50
2026-09-17 11:14:42 +00:00
Andrew Rumble 834730d011 Merge pull request #34580 from overleaf/lg-security-grpc-grpc-js
[Security upgrade] bump @grpc/grpc-js from 1.14.3 to 1.14.4

GitOrigin-RevId: bda9e568e55f6e536cfd6d28a9453726aca2c39c
2026-09-17 11:14:37 +00:00
Brian Gough efaeb90e39 Merge pull request #34924 from overleaf/lg-tar-root-security
[Security upgrade] bump tar to 7.5.16 in root workspace

GitOrigin-RevId: 73ac951792ce3d8ac8e9c01508bd467932621f49
2026-09-17 11:14:33 +00:00
Mathias Jakobsen 60051a1c9f Merge pull request #34920 from overleaf/lg-form-data-security-upgrade-v2
[Security upgrade] bump form-data to patched versions in root workspace

GitOrigin-RevId: 77aa4482bee6c84ea71255817263b26023d8943f
2026-09-17 11:14:27 +00:00
Jakob Ackermann b51275ca2d [real-time] add strict validation for incoming applyOtUpdate messages (#35045)
GitOrigin-RevId: 13156762e320db9c177c3cf82195baff42f68141
2026-09-17 11:14:16 +00:00
Jakob Ackermann 7932c99cfc [saas-e2e] add tests for reviewer role (#35024)
* [real-time] update tests for more realistic applyOtUpdate messages

* [saas-e2e] add tests for reviewer role

GitOrigin-RevId: f0a5307230beb00be3a1387129e06eb5b2106c09
2026-09-17 11:14:09 +00:00
roo hutton e5c815129b Merge pull request #34976 from overleaf/rh-cio-beta-program
Expose beta program participation to Customer.io

GitOrigin-RevId: e28d9b41afb2e50e62b7e06c94482f4240c6becc
2026-09-17 11:14:06 +00:00
Mathias Jakobsen d74ecc0f2c Merge pull request #34993 from overleaf/mj-compose-associativity-tests
[overleaf-editor-core] Ignore timestamps in associativity tests

GitOrigin-RevId: eed120881fc66fd804759f25565a2e5664f3776c
2026-09-17 11:13:49 +00:00
Mathias Jakobsen 8282519406 Merge pull request #35022 from overleaf/mj-review-styles-fix
[web] Fix review panel styles

GitOrigin-RevId: ceb252e494adfbeef770be2f7e55510a96c93377
2026-09-17 11:13:44 +00:00
Mathias Jakobsen cfb92c3b8f Merge pull request #34938 from overleaf/mj-icon-type-from-name
[web] Tidy iconTypeFromName

GitOrigin-RevId: e597e2ac6cdc8a6c82a3849dbcfcffb659310c45
2026-09-17 11:13:40 +00:00
Andrew Rumble 25d625d4ee Merge pull request #34918 from overleaf/lg-dev-tooling-security
[Security upgrade] bump webpack-dev-server, http-proxy-middleware, launch-editor (dev tooling)

GitOrigin-RevId: 50f2ffcf575beaf35d243f7059c82cb5210afbf8
2026-09-17 11:13:32 +00:00
Mathias Jakobsen 3a3adf5e5a Merge pull request #34980 from overleaf/mj-ide-settings-feature
[web] Consolidate ide settings in `features/ide-settings`

GitOrigin-RevId: cbf0ff069399f418d8cb0d8165628df190046ea9
2026-09-17 11:13:11 +00:00
Jakob Ackermann 8e2edc4d8d [web] remove limit when fetching split-tests (#35010)
GitOrigin-RevId: aa166d28359da83385b632ba2077a85384d08c36
2026-09-17 11:13:03 +00:00
Kristina 4622848a19 [web] optimize process notifications and add monitoring metrics (#34699)
GitOrigin-RevId: 411355cbdbb25d871b9cfb2afbb0ad235041337f
2026-09-17 11:12:59 +00:00
Musicminion 4db251b688 Fix OVERLEAF_INVITE_TOKEN_SECRET being ignored in server-ce settings 2026-07-09 22:22:49 +08:00
Mark 26aed11841 Fix false math-mode warning for \tag* with math content
\tag*{...} (amsmath's unnumbered tag) incorrectly triggered an "open
inline math \( used inside existing math mode" warning when its
argument contained inline math, e.g. \tag*{\(\dagger\)} or
\tag*{$abc$}. \tag{...} without the star already worked.

The linter marks \tag's argument group as text-mode so nested math
delimiters are allowed, but the literal * between \tag and { was
tokenized separately and reset that flag before the argument group
was reached. \tag now consumes an immediately following * so the
text-mode flag survives to the argument group.
2026-07-07 02:36:08 +10:00
Mark 60d2dc25ab Add \proof environment to autocomplete list
amsthm's \proof environment wasn't in the built-in environment
completions, so it only appeared in the autocomplete dropdown after
being typed several times elsewhere in the document.
2026-07-07 02:08:25 +10:00
Tim Gibbon 05565653a1 Merge branch 'main' into main 2026-06-26 22:08:06 +01:00
Kristina 28ad3b03b7 [web] add email notifications dry run split test (#34653)
GitOrigin-RevId: 62f73ce3850b994bf2a4a054edd6fa62134213a9
2026-06-26 08:08:18 +00:00
Alexandre Bourdin 33b8f27097 Merge pull request #34489 from overleaf/ab-keydown-event-logging
[web] Trigger full project search / focus mode on lowercase key matching for keyboard shortcuts

GitOrigin-RevId: a1786050c205ed46229b939c6325a3130c6fbcd3
2026-06-26 08:08:14 +00:00
Jakob Ackermann a6c98365d9 [web] use client-side filtering for split tests in admin panel (#34975)
* [web] use client-side filtering for split tests in admin panel

Filtering on non-indexed fields is not allowed with notablescan.

* [web] add initial value for DEFAULT_FILTERS.active

* [web] unset error before fetching split tests

GitOrigin-RevId: 0813873c5a90d57c4fdbfd0043fcc325f00408aa
2026-06-26 08:08:10 +00:00
Jakob Ackermann 1b553c0455 [migrations] only apply non-blocking migrations in CI (#34665)
* [migrations] only apply non-blocking migrations in CI

* [migrations] use "yarn run migrations" instead of "npm run migrations"

GitOrigin-RevId: d9f96db398adf2febcd82333295fb8d5eab741bb
2026-06-26 08:08:05 +00:00
Jakob Ackermann 9fd0d644cf [agents] refine guidance for running tests (#34977)
* [monorepo] fix handling of quoted MOCHA_GREP

* [agents] refine guidance for running tests

- nudge it towards scoped module targets
- nudge it to really run e2e tests when changed
- nudge it towards a11y cypress matchers
- recommend quoting for MOCHA_GREP

* [web] fix forwarding of mocha args from module Makefile

* [monorepo] use BRANCH_NAME_TAG_SAFE for building the ci image name

* [web] also make BRANCH_NAME_TAG_SAFE available in module Makefile

(It's not really needed, but will make copilot happy.)

GitOrigin-RevId: bfda3c8fe2b4f857ef111ac63f94b35a82ad75f8
2026-06-26 08:07:37 +00:00
Eric Mc Sween c645215afc Merge pull request #34907 from overleaf/em-promisify-github-sync
Promisify github-sync

GitOrigin-RevId: ac68f3f18ad211e204631d80bd5d4354ea23aaff
2026-06-26 08:07:30 +00:00
Eric Mc Sween 2c54574377 Merge pull request #34922 from overleaf/lg-vite-security-upgrade-v2
[Security upgrade] bump vite to 7.3.5 in root workspace

GitOrigin-RevId: 278475d98e23f0679ec45993e2a6816e9b93eb64
2026-06-26 08:07:25 +00:00
Eric Mc Sween 4fb8608908 Merge pull request #34884 from overleaf/em-already-in-your-library
Library import: flag entries that are already in the user's library

GitOrigin-RevId: f3c92fb59283631e1a9e78d2e08a3a365981df45
2026-06-26 08:07:21 +00:00
Mathias Jakobsen 75f7f82a3f Merge pull request #34923 from overleaf/mj-left-menu-field
[web] Clean up "left menu" concepts in new editor

GitOrigin-RevId: 9597c1c53af15f3a1587e62176bca36cd7c9069c
2026-06-26 08:07:10 +00:00
Mathias Jakobsen 5dd2c425ac Merge pull request #34912 from overleaf/mj-file-tree-style-cleanup
[web] Clean up file tree scss post editor redesign

GitOrigin-RevId: de2c5f3bd2979600897ffbbdcaead7ef72fa6ae1
2026-06-26 08:07:05 +00:00
Kate Crichton 9034e81c9b Merge pull request #34508 from overleaf/kc-email-admin-domain-reverification-failed
[web] email group admin when domain reverification failed

GitOrigin-RevId: d885f26b1b39184c25133f70024a72af66fa2d69
2026-06-26 08:07:01 +00:00
Kate Crichton 907f5a969a Merge pull request #34841 from overleaf/kc-grace-period-failedat-anchor
[web] Anchor domain-capture grace period on failedAt

GitOrigin-RevId: 5a31e5189512b22952c387722c7ad67f5c6397e3
2026-06-26 08:06:57 +00:00
Andrew Rumble bbf5586935 Merge pull request #34919 from overleaf/lg-undici-security-upgrade
[Security upgrade] bump undici to 7.28.0 in root workspace

GitOrigin-RevId: 39a6fb92235c6453fbd6106c05af66fb1d089607
2026-06-26 08:06:49 +00:00
Maria Florencia Besteiro Gonzalez 1e03046d68 Merge pull request #34587 from overleaf/lg-security-netty-codec-http2
[Security upgrade] bump io.netty:netty-codec-http2 to 4.1.135.Final in services/git-bridge

GitOrigin-RevId: e6b6b110bd4ce9c70c91f701cc3cb4f0868f870a
2026-06-25 08:06:26 +00:00
Jessica Lawshe 6c9b4aa498 Merge pull request #34768 from overleaf/jel-email-when-domain-verified
[web] Email group admin when domain is verified

GitOrigin-RevId: 5f8bc6187c561067d16863d3586a94ef498f3bdf
2026-06-25 08:06:22 +00:00
Jessica Lawshe 02d9c344f2 Merge pull request #34821 from overleaf/jel-clear-domain-notifications
[web] Make domain notifications dismissible

GitOrigin-RevId: fa1648efd0aa73213e5352b347a1b0f4c07dfc04
2026-06-25 08:06:18 +00:00
Jessica Lawshe 4a635d7599 Merge pull request #34819 from overleaf/jel-domain-table-spacing
[web] Update spacing in groups domain table

GitOrigin-RevId: 6e908b79e45212cf060068271ec56c3146fe20d7
2026-06-25 08:06:13 +00:00
Mathias Jakobsen a038e4b3a2 Merge pull request #34903 from overleaf/mj-settings-tabs-hover
[web] Add hover states to settings tabs

GitOrigin-RevId: c41c200231d0308267fc346d2949ef659a1950bc
2026-06-25 08:05:59 +00:00
Tim Down 3e29ce839f Merge pull request #34602 from overleaf/td-stripe-3ds1-redirect
Fix for Stripe redirecting to incorrect page

GitOrigin-RevId: ac90b2e501c4bf5f2738d61866ea72ba861e5539
2026-06-25 08:05:54 +00:00
ilkin-overleaf 13bc31cf25 Merge pull request #34870 from overleaf/ii-share-modal-group-sharing
[web] Make group project sharing available to Group Professional plans only

GitOrigin-RevId: ced66529818d573cfd20cef016ed4b3aafdd8d51
2026-06-25 08:05:50 +00:00
ilkin-overleaf f536b39b03 Merge pull request #34627 from overleaf/ii-share-modal-improvements
[web] New share modal improvements

GitOrigin-RevId: d8b47a6294f8d7e4833130fd2bb1720bfe2357d8
2026-06-25 08:05:45 +00:00
roo hutton 0fab15c58e Merge pull request #34730 from overleaf/rh-tax-id-verify
Validate tax ids and send emails on failed verification

GitOrigin-RevId: 84d53fdaf00d7b23ced22dd540a071e3028ced93
2026-06-25 08:05:34 +00:00
Brian Gough 29c8717b60 Merge pull request #33845 from overleaf/lg-ws-security-upgrade
[Security upgrade] pin ws@^8 to 8.21.0

GitOrigin-RevId: 40b839757122c1116debe8b7ebfa4eba9187bfa8
2026-06-24 08:06:51 +00:00
Andrew Rumble 255a6242cb Merge pull request #34340 from overleaf/ar-things-that-dont-need-to-be-pinned
[dependency-management] find unnecessary pins script

GitOrigin-RevId: b935ee7ae72c0339ea58f14d5f0a991ed4ae350c
2026-06-24 08:06:36 +00:00
Jakob Ackermann a8b4f2dc55 [web] reduce mongo lookups for compile requests (#34890)
* [web] _getUserCompileLimits: use owner.analyticsId directly

* [web] deduplicate project lookups for compile requests

* [web] add test case for prefetched project

GitOrigin-RevId: 0e140619c874accf8673153ed90af2ce70cf9234
2026-06-24 08:06:18 +00:00
Mathias Jakobsen 795ba4cd41 Merge pull request #34655 from overleaf/mj-ai-error-assistant-ide-redesign-cleanup
Clean up ai error assistant  style after editor redesign

GitOrigin-RevId: 8ca0747a567fe8f46b4d76886cd0723e1721ced1
2026-06-24 08:05:58 +00:00
Mathias Jakobsen fe52c3df12 Merge pull request #34872 from overleaf/mj-bulk-review-permissions
[web] Hide bulk track change actions for reviewers

GitOrigin-RevId: f636f4c82fdfb43a6dedf1abbb3cc53805eab7f8
2026-06-24 08:05:53 +00:00
Mathias Jakobsen c625ce428e Merge pull request #34875 from overleaf/mj-logs-link-colour
[web] Avoid default blue link colours in log headers

GitOrigin-RevId: 56359ededf5ba8c1a615bf1bf84ba6502f9c0078
2026-06-24 08:05:49 +00:00
David 7ce1e0bd48 Merge pull request #34797 from overleaf/dp-cleanup-reference-settings-migrated-field
[web] Add migration to unset reference settings migrated flag

GitOrigin-RevId: 570621d7a78f225a4bbf98f7046b096ffa735048
2026-06-24 08:05:38 +00:00
Brian Gough c6a46fbdad Merge pull request #33961 from overleaf/lg-qs-security-upgrade
[Security upgrade] pin qs to 6.15.2 in root yarn.lock

GitOrigin-RevId: 4819f0a1f9b41821ccda354a309d190d8395ef56
2026-06-24 08:05:31 +00:00
Brian Gough 81f85293f2 Merge pull request #33656 from overleaf/lg-protobufjs-security-upgrade
[Security upgrade] Pin protobufjs to 7.6.3 and @protobufjs/utf8 to 1.1.1 via resolutions

GitOrigin-RevId: 7d71d564d7296af76f04fda8b35f178c5014a9b9
2026-06-24 08:05:26 +00:00
Jakob Ackermann 1cc18dd0f0 [web] avoid UserAnalyticsIdCache for split-test assignments (#34803)
* [web] split-tests: add helpers to using an already fetched mongo user

* [web] split-tests: add ignoreOverrides option to getAssignment

* [web] split-tests: reuse already fetched mongo users where possible

* [web] notifications: fetch users in bulk for split-test assignment

* [web] split-tests: use session for assignment where possible

* [web] split-tests: pass req/res down into domain-capture hook

* [web] split-tests: explicitly throw early

* [web] split-tests: refine jsdoc types

* [web] mark analyticsId and labsProgram as required in user model

* [web] split-tests: refine jsdoc types

* Revert "[web] split-tests: pass req/res down into domain-capture hook"

This reverts commit 897a158384cd2b3e416fc34495b20d1e2a0e68f0.

GitOrigin-RevId: ea4afd15c2506f369827b5193fd16d220d3c7d0e
2026-06-23 08:06:42 +00:00
Jakob Ackermann 6b084424b5 [monorepo] fix running make lint from root (#33559)
* [monorepo] remove spurious package-name field from buildscript.txt

* [monorepo] align variables/paths across build script variants

* [monorepo] align linting/test setup for jobs/libraries

* [monorepo] fix "make lint/lint_fix" from root

* [wiki-algolia] integrate with yarn workspace

GitOrigin-RevId: a0954f2936d03cd4a69b3f2c8098d9ee2e361993
2026-06-23 08:06:37 +00:00
Eric Mc Sween 006b30c441 Merge pull request #34817 from overleaf/em-library-backend-search-2
Library search: make backend search tokens consistent with frontend (re-apply #34643)

GitOrigin-RevId: e5a143edf3226c12eaff77b2bc4cdb68e139bb10
2026-06-23 08:06:33 +00:00
Jimmy Domagala-Tang 5cd4447cbf Adding 2026 TeXLive behind admin (#34291)
* feat: adding 2026 behind admin

* feat: bumping packer and clsi memory to accomodate new image

* align packer build and packer verify disk sizes

* feat: adding more space for packer build

GitOrigin-RevId: 1961ea7937e2c751137a4b9564a34d4e371ce0f9
2026-06-23 08:06:14 +00:00
Jakob Ackermann 88267716a3 [web] adopt free/premium compile backend class (#34846)
* [web] adopt free/premium compile backend class

* [rails] adopt free compile backend class for pdf conversion

GitOrigin-RevId: e12c99a27363f5654132c2fcdc6c5d8ccc591868
2026-06-23 08:05:59 +00:00
Brian Gough 23dca72cd3 Merge pull request #34843 from overleaf/bg-add-compile-timer
add timer for compile with checkpoints

GitOrigin-RevId: ec758d34594f242a5cac8192233bfe2db783e42a
2026-06-23 08:05:55 +00:00
Brian Gough b1ed38c2d0 Merge pull request #34761 from overleaf/bg-enable-clsi-checkpoints
adds enableCheckpoint to compile requests from web to clsi

GitOrigin-RevId: 431b28602091cf8f7252ad836f083c7b092351d4
2026-06-23 08:05:50 +00:00
Jakob Ackermann 3be2f320f8 [web] fix preference of new/old clsiServerId in double-compiles (#34862)
GitOrigin-RevId: 81cbbcec9fef79ebd0c5e7d90048fc4bda302108
2026-06-23 08:05:42 +00:00
Brian Gough d75ef19bbf Merge pull request #34746 from overleaf/bg-clsi-apparmor
add new clsi apparmor profile for checkpoints

GitOrigin-RevId: 3a003263326613d3166343d54adb7050fe1e9847
2026-06-23 08:05:38 +00:00
Mathias Jakobsen e0cfb07f60 Merge pull request #34698 from overleaf/mj-context-menu-tracked-changes
[web] Add tracked changes options to context menu

GitOrigin-RevId: b49198c6c2c63063d96ace69f10b617ac819bb04
2026-06-23 08:05:33 +00:00
Kristina a53b164b45 [web] email content previews for notification emails (#34576)
* add content previews to notification emails
* add fetcher for doc lines
* gate content previews behind a split test
* make composable blocks for rendering

GitOrigin-RevId: c8df384d55a301084b98df76fd3edd6dde6ea50c
2026-06-22 08:06:25 +00:00
Domagoj Kriskovic 63a605484c Add support for rendering math in Markdown with MathJax
GitOrigin-RevId: dd1c1c72237a5364c642e46c66e08ea7a6977662
2026-06-22 08:05:40 +00:00
Mathias Jakobsen ef05288f81 Merge pull request #34714 from overleaf/mj-tracked-change-merges
[overleaf-editor-core+ranges-tracker] Pick lower timestamp when merging tracking info

GitOrigin-RevId: 5b1c8d7c719003df4506014c381f2a525d0a7ff4
2026-06-19 08:07:27 +00:00
Eric Mc Sween 7ee385fe45 Merge pull request #34816 from overleaf/revert-34643-em-library-backend-search
Revert "Library search: make backend search tokens consistent with frontend (follow-up to #34386)"

GitOrigin-RevId: 83328f2ee4a9c3f668325fa4de1ca652e7c718bf
2026-06-19 08:07:19 +00:00
Eric Mc Sween 80218df85e Merge pull request #34643 from overleaf/em-library-backend-search
Library search: make backend search tokens consistent with frontend (follow-up to #34386)

GitOrigin-RevId: 91ef5b5dc81ba8ebda43e746c9b747b7636f3e80
2026-06-19 08:07:12 +00:00
Eric Mc Sween 0371ba46f6 Merge pull request #34693 from overleaf/em-upload-bib
[web] Add "Upload .bib file" option to library

GitOrigin-RevId: 7067a4cb10f0973c0423507836ef13555329942c
2026-06-19 08:07:08 +00:00
Kate Crichton 0883e0fec0 Merge pull request #34513 from overleaf/kc-verify-domain-from-table
[web] verify domain from table

GitOrigin-RevId: 2e41f76be26b81f204208fab714f2e3399071499
2026-06-19 08:06:34 +00:00
Kate Crichton f7b1ccae9a Merge pull request #33852 from overleaf/kc-schedule-domain-verification
[web] schedule domain reverification

GitOrigin-RevId: 08217692960379d0a6f7ce457032b1182fb2bf4e
2026-06-19 08:06:30 +00:00
Liam O'BrienandClaude Opus 4.8 bc9c85019f Lo zotero initial library sync (#34456)
* Migration and index setup script

* Helper script to clear linked library

* Pull users collections

* Implementing get items from users zotero library

* Initial zotero sync client

* Pulling users overleaf collection

* Pulling users overleaf collection

* Initial sync pull from Zotero to Library

* Sync and import workflow from Zotero into Library

* Using user getter and setter for updating user library info

* Using new collection for library sync states

* Adding tests for new sync functionality

* Harden Zotero sync route

* Simplify Zotero sync state read and skip accounting

Fold the ensure+get double round-trip in sync-state discovery into one
findOneAndUpdate, and derive itemsSkipped from items.length - itemsInserted
instead of a running counter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Redact URL-encoded token secret from OAuth1 request logs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fixing type failure

* Rename upsertZoteroReferences to importZoteroReferences

The method is insert-only; the bulkWrite upsert just makes re-runs
idempotent rather than updating, so the name now reflects that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Validate librarySyncStates reads with zod

Replace the `as` / `as unknown as` casts in getLibrarySyncState and
ensureLibrarySyncState with a zod schema parse, matching
LibraryReferenceRepository. Types are now derived from the schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Defer Zotero sync watermark until pagination exists

Don't record lastSyncedVersion after a single-page sync: committing the
page-1 version before pagination (#34286) lands would let incremental
sync (#34282) skip items beyond the first page. Drops the commitSyncState
call and toVersion from SyncResult; the version envelope is left for the
follow-ups. commitSyncState stays in the repository for #34282.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Extract OAuth1Transport from OAuth1Handler

Lift the OAuth1 client/config registry, getAccessToken, redactSecret and
classifyOAuth1Error into oauth1/OAuth1Transport.js, plus a signedGet
helper for provider modules to reuse. OAuth1Handler now imports them; its
method bodies are unchanged. Test mocks move to esmock's global scope so
they reach the transport, with Errors pinned for instanceof checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Move Zotero collections/items into a per-provider ZoteroProvider

Carve the Zotero-only operations out of the generic OAuth1 handler into
providers/ZoteroProvider.js, dispatched via providers/ProviderRegistry.js.
ReferenceProviderHandler now resolves the provider from the registry and
throws UnsupportedOperationError (mapped to 501) for providers without the
operation, instead of bolting Zotero-only methods onto OAuth1Handler.

Zotero's collectionsUri/itemsTopUri are read from settings by ZoteroProvider
rather than copied into the shared OAuth1 registry. Removes getCollections/
getUsersItemsChanges (and their tests) from OAuth1Handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add ZoteroProvider unit tests and unsupported-provider coverage

Cover ZoteroProvider collections/items parsing, header envelope and
malformed-payload rejection, and assert ReferenceProviderHandler throws
UnsupportedOperationError for providers without a sync implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Make addSyncedCollection idempotent

Only push a synced collection when its key isn't already recorded, so a
repeat sync can't create duplicate syncedCollections entries regardless of
the caller-side guard. Adds a sequential test asserting no duplicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Hardcode Zotero library endpoint paths, derive host from site

Move the collections/items-top endpoints out of settings and into
ZoteroProvider as path constants, taking only the host from the
provider's `site`. Aligns Zotero `site` with the other providers
(API host) and avoids adding per-endpoint config points.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make Zotero sync routes explicit, 404 unknown providers

Hardcode the collections/items routes to /zotero rather than a generic
/:ref_provider, so a non-Zotero request 404s on the missing route
instead of returning a misleading 501. Drop the UnsupportedOperationError
501 branch, reverting OAuth2 unsupported ops to their prior 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: 50e16a9c46f995c2e9db6eed50ef81c3a71ea71d
2026-06-19 08:06:07 +00:00
Antoine ClausseandClaude Opus 4.8 e5f712a857 [web] Cleanup plans-2026-phase-1 - phase 2: pug/backend pricing (#34623)
* Remove `plans2026` from pug files in subscriptions module

* Remove `plans2026` param from `makeGroupPlans`

* Remove `plans2026` param from `makeUISettings` in interstitial-payment.mjs

* Remove `plans2026` param from `makeInterstitialPaymentConfig` in interstitial-payment.mjs

* Remove `plans2026` param from `makeIndividualPlans`

* Remove `plans2026` param from `makeStudentPlans`

* Remove `plans2026` from try-premium page

* Remove `plans2026` from controllers

* Remove unused files

* Update translations

* Update tests

* Update tests

* Remove unused translations

* Rename the collaborator upgrade-mode test to read explicitly as the
Standard-plan replacement for the removed redirect test, and drop the
stale "when plans 2026 is enabled" framing now the controller no longer
branches on the flag.

* Surface controller errors in InterstitialPayment tests

Move assertions out of the res.render callback into render/redirect
helpers that resolve with the captured args and reject on the wrong
path or a controller rejection, so failures show the real error
instead of timing out. Restore the ai-limit-upgrade-copy split-test
assignment for the AI copy tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* format

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: f0b055b4d92b494b531766a13b057336d9c2c9af
2026-06-19 08:06:03 +00:00
David d6e55f81ff Merge pull request #34750 from overleaf/dp-cleanup-reference-settings-migration
[web] Remove reference manager settings migration code

GitOrigin-RevId: 1062e1f08a6e6d4547f48c965c342d2ac15e9a4f
2026-06-19 08:05:56 +00:00
Copilot ea1871b0df Use Ctrl+Shift+C for the add-comment shortcut while keeping Cmd+Shift+C active (#32624)
GitOrigin-RevId: 5fdf9ae7dd0fa194c3ac6acf9abb70ac9721fd2e
2026-06-19 08:05:48 +00:00
Mathias Jakobsen b570078223 Merge pull request #34774 from overleaf/mj-fix-cleanup-locales
[web] Ignore cache files in cleanupUnusedLocales

GitOrigin-RevId: 72ad8fb265a6fe905671dd379b3ca29468c9f759
2026-06-19 08:05:10 +00:00
Mathias Jakobsen 5f79c2dd10 Merge pull request #34512 from overleaf/mj-floating-toolbar-option
[web] Add setting for floating toolbar menu

GitOrigin-RevId: bd28f1f287f2011535c7fd530f1217f01e0cfbbc
2026-06-18 08:07:33 +00:00
Jessica Lawshe 106dd1b07b Merge pull request #34702 from overleaf/jel-grace-period-ui
[web] Update domain verification failed grace period UI

GitOrigin-RevId: 267dc4e4cc60b959faaacecc5a13b662d670e313
2026-06-18 08:07:09 +00:00
MoxAmber a3036730b4 Merge pull request #34644 from overleaf/as-link-sharing-analytics
[web] Add sharing link analytics events

GitOrigin-RevId: b206732397065c399d38a8e42dbdebe60222fb24
2026-06-18 08:06:52 +00:00
MoxAmber ad9b28af18 Merge pull request #34537 from overleaf/ii-share-modal-invited-people
Show "No one invited yet" instead of "1 person invited" for solo projects

GitOrigin-RevId: 0423b507e0574dc5a381cbe4044912a25af7a3f0
2026-06-18 08:06:48 +00:00
MoxAmber a7a3ccdb71 Merge pull request #34639 from overleaf/as-link-sharing-editinvitecount
[web] Ignore reuable invites (aka sharing links) when checking edit invite count

GitOrigin-RevId: 355ec6e6b2d5d497392fe1bdc04e68ad80349518
2026-06-18 08:06:44 +00:00
MoxAmber bd8a156908 Merge pull request #34394 from overleaf/as-link-sharing-groups-split-test
[web] Move group link sharing behind a separate split test

GitOrigin-RevId: c973a9bccb88f427a9f51d121b58a8799993e7c9
2026-06-18 08:06:39 +00:00
Domagoj Kriskovic 62c2523aec Add table support for visual markdown editor
GitOrigin-RevId: b36f575d40b19fdf32622c9923cecd5b47658d5d
2026-06-18 08:06:26 +00:00
Eric Mc Sween 95e0a88bbb Merge pull request #34519 from overleaf/em-github-sync-octokit-migration
Replace octonode with octokit in github-sync

GitOrigin-RevId: 05886b87ed8625dbbcb77dc2d6ccbeda7b426e79
2026-06-18 08:06:22 +00:00
roo hutton 1dfe6e60b3 Merge pull request #34534 from overleaf/rh-rm-recurly
[web] Remove conditional Recurly checkout page

GitOrigin-RevId: 5bb773080bba682628f0f5428c72b7dff75b6015
2026-06-18 08:06:17 +00:00
Davinder SinghandCopilot Autofix powered by AI 6613ef2f18 [WEB] Move Review Toggle into the toolbar - 2 (#34578)
* wip of adding Review Toggle into the toolbar

* renaming to isToolbarMigration

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* removing extra comments

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 2c0b54ae983a32c083def8e9e64ddd0fb7ddf334
2026-06-18 08:06:10 +00:00
Malik Glossop b04fea4726 Merge pull request #34559 from overleaf/mg-writefull-workbench
Allow opening workbench and hide texgpt for ai menu behind flag

GitOrigin-RevId: f4de64ce9b68f4e337772e164e46e27d2a41a0e7
2026-06-18 08:06:06 +00:00
Antoine Clausse 0d004926e6 [web] Cleanup plans-2026-phase-1 - phase 1: frontend (#34619)
* Remove `plans-2026-phase-1` from frontend in subscriptions module

* Remove `plans-2026-phase-1` from general frontend

* Remove empty `AiErrorAssistantPaywall`

* Simplify `UpgradeNotification`

* Update translations

* Fix tests

* Remove `OverleafAssistPaywall`

* Remove unused locales

GitOrigin-RevId: 4cf04bbde134a3f7b778a0695d388f31d9765343
2026-06-18 08:06:01 +00:00
Mathias Jakobsen fb9ebfcc10 Merge pull request #34459 from overleaf/mj-review-panel-style-cleanup
[web] Clean up review styles post-editor redesign

GitOrigin-RevId: b8b535f765f446770e62e31519f7f1e98dc7e3d2
2026-06-18 08:05:54 +00:00
Mathias Jakobsen a14971b790 Merge pull request #34600 from overleaf/mj-collapsible-file-header-cleanup
[web] Clean up collapsible file header styles post editor redesign

GitOrigin-RevId: a5c8749360433213d6602d029ea6b66312c9032b
2026-06-18 08:05:49 +00:00
Rebeka Dekany 51fc266bc2 Redirect users without an active subscription to the subscription page instead of the error page (#34637)
* Redirect freemium users from billing portal routes to subscription page with banner

* Handle null management link and unknown service in subscription redirects

* Move deleted-subscription invoices link inside `getCustomerManagementLink` instead

GitOrigin-RevId: 7122f69e9b8bda6238111f76c15a35135ce5f0ac
2026-06-18 08:05:22 +00:00
Jakob AckermannandAndrew Rumble 6323fddbd8 [web] avoid UserAnalyticsIdCache (#33121)
* [web] always provide an analyticsId when registering new users

* [web] add recordEventForMongoUser and use it during login

* [web] use recordEventForMongoUser/recordEventForSession where possible

* [web] use session cache for analyticsId when refreshing features

* [web] add setUserPropertyForMongoUser and use it where possible

* [web] use setUserPropertyForSessionInBackground where possible

* [web] use mongo user for analyticsId when setting utm user properties

* [web] admin-panel: user updated user when sending events

* [web] enforce labsProgram being populated on user object

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>

* [web] revert FeaturesUpdater.refreshFeatures change

* [web] labs: move session maintenance into handler

* [web] analyticsMiddleware: ensure that all users have labsProgram field

* [web] migrate a few more events to recordEventForSession

* [migrations] mark back-fill of db.users.analyticsId as required

* [web] setUserPropertyForSessionInBackground: throw early

---------

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>
GitOrigin-RevId: 19c26bc36abd8c11f0a8f458d25abfb7cbda0bb7
2026-06-17 08:08:32 +00:00
Anna Claire Fields 7035a2e025 Revert "Acf pnp migration master" (#34709)
GitOrigin-RevId: ec7d63e1212e05dd6dfc7a0ddf785c11eca76ad6
2026-06-17 08:07:56 +00:00
Anna Claire Fields e1226515f6 PNP deployment: clsi-lb, web, latexqc fixes (#34696)
GitOrigin-RevId: fe1f5a7670d6fcf2128c5e2b50cc2a544afb1ebc
2026-06-17 08:07:36 +00:00
Anna Claire Fields 8f07308342 Acf pnp migration master (#34040)
GitOrigin-RevId: 418eab9bc733f27a6e0f8f04a2a8e21c2f5a491f
2026-06-17 08:07:16 +00:00
Jakob Ackermann d44e862a0c [web] throw early in UserCreator.createNewUser with missing analyticsId (#34664)
* [web] throw early in UserCreator.createNewUser with missing analyticsId

* [migrations] rerun back-fill of db.users.analyticsId

GitOrigin-RevId: eabf16326192fca4dbea00ad643cee4315c426c6
2026-06-17 08:06:59 +00:00
Jakob Ackermann 6bc9f1dae4 [server-ce] test: the first startWith must resetData when using dataDir (#34671)
* [server-ce] test: the first startWith must resetData when using dataDir

* [server-ce] test: fix order of creating user and resetting DB

GitOrigin-RevId: 2a6e6344919fe48d41704e1a3cb39c6e22b5a924
2026-06-17 08:06:54 +00:00
Malik Glossop bf22bf9eed Merge pull request #34487 from overleaf/mg-fix-nested-context-menu
Block native context menu on editor context menu and fix focus styling

GitOrigin-RevId: c2d59e7acc59019b5e2777cc1bf3ad24a7d8e54e
2026-06-17 08:06:49 +00:00
Malik Glossop 8ef463f8b0 Merge pull request #34357 from overleaf/mg-combine-floating-menus
Add combined floating menu for comment, tracked changes, writefull

GitOrigin-RevId: 4de5b9d3b1717fc76394f7c0edfe4d7230394c8d
2026-06-17 08:06:44 +00:00
David 9fae908602 Merge pull request #34585 from overleaf/lg-security-hapi-wreck-joi
[Security upgrade] bump @hapi/wreck from 18.1.0 to 18.1.2 in services/third-party-references

GitOrigin-RevId: a6efbbb9b7c8ddf995c247f8b787c16fa8b6d6f3
2026-06-17 08:06:34 +00:00
Davinder Singh 8e8804a318 [WEB] Add TeXGPT button to the main toolbar (#34219)
* feat: add writefull-toolbar-migration split test and
  sourceEditorToolbarStartButtons slot

* feat: add TeXGPT button to main editor toolbar (writefull-toolbar-migration)

* removing wrapper div

* hide toolbar start buttons from readonly users

GitOrigin-RevId: 6bafe3ad3fade98c318ea0d0b2714957c1d2d9b4
2026-06-17 08:06:16 +00:00
Davinder Singh e1881a7645 [WEB] Toolbar overflow: drop one button at a time (#34171)
* adding individual hiding logic for the toolbar icons

* using css for logic

GitOrigin-RevId: 87e9f72c1dab888e2701ef108ecaa0b190e366ff
2026-06-17 08:06:11 +00:00
CopilotandJakob Ackermann f40cebf60b [docstore] replace remaining Joi usage with zod (#34603)
Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
GitOrigin-RevId: 41ca0c35d59bf6accd619f6b5149be14a69e1078
2026-06-16 08:07:22 +00:00
Brian Gough 6f9b7031f4 pass through checkpoint setting (#34631)
GitOrigin-RevId: 50e9791b468253ee966b0eac822b2a01321a0ece
2026-06-16 08:07:14 +00:00
Brian Gough be4108a573 add glob matching to isExtraneousFile (#34633)
GitOrigin-RevId: f12aedb52195cfc9e3254936f1db6189f49c3f37
2026-06-16 08:07:09 +00:00
Domagoj Kriskovic 0778a0e4d3 Revert "Add markdown link tooltip support to the visual editor"
This reverts commit 7ab4fd0793f057c86e4fc0307d81df376efe4d34.

GitOrigin-RevId: 625a5cbbb39832f6a32a26d2ad454669d0ed1846
2026-06-16 08:07:01 +00:00
Rebeka DekanyandM Fahru 1f28eaecdb [web] Add Stripe acceptance tests (#34577)
* Add StripeClient mock-target support and Stripe test settings

Co-Authored-By: M Fahru <m.fahru@overleaf.com>

* Add Stripe payment page acceptance tests

Co-Authored-By: M Fahru <m.fahru@overleaf.com>

* Add MockStripeApi for stateful Stripe acceptance tests

Co-Authored-By: M Fahru <m.fahru@overleaf.com>

* Add Stripe subscription acceptance tests

* Add Stripe webhook acceptance tests

* Use a helper for setting the subscription status

---------

Co-authored-by: M Fahru <m.fahru@overleaf.com>
GitOrigin-RevId: b9bc50c914369da646ddb42ade1a02fd7baeb96b
2026-06-16 08:06:57 +00:00
Jakob AckermannandAndrew Rumble f2ebe46506 [web] add migration for back-filling db.users.labsProgram=false (#34632)
* [web] add migration for back-filling db.users.labsProgram=false

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>

* [web] include user.labsProgram=false in session

---------

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>
GitOrigin-RevId: 0b3322007f6a36d2999d646869dc84f2c6ad4d2d
2026-06-16 08:06:43 +00:00
Jakob Ackermann cb9e834a1f [project-history] return status "423 Locked" when redis lock is taken (#34429)
GitOrigin-RevId: f40230bc98e8ed8a5358e6db5c515cb2a58f9b85
2026-06-16 08:06:38 +00:00
Jakob Ackermann b11027d58f [monorepo] add clsi-sharded mode (#34614)
* [monorepo] provide clsi-sharded dev-env setup

* [monorepo] provide sharded clsi-nginx when using clsi-sharded

* [clsi-perf] run on all instances again

* [monorepo] switch default TL to 2025.1 in clsi (same as web)

* [monorepo] avoid starting base clsi when running clsi-perf with clsi-sharded

* [clsi-lb] use 3s check interval in clsi-sharded mode

GitOrigin-RevId: 3cae8f0c1db69dbd76b94b45dc45b3abb2fa0a83
2026-06-16 08:06:34 +00:00
Andrew Rumble d22cea01ce Merge pull request #33404 from overleaf/copilot/fix-accepts-language-deprecation
Fix deprecated req.acceptsLanguage → req.acceptsLanguages in Translations.mjs

GitOrigin-RevId: db9f09d999158ba5f9f084bc1a8334954a55cd79
2026-06-16 08:06:29 +00:00
Mathias Jakobsen b7946791f4 Merge pull request #34553 from overleaf/mj-pandoc-html-mathml
[clsi] Add mathml option to HTML export

GitOrigin-RevId: c0736f0ae78020c2c554e8aaed9472f050323fd4
2026-06-16 08:06:25 +00:00
Mathias Jakobsen 481a0b1da9 Merge pull request #34479 from overleaf/mj-toolbar-border
[web] Hide toolbar border when tabs are enabled

GitOrigin-RevId: 58a33980006d1bc148db16d864fc0547d9c5d421
2026-06-16 08:06:21 +00:00
Mathias Jakobsen 7096b0f8a3 Merge pull request #34496 from overleaf/mj-history-style-cleanup
[web] Clean up history styles post-editor redesign

GitOrigin-RevId: fffdc0074000f6076984b35e3f84467e4d881c09
2026-06-16 08:06:16 +00:00
Mathias Jakobsen b85702f169 Merge pull request #34592 from overleaf/mj-full-project-search-redesign-cleanup
[web] Clean up full-project-search styles post-editor redesign

GitOrigin-RevId: a93660b48f8b088244f5d99db41752402032953f
2026-06-16 08:06:12 +00:00
Mathias Jakobsen 69ec7cdeb6 Merge pull request #34575 from overleaf/mj-file-view-cleanup
[web] Clean up file view styles post editor redesign

GitOrigin-RevId: 702200b1dc0ff6dee44e9163cf9ee3540ad1f621
2026-06-16 08:06:07 +00:00
Mathias Jakobsen b7f984fc8d [web] View menu tweaks (#34548)
* [web] Add missing translation

* [web] Group PDF zoom options in view menu

* [web] Add review panel toggle to view menu

* [web] Address review comments

* [web] Show disabled buttons to anonymous users

* [web] Add e2e tests for editing mode in view menu

GitOrigin-RevId: d13dc307f1bcb7c50c76afbaa397c3aca8b96029
2026-06-16 08:06:03 +00:00
roo hutton efd12c8feb Merge pull request #34484 from overleaf/rh-ol-stripe-id
Sync updated customer email to Stripe/Recurly

GitOrigin-RevId: b2becc50cc63739bdf56e45c37939f84c6fcd7a8
2026-06-16 08:05:58 +00:00
Lucie Germain ed166a7291 [Security upgrade] upgrade react-router to 6.30.4 in latexqc (#34324)
* Upgrade react-router to 6.30.4 (GHSA-2j2x-hqr9-3h42)

GitOrigin-RevId: 1224dc24bac65a8d2ee2d7c3f83d900a22aae5a0
2026-06-16 08:05:54 +00:00
Domagoj Kriskovic 8ccdc0fbc4 Add markdown link tooltip support to the visual editor
GitOrigin-RevId: 7ab4fd0793f057c86e4fc0307d81df376efe4d34
2026-06-16 08:05:46 +00:00
Tim Gibbon 632c61d842 Point to my branch 2026-06-02 19:42:06 +01:00
Tim Gibbon fe01e7787f Remove WIP pod copies 2026-06-02 19:36:49 +01:00
Tim Gibbon 72db0c69e6 Merge pull request #1 from tommybobbins/add-kubernetes-in-podman-demo
Add sample kind setup
2026-06-02 19:32:25 +01:00
Tim Gibbon 1367ddeb60 Add sample kind setup 2026-06-02 19:28:36 +01:00
2625 changed files with 144445 additions and 71395 deletions
-25
View File
@@ -1,25 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[Makefile]
indent_style = tab
[*.go]
indent_style = tab
[*.{pug,coffee}]
indent_style = tab
[*.{pug,patch}]
trim_trailing_whitespace = false
[Jenkinsfile]
insert_final_newline = false
max_line_length = off
+2 -6
View File
@@ -8,12 +8,8 @@ assignees: ''
<!--
Note: If you are using www.overleaf.com and have a problem,
or if you would like to request a new feature please contact
the support team at support@overleaf.com
This form should only be used to report bugs in the
Community Edition release of Overleaf.
Note: This form should only be used to report bugs in the
Extended CE release of Overleaf.
-->
-4
View File
@@ -5,7 +5,3 @@
## Related issues / Pull Requests
<!-- Fixes #xyz, Contributes to #xyz, Related to #xyz-->
## Contributor Agreement
- [ ] I confirm I have signed the [Contributor License Agreement](https://github.com/overleaf/overleaf/blob/main/CONTRIBUTING.md#contributor-license-agreement)
+58
View File
@@ -0,0 +1,58 @@
// Resolve ESM imports through Yarn PnP.
//
// Yarn ships .pnp.loader.mjs for use with module.register(), which runs hooks on a dedicated
// thread — a second V8 isolate costing 50-110MB in every node process, whether or not the
// loader does any work. registerHooks() runs synchronously in-thread instead, and PnP's own
// resolveRequest already applies "exports" conditions, so the hook is just a lookup.
import { registerHooks, isBuiltin, createRequire } from 'node:module'
import { pathToFileURL, fileURLToPath } from 'node:url'
import path from 'node:path'
const pnp = createRequire(import.meta.url)('pnpapi')
registerHooks({
resolve(specifier, context, nextResolve) {
// require() is patched by .pnp.cjs, and builtins never reach the filesystem.
if (context.conditions.includes('require') || isBuiltin(specifier)) {
return nextResolve(specifier, context)
}
// Only bare specifiers need PnP. The entrypoint arrives as a path, and mocha imports test
// files by file:// URL; both are already resolved.
let url
if (specifier.startsWith('file://')) {
url = specifier
} else if (!context.parentURL) {
url = pathToFileURL(specifier).href
} else {
// Tools installed outside the project — npm, corepack — bring their own node_modules.
// PnP does not own those paths and refuses to resolve from them.
const issuer = path.dirname(fileURLToPath(context.parentURL)) + '/'
if (!pnp.findPackageLocator(issuer)) {
return nextResolve(specifier, context)
}
try {
url = pathToFileURL(
pnp.resolveRequest(specifier, issuer, {
conditions: new Set(context.conditions),
})
).href
} catch (err) {
// PnP reports a missing module with the CommonJS code. require() returned above, so this is an ESM import and node's own resolver would raise the ESM code, which is what callers of an optional import catch.
if (err.code === 'MODULE_NOT_FOUND') err.code = 'ERR_MODULE_NOT_FOUND'
throw err
}
}
try {
// Keep the resolved path in the chain so loaders registered later can still mock it.
return nextResolve(url, context)
} catch (err) {
// Node cannot look inside PnP's zips, nor at its __virtual__ paths, so PnP's answer is
// final for those. Everything else — a genuinely missing file, an error raised by
// another loader — is a real failure.
if (url.includes('/__virtual__/') || url.includes('.zip/')) {
return { url, shortCircuit: true }
}
throw err
}
},
})
@@ -0,0 +1,67 @@
diff --git a/lib/request.js b/lib/request.js
index f42960ca2b94b784497392cfb85ef31e4e63a1ac..7feea2ed29873e519aee0ba5e1f5b36e09fe865d 100644
--- a/lib/request.js
+++ b/lib/request.js
@@ -6,6 +6,30 @@
const InvalidArgumentError = require('./errors/invalid-argument-error');
/* type-is: https://github.com/jshttp/type-is */
const typeis = require('type-is');
+// --- Overleaf patch: request-input lockdown compatibility ---
+// The constructor below destructures query/body directly off the raw
+// framework request object it's handed (see AuthenticationController.mjs's
+// `new Oauth2Server.Request(req)`), which is otherwise indistinguishable
+// from unvalidated application code reading raw request input under
+// Overleaf's request-input lockdown (see libraries/validation-tools and the
+// patched express in .yarn/patches/express-npm-*.patch). The lockdown
+// captures the original parsed values in symbol-keyed fields, registered in
+// the global symbol registry (the same ones @overleaf/validation-tools'
+// lockdown.js reads), and only turns the *public* req.query/req.body
+// properties into throwing accessors -- reading through these raw fields
+// instead sidesteps that, and is identical to plain req.query/req.body when
+// the lockdown isn't installed (i.e. for every consumer of this package
+// outside this monorepo).
+const __overleafLockdownInstalled = Symbol.for('overleaf.lockdown.installed');
+const __overleafLockdownRawQuery = Symbol.for('overleaf.lockdown.rawQuery');
+const __overleafLockdownRawBody = Symbol.for('overleaf.lockdown.rawBody');
+function __overleafRawQuery(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query;
+}
+function __overleafRawBody(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawBody] : req.body;
+}
+// --- end Overleaf patch ---
/**
* Wrapper for webserver's request.
@@ -29,7 +53,12 @@ class Request {
* @param otherOptions {...object} any other properties that should be assigned to the request by your webserver
* @throws {InvalidArgumentError} if one of headers, method or query are missing.
*/
- constructor({ headers, method, query, body, ...otherOptions } = {}) {
+ constructor(req = {}) {
+ const headers = req.headers;
+ const method = req.method;
+ const query = __overleafRawQuery(req);
+ const body = __overleafRawBody(req);
+
if (!headers) {
throw new InvalidArgumentError('Missing parameter: `headers`');
}
@@ -52,7 +81,16 @@ class Request {
this.headers[header.toLowerCase()] = value;
});
- // Store additional properties of the request object passed in
+ // Store additional properties of the request object passed in (mirrors
+ // the original `...otherOptions` rest destructuring, but built off
+ // Object.assign so query/body -- already handled above via the raw
+ // lockdown fields -- aren't read a second time through the throwing
+ // accessors).
+ const otherOptions = Object.assign({}, req);
+ delete otherOptions.headers;
+ delete otherOptions.method;
+ delete otherOptions.query;
+ delete otherOptions.body;
Object.entries(otherOptions)
.filter(([property]) => !this[property])
.forEach(([property, value]) => {
@@ -0,0 +1,23 @@
diff --git a/lib/saml.js b/lib/saml.js
index 64180fdd7fe6302bf8da7d1ca65c01bb1ea5e2c1..7015c3b73603f624c15195a0a38c2fbf750a4c60 100644
--- a/lib/saml.js
+++ b/lib/saml.js
@@ -327,7 +327,8 @@ class SAML {
const requestOrResponse = request || response;
(0, utility_1.assertRequired)(requestOrResponse, "either request or response is required");
let buffer;
- if (this.options.skipRequestCompression) {
+ // logout requestOrResponse must be compressed anyway
+ if (this.options.skipRequestCompression && operation !== "logout") {
buffer = Buffer.from(requestOrResponse, "utf8");
}
else {
@@ -510,7 +511,7 @@ class SAML {
try {
xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8");
doc = await (0, xml_1.parseDomFromString)(xml);
- const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
+ const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response' or local-name()='LogoutResponse']/@InResponseTo");
if (inResponseToNodes) {
inResponseTo = inResponseToNodes.length ? inResponseToNodes[0].nodeValue : null;
await this.validateInResponseTo(inResponseTo);
@@ -0,0 +1,99 @@
diff --git a/lib/strategy.js b/lib/strategy.js
index 4de162f7e92a20b1ccd4d34dd9bcc0916f4c8c43..c67bb4e0b0287d615eac4c01b84c83d2bac91446 100644
--- a/lib/strategy.js
+++ b/lib/strategy.js
@@ -5,6 +5,29 @@ const passport_strategy_1 = require("passport-strategy");
const assert_1 = require("assert");
const url = require("url");
const node_saml_1 = require("@node-saml/node-saml");
+// --- Overleaf patch: request-input lockdown compatibility ---
+// This library reads req.query/req.body directly to parse the SAML protocol
+// fields (SAMLResponse, SAMLRequest, RelayState) it's built around, which is
+// otherwise indistinguishable from unvalidated application code reading raw
+// request input under Overleaf's request-input lockdown (see
+// libraries/validation-tools and the patched express in
+// .yarn/patches/express-npm-*.patch). The lockdown captures the original
+// parsed values in symbol-keyed fields, registered in the global symbol
+// registry (the same ones @overleaf/validation-tools' lockdown.js reads), and
+// only turns the *public* req.query/req.body properties into throwing
+// accessors -- reading through these raw fields instead sidesteps that, and
+// is identical to plain req.query/req.body when the lockdown isn't installed
+// (i.e. for every consumer of this package outside this monorepo).
+const __overleafLockdownInstalled = Symbol.for("overleaf.lockdown.installed");
+const __overleafLockdownRawQuery = Symbol.for("overleaf.lockdown.rawQuery");
+const __overleafLockdownRawBody = Symbol.for("overleaf.lockdown.rawBody");
+function __overleafRawQuery(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query;
+}
+function __overleafRawBody(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawBody] : req.body;
+}
+// --- end Overleaf patch ---
class AbstractStrategy extends passport_strategy_1.Strategy {
constructor(options, signonVerify, logoutVerify) {
super();
@@ -52,7 +75,7 @@ class AbstractStrategy extends passport_strategy_1.Strategy {
catch (_c) {
userMatch = false;
}
- const RelayState = ((_a = req.query) === null || _a === void 0 ? void 0 : _a.RelayState) || ((_b = req.body) === null || _b === void 0 ? void 0 : _b.RelayState);
+ const RelayState = ((_a = __overleafRawQuery(req)) === null || _a === void 0 ? void 0 : _a.RelayState) || ((_b = __overleafRawBody(req)) === null || _b === void 0 ? void 0 : _b.RelayState);
if (this._saml == null) {
return this.error(new Error("Can't get logout response URL without a SAML provider defined."));
}
@@ -125,22 +148,22 @@ class AbstractStrategy extends passport_strategy_1.Strategy {
this.redirect(url);
}
};
- if (((_a = req.query) === null || _a === void 0 ? void 0 : _a.SAMLResponse) || ((_b = req.query) === null || _b === void 0 ? void 0 : _b.SAMLRequest)) {
+ if (((_a = __overleafRawQuery(req)) === null || _a === void 0 ? void 0 : _a.SAMLResponse) || ((_b = __overleafRawQuery(req)) === null || _b === void 0 ? void 0 : _b.SAMLRequest)) {
const originalQuery = (_c = url.parse(req.url).query) !== null && _c !== void 0 ? _c : "";
this._saml
- .validateRedirectAsync(req.query, originalQuery)
+ .validateRedirectAsync(__overleafRawQuery(req), originalQuery)
.then(validateCallback)
.catch((err) => this.error(err));
}
- else if ((_d = req.body) === null || _d === void 0 ? void 0 : _d.SAMLResponse) {
+ else if ((_d = __overleafRawBody(req)) === null || _d === void 0 ? void 0 : _d.SAMLResponse) {
this._saml
- .validatePostResponseAsync(req.body)
+ .validatePostResponseAsync(__overleafRawBody(req))
.then(validateCallback)
.catch((err) => this.error(err));
}
- else if ((_e = req.body) === null || _e === void 0 ? void 0 : _e.SAMLRequest) {
+ else if ((_e = __overleafRawBody(req)) === null || _e === void 0 ? void 0 : _e.SAMLRequest) {
this._saml
- .validatePostRequestAsync(req.body)
+ .validatePostRequestAsync(__overleafRawBody(req))
.then(validateCallback)
.catch((err) => this.error(err));
}
@@ -151,7 +174,7 @@ class AbstractStrategy extends passport_strategy_1.Strategy {
if (this._saml == null) {
throw new Error("Can't process login request without a SAML provider defined.");
}
- const RelayState = (req.query && req.query.RelayState) || (req.body && req.body.RelayState);
+ const RelayState = (__overleafRawQuery(req) && __overleafRawQuery(req).RelayState) || (__overleafRawBody(req) && __overleafRawBody(req).RelayState);
const host = req.headers && req.headers.host;
if (this._saml.options.authnRequestBinding === "HTTP-POST") {
const data = await this._saml.getAuthorizeFormAsync(RelayState, host);
@@ -172,7 +195,7 @@ class AbstractStrategy extends passport_strategy_1.Strategy {
throw new Error("Can't process logout request without a SAML provider defined.");
}
try {
- const RelayState = (req.query && req.query.RelayState) || (req.body && req.body.RelayState);
+ const RelayState = (__overleafRawQuery(req) && __overleafRawQuery(req).RelayState) || (__overleafRawBody(req) && __overleafRawBody(req).RelayState);
// Defaults to HTTP-Redirect
this.redirect(await this._saml.getLogoutUrlAsync(req.user, RelayState, options));
}
@@ -188,7 +211,7 @@ class AbstractStrategy extends passport_strategy_1.Strategy {
if (this._saml == null) {
throw new Error("Can't logout without a SAML provider defined.");
}
- const RelayState = (req.query && req.query.RelayState) || (req.body && req.body.RelayState);
+ const RelayState = (__overleafRawQuery(req) && __overleafRawQuery(req).RelayState) || (__overleafRawBody(req) && __overleafRawBody(req).RelayState);
this._saml
.getLogoutUrlAsync(req.user, RelayState, {})
.then((url) => callback(null, url))
@@ -0,0 +1,132 @@
diff --git a/lib/read.js b/lib/read.js
index fce6283f50961e68c2f576031ed5e3d4fdc39984..6131c31fb8ba8d413e0a3bf887ebd87f1c3a4007 100644
--- a/lib/read.js
+++ b/lib/read.js
@@ -18,7 +18,7 @@ var iconv = require('iconv-lite')
var onFinished = require('on-finished')
var unpipe = require('unpipe')
var zlib = require('zlib')
-
+var Stream = require('stream')
/**
* Module exports.
*/
@@ -166,25 +166,25 @@ function contentstream (req, debug, inflate) {
case 'deflate':
stream = zlib.createInflate()
debug('inflate body')
- req.pipe(stream)
+ // req.pipe(stream)
break
case 'gzip':
stream = zlib.createGunzip()
debug('gunzip body')
- req.pipe(stream)
+ // req.pipe(stream)
break
case 'identity':
stream = req
stream.length = length
- break
+ return req
default:
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
encoding: encoding,
type: 'encoding.unsupported'
})
}
-
- return stream
+ var pass = new Stream.PassThrough(); Stream.pipeline(req, stream, pass, () => {})
+ return pass
}
/**
diff --git a/lib/types/json.js b/lib/types/json.js
index 59f3f7e28f2416cac957b5e35dc78839a5019e0d..f23e52eca828a726f12e915f8ea52698d3761d68 100644
--- a/lib/types/json.js
+++ b/lib/types/json.js
@@ -17,6 +17,8 @@ var contentType = require('content-type')
var createError = require('http-errors')
var debug = require('debug')('body-parser:json')
var read = require('../read')
+var LOCKDOWN_INSTALLED = Symbol.for('overleaf.lockdown.installed')
+var LOCKDOWN_RAW_BODY = Symbol.for('overleaf.lockdown.rawBody')
var typeis = require('type-is')
/**
@@ -105,7 +107,7 @@ function json (options) {
return
}
- req.body = req.body || {}
+ req.body = (req[LOCKDOWN_INSTALLED] ? req[LOCKDOWN_RAW_BODY] : req.body) || {}
// skip requests without bodies
if (!typeis.hasBody(req)) {
diff --git a/lib/types/raw.js b/lib/types/raw.js
index f5d1b67475405284e3dac312f92ade101571329f..251780a624b09340b8a821974565fa330f9792b7 100644
--- a/lib/types/raw.js
+++ b/lib/types/raw.js
@@ -13,6 +13,8 @@
var bytes = require('bytes')
var debug = require('debug')('body-parser:raw')
var read = require('../read')
+var LOCKDOWN_INSTALLED = Symbol.for('overleaf.lockdown.installed')
+var LOCKDOWN_RAW_BODY = Symbol.for('overleaf.lockdown.rawBody')
var typeis = require('type-is')
/**
@@ -59,7 +61,7 @@ function raw (options) {
return
}
- req.body = req.body || {}
+ req.body = (req[LOCKDOWN_INSTALLED] ? req[LOCKDOWN_RAW_BODY] : req.body) || {}
// skip requests without bodies
if (!typeis.hasBody(req)) {
diff --git a/lib/types/text.js b/lib/types/text.js
index 083a00908a6299a8ef72f477983359f5675f82ef..4ea3ee88191bf02ecedf6c975d20144d4d7f2f47 100644
--- a/lib/types/text.js
+++ b/lib/types/text.js
@@ -14,6 +14,8 @@ var bytes = require('bytes')
var contentType = require('content-type')
var debug = require('debug')('body-parser:text')
var read = require('../read')
+var LOCKDOWN_INSTALLED = Symbol.for('overleaf.lockdown.installed')
+var LOCKDOWN_RAW_BODY = Symbol.for('overleaf.lockdown.rawBody')
var typeis = require('type-is')
/**
@@ -61,7 +63,7 @@ function text (options) {
return
}
- req.body = req.body || {}
+ req.body = (req[LOCKDOWN_INSTALLED] ? req[LOCKDOWN_RAW_BODY] : req.body) || {}
// skip requests without bodies
if (!typeis.hasBody(req)) {
diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js
index 832992c2c3e0324c9bcb612a01f2fe2fb3e19414..4da13852cd5a89c97d8d7be2b81ba8fd62551f9e 100644
--- a/lib/types/urlencoded.js
+++ b/lib/types/urlencoded.js
@@ -18,6 +18,8 @@ var createError = require('http-errors')
var debug = require('debug')('body-parser:urlencoded')
var deprecate = require('depd')('body-parser')
var read = require('../read')
+var LOCKDOWN_INSTALLED = Symbol.for('overleaf.lockdown.installed')
+var LOCKDOWN_RAW_BODY = Symbol.for('overleaf.lockdown.rawBody')
var typeis = require('type-is')
/**
@@ -83,7 +85,7 @@ function urlencoded (options) {
return
}
- req.body = req.body || {}
+ req.body = (req[LOCKDOWN_INSTALLED] ? req[LOCKDOWN_RAW_BODY] : req.body) || {}
// skip requests without bodies
if (!typeis.hasBody(req)) {
@@ -1,44 +0,0 @@
diff --git a/lib/read.js b/lib/read.js
index fce6283..6131c31 100644
--- a/lib/read.js
+++ b/lib/read.js
@@ -18,7 +18,7 @@ var iconv = require('iconv-lite')
var onFinished = require('on-finished')
var unpipe = require('unpipe')
var zlib = require('zlib')
-
+var Stream = require('stream')
/**
* Module exports.
*/
@@ -166,25 +166,25 @@ function contentstream (req, debug, inflate) {
case 'deflate':
stream = zlib.createInflate()
debug('inflate body')
- req.pipe(stream)
+ // req.pipe(stream)
break
case 'gzip':
stream = zlib.createGunzip()
debug('gunzip body')
- req.pipe(stream)
+ // req.pipe(stream)
break
case 'identity':
stream = req
stream.length = length
- break
+ return req
default:
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
encoding: encoding,
type: 'encoding.unsupported'
})
}
-
- return stream
+ var pass = new Stream.PassThrough(); Stream.pipeline(req, stream, pass, () => {})
+ return pass
}
/**
@@ -0,0 +1,46 @@
diff --git a/index.js b/index.js
index 367bdbafbc71b61daf3907913c3861d155140f07..b31ff2d98186ac29446718edfccc03a3562e796a 100644
--- a/index.js
+++ b/index.js
@@ -17,6 +17,30 @@ var Cookie = require('cookie')
var createError = require('http-errors')
var sign = require('cookie-signature').sign
var Tokens = require('csrf')
+// --- Overleaf patch: request-input lockdown compatibility ---
+// The default value getter reads req.body/req.query directly to find the
+// CSRF token submitted alongside a mutating request -- otherwise
+// indistinguishable from unvalidated application code reading raw request
+// input under Overleaf's request-input lockdown (see
+// libraries/validation-tools and the patched express in
+// .yarn/patches/express-npm-*.patch). The lockdown captures the original
+// parsed values in symbol-keyed fields, registered in the global symbol
+// registry (the same ones @overleaf/validation-tools' lockdown.js reads),
+// and only turns the *public* req.query/req.body properties into throwing
+// accessors -- reading through these raw fields instead sidesteps that, and
+// is identical to plain req.query/req.body when the lockdown isn't
+// installed (i.e. for every consumer of this package outside this
+// monorepo).
+var __overleafLockdownInstalled = Symbol.for('overleaf.lockdown.installed')
+var __overleafLockdownRawQuery = Symbol.for('overleaf.lockdown.rawQuery')
+var __overleafLockdownRawBody = Symbol.for('overleaf.lockdown.rawBody')
+function __overleafRawQuery (req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query
+}
+function __overleafRawBody (req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawBody] : req.body
+}
+// --- end Overleaf patch ---
/**
* Module exports.
@@ -128,8 +152,8 @@ function csurf (options) {
*/
function defaultValue (req) {
- return (req.body && req.body._csrf) ||
- (req.query && req.query._csrf) ||
+ return (__overleafRawBody(req) && __overleafRawBody(req)._csrf) ||
+ (__overleafRawQuery(req) && __overleafRawQuery(req)._csrf) ||
(req.headers['csrf-token']) ||
(req.headers['xsrf-token']) ||
(req.headers['x-csrf-token']) ||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,55 @@
diff --git a/index.js b/index.js
index 49f181005f74cf11db3bd5b0ecf512520163ec71..d47ad4270883b90766152342c99495a07cb952e0 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,29 @@
const parseCookie = require('cookie').parse;
const decodeCookie = require('cookie-parser').signedCookie;
+// --- Overleaf patch: request-input lockdown compatibility ---
+// This middleware is mounted globally, before any router split, and reads
+// req.query/req.body directly to look for a bearer token -- otherwise
+// indistinguishable from unvalidated application code reading raw request
+// input under Overleaf's request-input lockdown (see
+// libraries/validation-tools and the patched express in
+// .yarn/patches/express-npm-*.patch). The lockdown captures the original
+// parsed values in symbol-keyed fields, registered in the global symbol
+// registry (the same ones @overleaf/validation-tools' lockdown.js reads),
+// and only turns the *public* req.query/req.body properties into throwing
+// accessors -- reading through these raw fields instead sidesteps that, and
+// is identical to plain req.query/req.body when the lockdown isn't
+// installed (i.e. for every consumer of this package outside this
+// monorepo).
+const __overleafLockdownInstalled = Symbol.for('overleaf.lockdown.installed');
+const __overleafLockdownRawQuery = Symbol.for('overleaf.lockdown.rawQuery');
+const __overleafLockdownRawBody = Symbol.for('overleaf.lockdown.rawBody');
+function __overleafRawQuery(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query;
+}
+function __overleafRawBody(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawBody] : req.body;
+}
+// --- end Overleaf patch ---
const getCookie = (serialized_cookies, key) => parseCookie(serialized_cookies)[key] || false;
@@ -32,16 +56,16 @@ module.exports = opts => {
let error;
// query
- if (req.query && req.query[queryKey]) {
- token = req.query[queryKey];
+ if (__overleafRawQuery(req) && __overleafRawQuery(req)[queryKey]) {
+ token = __overleafRawQuery(req)[queryKey];
}
// body
- if (req.body && req.body[bodyKey]) {
+ if (__overleafRawBody(req) && __overleafRawBody(req)[bodyKey]) {
if (token) {
error = true;
}
- token = req.body[bodyKey];
+ token = __overleafRawBody(req)[bodyKey];
}
// headers
@@ -0,0 +1,287 @@
diff --git a/lib/lockdown.js b/lib/lockdown.js
new file mode 100644
index 0000000000000000000000000000000000000000..7acc55484421812ddb66edf015cdf0e1125b1cab
--- /dev/null
+++ b/lib/lockdown.js
@@ -0,0 +1,157 @@
+/*!
+ * Overleaf request-input lockdown.
+ *
+ * Captures parsed request input in symbol-keyed fields and, when
+ * REQ_LOCKDOWN_MODE is 'warn' or 'throw', turns the public req.body,
+ * req.query and req.params properties into lockdown accessors:
+ *
+ * - reads log a warning ('warn' mode) or throw ('throw' mode); request
+ * input must be read through parseReq() from @overleaf/validation-tools,
+ * which validates it against a zod schema, or through getRawReqInput()
+ * for allowlisted middleware
+ * - writes always work and are stored in the symbol-keyed fields, so body
+ * parsers and middleware that assign req.body keep functioning unchanged
+ *
+ * With REQ_LOCKDOWN_MODE unset (or 'off') nothing is installed and express
+ * behaves exactly like the unpatched package.
+ *
+ * The symbols are registered in the global symbol registry so that
+ * @overleaf/validation-tools can read the raw fields without importing
+ * express internals. Express' own internals (router param processing, the
+ * query middleware, res.jsonp) go through the helpers below instead of the
+ * public properties so that only application code trips the lockdown.
+ *
+ * Note: unlike the stock properties, the locked accessors are defined
+ * non-enumerable so that iterating/serializing a request (e.g. in logging)
+ * does not trip the lockdown.
+ */
+
+'use strict';
+
+var RAW_BODY = Symbol.for('overleaf.lockdown.rawBody');
+var RAW_QUERY = Symbol.for('overleaf.lockdown.rawQuery');
+var RAW_PARAMS = Symbol.for('overleaf.lockdown.rawParams');
+var INSTALLED = Symbol.for('overleaf.lockdown.installed');
+
+var VALID_MODES = ['off', 'warn', 'throw'];
+var cachedMode = null;
+
+function mode() {
+ if (cachedMode === null) {
+ var m = process.env.REQ_LOCKDOWN_MODE || 'off';
+ cachedMode = VALID_MODES.indexOf(m) === -1 ? 'off' : m;
+ }
+ return cachedMode;
+}
+
+/**
+ * Override the mode for tests. Pass null to re-read REQ_LOCKDOWN_MODE on the
+ * next access. Only affects requests that have not been handled yet.
+ */
+function setModeForTests(m) {
+ if (m === null) {
+ cachedMode = null;
+ } else {
+ cachedMode = VALID_MODES.indexOf(m) === -1 ? 'off' : m;
+ }
+}
+
+var warned = new Set();
+
+function report(req, field) {
+ var route = req.route && req.route.path;
+ var location =
+ (req.method || 'UNKNOWN') +
+ ' ' +
+ (route || req.originalUrl || req.url || 'unknown');
+ if (mode() === 'warn') {
+ var key = location + ' ' + field;
+ if (!warned.has(key)) {
+ warned.add(key);
+ var stack = new Error('trace').stack.split('\n').slice(2, 7).join('\n');
+ console.warn(
+ 'req-lockdown: raw request input read: req.' +
+ field +
+ ' at ' +
+ location +
+ '\n' +
+ stack
+ );
+ }
+ return;
+ }
+ var err = new Error(
+ 'raw request input is forbidden — use parseReq() (read req.' +
+ field +
+ ' at ' +
+ location +
+ ')'
+ );
+ err.code = 'ERR_RAW_REQ_INPUT';
+ throw err;
+}
+
+/**
+ * Install the lockdown accessors on a request. Idempotent, no-op in 'off'
+ * mode. Called from the router, the query middleware and expressInit so that
+ * every request is covered no matter how the app is assembled.
+ */
+function install(req) {
+ if (mode() === 'off') return;
+ if (req[INSTALLED]) return;
+ req[INSTALLED] = true;
+ lockField(req, 'body', RAW_BODY);
+ lockField(req, 'query', RAW_QUERY);
+ lockField(req, 'params', RAW_PARAMS);
+}
+
+function lockField(req, field, symbol) {
+ // preserve a value assigned before lockdown was installed
+ if (Object.prototype.hasOwnProperty.call(req, field)) {
+ req[symbol] = req[field];
+ delete req[field];
+ }
+ Object.defineProperty(req, field, {
+ configurable: true,
+ enumerable: false,
+ get: function () {
+ report(this, field);
+ return this[symbol]; // only reached in 'warn' mode
+ },
+ set: function (value) {
+ this[symbol] = value;
+ },
+ });
+}
+
+/**
+ * Effective params/query for express internals: the raw fields once lockdown
+ * is installed, the plain properties otherwise.
+ */
+function getParams(req) {
+ return req[INSTALLED] ? req[RAW_PARAMS] : req.params;
+}
+
+function getQuery(req) {
+ return req[INSTALLED] ? req[RAW_QUERY] : req.query;
+}
+
+/**
+ * Property key under which the router saves/restores params on the request.
+ */
+function paramsProp(req) {
+ return req[INSTALLED] ? RAW_PARAMS : 'params';
+}
+
+module.exports = {
+ RAW_BODY: RAW_BODY,
+ RAW_QUERY: RAW_QUERY,
+ RAW_PARAMS: RAW_PARAMS,
+ INSTALLED: INSTALLED,
+ mode: mode,
+ setModeForTests: setModeForTests,
+ install: install,
+ getParams: getParams,
+ getQuery: getQuery,
+ paramsProp: paramsProp,
+};
diff --git a/lib/middleware/init.js b/lib/middleware/init.js
index dfd042747bdb971d9760e9575241ddaa1b776cd8..162ec6da452bde5f5384d3d5bd79d5727742ed8f 100644
--- a/lib/middleware/init.js
+++ b/lib/middleware/init.js
@@ -14,6 +14,7 @@
*/
var setPrototypeOf = require('setprototypeof')
+var lockdown = require('../lockdown')
/**
* Initialization middleware, exposing the
@@ -27,6 +28,7 @@ var setPrototypeOf = require('setprototypeof')
exports.init = function(app){
return function expressInit(req, res, next){
+ lockdown.install(req);
if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
req.res = res;
res.req = req;
diff --git a/lib/middleware/query.js b/lib/middleware/query.js
index 7e9166947aff3be10f7ffec3771c92581ce211da..aad897b7ecd43477c1768e135ac06211a2424d36 100644
--- a/lib/middleware/query.js
+++ b/lib/middleware/query.js
@@ -15,6 +15,7 @@
var merge = require('utils-merge')
var parseUrl = require('parseurl');
var qs = require('qs');
+var lockdown = require('../lockdown');
/**
* @param {Object} options
@@ -37,7 +38,9 @@ module.exports = function query(options) {
}
return function query(req, res, next){
- if (!req.query) {
+ lockdown.install(req);
+
+ if (!lockdown.getQuery(req)) {
var val = parseUrl(req).query;
req.query = queryparse(val, opts);
}
diff --git a/lib/response.js b/lib/response.js
index 2b654f4c6629c821bbeaaeee970b199a420fd8b3..103df49d3d94abf017022cdcf4611e62cad84a74 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -29,6 +29,7 @@ var normalizeType = require('./utils').normalizeType;
var normalizeTypes = require('./utils').normalizeTypes;
var setCharset = require('./utils').setCharset;
var cookie = require('cookie');
+var lockdown = require('./lockdown');
var send = require('send');
var extname = path.extname;
var mime = send.mime;
@@ -312,7 +313,7 @@ res.jsonp = function jsonp(obj) {
var replacer = app.get('json replacer');
var spaces = app.get('json spaces');
var body = stringify(val, replacer, spaces, escape)
- var callback = this.req.query[app.get('jsonp callback name')];
+ var callback = lockdown.getQuery(this.req)[app.get('jsonp callback name')];
// content-type
if (!this.get('Content-Type')) {
diff --git a/lib/router/index.js b/lib/router/index.js
index abb3a6f589e834c5e105f2a96907ed2eb6fc0185..66fef252003098bcc268ef2ebcef0d20b3cce62b 100644
--- a/lib/router/index.js
+++ b/lib/router/index.js
@@ -22,6 +22,7 @@ var deprecate = require('depd')('express');
var flatten = require('array-flatten');
var parseUrl = require('parseurl');
var setPrototypeOf = require('setprototypeof')
+var lockdown = require('../lockdown');
/**
* Module variables.
@@ -136,6 +137,8 @@ proto.param = function param(name, fn) {
proto.handle = function handle(req, res, out) {
var self = this;
+ lockdown.install(req);
+
debug('dispatching %s %s', req.method, req.url);
var idx = 0;
@@ -153,9 +156,9 @@ proto.handle = function handle(req, res, out) {
var stack = self.stack;
// manage inter-router variables
- var parentParams = req.params;
+ var parentParams = lockdown.getParams(req);
var parentUrl = req.baseUrl || '';
- var done = restore(out, req, 'baseUrl', 'next', 'params');
+ var done = restore(out, req, 'baseUrl', 'next', lockdown.paramsProp(req));
// setup next layer
req.next = next;
@@ -368,7 +371,7 @@ proto.process_params = function process_params(layer, called, req, res, done) {
paramIndex = 0;
key = keys[i++];
name = key.name;
- paramVal = req.params[name];
+ paramVal = lockdown.getParams(req)[name];
paramCallbacks = params[name];
paramCalled = called[name];
@@ -380,7 +383,7 @@ proto.process_params = function process_params(layer, called, req, res, done) {
if (paramCalled && (paramCalled.match === paramVal
|| (paramCalled.error && paramCalled.error !== 'route'))) {
// restore value
- req.params[name] = paramCalled.value;
+ lockdown.getParams(req)[name] = paramCalled.value;
// next param
return param(paramCalled.error);
@@ -400,7 +403,7 @@ proto.process_params = function process_params(layer, called, req, res, done) {
var fn = paramCallbacks[paramIndex++];
// store updated value
- paramCalled.value = req.params[key.name];
+ paramCalled.value = lockdown.getParams(req)[key.name];
if (err) {
// store error
@@ -0,0 +1,73 @@
diff --git a/lib/ldapauth.js b/lib/ldapauth.js
index 88398ed31afee34c8958ad5e7a53653e1b5f6f82..0334177af8a65c7c5f9de7570924ab0f359e3f38 100644
--- a/lib/ldapauth.js
+++ b/lib/ldapauth.js
@@ -87,6 +87,7 @@ function LdapAuth(opts) {
this.opts.bindProperty || (this.opts.bindProperty = 'dn');
this.opts.groupSearchScope || (this.opts.groupSearchScope = 'sub');
this.opts.groupDnProperty || (this.opts.groupDnProperty = 'dn');
+ this.opts.tlsStarted = false;
EventEmitter.call(this);
@@ -126,21 +127,7 @@ function LdapAuth(opts) {
this._userClient.on('error', this._handleError.bind(this));
var self = this;
- if (this.opts.starttls) {
- // When starttls is enabled, this callback supplants the 'connect' callback
- this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function (err) {
- if (err) {
- self._handleError(err);
- } else {
- self._onConnectAdmin();
- }
- });
- this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function (err) {
- if (err) {
- self._handleError(err);
- }
- });
- } else if (opts.reconnect) {
+ if (opts.reconnect && !this.opts.starttls) {
this.once('_installReconnectListener', function () {
self.log && self.log.trace('install reconnect listener');
self._adminClient.on('connect', function () {
@@ -401,6 +388,37 @@ LdapAuth.prototype._findGroups = function (user, callback) {
*/
LdapAuth.prototype.authenticate = function (username, password, callback) {
var self = this;
+ if (this.opts.starttls && !this.opts.tlsStarted) {
+ // When starttls is enabled, this callback supplants the 'connect' callback
+ this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function (err) {
+ if (err) {
+ self._handleError(err);
+ } else {
+ self._onConnectAdmin(function(){self._handleAuthenticate(username, password, callback);});
+ }
+ });
+ this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function (err) {
+ if (err) {
+ self._handleError(err);
+ }
+ });
+ } else {
+ self._handleAuthenticate(username, password, callback);
+ }
+};
+
+/**
+ * Authenticate given credentials against LDAP server
+ *
+ * @private
+ * @param {string} username - The username to authenticate
+ * @param {string} password - The password to verify
+ * @param {resultCallback} callback - Result handling callback
+ * @returns {undefined}
+ */
+LdapAuth.prototype._handleAuthenticate = function (username, password, callback) {
+ this.opts.tlsStarted = true;
+ var self = this;
if (typeof password === 'undefined' || password === null || password === '') {
return callback(new Error('no password given'));
-13
View File
@@ -1,13 +0,0 @@
diff --git a/lib/make-middleware.js b/lib/make-middleware.js
index ee50988..de77364 100644
--- a/lib/make-middleware.js
+++ b/lib/make-middleware.js
@@ -164,7 +164,7 @@ function makeMiddleware (setup) {
if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
// don't attach to the files object, if there is no file
- if (!filename) return fileStream.resume()
+ if (!filename) filename = 'undefined'
// Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) {
@@ -0,0 +1,34 @@
diff --git a/lib/make-middleware.js b/lib/make-middleware.js
index 2c15f248d4f4d221bd9e2b2e1092b8d5c46d2bdd..859f55f56729f0a3874cfdac53722497cfc3c461 100644
--- a/lib/make-middleware.js
+++ b/lib/make-middleware.js
@@ -26,7 +26,10 @@ function makeMiddleware (setup) {
var preservePath = options.preservePath
var defParamCharset = options.defParamCharset
- req.body = Object.create(null)
+ // keep a local reference: with the Overleaf request-input lockdown
+ // installed, writing req.body is allowed but reading it warns/throws
+ var body = Object.create(null)
+ req.body = body
var busboy
var appender = null
@@ -157,7 +160,7 @@ function makeMiddleware (setup) {
if (fieldname.split('[').length - 1 > limits.fieldNestingDepth) return abortWithCode('LIMIT_FIELD_NESTING', fieldname)
}
- appendField(req.body, fieldname, value)
+ appendField(body, fieldname, value)
})
// handle files
@@ -174,7 +177,7 @@ function makeMiddleware (setup) {
if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
// don't attach to the files object, if there is no file
- if (!filename) return fileStream.resume()
+ if (!filename) filename = 'undefined'
// Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) {
+22 -1
View File
@@ -1,5 +1,5 @@
diff --git a/lib/index.js b/lib/index.js
index 567ff5d..8eb45f7 100644
index 567ff5d..a7a38dd 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -545,8 +545,8 @@ function clone(instance) {
@@ -74,3 +74,24 @@ index 567ff5d..8eb45f7 100644
response = new Response(body, response_options);
resolve(response);
return;
@@ -1738,13 +1738,13 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) {
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
response.once('close', function (hadError) {
- // tests for socket presence, as in some situations the
- // the 'socket' event is not triggered for the request
- // (happens in deno), avoids `TypeError`
- // if a data listener is still present we didn't end cleanly
- const hasDataListener = socket && socket.listenerCount('data') > 0;
-
- if (hasDataListener && !hadError) {
+ // A kept-alive socket (Node >= 24.16) still has its 'data'
+ // listener attached when the response 'close' event fires, so
+ // the old socket.listenerCount('data') check reported a false
+ // "Premature close". response.complete is true only once the
+ // terminating zero-length chunk has been received, so it tells
+ // a clean end apart from a genuine truncation.
+ if (!response.complete && !hadError) {
const err = new Error('Premature close');
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
errorCallback(err);
@@ -0,0 +1,25 @@
diff --git a/lib/strategy.js b/lib/strategy.js
index e54e92a86c8e3d40338ed8c6e57e8729393d85a3..cad017ef2027ad676782515a9803835138efa6b3 100644
--- a/lib/strategy.js
+++ b/lib/strategy.js
@@ -66,10 +66,18 @@ util.inherits(Strategy, passport.Strategy);
* @param {Object} req
* @api protected
*/
+var LOCKDOWN_INSTALLED = Symbol.for('overleaf.lockdown.installed');
+var LOCKDOWN_RAW_BODY = Symbol.for('overleaf.lockdown.rawBody');
+var LOCKDOWN_RAW_QUERY = Symbol.for('overleaf.lockdown.rawQuery');
+
Strategy.prototype.authenticate = function(req, options) {
options = options || {};
- var username = lookup(req.body, this._usernameField) || lookup(req.query, this._usernameField);
- var password = lookup(req.body, this._passwordField) || lookup(req.query, this._passwordField);
+ // with the Overleaf request-input lockdown installed, read the raw
+ // symbol-keyed fields; the public req.body/req.query accessors warn/throw
+ var body = req[LOCKDOWN_INSTALLED] ? req[LOCKDOWN_RAW_BODY] : req.body;
+ var query = req[LOCKDOWN_INSTALLED] ? req[LOCKDOWN_RAW_QUERY] : req.query;
+ var username = lookup(body, this._usernameField) || lookup(query, this._usernameField);
+ var password = lookup(body, this._passwordField) || lookup(query, this._passwordField);
if (!username || !password) {
return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400);
+73 -1
View File
@@ -1,10 +1,82 @@
diff --git a/lib/strategy.js b/lib/strategy.js
index b05aacce9a16fd32743a989b68f179bb35b20545..de1293a75c82452089eea37ab1031d4bbe42849c 100644
--- a/lib/strategy.js
+++ b/lib/strategy.js
@@ -14,6 +14,25 @@ var passport = require('passport-strategy')
, AuthorizationError = require('./errors/authorizationerror')
, TokenError = require('./errors/tokenerror')
, InternalOAuthError = require('./errors/internaloautherror');
+// --- Overleaf patch: request-input lockdown compatibility ---
+// This strategy reads req.query directly to parse the OAuth2 protocol
+// fields (error/error_description/error_uri, code, state) it's built
+// around, which is otherwise indistinguishable from unvalidated application
+// code reading raw request input under Overleaf's request-input lockdown
+// (see libraries/validation-tools and the patched express in
+// .yarn/patches/express-npm-*.patch). The lockdown captures the original
+// parsed values in symbol-keyed fields, registered in the global symbol
+// registry (the same ones @overleaf/validation-tools' lockdown.js reads),
+// and only turns the *public* req.query property into a throwing accessor
+// -- reading through this raw field instead sidesteps that, and is
+// identical to plain req.query when the lockdown isn't installed (i.e. for
+// every consumer of this package outside this monorepo).
+var __overleafLockdownInstalled = Symbol.for('overleaf.lockdown.installed');
+var __overleafLockdownRawQuery = Symbol.for('overleaf.lockdown.rawQuery');
+function __overleafRawQuery(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query;
+}
+// --- end Overleaf patch ---
/**
@@ -131,11 +150,11 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
options = options || {};
var self = this;
- if (req.query && req.query.error) {
- if (req.query.error == 'access_denied') {
- return this.fail({ message: req.query.error_description });
+ if (__overleafRawQuery(req) && __overleafRawQuery(req).error) {
+ if (__overleafRawQuery(req).error == 'access_denied') {
+ return this.fail({ message: __overleafRawQuery(req).error_description });
} else {
- return this.error(new AuthorizationError(req.query.error_description, req.query.error, req.query.error_uri));
+ return this.error(new AuthorizationError(__overleafRawQuery(req).error_description, __overleafRawQuery(req).error, __overleafRawQuery(req).error_uri));
}
}
@@ -156,14 +175,14 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
callbackURL: callbackURL
}
- if (req.query && req.query.code) {
+ if (__overleafRawQuery(req) && __overleafRawQuery(req).code) {
function loaded(err, ok, state) {
if (err) { return self.error(err); }
if (!ok) {
return self.fail(state, 403);
}
- var code = req.query.code;
+ var code = __overleafRawQuery(req).code;
var params = self.tokenParams(options);
params.grant_type = 'authorization_code';
@@ -213,7 +232,7 @@ OAuth2Strategy.prototype.authenticate = function(req, options) {
);
}
- var state = req.query.state;
+ var state = __overleafRawQuery(req).state;
try {
var arity = this._stateStore.verify.length;
if (arity == 4) {
diff --git a/lib/utils.js b/lib/utils.js
index 486f9e1..4584507 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -24,7 +24,7 @@ exports.originalURL = function(req, options) {
var trustProxy = options.proxy;
var proto = (req.headers['x-forwarded-proto'] || '').toLowerCase()
- , tls = req.connection.encrypted || (trustProxy && 'https' == proto.split(/\s*,\s*/)[0])
+ , tls = (req.connection && req.connection.encrypted) || (trustProxy && 'https' == proto.split(/\s*,\s*/)[0])
+14
View File
@@ -0,0 +1,14 @@
diff --git a/internal/legacy-cli.mjs b/internal/legacy-cli.mjs
index a76a536..f4d95ad 100644
--- a/internal/legacy-cli.mjs
+++ b/internal/legacy-cli.mjs
@@ -5832,6 +5832,9 @@ async function formatFiles(context) {
}
output = result.formatted;
} catch (error2) {
+ // invalidate cache on format error
+ formatResultsCache?.removeFormatResultsCache(filename);
+
const errorIsIgnored = handleError(
context,
fileNameToDisplay,
@@ -9,18 +9,6 @@ diff --git a/package.json b/package.json
"description": "library for extracting marketing attribution data (such as search terms) from referrer URLs",
"main": "index.js",
"scripts": {
@@ -15,9 +15,9 @@
}
],
"dependencies": {
- "js-yaml": "~2.1.0"
+ "js-yaml": "^4.1.0"
},
"devDependencies": {
- "mocha": "~1.12.0"
+ "mocha": "~10.2.0"
}
}
diff --git a/index.js b/index.js
--- a/index.js
+++ b/index.js
@@ -0,0 +1,251 @@
diff --git a/lib/logout.js b/lib/logout.js
index 77c417213851ffbe4a29f1b501027b8727931d8f..03d1bd8452ce80964f187f541ff47986931d3b73 100644
--- a/lib/logout.js
+++ b/lib/logout.js
@@ -10,6 +10,20 @@ var zlib = require('zlib');
var qs = require('querystring');
var xtend = require('xtend');
var constants = require('./constants');
+// --- Overleaf patch: request-input lockdown compatibility ---
+// See lib/samlp.js for the full rationale; the IdP-initiated and
+// SP-initiated logout flows read SAMLRequest/SAMLResponse/RelayState off
+// req.query/req.body directly, same as the login flow there.
+var __overleafLockdownInstalled = Symbol.for('overleaf.lockdown.installed');
+var __overleafLockdownRawQuery = Symbol.for('overleaf.lockdown.rawQuery');
+var __overleafLockdownRawBody = Symbol.for('overleaf.lockdown.rawBody');
+function __overleafRawQuery(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query;
+}
+function __overleafRawBody(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawBody] : req.body;
+}
+// --- end Overleaf patch ---
var BINDINGS = constants.BINDINGS;
var STATUS = constants.STATUS;
@@ -87,7 +101,7 @@ module.exports.logout = function (options) {
}
function validateSamlResponse(req, sessionParticipant, cb) {
- var SAMLResponse = req.query.SAMLResponse || req.body.SAMLResponse;
+ var SAMLResponse = __overleafRawQuery(req).SAMLResponse || __overleafRawBody(req).SAMLResponse;
function parseAndValidate(err, buffer) {
if (err) { return cb(err); }
@@ -141,7 +155,7 @@ module.exports.logout = function (options) {
cb(null, parsedResponse);
}
- if (req.body.SAMLResponse || !options.deflate) {
+ if (__overleafRawBody(req).SAMLResponse || !options.deflate) {
// HTTP-POST or HTTP-Redirect without deflate encoding
return parseAndValidate(null, new Buffer(SAMLResponse, 'base64'));
}
@@ -153,7 +167,7 @@ module.exports.logout = function (options) {
return function (req, res, next) {
try {
// SP Initated flow.
- if (req.query.SAMLRequest || req.body.SAMLRequest) {
+ if (__overleafRawQuery(req).SAMLRequest || __overleafRawBody(req).SAMLRequest) {
const opts = {
getCredentials: function getCredentials(issuer, sessionIndices, nameId, cb) {
options.sessionParticipants.get(issuer, sessionIndices, nameId, function (err, session) {
@@ -168,7 +182,7 @@ module.exports.logout = function (options) {
});
}
};
- parseIncomingLogoutRequest(req, req.query.SAMLRequest || req.body.SAMLRequest, opts, function (err, requestData) {
+ parseIncomingLogoutRequest(req, __overleafRawQuery(req).SAMLRequest || __overleafRawBody(req).SAMLRequest, opts, function (err, requestData) {
if (err) { return next(err); }
if (!requestData.issuer) {
@@ -185,7 +199,7 @@ module.exports.logout = function (options) {
id: requestData.id,
serviceProviderLogoutURL: (session|| {}).serviceProviderLogoutURL || options.destination
},
- relayState: req.query.RelayState || (req.body && req.body.RelayState)
+ relayState: __overleafRawQuery(req).RelayState || (__overleafRawBody(req) && __overleafRawBody(req).RelayState)
};
options.store.save(req, spData, function (err, transactionId) {
@@ -206,7 +220,7 @@ module.exports.logout = function (options) {
});
// Logout flow in progress, incoming SAMLResponse from SP. (Could be SP initiated or IdP initiated)
- } else if (req.query.SAMLResponse || req.body.SAMLResponse) {
+ } else if (__overleafRawQuery(req).SAMLResponse || __overleafRawBody(req).SAMLResponse) {
function process(state, transactionId) {
// LogoutResponse was OK, we remove the session participant from the IdP
options.sessionParticipants.remove(state.issuer, state.sessionIndex, state.nameId, function (err) {
@@ -218,7 +232,7 @@ module.exports.logout = function (options) {
}
// Verify that the state sent to the SP matches the one returned
- var h = req.query.RelayState || (req.body && req.body.RelayState);
+ var h = __overleafRawQuery(req).RelayState || (__overleafRawBody(req) && __overleafRawBody(req).RelayState);
options.store.load(req, h, {destroy: true}, function (err, state) {
if (err) { return next(err); }
if (!state) { return next(new Error('Invalid RelayState')); }
@@ -379,7 +393,7 @@ module.exports.sendLogoutError = function(options){
// Signature location
options.reference = constants.ELEMENTS.LOGOUT_RESPONSE.SIGNATURE_LOCATION_PATH;
- options.relayState = options.relayState || req.query.RelayState || (req.body || {}).RelayState;
+ options.relayState = options.relayState || __overleafRawQuery(req).RelayState || (__overleafRawBody(req) || {}).RelayState;
return prepareAndSendToken(req, res, 'LOGOUT_RESPONSE', logoutResponse, options, next);
};
diff --git a/lib/samlp.js b/lib/samlp.js
index 19a13328d60124417e9aa844c6d54745f7121166..08bef8456e1dcbadceb0460588bb78fff1e3f122 100644
--- a/lib/samlp.js
+++ b/lib/samlp.js
@@ -7,6 +7,30 @@ var templates = require('./templates');
var encoders = require('./encoders');
var PassportProfileMapper = require('./claims/PassportProfileMapper');
var constants = require('./constants');
+// --- Overleaf patch: request-input lockdown compatibility ---
+// This library reads req.query/req.body directly to parse the SAML protocol
+// fields (Signature, SigAlg, RelayState, SAMLRequest/SAMLResponse) it is
+// built around, which is otherwise indistinguishable from unvalidated
+// application code reading raw request input under Overleaf's
+// request-input lockdown (see libraries/validation-tools and the patched
+// express in .yarn/patches/express-npm-*.patch). The lockdown captures the
+// original parsed values in symbol-keyed fields, registered in the global
+// symbol registry (the same ones @overleaf/validation-tools' lockdown.js
+// reads), and only turns the *public* req.query/req.body properties into
+// throwing accessors -- reading through these raw fields instead sidesteps
+// that, and is identical to plain req.query/req.body when the lockdown
+// isn't installed (i.e. for every consumer of this package outside this
+// monorepo).
+var __overleafLockdownInstalled = Symbol.for('overleaf.lockdown.installed');
+var __overleafLockdownRawQuery = Symbol.for('overleaf.lockdown.rawQuery');
+var __overleafLockdownRawBody = Symbol.for('overleaf.lockdown.rawBody');
+function __overleafRawQuery(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query;
+}
+function __overleafRawBody(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawBody] : req.body;
+}
+// --- end Overleaf patch ---
function buildSamlResponse(options) {
var SAMLResponse = templates.samlresponse({
@@ -153,10 +177,10 @@ module.exports.auth = function (options) {
return function (req, res, next) {
var opts = xtend({}, options || {}); // clone options
- if (req.method === 'GET' && req.query.Signature) {
- opts.signature = req.query.Signature;
- opts.sigAlg = req.query.SigAlg;
- opts.relayState = opts.RelayState || req.query.RelayState;
+ if (req.method === 'GET' && __overleafRawQuery(req).Signature) {
+ opts.signature = __overleafRawQuery(req).Signature;
+ opts.sigAlg = __overleafRawQuery(req).SigAlg;
+ opts.relayState = opts.RelayState || __overleafRawQuery(req).RelayState;
}
function execute(postUrl, audience, req, res, next) {
@@ -178,14 +202,14 @@ module.exports.auth = function (options) {
res.send(templates.form({
type: 'SAMLResponse',
callback: postUrl,
- RelayState: opts.RelayState || (req.query || {}).RelayState || (req.body || {}).RelayState || '',
+ RelayState: opts.RelayState || (__overleafRawQuery(req) || {}).RelayState || (__overleafRawBody(req) || {}).RelayState || '',
token: response.toString('base64')
}));
}
});
}
- utils.parseSamlRequest(req, (req.query || {}).SAMLRequest || (req.body || {}).SAMLRequest, "AUTHN_REQUEST", opts, function (err, samlRequestDom) {
+ utils.parseSamlRequest(req, (__overleafRawQuery(req) || {}).SAMLRequest || (__overleafRawBody(req) || {}).SAMLRequest, "AUTHN_REQUEST", opts, function (err, samlRequestDom) {
if (err) return next(err);
var audience = opts.audience;
@@ -216,7 +240,7 @@ module.exports.parseRequest = function (req, options, callback) {
options = {};
}
- var samlRequest = (req.query || {}).SAMLRequest || (req.body || {}).SAMLRequest;
+ var samlRequest = (__overleafRawQuery(req) || {}).SAMLRequest || (__overleafRawBody(req) || {}).SAMLRequest;
if (!samlRequest)
return callback();
diff --git a/lib/utils.js b/lib/utils.js
index c091ac335be08b2bf55907b269e8e1ef79b686bb..23940db43725664367329341a713f2a1077ad071 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -6,6 +6,21 @@ const url = require('url');
const xtend = require('xtend');
const signers = require('./signers');
const constants = require('./constants');
+// --- Overleaf patch: request-input lockdown compatibility ---
+// See lib/samlp.js for the full rationale; this module validates SAML
+// signatures carried on req.query/req.body (Signature, SigAlg, RelayState,
+// SAMLRequest/SAMLResponse) for both the login (samlp.js) and logout
+// (logout.js) flows.
+const __overleafLockdownInstalled = Symbol.for('overleaf.lockdown.installed');
+const __overleafLockdownRawQuery = Symbol.for('overleaf.lockdown.rawQuery');
+const __overleafLockdownRawBody = Symbol.for('overleaf.lockdown.rawBody');
+function __overleafRawQuery(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawQuery] : req.query;
+}
+function __overleafRawBody(req) {
+ return req[__overleafLockdownInstalled] ? req[__overleafLockdownRawBody] : req.body;
+}
+// --- end Overleaf patch ---
const IdGenerator = require('auth0-id-generator');
const generator = new IdGenerator({len: 20, alphabet:"abcdef0123456789"});
@@ -20,7 +35,7 @@ module.exports.parseSamlRequest = function(req, samlRequest, type, options, call
const opts = {
signingCert: options.signingCert,
thumbprints: options.thumbprints,
- deflate: !!req.query.Signature,
+ deflate: !!__overleafRawQuery(req).Signature,
relayState: options.relayState
};
@@ -186,10 +201,10 @@ function validateSignature(req, element_type, xml, options) {
const type = constants.ELEMENTS[element_type].PROP;
const isRequestSigned = !options.deflate ?
- xpath.select(options.signaturePath || constants.ELEMENTS[element_type].SIGNATURE_VALIDATION_PATH, xml).length > 0 : !!req.query.Signature;
+ xpath.select(options.signaturePath || constants.ELEMENTS[element_type].SIGNATURE_VALIDATION_PATH, xml).length > 0 : !!__overleafRawQuery(req).Signature;
if (isRequestSigned) {
- if ((req.body && req.body[type]) || !options.deflate) {
+ if ((__overleafRawBody(req) && __overleafRawBody(req)[type]) || !options.deflate) {
// HTTP-POST or HTTP-Redirect without deflate encoding
const validationErrors = signers.validateXmlEmbeddedSignature(xml, options);
if (validationErrors && validationErrors.length > 0) {
@@ -199,9 +214,9 @@ function validateSignature(req, element_type, xml, options) {
else {
// HTTP-Redirect with deflate encoding
let signedContent = {};
- signedContent[type] = req.query[type];
- signedContent.RelayState = req.query.RelayState || options.relayState;
- signedContent.SigAlg = req.query.SigAlg;
+ signedContent[type] = __overleafRawQuery(req)[type];
+ signedContent.RelayState = __overleafRawQuery(req).RelayState || options.relayState;
+ signedContent.SigAlg = __overleafRawQuery(req).SigAlg;
if (!signedContent.RelayState && !options.relayState) {
delete signedContent.RelayState;
@@ -215,13 +230,13 @@ function validateSignature(req, element_type, xml, options) {
throw new Error('Invalid signature algorithm. Supported algorithms are http://www.w3.org/2001/04/xmldsig-more#rsa-sha1 and http://www.w3.org/2001/04/xmldsig-more#rsa-sha256');
}
- const valid = signers.isValidContentAndSignature(qs.stringify(signedContent), req.query.Signature, {
+ const valid = signers.isValidContentAndSignature(qs.stringify(signedContent), __overleafRawQuery(req).Signature, {
signingCert: options.signingCert,
- signatureAlgorithm: req.query.SigAlg
+ signatureAlgorithm: __overleafRawQuery(req).SigAlg
});
if (!valid) {
- throw new Error('Signature check errors: The signature provided (' + req.query.Signature + ') does not match the one calculated');
+ throw new Error('Signature check errors: The signature provided (' + __overleafRawQuery(req).Signature + ') does not match the one calculated');
}
}
} else if (options.signingCert) {
+85 -1
View File
@@ -7,10 +7,94 @@ enableScripts: true
enableTelemetry: false
nodeLinker: node-modules
nodeLinker: pnp
npmMinimalAgeGate: 3d
packageExtensions:
"bootstrap@*":
dependencies:
"@popperjs/core": "*"
jquery: "^3"
"@overleaf/settings@*":
peerDependencies:
"@overleaf/logger": "*"
"mongodb@*":
peerDependencies:
aws4: "*"
"bunyan@*":
peerDependencies:
source-map-support: "*"
"mocha-multi-reporters@*":
peerDependencies:
mocha-junit-reporter: "*"
"mongodb-legacy@*":
peerDependencies:
mongodb: "*"
"knex@*":
dependencies:
pg-query-stream: "*"
"zod-validation-error@*":
peerDependencies:
zod: "*"
"@vitejs/plugin-react@*":
peerDependencies:
vite: "*"
"@babel/preset-env@*":
peerDependencies:
core-js: "*"
"express@^4.0.0":
dependencies:
pug: "*"
"@prettier/plugin-pug@*":
dependencies:
pug: "*"
pug-filters: "*"
pug-lexer: "*"
pug-linker: "*"
pug-load: "*"
pug-parser: "*"
pug-runtime: "*"
pug-strip-comments: "*"
pug-walk: "*"
"postcss-loader@*":
peerDependencies:
autoprefixer: "*"
tailwindcss: "*"
"@ai-sdk/gateway@*":
peerDependencies:
zod: "*"
"@ai-sdk/provider-utils@*":
peerDependencies:
zod: "*"
"ai@*":
peerDependencies:
zod: "*"
"@uppy/react@*":
peerDependencies:
"@uppy/status-bar": "*"
"inversify@*":
dependencies:
reflect-metadata: "*"
# Must match services/web's pinned mongoose version exactly (not a range):
# sinon-mongoose checks `instanceof mongoose.Model` against its own resolved
# copy, so any version drift from web's copy breaks that check silently.
"sinon-mongoose@*":
dependencies:
mongoose: "8.24.3"
"react-linkify@*":
peerDependencies:
react: "*"
"socket.io-client@*":
dependencies:
ws: "*"
"debug@*":
peerDependenciesMeta:
supports-color:
optional: true
pnpZipBackend: js
supportedArchitectures:
cpu:
- current
+6 -15
View File
@@ -1,38 +1,29 @@
# Contributing to Overleaf
# Contributing to Overleaf CE+
Thank you for reading this! If you'd like to report a bug or join in the development
of Overleaf, then here are some notes on how to do that.
of Overleaf CE+, then here are some notes on how to do that.
## Reporting bugs and opening issues
If you'd like to report a bug or open an issue, please **[check if there is an existing issue](https://github.com/overleaf/overleaf/issues).**
If you'd like to report a bug or open an issue, please **[check if there is an existing issue](https://github.com/yu-i-i/overleaf-cep).**
If there is then please add any more information that you have, or give it a 👍.
When submitting an issue please describe the issue as clearly as possible, including how to
reproduce the bug, which situations it appears in, what you expected to happen, and what actually happens.
If you can include a screenshot for front end issues that is very helpful.
**Note**: If you are using [www.overleaf.com](www.overleaf.com) and have a problem, or if you would like to request a new feature, please contact the Support team at support@overleaf.com. Raise an issue here only to report bugs in the Community Edition release of Overleaf.
## Pull Requests
See [our wiki](https://github.com/overleaf/overleaf/wiki)
See [Overleaf wiki](https://github.com/overleaf/overleaf/wiki)
for how to manage the Overleaf development environment and for our developer guidelines.
We love pull requests, so be bold with them! Don't be afraid of going ahead
and changing something, or adding a new feature. We're very happy to work with you
to get your changes merged into Overleaf.
If you're looking for something to work on, have a look at the [open issues](https://github.com/overleaf/overleaf/issues).
If you're looking for something to work on, have a look at the [open issues](https://github.com/yu-i-i/overleaf-cep/issues).
## Security
Please see [our security policy](https://github.com/overleaf/overleaf/security/policy) if you would like to report a potential security vulnerability.
## Contributor License Agreement
Before we can accept any contributions of code, we need you to agree to our
[Contributor License Agreement](https://docs.google.com/forms/d/e/1FAIpQLSef79XH3mb7yIiMzZw-yALEegS-wyFetvjTiNBfZvf_IHD2KA/viewform?usp=sf_link).
This is to ensure that you own the copyright of your contribution, and that you
agree to give us a license to use it in both the open source version, and the version
of Overleaf running at www.overleaf.com, which may have additional changes.
Please see [Overleaf security policy](https://github.com/overleaf/overleaf/security/policy) if you would like to report a potential security vulnerability.
+58
View File
@@ -0,0 +1,58 @@
# CREDITS
This project includes ideas and adapted code from several open-source projects.\
In most cases the original code has been modified, optimized, or extended.
## Symbol palette
The Symbol palette feature is based on the
[original Overleaf implementation](https://github.com/overleaf/web/tree/master/frontend/js/features/symbol-palette)
The original code was slightly improved, particularly in the parts related to keyboard input.
## LDAP authentication
LDAP authentication was inspired by [this project](https://github.com/smhaller/ldap-overleaf-sl).
The project provided the idea and part of the implementation for adding LDAP users to the user's contacts.
## Real-time track changes and comments
The Track Changes and Comments feature exists largely in the original Overleaf codebase.\
The missing parts were implemented based on [this code](https://github.com/ertuil/overleaf).
The referenced code was fixed, optimized, and extended.
## Sandboxed compiles
The Sandboxed Compiles feature was until recently largely present in the original Overleaf codebase.\
The missing parts were implemented in this project.
## Import file from external URL
The "From External URL" feature exists in the original Overleaf code.\
The missing proxy component was implemented in this project.
## Git integration
The Git integration feature includes parts of the frontend code
from [here](https://github.com/ayaka-notes/overleaf-pro/tree/feat-git-bridge)
## GitHub synchronization
The GitHub synchronization feature includes parts of the frontend code and the OAuth2 backend implementation
from [here](https://github.com/ayaka-notes/overleaf-pro/commit/06a30fe9a0ed75e5ab40b50a8a4e94f43161cf71).
## Sign Up page
The Sign Up page is based on [this code](https://github.com/ayaka-notes/overleaf-pro/tree/feat-public-registeration).
## Python script runner
The Python script runner feature is the original Overleaf implementation, which
[was hidden behind a paywall](https://github.com/overleaf/overleaf/commit/b90a8500d5df763d1a9b00352928a79c7e247de7) in later releases.
# Acknowledgments
Thanks to the users of the project for valuable feedback, suggestions,
and help in identifying and fixing bugs.
+45 -31
View File
@@ -6,52 +6,66 @@
<h4 align="center">An open-source online real-time collaborative LaTeX editor.</h4>
<p align="center">
<a href="https://github.com/overleaf/overleaf/wiki">Wiki</a> •
<a href="https://github.com/yu-i-i/overleaf-cep/wiki">Wiki</a> •
<a href="https://www.overleaf.com/for/enterprises">Server Pro</a> •
<a href="#contributing">Contributing</a> •
<a href="https://mailchi.mp/overleaf.com/community-edition-and-server-pro">Mailing List</a> •
<a href="#authors">Authors</a> •
<a href="#license">License</a>
</p>
<img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Community Edition">
<img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Extended Community Edition">
<p align="center">
Figure 1: A screenshot of a project being edited in Overleaf Community Edition.
Figure 1: A screenshot of a project being edited in Overleaf CE+
</p>
## Community Edition
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. We run a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. Overleaf runs a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
## Extended Community Edition (CE+)
The present "extended" version of Overleaf CE includes:
- Sandboxed compiles with TeX Live image selection
- Sign Up page
- LDAP authentication
- SAML authentication
- OpenID Connect authentication
- Real-time track changes and comments
- Symbol palette
- Template gallery
- Import file from external URL
- Git integration
- GitHub synchronization
- Zotero integration
- Reference Search and Pick Tool
- Document Import (`.docx`, `.md`) and Export (`.docx`, `.md`, `.html`)
- Advanced administrator tools for managing user accounts and projects
- Python script runner
- Logo tools
> [!CAUTION]
> Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles.
> Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles.
Therefore, in any environment where not all users can be fully trusted, it is strongly recommended to enable the Sandboxed Compiles feature available in the Extended Community Edition.
For more information on Sandbox Compiles check out our [documentation](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles).
For more information on Sandbox Compiles check out Overleaf [documentation](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles).
## Enterprise
If you want help installing and maintaining Overleaf in your lab or workplace, we offer an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises). It also includes more features for security (SSO with LDAP or SAML), administration and collaboration (e.g. tracked changes). [Find out more!](https://www.overleaf.com/for/enterprises)
## Keeping up to date
Sign up to the [mailing list](https://mailchi.mp/overleaf.com/community-edition-and-server-pro) to get updates on Overleaf releases and development.
If you want help installing and maintaining Overleaf in your lab or workplace, Overleaf offers an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises).
## Installation
We have detailed installation instructions in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
## Upgrading
If you are upgrading from a previous version of Overleaf, please see the [Release Notes section on the Wiki](https://github.com/overleaf/overleaf/wiki#release-notes) for all of the versions between your current version and the version you are upgrading to.
Detailed installation instructions can be found in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
Configuration details and release history for the Extended Community Edition can be found on the [Extended CE Wiki Page](https://github.com/yu-i-i/overleaf-cep/wiki).
## Overleaf Docker Image
This repo contains two dockerfiles, [`Dockerfile-base`](server-ce/Dockerfile-base), which builds the
`sharelatex/sharelatex-base` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the
`sharelatex/sharelatex` (or "community") image.
`sharelatex/sharelatex-base:ext-ce` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the
`sharelatex/sharelatex:ext-ce` image.
The Base image generally contains the basic dependencies like `wget`, plus `texlive`.
We split this out because it's a pretty heavy set of
This is split out because it's a pretty heavy set of
dependencies, and it's nice to not have to rebuild all of that every time.
The `sharelatex/sharelatex` image extends the base image and adds the actual Overleaf code
@@ -59,22 +73,22 @@ and services.
Use `make build-base` and `make build-community` from `server-ce/` to build these images.
We use the [Phusion base-image](https://github.com/phusion/baseimage-docker)
(which is extended by our `base` image) to provide us with a VM-like container
The [Phusion base-image](https://github.com/phusion/baseimage-docker)
(which is extended by the `base` image) provides a VM-like container
in which to run the Overleaf services. Baseimage uses the `runit` service
manager to manage services, and we add our init-scripts from the `server-ce/runit`
folder.
## Contributing
Please see the [CONTRIBUTING](CONTRIBUTING.md) file for information on contributing to the development of Overleaf.
manager to manage services, and init scripts from the `server-ce/runit`
folder are added.
## Authors
[The Overleaf Team](https://www.overleaf.com/about)
[The Overleaf Team](https://www.overleaf.com/about)\
[yu-i-i](https://github.com/yu-i-i), [davrot](https://github.com/davrot) — CE extensions; references to adapted code are listed in [`CREDITS`](CREDITS.md). See also the authors of individual commits.
## License
The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the [`LICENSE`](LICENSE) file.
Copyright (c) Overleaf, 2014-2025.
Copyright (c) Overleaf, 2014-2026.\
Copyright (c) @yu-i-i, @davrot, 2024-2026, for CE extensions.
Portions of the code are derived from other open-source projects; see [`CREDITS`](CREDITS.md).
-3
View File
@@ -1,3 +0,0 @@
/compiles/*
!.gitkeep
.env
-76
View File
@@ -1,76 +0,0 @@
# Overleaf Community Edition, development environment
## Building and running
In this `develop` directory, build the services:
```shell
bin/build
```
> [!NOTE]
> If Docker is running out of RAM while building the services in parallel, create a `.env` file in this directory containing `COMPOSE_PARALLEL_LIMIT=1`.
Then start the services:
```shell
bin/up
```
Once the services are running, open <http://localhost/launchpad> to create the first admin account.
## Development
To avoid running `bin/build && bin/up` after every code change, you can run Overleaf
Community Edition in _development mode_, where services will automatically update on code changes.
To do this, use the included `bin/dev` script:
```shell
bin/dev
```
This will start all services using `node --watch`, which will automatically monitor the code and restart the services as necessary.
To improve performance, you can start only a subset of the services in development mode by providing a space-separated list to the `bin/dev` script:
```shell
bin/dev [service1] [service2] ... [serviceN]
```
> [!NOTE]
> Starting the `web` service in _development mode_ will only update the `web`
> service when backend code changes. In order to automatically update frontend
> code as well, make sure to start the `webpack` service in _development mode_
> as well.
If no services are named, all services will start in development mode.
## Debugging
When run in _development mode_ most services expose a debugging port to which
you can attach a debugger such as
[the inspector in Chrome's Dev Tools](chrome://inspect/) or one integrated into
an IDE. The following table shows the port exposed on the **host machine** for
each service:
| Service | Port |
| ------------------ | ---- |
| `web` | 9229 |
| `clsi` | 9230 |
| `chat` | 9231 |
| `docstore` | 9233 |
| `document-updater` | 9234 |
| `filestore` | 9235 |
| `notifications` | 9236 |
| `real-time` | 9237 |
| `history-v1` | 9239 |
| `project-history` | 9240 |
To attach to a service using Chrome's _remote debugging_, go to
<chrome://inspect/> and make sure _Discover network targets_ is checked. Next
click _Configure..._ and add an entry `localhost:[service port]` for each of the
services you want to attach a debugger to.
After adding an entry, the service will show up as a _Remote Target_ that you
can inspect and debug.
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
docker compose build --pull "$@"
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --no-deps --detach "$@"
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
docker compose down "$@"
-9
View File
@@ -1,9 +0,0 @@
#!/usr/bin/env bash
docker compose logs --follow --tail 10 --no-color "$@" \
| ggrep --line-buffered --invert-match "global.gc" \
| ggrep --line-buffered --invert-match "health.check" \
| ggrep --line-buffered --invert-match "slow event loop" \
| ggrep --line-buffered --invert-match "process.memoryUsage" \
| ggrep --line-buffered --only-matching "[{].*" \
| bunyan --output short
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
docker compose exec -it "$@" /bin/bash
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
docker compose up --detach "$@"
View File
-24
View File
@@ -1,24 +0,0 @@
CHAT_HOST=chat
CLSI_HOST=clsi
DOWNLOAD_HOST=clsi-nginx
DOCSTORE_HOST=docstore
DOCUMENT_UPDATER_HOST=document-updater
FILESTORE_HOST=filestore
GRACEFUL_SHUTDOWN_DELAY_SECONDS=0
HISTORY_V1_HOST=history-v1
HISTORY_REDIS_HOST=redis
LISTEN_ADDRESS=0.0.0.0
MONGO_HOST=mongo
MONGO_URL=mongodb://mongo/sharelatex?directConnection=true
NOTIFICATIONS_HOST=notifications
PROJECT_HISTORY_HOST=project-history
QUEUES_REDIS_HOST=redis
DSMP_REDIS_HOST=redis
REALTIME_HOST=real-time
REDIS_HOST=redis
SESSION_SECRET=foo
V1_HISTORY_HOST=history-v1
WEBPACK_HOST=webpack
WEB_API_PASSWORD=overleaf
WEB_API_USER=overleaf
WEB_HOST=web
-128
View File
@@ -1,128 +0,0 @@
services:
clsi:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9230:9229"
volumes:
- ../services/clsi/app:/overleaf/services/clsi/app
- ../services/clsi/app.js:/overleaf/services/clsi/app.js
- ../services/clsi/config:/overleaf/services/clsi/config
chat:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9231:9229"
volumes:
- ../services/chat/app:/overleaf/services/chat/app
- ../services/chat/app.js:/overleaf/services/chat/app.js
- ../services/chat/config:/overleaf/services/chat/config
docstore:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9233:9229"
volumes:
- ../services/docstore/app:/overleaf/services/docstore/app
- ../services/docstore/app.js:/overleaf/services/docstore/app.js
- ../services/docstore/config:/overleaf/services/docstore/config
document-updater:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9234:9229"
volumes:
- ../services/document-updater/app:/overleaf/services/document-updater/app
- ../services/document-updater/app.js:/overleaf/services/document-updater/app.js
- ../services/document-updater/config:/overleaf/services/document-updater/config
filestore:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9235:9229"
volumes:
- ../services/filestore/app:/overleaf/services/filestore/app
- ../services/filestore/app.js:/overleaf/services/filestore/app.js
- ../services/filestore/config:/overleaf/services/filestore/config
history-v1:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9239:9229"
volumes:
- ../services/history-v1/api:/overleaf/services/history-v1/api
- ../services/history-v1/app.js:/overleaf/services/history-v1/app.js
- ../services/history-v1/config:/overleaf/services/history-v1/config
- ../services/history-v1/storage:/overleaf/services/history-v1/storage
- ../services/history-v1/knexfile.js:/overleaf/services/history-v1/knexfile.js
- ../services/history-v1/migrations:/overleaf/services/history-v1/migrations
notifications:
command: ["node", "--watch", "app.ts"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9236:9229"
volumes:
- ../services/notifications/app:/overleaf/services/notifications/app
- ../services/notifications/app.ts:/overleaf/services/notifications/app.ts
- ../services/notifications/config:/overleaf/services/notifications/config
project-history:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9240:9229"
volumes:
- ../services/project-history/app:/overleaf/services/project-history/app
- ../services/project-history/app.js:/overleaf/services/project-history/app.js
- ../services/project-history/config:/overleaf/services/project-history/config
real-time:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9237:9229"
volumes:
- ../services/real-time/app:/overleaf/services/real-time/app
- ../services/real-time/app.js:/overleaf/services/real-time/app.js
- ../services/real-time/config:/overleaf/services/real-time/config
web:
command: ["node", "--watch", "app.mjs", "--watch-locales"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9229:9229"
volumes:
- ../services/web/app:/overleaf/services/web/app
- ../services/web/app.mjs:/overleaf/services/web/app.mjs
- ../services/web/config:/overleaf/services/web/config
- ../services/web/locales:/overleaf/services/web/locales
- ../services/web/modules:/overleaf/services/web/modules
- ../services/web/public:/overleaf/services/web/public
webpack:
volumes:
- ../services/web/app:/overleaf/services/web/app
- ../services/web/config:/overleaf/services/web/config
- ../services/web/frontend:/overleaf/services/web/frontend
- ../services/web/locales:/overleaf/services/web/locales
- ../services/web/modules:/overleaf/services/web/modules
- ../services/web/public:/overleaf/services/web/public
- ../services/web/transform:/overleaf/services/web/transform
- ../services/web/types:/overleaf/services/web/types
- ../services/web/webpack-plugins:/overleaf/services/web/webpack-plugins
-175
View File
@@ -1,175 +0,0 @@
volumes:
clsi-cache:
filestore-public-files:
filestore-template-files:
filestore-uploads:
filestore-user-files:
mongo-data:
redis-data:
sharelatex-data:
web-data:
history-v1-buckets:
services:
chat:
build:
context: ..
dockerfile: services/chat/Dockerfile
env_file:
- dev.env
clsi:
build:
context: ..
dockerfile: services/clsi/Dockerfile
target: with-texlive
env_file:
- dev.env
environment:
- SANDBOXED_COMPILES=false
user: root
volumes:
- ${PWD}/compiles:/overleaf/services/clsi/compiles
- ${PWD}/output:/overleaf/services/clsi/output
- ${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock
- clsi-cache:/overleaf/services/clsi/cache
clsi-nginx:
image: nginx:1.28
read_only: true
tmpfs:
- /tmp
- /var/cache/nginx
- /run
volumes:
- ${PWD}/output:/output:ro
- ../services/clsi/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro
docstore:
build:
context: ..
dockerfile: services/docstore/Dockerfile
env_file:
- dev.env
document-updater:
build:
context: ..
dockerfile: services/document-updater/Dockerfile
env_file:
- dev.env
filestore:
build:
context: ..
dockerfile: services/filestore/Dockerfile
env_file:
- dev.env
# environment:
# - ENABLE_CONVERSIONS=true
volumes:
- filestore-public-files:/overleaf/services/filestore/public_files
- filestore-template-files:/overleaf/services/filestore/template_files
- filestore-uploads:/overleaf/services/filestore/uploads
history-v1:
build:
context: ..
dockerfile: services/history-v1/Dockerfile
env_file:
- dev.env
environment:
OVERLEAF_EDITOR_ANALYTICS_BUCKET: "/buckets/analytics"
OVERLEAF_EDITOR_BLOBS_BUCKET: "/buckets/blobs"
OVERLEAF_EDITOR_CHUNKS_BUCKET: "/buckets/chunks"
OVERLEAF_EDITOR_PROJECT_BLOBS_BUCKET: "/buckets/project_blobs"
OVERLEAF_EDITOR_ZIPS_BUCKET: "/buckets/zips"
PERSISTOR_BACKEND: fs
volumes:
- history-v1-buckets:/buckets
mongo:
image: mongo:8
command: --replSet overleaf
ports:
- "127.0.0.1:27017:27017" # for debugging
volumes:
- mongo-data:/data/db
- ../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js
environment:
MONGO_INITDB_DATABASE: sharelatex
extra_hosts:
# Required when using the automatic database setup for initializing the
# replica set. This override is not needed when running the setup after
# starting up mongo.
- mongo:127.0.0.1
notifications:
build:
context: ..
dockerfile: services/notifications/Dockerfile
env_file:
- dev.env
project-history:
build:
context: ..
dockerfile: services/project-history/Dockerfile
env_file:
- dev.env
real-time:
build:
context: ..
dockerfile: services/real-time/Dockerfile
env_file:
- dev.env
redis:
image: redis:7
ports:
- "127.0.0.1:6379:6379" # for debugging
volumes:
- redis-data:/data
web:
build:
context: ..
dockerfile: services/web/Dockerfile
target: dev
env_file:
- dev.env
environment:
- APP_NAME=Overleaf Community Edition
- ENABLED_LINKED_FILE_TYPES=project_file,project_output_file
- EMAIL_CONFIRMATION_DISABLED=true
- NODE_ENV=development
- OVERLEAF_ALLOW_PUBLIC_ACCESS=true
command: ["node", "app.mjs"]
volumes:
- sharelatex-data:/var/lib/overleaf
- web-data:/overleaf/services/web/data
depends_on:
- mongo
- redis
- chat
- clsi
- docstore
- document-updater
- filestore
- history-v1
- notifications
- project-history
- real-time
webpack:
build:
context: ..
dockerfile: services/web/Dockerfile
target: webpack
command:
["npx", "webpack", "serve", "--config", "webpack.config.dev-env.js"]
ports:
- "127.0.0.1:80:3808"
volumes:
- ./webpack.config.dev-env.js:/overleaf/services/web/webpack.config.dev-env.js
-23
View File
@@ -1,23 +0,0 @@
const { merge } = require('webpack-merge')
const base = require('./webpack.config.dev')
module.exports = merge(base, {
devServer: {
allowedHosts: 'auto',
devMiddleware: {
index: false,
},
proxy: [
{
context: '/socket.io/**',
target: 'http://real-time:3026',
ws: true,
},
{
context: ['!**/*.js', '!**/*.css', '!**/*.json'],
target: 'http://web:3000',
},
],
},
})
Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 412 KiB

+3 -1
View File
@@ -33,7 +33,7 @@ services:
# IMPORTANT: Required for invite token generation
# OVERLEAF_INVITE_TOKEN_SECRET: generate with `openssl rand -base64 32`
ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file"
ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file,zotero"
# Enables Thumbnail generation using ImageMagick
ENABLE_CONVERSIONS: "true"
@@ -47,6 +47,8 @@ services:
# OVERLEAF_SITE_URL: http://overleaf.example.com
# OVERLEAF_NAV_TITLE: Overleaf Community Edition
# OVERLEAF_HEADER_IMAGE_URL: http://example.com/mylogo.png
# OVERLEAF_HEADER_IMAGE_URL_LIGHT: http://example.com/mylogo-light.png
# OVERLEAF_HEADER_IMAGE_URL_DARK: http://example.com/mylogo-dark.png
# OVERLEAF_ADMIN_EMAIL: support@it.com
# OVERLEAF_LEFT_FOOTER: '[{"text": "Another page I want to link to can be found <a href=\"here\">here</a>"} ]'
-18
View File
@@ -1,18 +0,0 @@
FROM cypress/included:15.12.0
ARG USER_UID=1000
ARG USER_GID=1000
# Corepack setup, shared between all the images.
ENV PATH="/overleaf/node_modules/.bin:$PATH"
ENV COREPACK_HOME=/opt/corepack
RUN corepack enable && corepack install -g yarn@4.14.1
ENV COREPACK_ENABLE_NETWORK=0
WORKDIR /overleaf
RUN sed -i s/node:x:1000:/node:x:${USER_GID}:/ /etc/group \
&& sed -i s_node:x:1000:1000::/home/node:/bin/bash_node:x:${USER_UID}:${USER_GID}::/home/node:/bin/bash_ /etc/passwd \
&& chown -R node:node /home/node \
&& chown node:node /overleaf
USER node
@@ -1,10 +0,0 @@
access-token-encryptor
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/access-token-encryptor
--pipeline-owner=32
--public-repo=False
@@ -3,9 +3,13 @@
"version": "3.0.0",
"description": "",
"main": "index.js",
"exports": {
".": "./index.js",
"./*": "./*"
},
"scripts": {
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
@@ -17,8 +21,10 @@
"lodash": "^4.18.1"
},
"devDependencies": {
"@types/node": "^24.5.2",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"eslint": "^10.4.0",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
@@ -1,7 +1,7 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
mochaFile: `reports/junit-mocha-${process.env.TEST_NAME_PATTERN}.xml`,
includePending: true,
jenkinsMode: true,
output: true,
+76
View File
@@ -0,0 +1,76 @@
'use strict'
/**
* cypress-multi-reporters, loadable from inside Cypress.
*
* Cypress builds mocha reporters in its Electron process. That process resolves
* modules through PackherdModuleLoader and does not inherit NODE_OPTIONS, so
* Yarn PnP is not installed there and `require('cypress-multi-reporters')`
* fails with "Cannot find module".
*
* Installing PnP for the rest of that process is not an option either: Cypress
* asks packherd for its own bundled modules under ids like
* `packages/server/lib/browsers/`, which PnP reads as paths inside this
* workspace and then rejects ("it isn't declared in your dependencies"),
* breaking the browser launch.
*
* So install PnP, pull the reporters in through it, and hand resolution back to
* Cypress. Requires made by the modules that came in through PnP keep going
* through PnP, so anything they load later still resolves.
*
* Point Cypress at this package rather than at cypress-multi-reporters:
*
* reporter: require.resolve('@overleaf/cypress-pnp-reporter')
*/
const Module = require('node:module')
const path = require('node:path')
function loadReporters() {
const MultiReporters = require('cypress-multi-reporters')
// MultiReporters resolves the reporters named in reporterEnabled when a run
// starts, long after PnP is gone. It reads mocha's own registry before
// falling back to require(), so register mocha-junit-reporter there while it
// can still be found.
require('mocha').reporters[
'mocha-junit-reporter'
] = require('mocha-junit-reporter')
return MultiReporters
}
if (process.versions.pnp) {
module.exports = loadReporters()
} else {
const stockLoad = Module._load
const stockResolveFilename = Module._resolveFilename
const stockFindPath = Module._findPath
require(path.join(__dirname, '..', '..', '.pnp.cjs')).setup()
const pnpResolveFilename = Module._resolveFilename
const cachedBefore = new Set(Object.keys(Module._cache))
const loadedViaPnp = new Set()
try {
module.exports = loadReporters()
} finally {
for (const filename of Object.keys(Module._cache)) {
if (!cachedBefore.has(filename)) loadedViaPnp.add(filename)
}
// Leave the patched fs in place: the reporters live in zip archives, so
// anything they read lazily still has to go through it.
Module._load = stockLoad
Module._findPath = stockFindPath
Module._resolveFilename = function (request, parent, ...args) {
if (!parent || !loadedViaPnp.has(parent.filename)) {
return stockResolveFilename.call(this, request, parent, ...args)
}
const filename = pnpResolveFilename.call(this, request, parent, ...args)
loadedViaPnp.add(filename)
return filename
}
}
}
@@ -0,0 +1,30 @@
{
"name": "@overleaf/cypress-pnp-reporter",
"description": "cypress-multi-reporters, loadable from Cypress's Electron process, which has no Yarn PnP",
"main": "index.cjs",
"files": [
"index.cjs"
],
"author": "Overleaf (https://www.overleaf.com)",
"private": true,
"scripts": {
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
"dependencies": {
"cypress-multi-reporters": "^2.0.5"
},
"devDependencies": {
"@types/node": "^24.5.2",
"chai": "^4.3.6",
"eslint": "^10.4.0",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"typescript": "^5.0.4"
}
}
@@ -1,7 +1,7 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
mochaFile: `reports/junit-mocha-${process.env.TEST_NAME_PATTERN}.xml`,
includePending: true,
jenkinsMode: true,
output: true,
@@ -0,0 +1 @@
// no setup yet
@@ -0,0 +1,52 @@
'use strict'
const { expect } = require('chai')
const Mocha = require('mocha')
const JUnitReporter = require('mocha-junit-reporter')
const MultiReporters = require('../../index.cjs')
function buildReporters(reporterEnabled) {
const runner = new Mocha.Runner(new Mocha.Suite(''))
const reporter = new MultiReporters(runner, {
reporterOptions: { reporterEnabled },
})
return reporter._reporters
}
describe('cypress-pnp-reporter', function () {
it('exports cypress-multi-reporters', function () {
expect(MultiReporters).to.equal(require('cypress-multi-reporters'))
})
it('registers mocha-junit-reporter on mocha', function () {
expect(Mocha.reporters['mocha-junit-reporter']).to.equal(JUnitReporter)
})
it('builds the reporters used across the Cypress suites', function () {
const reporters = buildReporters('spec, mocha-junit-reporter')
expect(reporters).to.have.length(2)
expect(reporters[0]).to.be.an.instanceOf(Mocha.reporters.spec)
expect(reporters[1]).to.be.an.instanceOf(JUnitReporter)
})
it('prefers the reporter registered on mocha over resolving it by name', function () {
// Cypress builds the reporters after the shim has handed module resolution
// back, so mocha's registry is the only place mocha-junit-reporter can
// still be found. Everything above this still passes if
// cypress-multi-reporters resolves it by name instead, so pin the lookup
// order with a reporter that is only reachable through the registry.
class Sentinel {}
const registered = Mocha.reporters['mocha-junit-reporter']
Mocha.reporters['mocha-junit-reporter'] = Sentinel
try {
expect(buildReporters('mocha-junit-reporter')[0]).to.be.an.instanceOf(
Sentinel
)
} finally {
Mocha.reporters['mocha-junit-reporter'] = registered
}
})
})
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.backend.json",
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
}
+3
View File
@@ -13,13 +13,16 @@ module.exports = {
'require-script-runner': require('./require-script-runner'),
'require-vi-doMock-valid-path': require('./require-vi-doMock-valid-path'),
'require-loading-label': require('./require-loading-label'),
'require-rel-for-docs-links': require('./require-rel-for-docs-links'),
'require-cio-snake-case-properties': require('./require-cio-snake-case-properties'),
'no-throw-in-callback': require('./no-throw-in-callback'),
'no-raw-req-access': require('./no-raw-req-access'),
'no-consecutive-spaces-in-locales': require('./no-consecutive-spaces-in-locales'),
'no-straight-apostrophes-in-locales': require('./no-straight-apostrophes-in-locales'),
'french-typography-in-locales': require('./french-typography-in-locales'),
'sorted-keys-in-locales': require('./sorted-keys-in-locales'),
'locale-variables-match-en': require('./locale-variables-match-en'),
'no-orphan-locale-keys': require('./no-orphan-locale-keys'),
'no-mocha-before': require('./no-mocha-before'),
},
}
+112
View File
@@ -0,0 +1,112 @@
const Path = require('node:path')
/**
* Block bare mocha `before()` hooks in Cypress e2e suites.
*
* `before()` hooks run once per suite and do NOT re-run when a test retries,
* while `beforeEach`-based hooks (like `beforeWithReRunOnTestRetry`) do.
* Mixing the two also changes ordering: all `before()` hooks run ahead of
* every `beforeEach`, not in source order. This matters in the Server Pro
* e2e suite, where suites change the server config via `startWith()` while
* test setup runs in `beforeWithReRunOnTestRetry()`: when a later suite
* changes the config, both must re-run on retry and in registration order,
* otherwise a retried test runs against setup or config belonging to a
* different suite.
*
* The autofix rewrites `before(fn)` to `beforeWithReRunOnTestRetry(fn)` and
* adds the helper import when missing. Configure the helper location (repo
* root relative, no extension) via the `helperPath` option.
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow mocha before() hooks in favor of beforeWithReRunOnTestRetry()',
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
helperPath: { type: 'string' },
},
additionalProperties: false,
},
],
messages: {
noMochaBefore:
'before() hooks do not re-run when a test retries, so retried tests run against stale setup. Use beforeWithReRunOnTestRetry() instead.',
},
},
create(context) {
const helperPath = context.options[0]?.helperPath
let lastImport = null
let hasHelperImport = false
function helperImportSpecifier() {
const absolute = Path.resolve(context.cwd, helperPath)
let relative = Path.relative(
Path.dirname(Path.resolve(context.cwd, context.filename)),
absolute
)
relative = relative.split(Path.sep).join('/')
if (!relative.startsWith('.')) {
relative = `./${relative}`
}
return relative
}
return {
ImportDeclaration(node) {
lastImport = node
if (
node.specifiers.some(
specifier =>
specifier.type === 'ImportSpecifier' &&
specifier.imported.name === 'beforeWithReRunOnTestRetry'
)
) {
hasHelperImport = true
}
},
CallExpression(node) {
if (
node.callee.type !== 'Identifier' ||
node.callee.name !== 'before'
) {
return
}
// Only the global mocha hook; skip locally defined/imported `before`
const variable = context.sourceCode
.getScope(node)
.references.find(ref => ref.identifier === node.callee)?.resolved
if (variable && variable.defs.length > 0) {
return
}
const fixable = node.arguments.length === 1 && helperPath
context.report({
node: node.callee,
messageId: 'noMochaBefore',
fix: fixable
? fixer => {
const fixes = [
fixer.replaceText(node.callee, 'beforeWithReRunOnTestRetry'),
]
if (!hasHelperImport) {
const importText = `import { beforeWithReRunOnTestRetry } from '${helperImportSpecifier()}'`
fixes.push(
lastImport
? fixer.insertTextAfter(lastImport, `\n${importText}`)
: fixer.insertTextBeforeRange([0, 0], `${importText}\n`)
)
}
return fixes
}
: undefined,
})
},
}
},
}
@@ -0,0 +1,64 @@
const RAW_FIELDS = new Set(['body', 'query', 'params'])
function isReqIdentifier(node) {
return node && node.type === 'Identifier' && node.name === 'req'
}
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Disallow reading raw request input (req.body/req.query/req.params); validate it with parseReq() instead',
},
messages: {
noRawReqAccess:
'Do not read req.{{field}} directly; validate request input with parseReq() from @overleaf/validation-tools. Allowlisted infrastructure middleware may use getRawReqInput().',
},
},
create(context) {
return {
MemberExpression(node) {
if (!isReqIdentifier(node.object)) return
const property = node.property
const field =
!node.computed && property.type === 'Identifier'
? property.name
: node.computed &&
property.type === 'Literal' &&
typeof property.value === 'string'
? property.value
: null
if (field === null || !RAW_FIELDS.has(field)) return
// writes stay allowed: body parsers and middleware assign req.body
const parent = node.parent
if (
parent.type === 'AssignmentExpression' &&
parent.left === node &&
parent.operator === '='
) {
return
}
context.report({ node, messageId: 'noRawReqAccess', data: { field } })
},
VariableDeclarator(node) {
if (!isReqIdentifier(node.init)) return
if (node.id.type !== 'ObjectPattern') return
for (const prop of node.id.properties) {
if (
prop.type === 'Property' &&
!prop.computed &&
prop.key.type === 'Identifier' &&
RAW_FIELDS.has(prop.key.name)
) {
context.report({
node: prop,
messageId: 'noRawReqAccess',
data: { field: prop.key.name },
})
}
}
},
}
},
}
+11 -5
View File
@@ -9,12 +9,18 @@
},
"devDependencies": {
"@eslint/json": "^2.0.0",
"@typescript-eslint/parser": "^8.59.4"
},
"peerDependencies": {
"eslint": "^10.4.0"
"@types/node": "^24.5.2",
"@typescript-eslint/parser": "^8.59.4",
"eslint": "^10.4.0",
"typescript": "^5.0.4",
"vitest": "4.1.5"
},
"scripts": {
"test": "node rules.test.js"
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "yarn run test:unit",
"test:unit": "vitest --config vitest.config.ts",
"types:check": "tsc --noEmit"
}
}
@@ -71,6 +71,7 @@ const ignoreWords = {
'resendManagedUserInvite',
'salesContactForm',
'showSupport',
'uniExternalCollaboration',
]),
other: new Set([
'Project',
+2 -4
View File
@@ -29,7 +29,7 @@ const getSuggestion = routePath => {
if (routePath instanceof RegExp) {
const words = removeTextBetweenBrackets(routePath.source).match(/[\w-]+/g)
if (!words) return routePath
if (!words) return null
let newSource = routePath.source
for (const word of words) {
@@ -42,9 +42,7 @@ const getSuggestion = routePath => {
}
const kebabed = new RegExp(newSource, routePath.flags)
return kebabed.source.toString() === routePath.source.toString()
? null
: kebabed
return kebabed.source === routePath.source ? null : kebabed.toString()
}
}
@@ -0,0 +1,87 @@
const DOCS_HOST = 'docs.overleaf.com'
const REQUIRED_REL_TOKENS = ['noopener', 'noreferrer']
// Elements that forward href/target/rel straight onto a rendered <a>.
const LINK_ELEMENT_NAMES = ['a', 'OLButton']
function getStringLiteralValue(node) {
if (!node) return undefined
if (node.type === 'Literal' && typeof node.value === 'string') {
return node.value
}
if (
node.type === 'JSXExpressionContainer' &&
node.expression.type === 'Literal' &&
typeof node.expression.value === 'string'
) {
return node.expression.value
}
return undefined
}
module.exports = {
meta: {
type: 'problem',
fixable: 'code',
docs: {
description:
'Require rel="noopener noreferrer" on links to docs.overleaf.com that open in a new tab, to prevent reverse-tabnabbing',
},
schema: [],
},
create(context) {
const selector = `JSXOpeningElement[name.name=/^(${LINK_ELEMENT_NAMES.join('|')})$/]`
return {
[selector](node) {
const attributes = new Map(
node.attributes
.filter(attr => attr.type === 'JSXAttribute')
.map(attr => [attr.name.name, attr])
)
const hrefAttr = attributes.get('href')
const href = getStringLiteralValue(hrefAttr?.value)
if (!href || !href.includes(DOCS_HOST)) {
return
}
const targetAttr = attributes.get('target')
const target = getStringLiteralValue(targetAttr?.value)
if (!target || target.toLowerCase() !== '_blank') {
return
}
const relAttr = attributes.get('rel')
const rel = getStringLiteralValue(relAttr?.value) || ''
const relTokens = rel.toLowerCase().split(/\s+/).filter(Boolean)
const missingTokens = REQUIRED_REL_TOKENS.filter(
token => !relTokens.includes(token)
)
if (missingTokens.length === 0) {
return
}
context.report({
node: relAttr || node,
message:
'Links to docs.overleaf.com with target="_blank" must have rel="noopener noreferrer" to prevent reverse-tabnabbing.',
fix(fixer) {
const newRel = Array.from(
new Set([...relTokens, ...REQUIRED_REL_TOKENS])
).join(' ')
if (relAttr) {
if (!relAttr.value) {
return fixer.replaceText(relAttr, `rel="${newRel}"`)
}
return fixer.replaceText(relAttr.value, `"${newRel}"`)
}
return fixer.insertTextAfter(targetAttr, ` rel="${newRel}"`)
},
})
},
}
},
}
+9
View File
@@ -0,0 +1,9 @@
import { afterAll, describe, it } from 'vitest'
import { RuleTester } from 'eslint'
// ESLint's RuleTester discovers the test framework via these hooks. The repo's
// vitest configs deliberately avoid `globals: true`, so wire them explicitly.
RuleTester.afterAll = afterAll
RuleTester.describe = describe
RuleTester.it = it
RuleTester.itOnly = it.only
@@ -1,17 +1,23 @@
const { RuleTester } = require('eslint')
const tsParser = require('@typescript-eslint/parser')
const json = require('@eslint/json').default
const noThrowInCallback = require('./no-throw-in-callback')
const preferKebabUrl = require('./prefer-kebab-url')
const noUnnecessaryTrans = require('./no-unnecessary-trans')
const shouldUnescapeTrans = require('./should-unescape-trans')
const noGeneratedEditorThemes = require('./no-generated-editor-themes')
const viDoMockValidPath = require('./require-vi-doMock-valid-path')
const requireCioSnakeCaseProperties = require('./require-cio-snake-case-properties')
const noConsecutiveSpacesInLocales = require('./no-consecutive-spaces-in-locales')
const noStraightApostrophesInLocales = require('./no-straight-apostrophes-in-locales')
const frenchTypographyInLocales = require('./french-typography-in-locales')
const sortedKeysInLocales = require('./sorted-keys-in-locales')
import { fileURLToPath } from 'node:url'
import { RuleTester } from 'eslint'
import tsParser from '@typescript-eslint/parser'
import json from '@eslint/json'
import noThrowInCallback from '../../no-throw-in-callback.js'
import noRawReqAccess from '../../no-raw-req-access.js'
import preferKebabUrl from '../../prefer-kebab-url.js'
import noUnnecessaryTrans from '../../no-unnecessary-trans.js'
import shouldUnescapeTrans from '../../should-unescape-trans.js'
import noGeneratedEditorThemes from '../../no-generated-editor-themes.js'
import viDoMockValidPath from '../../require-vi-doMock-valid-path.js'
import requireCioSnakeCaseProperties from '../../require-cio-snake-case-properties.js'
import requireRelForDocsLinks from '../../require-rel-for-docs-links.js'
import noConsecutiveSpacesInLocales from '../../no-consecutive-spaces-in-locales.js'
import noStraightApostrophesInLocales from '../../no-straight-apostrophes-in-locales.js'
import frenchTypographyInLocales from '../../french-typography-in-locales.js'
import sortedKeysInLocales from '../../sorted-keys-in-locales.js'
import noMochaBefore from '../../no-mocha-before.js'
const __filename = fileURLToPath(import.meta.url)
const ruleTester = new RuleTester({
languageOptions: {
@@ -63,7 +69,15 @@ ruleTester.run('prefer-kebab-url', preferKebabUrl, {
{
code: `router.get(/^\\/downLoad\\/pro-ject\\/([^/]*)\\/OutPut\\/out-put\\.pdf$/)`,
errors: [
{ message: 'Route path should be in kebab-case.', suggestions: 1 },
{
message: 'Route path should be in kebab-case.',
suggestions: [
{
desc: 'Change to kebab-case: /^\\/down-load\\/pro-ject\\/([^/]*)\\/out-put\\/out-put\\.pdf$/',
output: `router.get(/^\\/down-load\\/pro-ject\\/([^/]*)\\/out-put\\/out-put\\.pdf$/)`,
},
],
},
],
},
],
@@ -156,28 +170,28 @@ ruleTester.run('no-generated-editor-themes', noGeneratedEditorThemes, {
ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
valid: [
{
code: 'vi.doMock("./require-vi-doMock-valid-path.js")',
code: 'vi.doMock("../../require-vi-doMock-valid-path.js")',
filename: __filename,
},
{
code: 'const filename = "./require-vi-doMock-valid-path.js"; vi.doMock(filename);',
code: 'const filename = "../../require-vi-doMock-valid-path.js"; vi.doMock(filename);',
filename: __filename,
},
],
invalid: [
{
code: "vi.doMock('./require-vi-doMock-valid-path2')",
code: "vi.doMock('../../require-vi-doMock-valid-path2')",
filename: __filename,
errors: [
{
message:
'The path "./require-vi-doMock-valid-path2" in vi.doMock() cannot be resolved relative to the current file.',
'The path "../../require-vi-doMock-valid-path2" in vi.doMock() cannot be resolved relative to the current file.',
suggestions: [],
},
],
},
{
code: 'const filename = "./require-vi-doMock-valid-path2.js"; vi.doMock(filename);',
code: 'const filename = "../../require-vi-doMock-valid-path2.js"; vi.doMock(filename);',
filename: __filename,
errors: [
{
@@ -340,6 +354,146 @@ ruleTester.run('no-throw-in-callback', noThrowInCallback, {
],
})
ruleTester.run('no-raw-req-access', noRawReqAccess, {
valid: [
// validated access goes through parseReq
{ code: `const { params, body } = parseReq(req, schema)` },
// allowlisted middleware reads raw input explicitly
{ code: `const { query } = getRawReqInput(req)` },
// writes stay allowed: body parsers and middleware assign req.body
{ code: `req.body = {}` },
{ code: `req.query = parsed` },
// other request fields are not locked
{ code: `const ua = req.headers['user-agent']` },
{ code: `const s = req.session` },
// other objects with the same field names are fine
{ code: `const b = options.body` },
{ code: `const { body } = response` },
],
invalid: [
{
code: `const id = req.params.project_id`,
errors: [{ messageId: 'noRawReqAccess', data: { field: 'params' } }],
},
{
code: `if (req.query.force) { doIt() }`,
errors: [{ messageId: 'noRawReqAccess', data: { field: 'query' } }],
},
{
code: `send(req.body)`,
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
},
// computed string access
{
code: `const b = req['body']`,
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
},
// destructuring reads
{
code: `const { body } = req`,
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
},
{
code: `const { params, query } = req`,
errors: [
{ messageId: 'noRawReqAccess', data: { field: 'params' } },
{ messageId: 'noRawReqAccess', data: { field: 'query' } },
],
},
// compound assignment reads before writing
{
code: `req.body ||= {}`,
errors: [{ messageId: 'noRawReqAccess', data: { field: 'body' } }],
},
// optional chaining
{
code: `const q = req?.query`,
errors: [{ messageId: 'noRawReqAccess', data: { field: 'query' } }],
},
],
})
ruleTester.run('require-rel-for-docs-links', requireRelForDocsLinks, {
valid: [
{
code: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noopener noreferrer">x</a>`,
},
{
code: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noreferrer noopener">x</a>`,
},
{ code: `<a href="/learn/foo" target="_blank">x</a>` },
{ code: `<a href="https://docs.overleaf.com/foo">x</a>` },
{ code: `<a href="https://docs.overleaf.com/foo" target="_self">x</a>` },
{
code: `<OLButton href="https://docs.overleaf.com/foo" target="_blank" rel="noopener noreferrer">x</OLButton>`,
},
{ code: `<OLButton href="https://docs.overleaf.com/foo">x</OLButton>` },
],
invalid: [
{
code: `<a href="https://docs.overleaf.com/foo" target="_blank">x</a>`,
errors: [
{
message:
'Links to docs.overleaf.com with target="_blank" must have rel="noopener noreferrer" to prevent reverse-tabnabbing.',
},
],
output: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noopener noreferrer">x</a>`,
},
{
code: `<a href="https://docs.overleaf.com/foo" target="_BLANK">x</a>`,
errors: [
{
message:
'Links to docs.overleaf.com with target="_blank" must have rel="noopener noreferrer" to prevent reverse-tabnabbing.',
},
],
output: `<a href="https://docs.overleaf.com/foo" target="_BLANK" rel="noopener noreferrer">x</a>`,
},
{
code: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noopener">x</a>`,
errors: [
{
message:
'Links to docs.overleaf.com with target="_blank" must have rel="noopener noreferrer" to prevent reverse-tabnabbing.',
},
],
output: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noopener noreferrer">x</a>`,
},
{
code: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noreferrer">x</a>`,
errors: [
{
message:
'Links to docs.overleaf.com with target="_blank" must have rel="noopener noreferrer" to prevent reverse-tabnabbing.',
},
],
output: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noreferrer noopener">x</a>`,
},
{
code: `<OLButton href="https://docs.overleaf.com/foo" target="_blank">x</OLButton>`,
errors: [
{
message:
'Links to docs.overleaf.com with target="_blank" must have rel="noopener noreferrer" to prevent reverse-tabnabbing.',
},
],
output: `<OLButton href="https://docs.overleaf.com/foo" target="_blank" rel="noopener noreferrer">x</OLButton>`,
},
{
// Boolean attribute (no value) - relAttr.value is null, must not crash the fixer.
code: `<a href="https://docs.overleaf.com/foo" target="_blank" rel />`,
errors: [
{
message:
'Links to docs.overleaf.com with target="_blank" must have rel="noopener noreferrer" to prevent reverse-tabnabbing.',
},
],
output: `<a href="https://docs.overleaf.com/foo" target="_blank" rel="noopener noreferrer" />`,
},
],
})
jsonRuleTester.run(
'no-consecutive-spaces-in-locales',
noConsecutiveSpacesInLocales,
@@ -453,3 +607,81 @@ jsonRuleTester.run('french-typography-in-locales', frenchTypographyInLocales, {
},
],
})
const noMochaBeforeOptions = [
{ helperPath: 'server-ce/test/helpers/beforeWithReRunOnTestRetry' },
]
ruleTester.run('no-mocha-before', noMochaBefore, {
valid: [
{
code: `beforeEach(function () { setup() })`,
options: noMochaBeforeOptions,
},
{
code: `import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
beforeWithReRunOnTestRetry(function () { setup() })`,
options: noMochaBeforeOptions,
},
{
// locally defined `before` is not the mocha hook
code: `function before(fn) { fn() }
before(function () { setup() })`,
options: noMochaBeforeOptions,
},
],
invalid: [
{
// rename + import added after the last existing import
filename: 'server-ce/test/foo.spec.ts',
code: `import { login } from './helpers/login'
before(async function () { this.timeout(1000) })`,
options: noMochaBeforeOptions,
errors: 1,
output: `import { login } from './helpers/login'
import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
beforeWithReRunOnTestRetry(async function () { this.timeout(1000) })`,
},
{
// no imports yet: import inserted at the top
filename: 'server-ce/test/foo.spec.ts',
code: `before(function () { setup() })`,
options: noMochaBeforeOptions,
errors: 1,
output: `import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
beforeWithReRunOnTestRetry(function () { setup() })`,
},
{
// helper already imported: rename only
filename: 'server-ce/test/foo.spec.ts',
code: `import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
before(function () { setup() })`,
options: noMochaBeforeOptions,
errors: 1,
output: `import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
beforeWithReRunOnTestRetry(function () { setup() })`,
},
{
// file inside helpers/: relative path has no directory prefix
filename: 'server-ce/test/helpers/config.ts',
code: `before(function () { setup() })`,
options: noMochaBeforeOptions,
errors: 1,
output: `import { beforeWithReRunOnTestRetry } from './beforeWithReRunOnTestRetry'
beforeWithReRunOnTestRetry(function () { setup() })`,
},
{
// two-argument mocha form: report without autofix
filename: 'server-ce/test/foo.spec.ts',
code: `before('named hook', function () { setup() })`,
options: noMochaBeforeOptions,
errors: 1,
},
{
// no helperPath configured: report without autofix
filename: 'server-ce/test/foo.spec.ts',
code: `before(function () { setup() })`,
errors: 1,
},
],
})
+1 -1
View File
@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.backend.json",
"include": ["**/*.js"]
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
}
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig, ViteUserConfig } from 'vitest/config'
let reporterOptions: ViteUserConfig['test'] = {}
if (process.env.CI) {
reporterOptions = {
reporters: [
'default',
[
'junit',
{
classnameTemplate: `Unit tests.{filename}`,
},
],
],
outputFile: 'reports/junit-vitest-unit.xml',
}
}
export default defineConfig({
test: {
include: ['test/unit/**/*.test.{js,ts}'],
setupFiles: ['./test/setup.js'],
isolate: false,
...reporterOptions,
},
})
-1
View File
@@ -1 +0,0 @@
24.14.1
-10
View File
@@ -1,10 +0,0 @@
fetch-utils
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/fetch-utils
--pipeline-owner=32
--public-repo=False
-354
View File
@@ -1,354 +0,0 @@
const _ = require('lodash')
const { Readable } = require('node:stream')
const OError = require('@overleaf/o-error')
const fetch = require('node-fetch')
const http = require('node:http')
const https = require('node:https')
let logger
function setLogger(loggerInstance) {
logger = loggerInstance
}
/**
* @import { Response } from 'node-fetch'
*/
/**
* Make a request and return the parsed JSON response.
*
* @param {string | URL} url - request URL
* @param {any} [opts] - fetch options
* @return {Promise<any>} the parsed JSON response
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchJson(url, opts = {}) {
const { json } = await fetchJsonWithResponse(url, opts)
return json
}
async function fetchJsonWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
fetchOpts.headers = fetchOpts.headers ?? {}
fetchOpts.headers.Accept = fetchOpts.headers.Accept ?? 'application/json'
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
const json = await response.json()
return { json, response }
}
/**
* Make a request and return a stream.
*
* If the response body is destroyed, the request is aborted.
*
* @param {string | URL} url - request URL
* @param {any} [opts] - fetch options
* @return {Promise<Readable>}
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchStream(url, opts = {}) {
const { stream } = await fetchStreamWithResponse(url, opts)
return stream
}
async function fetchStreamWithResponse(url, opts = {}) {
const { fetchOpts, abortController, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
abortOnDestroyedResponse(abortController, response)
const stream = response.body
return { stream, response }
}
/**
* Make a request and discard the response.
*
* @param {string | URL} url - request URL
* @param {any} [opts] - fetch options
* @return {Promise<Response>}
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchNothing(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
await discardResponseBody(response)
return response
}
/**
* Make a request and extract the redirect from the response.
*
* @param {string | URL} url - request URL
* @param {any} [opts] - fetch options
* @return {Promise<string>}
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirect(url, opts = {}) {
const { location } = await fetchRedirectWithResponse(url, opts)
return location
}
/**
* Make a request and extract the redirect from the response.
*
* @param {string | URL} url - request URL
* @param {object} opts - fetch options
* @return {Promise<{location: string, response: Response}>}
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirectWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
fetchOpts.redirect = 'manual'
const response = await performRequest(url, fetchOpts, detachSignal)
if (response.status < 300 || response.status >= 400) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
const location = response.headers.get('Location')
if (!location) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body).withCause(
new OError('missing Location response header on 3xx response', {
headers: Object.fromEntries(response.headers.entries()),
})
)
}
await discardResponseBody(response)
return { location, response }
}
/**
* Make a request and return a string.
*
* @param {string | URL} url - request URL
* @param {any} [opts] - fetch options
* @return {Promise<string>}
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchString(url, opts = {}) {
const { body } = await fetchStringWithResponse(url, opts)
return body
}
async function fetchStringWithResponse(url, opts = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
const body = await response.text()
return { body, response }
}
class RequestFailedError extends OError {
constructor(url, opts, response, body) {
super('request failed', {
url,
method: opts.method ?? 'GET',
status: response.status,
})
this.response = response
if (body != null) {
this.body = body
}
}
}
function parseOpts(opts, url) {
const fetchOpts = _.omit(opts, ['json', 'signal', 'basicAuth'])
if (opts.json) {
setupJsonBody(fetchOpts, opts.json)
}
if (opts.basicAuth) {
setupBasicAuth(fetchOpts, opts.basicAuth)
}
const abortController = new AbortController()
fetchOpts.signal = abortController.signal
let detachSignal
if (opts.signal) {
detachSignal = abortOnSignal(abortController, opts.signal)
} else {
let overTimeoutStart
const stack = new Error().stack
const timeout = setTimeout(() => {
overTimeoutStart = process.hrtime.bigint()
}, 120000)
detachSignal = () => {
clearTimeout(timeout)
if (overTimeoutStart && logger) {
logger.warn(
{
url,
method: opts.method ?? 'GET',
overTimeoutMs:
Number(process.hrtime.bigint() - overTimeoutStart) / 1e6,
stack,
},
'Fetch request did not complete within 120 seconds'
)
}
}
}
if (opts.body instanceof Readable) {
abortOnDestroyedRequest(abortController, fetchOpts.body)
}
return { fetchOpts, abortController, detachSignal }
}
function setupJsonBody(fetchOpts, json) {
fetchOpts.body = JSON.stringify(json)
fetchOpts.headers = fetchOpts.headers ?? {}
fetchOpts.headers['Content-Type'] = 'application/json'
}
function setupBasicAuth(fetchOpts, basicAuth) {
fetchOpts.headers = fetchOpts.headers ?? {}
fetchOpts.headers.Authorization =
'Basic ' +
Buffer.from(`${basicAuth.user}:${basicAuth.password}`).toString('base64')
}
function abortOnSignal(abortController, signal) {
const listener = () => {
abortController.abort(signal.reason)
}
if (signal.aborted) {
abortController.abort(signal.reason)
}
signal.addEventListener('abort', listener)
return () => {
signal.removeEventListener('abort', listener)
}
}
function abortOnDestroyedRequest(abortController, stream) {
stream.on('close', () => {
if (!stream.readableEnded) {
abortController.abort()
}
})
}
function abortOnDestroyedResponse(abortController, response) {
response.body.on('close', () => {
if (!response.bodyUsed) {
abortController.abort()
}
})
}
async function performRequest(url, fetchOpts, detachSignal) {
let response
try {
response = await fetch(url, fetchOpts)
} catch (err) {
detachSignal()
if (fetchOpts.body instanceof Readable) {
fetchOpts.body.destroy()
}
throw OError.tag(err, err.message, {
url,
method: fetchOpts.method ?? 'GET',
})
}
response.body.on('close', detachSignal)
if (fetchOpts.body instanceof Readable) {
response.body.on('close', () => {
if (!fetchOpts.body.readableEnded) {
fetchOpts.body.destroy()
}
})
}
return response
}
async function discardResponseBody(response) {
// eslint-disable-next-line no-unused-vars
for await (const chunk of response.body) {
// discard the body
}
}
/**
* @param {Response} response
*/
async function maybeGetResponseBody(response) {
try {
return await response.text()
} catch (err) {
return null
}
}
// Define custom http and https agents with support for connect timeouts
class ConnectTimeoutError extends OError {
constructor(options) {
super('connect timeout', options)
}
}
function withTimeout(createConnection, options, callback) {
if (options.connectTimeout) {
// Wrap createConnection in a timeout
const timer = setTimeout(() => {
socket.destroy(new ConnectTimeoutError(options))
}, options.connectTimeout)
const socket = createConnection(options, (err, stream) => {
clearTimeout(timer)
callback(err, stream)
})
return socket
} else {
// Fallback to default createConnection
return createConnection(options, callback)
}
}
class CustomHttpAgent extends http.Agent {
createConnection(options, callback) {
return withTimeout(super.createConnection.bind(this), options, callback)
}
}
class CustomHttpsAgent extends https.Agent {
createConnection(options, callback) {
return withTimeout(super.createConnection.bind(this), options, callback)
}
}
module.exports = {
fetchJson,
fetchJsonWithResponse,
fetchStream,
fetchStreamWithResponse,
fetchNothing,
fetchRedirect,
fetchRedirectWithResponse,
fetchString,
fetchStringWithResponse,
RequestFailedError,
ConnectTimeoutError,
CustomHttpAgent,
CustomHttpsAgent,
setLogger,
}
+490
View File
@@ -0,0 +1,490 @@
import { Readable, Duplex } from 'node:stream'
import OError from '@overleaf/o-error'
import fetch, { Headers } from 'node-fetch'
import type { RequestInit, Response } from 'node-fetch'
import http from 'node:http'
import https from 'node:https'
import net from 'node:net'
import tls from 'node:tls'
let logger: { warn: (...args: any[]) => void } | undefined
type BasicAuthOptions = {
user: string
password: string
}
type FetchRequestOptions = RequestInit & {
json?: unknown
basicAuth?: BasicAuthOptions
}
type FetchHeaders = NonNullable<RequestInit['headers']>
type ParsedFetchOptions = Omit<
FetchRequestOptions,
'json' | 'signal' | 'basicAuth' | 'headers'
> & {
headers: Headers
signal?: AbortSignal
}
function setLogger(loggerInstance: { warn: (...args: any[]) => void }) {
logger = loggerInstance
}
/**
* Make a request and return the parsed JSON response.
*
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchJson(url: string | URL, opts: FetchRequestOptions = {}) {
const { json } = await fetchJsonWithResponse(url, opts)
return json
}
async function fetchJsonWithResponse(
url: string | URL,
opts: FetchRequestOptions = {}
) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
if (!fetchOpts.headers.has('Accept')) {
fetchOpts.headers.set('Accept', 'application/json')
}
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
const json = await response.json()
return { json, response }
}
/**
* Make a request and return a stream.
*
* If the response body is destroyed, the request is aborted.
*
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchStream(url: string | URL, opts: FetchRequestOptions = {}) {
const { stream } = await fetchStreamWithResponse(url, opts)
return stream
}
async function fetchStreamWithResponse(
url: string | URL,
opts: FetchRequestOptions = {}
) {
const { fetchOpts, abortController, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
abortOnDestroyedResponse(abortController, response)
const stream = response.body
return { stream, response }
}
/**
* Make a request and discard the response.
*
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchNothing(url: string | URL, opts: FetchRequestOptions = {}) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
await discardResponseBody(response)
return response
}
/**
* Make a request and extract the redirect from the response.
*
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirect(
url: string | URL,
opts: FetchRequestOptions = {}
) {
const { location } = await fetchRedirectWithResponse(url, opts)
return location
}
/**
* Make a request and extract the redirect from the response.
*
* @throws {RequestFailedError} if the response has a non redirect status code or missing Location header
*/
async function fetchRedirectWithResponse(
url: string | URL,
opts: FetchRequestOptions = {}
) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
fetchOpts.redirect = 'manual'
const response = await performRequest(url, fetchOpts, detachSignal)
if (response.status < 300 || response.status >= 400) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
const location = response.headers.get('Location')
if (!location) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body).withCause(
new OError('missing Location response header on 3xx response', {
headers: Object.fromEntries(response.headers.entries()),
})
)
}
await discardResponseBody(response)
return { location, response }
}
/**
* Make a request and return a string.
*
* @throws {RequestFailedError} if the response has a failure status code
*/
async function fetchString(url: string | URL, opts: FetchRequestOptions = {}) {
const { body } = await fetchStringWithResponse(url, opts)
return body
}
async function fetchStringWithResponse(
url: string | URL,
opts: FetchRequestOptions = {}
) {
const { fetchOpts, detachSignal } = parseOpts(opts, url)
const response = await performRequest(url, fetchOpts, detachSignal)
if (!response.ok) {
const body = await maybeGetResponseBody(response)
throw new RequestFailedError(url, opts, response, body)
}
const body = await response.text()
return { body, response }
}
class RequestFailedError extends OError {
response: Response
body?: string
constructor(
url: string | URL,
opts: Pick<FetchRequestOptions, 'method'>,
response: Response,
body: string | null
) {
super('request failed', {
url,
method: opts.method ?? 'GET',
status: response.status,
...([400, 409, 413, 422].includes(response.status) ? { body } : {}),
})
this.response = response
if (body != null) {
this.body = body
}
}
}
function setupDefaultAgent(fetchOpts: ParsedFetchOptions) {
// Provide a function to get the agent for each request as there may be
// multiple requests with different protocols due to redirects.
fetchOpts.agent = (url: URL) =>
url.protocol === 'https:' ? httpsAgent : httpAgent
}
/**
* Normalise the headers option into a Headers instance, which is what
* node-fetch builds from it anyway.
*
* Headers with an unset value are dropped: header values are stringified, so
* they would otherwise be sent as the literal string `undefined`.
*/
function parseHeaders(headers: FetchHeaders): Headers {
// A Headers instance can't hold unset values. Pass it through rather than
// iterate it, which would collapse repeated headers into a single
// comma-separated value.
if (headers instanceof Headers) {
return headers
}
// Not in the TS type, but node-fetch accepts any iterable of entries, such as
// a Map or a global Headers instance. Those expose no own enumerable
// properties, so they have to be iterated rather than passed to
// Object.entries().
const entries: Iterable<[string, unknown]> =
Symbol.iterator in headers
? (headers as Iterable<[string, unknown]>)
: Object.entries(headers)
const parsedHeaders = new Headers()
for (const [name, value] of entries) {
if (value != null) {
parsedHeaders.append(name, String(value))
}
}
return parsedHeaders
}
function parseOpts(opts: FetchRequestOptions, url: string | URL) {
const { json, signal, basicAuth, headers, ...rawFetchOpts } = opts
const fetchOpts: ParsedFetchOptions = {
...rawFetchOpts,
headers: parseHeaders(headers ?? {}),
}
if (json) {
setupJsonBody(fetchOpts, json)
}
if (basicAuth) {
setupBasicAuth(fetchOpts, basicAuth)
}
if (!fetchOpts.agent) {
setupDefaultAgent(fetchOpts)
}
const abortController = new AbortController()
fetchOpts.signal = abortController.signal
let detachSignal: () => void
if (signal) {
detachSignal = abortOnSignal(abortController, signal)
} else {
let overTimeoutStart: bigint | undefined
const stack = new Error().stack
const timeout = setTimeout(() => {
overTimeoutStart = process.hrtime.bigint()
}, 120000)
detachSignal = () => {
clearTimeout(timeout)
if (overTimeoutStart && logger) {
logger.warn(
{
url,
method: fetchOpts.method ?? 'GET',
overTimeoutMs:
Number(process.hrtime.bigint() - overTimeoutStart) / 1e6,
stack,
},
'Fetch request did not complete within 120 seconds'
)
}
}
}
if (fetchOpts.body instanceof Readable) {
abortOnDestroyedRequest(abortController, fetchOpts.body)
}
return { fetchOpts, abortController, detachSignal }
}
function setupJsonBody(fetchOpts: ParsedFetchOptions, json: unknown) {
fetchOpts.body = JSON.stringify(json)
fetchOpts.headers.set('Content-Type', 'application/json')
}
function setupBasicAuth(
fetchOpts: ParsedFetchOptions,
basicAuth: BasicAuthOptions
) {
fetchOpts.headers.set(
'Authorization',
'Basic ' +
Buffer.from(`${basicAuth.user}:${basicAuth.password}`).toString('base64')
)
}
function abortOnSignal(abortController: AbortController, signal: AbortSignal) {
const listener = () => {
abortController.abort(signal.reason)
}
if (signal.aborted) {
abortController.abort(signal.reason)
}
signal.addEventListener('abort', listener)
return () => {
signal.removeEventListener('abort', listener)
}
}
function abortOnDestroyedRequest(
abortController: AbortController,
stream: Readable
) {
stream.on('close', () => {
if (!stream.readableEnded) {
abortController.abort()
}
})
}
function abortOnDestroyedResponse(
abortController: AbortController,
response: Response
) {
response.body.on('close', () => {
if (!response.bodyUsed) {
abortController.abort()
}
})
}
async function performRequest(
url: string | URL,
fetchOpts: ParsedFetchOptions,
detachSignal: () => void
) {
let response
try {
response = await fetch(url, fetchOpts)
} catch (err: any) {
detachSignal()
if (fetchOpts.body instanceof Readable) {
fetchOpts.body.destroy()
}
throw OError.tag(err, err.message, {
url,
method: fetchOpts.method ?? 'GET',
})
}
response.body.on('close', detachSignal)
if (fetchOpts.body instanceof Readable) {
const requestBodyStream = fetchOpts.body
response.body.on('close', () => {
if (!requestBodyStream.readableEnded) {
requestBodyStream.destroy()
}
})
}
return response
}
async function discardResponseBody(response: Response) {
// eslint-disable-next-line no-unused-vars
for await (const chunk of response.body) {
// discard the body
}
}
async function maybeGetResponseBody(response: Response) {
try {
return await response.text()
} catch (err) {
return null
}
}
// Define custom http and https agents with support for connect timeouts
class ConnectTimeoutError extends OError {
constructor(options: any) {
super('connect timeout', options)
}
}
function tryToCreateConnection(
createConnection: (options: any) => Duplex,
options: any,
callback: (err: any, socket: any) => void
) {
let socket: Duplex
try {
socket = createConnection(options)
} catch (err) {
callback(err, null)
return
}
const timer = setTimeout(() => {
socket.destroy(new ConnectTimeoutError(options))
}, options.connectTimeout)
const onConnect = () => {
clearTimeout(timer)
socket.off('error', onError)
callback(null, socket)
}
const onError = (err: any) => {
clearTimeout(timer)
socket.off('connect', onConnect)
callback(err, null)
}
socket.once('connect', onConnect)
socket.once('error', onError)
}
function withTimeout(
createConnection: (options: any) => Duplex,
options: any,
callback: (err: any, socket: any) => void
) {
const attempt = (remainingAttempts: number) => {
remainingAttempts--
tryToCreateConnection(createConnection, options, (err, socket) => {
if (err && remainingAttempts > 0) {
setTimeout(() => {
attempt(remainingAttempts)
}, options.connectRetryInterval ?? 100)
return
}
callback(err, socket)
})
}
attempt(3)
}
class CustomHttpAgent extends http.Agent {
constructor(options: any) {
if (!(options.connectTimeout > 0)) {
throw new Error(
'CustomHttpAgent must be called with positive connectTimeout'
)
}
super(options)
}
createConnection(options: any, callback: any) {
withTimeout(net.createConnection, options, callback)
return undefined
}
}
class CustomHttpsAgent extends https.Agent {
constructor(options: any) {
if (!(options.connectTimeout > 0)) {
throw new Error(
'CustomHttpsAgent must be called with positive connectTimeout'
)
}
super(options)
}
createConnection(options: any, callback: any) {
withTimeout(tls.connect, options, callback)
return undefined
}
}
const MAX_CONNECT_TIME = 1000
const httpAgent = new CustomHttpAgent({ connectTimeout: MAX_CONNECT_TIME })
const httpsAgent = new CustomHttpsAgent({ connectTimeout: MAX_CONNECT_TIME })
export {
fetchJson,
fetchJsonWithResponse,
fetchStream,
fetchStreamWithResponse,
fetchNothing,
fetchRedirect,
fetchRedirectWithResponse,
fetchString,
fetchStringWithResponse,
RequestFailedError,
ConnectTimeoutError,
CustomHttpAgent,
CustomHttpsAgent,
setLogger,
}
+14 -8
View File
@@ -2,28 +2,34 @@
"name": "@overleaf/fetch-utils",
"version": "0.1.0",
"description": "utilities for node-fetch",
"main": "index.js",
"main": "index.ts",
"exports": {
"import": "./index.ts",
"require": "./index.ts",
"default": "./index.ts"
},
"type": "module",
"scripts": {
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"test:unit": "vitest --config vitest.config.ts",
"types:check": "tsc --noEmit"
},
"author": "Overleaf (https://www.overleaf.com)",
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/lodash": "^4.17.24",
"@types/node": "^24.5.2",
"@types/node-fetch": "^2.6.13",
"body-parser": "1.20.4",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"eslint": "^10.4.0",
"express": "4.22.1",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"selfsigned": "^5.5.0",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"vitest": "4.1.5"
},
"dependencies": {
"@overleaf/o-error": "workspace:*",
+2 -2
View File
@@ -1,4 +1,4 @@
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
@@ -0,0 +1,652 @@
import { expect } from 'chai'
import dns from 'node:dns'
import http from 'node:http'
import { once } from 'node:events'
import events from 'node:events'
import fs from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { Readable } from 'node:stream'
import { describe, beforeAll, beforeEach, afterAll, it } from 'vitest'
import { AbortError, FetchError, Headers } from 'node-fetch'
import selfsigned from 'selfsigned'
import { TestServer } from './helpers/TestServer.js'
import {
fetchJson,
fetchStream,
fetchNothing,
fetchRedirect,
fetchString,
RequestFailedError,
CustomHttpAgent,
CustomHttpsAgent,
} from '../../index.ts'
const HTTP_PORT = 30001
const HTTPS_PORT = 30002
const url = pathname => `http://example.com:${HTTP_PORT}${pathname}`
const httpsUrl = pathname => `https://example.com:${HTTPS_PORT}${pathname}`
const _originalLookup = dns.lookup
// Custom DNS resolver function
dns.lookup = (hostname, options, callback) => {
if (hostname === 'example.com') {
// If the hostname is our test case, return the ip address for the test server
if (options?.all) {
callback(null, [{ address: '127.0.0.1', family: 4 }])
} else {
callback(null, '127.0.0.1', 4)
}
} else {
// Otherwise, use the default lookup
_originalLookup(hostname, options, callback)
}
}
describe('fetch-utils', function () {
let server
let PUBLIC_CERT
beforeAll(async function () {
server = new TestServer()
const attrs = [{ name: 'commonName', value: 'example.com' }]
const pems = await selfsigned.generate(attrs, { days: 365, keySize: 2048 })
const PRIVATE_KEY = pems.private
PUBLIC_CERT = pems.cert
await server.start(HTTP_PORT, HTTPS_PORT, {
key: PRIVATE_KEY,
cert: PUBLIC_CERT,
})
})
beforeEach(function () {
server.lastReq = undefined
})
afterAll(async function () {
await server.stop()
})
describe('fetchJson', function () {
it('parses a JSON response', async function () {
const json = await fetchJson(url('/json/hello'))
expect(json).to.deep.equal({ msg: 'hello' })
})
it('parses JSON in the request', async function () {
const json = await fetchJson(url('/json/add'), {
method: 'POST',
json: { a: 2, b: 3 },
})
expect(json).to.deep.equal({ sum: 5 })
})
it('accepts stringified JSON as body', async function () {
const json = await fetchJson(url('/json/add'), {
method: 'POST',
body: JSON.stringify({ a: 2, b: 3 }),
headers: { 'Content-Type': 'application/json' },
})
expect(json).to.deep.equal({ sum: 5 })
})
it('throws a FetchError when the payload is not JSON', async function () {
await expect(fetchJson(url('/hello'))).to.be.rejectedWith(FetchError)
})
it('aborts the request if JSON parsing fails', async function () {
await expect(fetchJson(url('/large'))).to.be.rejectedWith(FetchError)
await expectRequestAborted(server.lastReq)
})
it('handles errors when the payload is JSON', async function () {
await expect(fetchJson(url('/json/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(server.lastReq)
})
it('handles errors when the payload is not JSON', async function () {
await expect(fetchJson(url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(server.lastReq)
})
it('supports abort signals', async function () {
await expect(
abortOnceReceived(signal => fetchJson(url('/hang'), { signal }), server)
).to.be.rejectedWith(AbortError)
await expectRequestAborted(server.lastReq)
})
it('supports basic auth', async function () {
const json = await fetchJson(url('/json/basic-auth'), {
basicAuth: { user: 'user', password: 'pass' },
})
expect(json).to.deep.equal({ key: 'verysecret' })
})
it('sets an Authorization header for basic auth', async function () {
await fetchJson(url('/json/basic-auth'), {
basicAuth: { user: 'user', password: 'pass' },
})
expect(server.lastReq.headers.authorization).to.equal(
'Basic ' + Buffer.from('user:pass').toString('base64')
)
})
it("destroys the request body if it doesn't get consumed", async function () {
const stream = Readable.from(infiniteIterator())
await fetchJson(url('/json/ignore-request'), {
method: 'POST',
body: stream,
})
expect(stream.destroyed).to.be.true
})
describe('headers', function () {
it('sets an Accept header of application/json by default', async function () {
await fetchJson(url('/json/hello'))
expect(server.lastReq.headers.accept).to.equal('application/json')
})
it('passes provided headers', async function () {
await fetchJson(url('/json/hello'), {
headers: { 'x-some-value': 'value' },
})
expect(server.lastReq.headers['x-some-value']).to.equal('value')
})
it('respects an explicitly provided Accept header', async function () {
await fetchJson(url('/json/hello'), {
headers: { Accept: 'application/vnd.api+json' },
})
expect(server.lastReq.headers.accept).to.equal(
'application/vnd.api+json'
)
})
it('sets the default Accept header when headers are provided as a Headers instance', async function () {
await fetchJson(url('/json/hello'), {
headers: new Headers({ 'X-Foo': 'bar' }),
})
expect(server.lastReq.headers.accept).to.equal('application/json')
expect(server.lastReq.headers['x-foo']).to.equal('bar')
})
it('sets the default Accept header when headers are provided as an array of tuples', async function () {
await fetchJson(url('/json/hello'), {
headers: [['X-Foo', 'bar']],
})
expect(server.lastReq.headers.accept).to.equal('application/json')
expect(server.lastReq.headers['x-foo']).to.equal('bar')
})
it('treats an unset Accept header as absent and applies the default', async function () {
await fetchJson(url('/json/hello'), {
headers: { Accept: undefined },
})
expect(server.lastReq.headers.accept).to.equal('application/json')
})
it('omits headers with unset values rather than sending "undefined"', async function () {
await fetchJson(url('/json/hello'), {
headers: { 'x-some-value': undefined, 'x-other-value': null },
})
expect(server.lastReq.headers).to.not.have.property('x-some-value')
expect(server.lastReq.headers).to.not.have.property('x-other-value')
})
it('omits headers with unset values provided as an array of tuples', async function () {
await fetchJson(url('/json/hello'), {
headers: [
['X-Foo', 'bar'],
['X-Unset', undefined],
],
})
expect(server.lastReq.headers['x-foo']).to.equal('bar')
expect(server.lastReq.headers).to.not.have.property('x-unset')
})
it('omits headers with unset values provided as a Map', async function () {
await fetchJson(url('/json/hello'), {
headers: new Map([
['X-Foo', 'bar'],
['X-Unset', undefined],
]),
})
expect(server.lastReq.headers['x-foo']).to.equal('bar')
expect(server.lastReq.headers).to.not.have.property('x-unset')
})
it('keeps headers provided as a global Headers instance', async function () {
// Not an instance of node-fetch's Headers: it exposes no own
// enumerable properties to iterate over.
await fetchJson(url('/json/hello'), {
headers: new globalThis.Headers({ 'X-Foo': 'bar' }),
})
expect(server.lastReq.headers['x-foo']).to.equal('bar')
})
it('sets a Content-Type header of application/json when sending a JSON body', async function () {
await fetchJson(url('/json/add'), {
method: 'POST',
json: { a: 2, b: 3 },
})
expect(server.lastReq.headers['content-type']).to.equal(
'application/json'
)
})
})
})
describe('fetchStream', function () {
it('returns a stream', async function () {
const stream = await fetchStream(url('/large'))
const text = await streamToString(stream)
expect(text).to.equal(server.largePayload)
})
it('aborts the request when the stream is destroyed', async function () {
const stream = await fetchStream(url('/large'))
stream.destroy()
await expectRequestAborted(server.lastReq)
})
it('aborts the request when the request body is destroyed before transfer', async function () {
const stream = Readable.from(infiniteIterator())
const promise = fetchStream(url('/hang'), {
method: 'POST',
body: stream,
})
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
await wait(80)
expect(server.lastReq).to.be.undefined
})
it('aborts the request when the request body is destroyed during transfer', async function () {
const stream = Readable.from(infiniteIterator())
// Note: this test won't work on `/hang`
const promise = fetchStream(url('/sink'), {
method: 'POST',
body: stream,
})
await once(server.events, 'request-received')
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
await expectRequestAborted(server.lastReq)
})
it('handles errors', async function () {
await expect(fetchStream(url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(server.lastReq)
})
it('supports abort signals', async function () {
await expect(
abortOnceReceived(
signal => fetchStream(url('/hang'), { signal }),
server
)
).to.be.rejectedWith(AbortError)
await expectRequestAborted(server.lastReq)
})
it('destroys the request body when an error occurs', async function () {
const stream = Readable.from(infiniteIterator())
await expect(
abortOnceReceived(
signal =>
fetchStream(url('/hang'), {
method: 'POST',
body: stream,
signal,
}),
server
)
).to.be.rejectedWith(AbortError)
expect(stream.destroyed).to.be.true
})
it('detaches from signal on success', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
const s = await fetchStream(url('/hello'), { signal })
expect(events.getEventListeners(signal, 'abort')).to.have.length(1)
await pipeline(s, fs.createWriteStream('/dev/null'))
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
})
it('detaches from signal on error', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
try {
await fetchStream(url('/500'), { signal })
} catch (err) {
if (err instanceof RequestFailedError && err.response.status === 500)
continue
throw err
} finally {
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
}
})
})
describe('fetchNothing', function () {
it('closes the connection', async function () {
await fetchNothing(url('/large'))
await expectRequestAborted(server.lastReq)
})
it('aborts the request when the request body is destroyed before transfer', async function () {
const stream = Readable.from(infiniteIterator())
const promise = fetchNothing(url('/hang'), {
method: 'POST',
body: stream,
})
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
expect(server.lastReq).to.be.undefined
})
it('aborts the request when the request body is destroyed during transfer', async function () {
const stream = Readable.from(infiniteIterator())
// Note: this test won't work on `/hang`
const promise = fetchNothing(url('/sink'), {
method: 'POST',
body: stream,
})
await once(server.events, 'request-received')
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
await wait(80)
await expectRequestAborted(server.lastReq)
})
it("doesn't abort the request if the request body ends normally", async function () {
const stream = Readable.from('hello there')
await fetchNothing(url('/sink'), { method: 'POST', body: stream })
})
it('handles errors', async function () {
await expect(fetchNothing(url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(server.lastReq)
})
it('supports abort signals', async function () {
await expect(
abortOnceReceived(
signal => fetchNothing(url('/hang'), { signal }),
server
)
).to.be.rejectedWith(AbortError)
await expectRequestAborted(server.lastReq)
})
it('destroys the request body when an error occurs', async function () {
const stream = Readable.from(infiniteIterator())
await expect(
abortOnceReceived(
signal =>
fetchNothing(url('/hang'), {
method: 'POST',
body: stream,
signal,
}),
server
)
).to.be.rejectedWith(AbortError)
expect(stream.destroyed).to.be.true
})
})
describe('RequestFailedError', function () {
it('includes the response body in OError.info for a 400', async function () {
const err = await getRejection(fetchNothing(url('/400')))
expect(err.info).to.have.property('body', 'boom-400')
})
it('includes the response body in OError.info for a 409', async function () {
const err = await getRejection(fetchNothing(url('/409')))
expect(err.info).to.have.property('body', 'boom-409')
})
it('includes the response body in OError.info for a 413', async function () {
const err = await getRejection(fetchNothing(url('/413')))
expect(err.info).to.have.property('body', 'boom-413')
})
it('includes the response body in OError.info for a 422', async function () {
const err = await getRejection(fetchNothing(url('/422')))
expect(err.info).to.have.property('body', 'boom-422')
})
it('omits the response body from OError.info for a 500', async function () {
const err = await getRejection(fetchNothing(url('/500')))
expect(err.body).to.equal('Internal Server Error')
expect(err.info).to.not.have.property('body')
})
})
describe('fetchString', function () {
it('returns a string', async function () {
const body = await fetchString(url('/hello'))
expect(body).to.equal('hello')
})
it('handles errors', async function () {
await expect(fetchString(url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(server.lastReq)
})
})
describe('fetchRedirect', function () {
it('returns the immediate redirect', async function () {
const body = await fetchRedirect(url('/redirect/1'))
expect(body).to.equal(url('/redirect/2'))
})
it('rejects status 200', async function () {
await expect(fetchRedirect(url('/hello'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(server.lastReq)
})
it('rejects empty redirect', async function () {
await expect(fetchRedirect(url('/redirect/empty-location')))
.to.be.rejectedWith(RequestFailedError)
.and.eventually.have.property('cause')
.and.to.have.property('message')
.to.equal('missing Location response header on 3xx response')
await expectRequestAborted(server.lastReq)
})
it('handles errors', async function () {
await expect(fetchRedirect(url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(server.lastReq)
})
})
describe('CustomHttpAgent', function () {
it('makes an http request successfully', async function () {
const agent = new CustomHttpAgent({ connectTimeout: 100 })
const body = await fetchString(url('/hello'), { agent })
expect(body).to.equal('hello')
})
it('times out when accessing a non-routable address', async function () {
const agent = new CustomHttpAgent({ connectTimeout: 10 })
await expect(fetchString('http://10.255.255.255/', { agent }))
.to.be.rejectedWith(FetchError)
.and.eventually.have.property('message')
.and.to.equal(
'request to http://10.255.255.255/ failed, reason: connect timeout'
)
})
it('retries after a delay', { timeout: 5_000 }, async function () {
// The test should finish after 1.5s.
// t 0ms: first connect fails with ECONNREFUSED
// t 500ms: listen on port
// t1000ms: retry to connect again, works
// t1500ms: respond to request
const agent = new CustomHttpAgent({
connectTimeout: 100,
connectRetryInterval: 1_000,
})
const server = http.createServer((req, res) => {
setTimeout(() => {
res.write('hello')
res.end()
server.close()
}, 500)
})
// Grab a dynamic port, then close again so that the first connect
// attempt is refused before the server starts listening for real.
await once(server.listen(0), 'listening')
const { port } = server.address()
await new Promise(resolve => server.close(resolve))
const t0 = performance.now()
setTimeout(() => {
server.listen(port)
}, 500)
const body = await fetchString(`http://127.0.0.1:${port}`, { agent })
expect(body).to.equal('hello')
const t1 = performance.now()
expect(t1 - t0).to.be.at.least(1_500)
})
it('does not open a stray connection when the socket errors after connect', async function () {
const agent = new CustomHttpAgent({
connectTimeout: 100,
connectRetryInterval: 10,
})
const connections = []
const server = http.createServer(req => {
// Reset the established connection before responding. Doing this in
// the request handler guarantees the client finished the connect
// phase, so the ECONNRESET arrives after 'connect' has fired.
req.socket.resetAndDestroy()
})
server.on('connection', socket => connections.push(socket))
await once(server.listen(0), 'listening')
const { port } = server.address()
try {
await expect(
fetchString(`http://127.0.0.1:${port}/`, { agent })
).to.be.rejectedWith(FetchError)
// Leave time for a buggy connect retry to open a stray connection.
await wait(200)
expect(connections).to.have.length(1)
} finally {
for (const socket of connections) socket.destroy()
server.close()
}
})
})
describe('CustomHttpsAgent', function () {
it('makes an https request successfully', async function () {
const agent = new CustomHttpsAgent({
connectTimeout: 100,
ca: PUBLIC_CERT,
})
const body = await fetchString(httpsUrl('/hello'), { agent })
expect(body).to.equal('hello')
})
it('rejects an untrusted server', async function () {
const agent = new CustomHttpsAgent({
connectTimeout: 100,
})
await expect(fetchString(httpsUrl('/hello'), { agent }))
.to.be.rejectedWith(FetchError)
.and.eventually.have.property('code')
.and.to.equal('DEPTH_ZERO_SELF_SIGNED_CERT')
})
it('times out when accessing a non-routable address', async function () {
const agent = new CustomHttpsAgent({ connectTimeout: 10 })
await expect(fetchString('https://10.255.255.255/', { agent }))
.to.be.rejectedWith(FetchError)
.and.eventually.have.property('message')
.and.to.equal(
'request to https://10.255.255.255/ failed, reason: connect timeout'
)
})
})
})
async function streamToString(stream) {
let s = ''
for await (const chunk of stream) {
s += chunk
}
return s
}
async function* infiniteIterator() {
let i = 1
while (true) {
yield `chunk ${i++}\n`
}
}
/**
* @param {(signal: AbortSignal) => Promise<any>} func
* @param {TestServer} server
*/
async function abortOnceReceived(func, server) {
const controller = new AbortController()
const promise = func(controller.signal)
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(1)
await once(server.events, 'request-received')
controller.abort()
try {
return await promise
} finally {
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(
0
)
}
}
async function expectRequestAborted(req) {
if (!req.destroyed) {
try {
await once(req, 'close')
} catch (err) {
// `once` throws if req emits an 'error' event.
// We ignore `Error: aborted` when the request is aborted.
if (err.message !== 'aborted') {
throw err
}
}
}
expect(req.destroyed).to.be.true
}
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
async function getRejection(promise) {
try {
await promise
} catch (err) {
return err
}
expect.fail('expected promise to reject')
}
@@ -1,450 +0,0 @@
const { expect } = require('chai')
const fs = require('node:fs')
const events = require('node:events')
const { FetchError, AbortError } = require('node-fetch')
const { Readable } = require('node:stream')
const { pipeline } = require('node:stream/promises')
const { once } = require('node:events')
const { TestServer } = require('./helpers/TestServer')
const selfsigned = require('selfsigned')
const {
fetchJson,
fetchStream,
fetchNothing,
fetchRedirect,
fetchString,
RequestFailedError,
CustomHttpAgent,
CustomHttpsAgent,
} = require('../..')
const HTTP_PORT = 30001
const HTTPS_PORT = 30002
const dns = require('node:dns')
const _originalLookup = dns.lookup
// Custom DNS resolver function
dns.lookup = (hostname, options, callback) => {
if (hostname === 'example.com') {
// If the hostname is our test case, return the ip address for the test server
if (options?.all) {
callback(null, [{ address: '127.0.0.1', family: 4 }])
} else {
callback(null, '127.0.0.1', 4)
}
} else {
// Otherwise, use the default lookup
_originalLookup(hostname, options, callback)
}
}
describe('fetch-utils', function () {
let PUBLIC_CERT
before(async function () {
this.server = new TestServer()
const attrs = [{ name: 'commonName', value: 'example.com' }]
const pems = await selfsigned.generate(attrs, { days: 365, keySize: 2048 })
const PRIVATE_KEY = pems.private
PUBLIC_CERT = pems.cert
await this.server.start(HTTP_PORT, HTTPS_PORT, {
key: PRIVATE_KEY,
cert: PUBLIC_CERT,
})
this.url = path => `http://example.com:${HTTP_PORT}${path}`
this.httpsUrl = path => `https://example.com:${HTTPS_PORT}${path}`
})
beforeEach(function () {
this.server.lastReq = undefined
})
after(async function () {
await this.server.stop()
})
describe('fetchJson', function () {
it('parses a JSON response', async function () {
const json = await fetchJson(this.url('/json/hello'))
expect(json).to.deep.equal({ msg: 'hello' })
})
it('parses JSON in the request', async function () {
const json = await fetchJson(this.url('/json/add'), {
method: 'POST',
json: { a: 2, b: 3 },
})
expect(json).to.deep.equal({ sum: 5 })
})
it('accepts stringified JSON as body', async function () {
const json = await fetchJson(this.url('/json/add'), {
method: 'POST',
body: JSON.stringify({ a: 2, b: 3 }),
headers: { 'Content-Type': 'application/json' },
})
expect(json).to.deep.equal({ sum: 5 })
})
it('throws a FetchError when the payload is not JSON', async function () {
await expect(fetchJson(this.url('/hello'))).to.be.rejectedWith(FetchError)
})
it('aborts the request if JSON parsing fails', async function () {
await expect(fetchJson(this.url('/large'))).to.be.rejectedWith(FetchError)
await expectRequestAborted(this.server.lastReq)
})
it('handles errors when the payload is JSON', async function () {
await expect(fetchJson(this.url('/json/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(this.server.lastReq)
})
it('handles errors when the payload is not JSON', async function () {
await expect(fetchJson(this.url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(this.server.lastReq)
})
it('supports abort signals', async function () {
await expect(
abortOnceReceived(
signal => fetchJson(this.url('/hang'), { signal }),
this.server
)
).to.be.rejectedWith(AbortError)
await expectRequestAborted(this.server.lastReq)
})
it('supports basic auth', async function () {
const json = await fetchJson(this.url('/json/basic-auth'), {
basicAuth: { user: 'user', password: 'pass' },
})
expect(json).to.deep.equal({ key: 'verysecret' })
})
it("destroys the request body if it doesn't get consumed", async function () {
const stream = Readable.from(infiniteIterator())
await fetchJson(this.url('/json/ignore-request'), {
method: 'POST',
body: stream,
})
expect(stream.destroyed).to.be.true
})
})
describe('fetchStream', function () {
it('returns a stream', async function () {
const stream = await fetchStream(this.url('/large'))
const text = await streamToString(stream)
expect(text).to.equal(this.server.largePayload)
})
it('aborts the request when the stream is destroyed', async function () {
const stream = await fetchStream(this.url('/large'))
stream.destroy()
await expectRequestAborted(this.server.lastReq)
})
it('aborts the request when the request body is destroyed before transfer', async function () {
const stream = Readable.from(infiniteIterator())
const promise = fetchStream(this.url('/hang'), {
method: 'POST',
body: stream,
})
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
await wait(80)
expect(this.server.lastReq).to.be.undefined
})
it('aborts the request when the request body is destroyed during transfer', async function () {
const stream = Readable.from(infiniteIterator())
// Note: this test won't work on `/hang`
const promise = fetchStream(this.url('/sink'), {
method: 'POST',
body: stream,
})
await once(this.server.events, 'request-received')
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
await expectRequestAborted(this.server.lastReq)
})
it('handles errors', async function () {
await expect(fetchStream(this.url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(this.server.lastReq)
})
it('supports abort signals', async function () {
await expect(
abortOnceReceived(
signal => fetchStream(this.url('/hang'), { signal }),
this.server
)
).to.be.rejectedWith(AbortError)
await expectRequestAborted(this.server.lastReq)
})
it('destroys the request body when an error occurs', async function () {
const stream = Readable.from(infiniteIterator())
await expect(
abortOnceReceived(
signal =>
fetchStream(this.url('/hang'), {
method: 'POST',
body: stream,
signal,
}),
this.server
)
).to.be.rejectedWith(AbortError)
expect(stream.destroyed).to.be.true
})
it('detaches from signal on success', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
const s = await fetchStream(this.url('/hello'), { signal })
expect(events.getEventListeners(signal, 'abort')).to.have.length(1)
await pipeline(s, fs.createWriteStream('/dev/null'))
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
})
it('detaches from signal on error', async function () {
const signal = AbortSignal.timeout(10_000)
for (let i = 0; i < 20; i++) {
try {
await fetchStream(this.url('/500'), { signal })
} catch (err) {
if (err instanceof RequestFailedError && err.response.status === 500)
continue
throw err
} finally {
expect(events.getEventListeners(signal, 'abort')).to.have.length(0)
}
}
})
})
describe('fetchNothing', function () {
it('closes the connection', async function () {
await fetchNothing(this.url('/large'))
await expectRequestAborted(this.server.lastReq)
})
it('aborts the request when the request body is destroyed before transfer', async function () {
const stream = Readable.from(infiniteIterator())
const promise = fetchNothing(this.url('/hang'), {
method: 'POST',
body: stream,
})
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
expect(this.server.lastReq).to.be.undefined
})
it('aborts the request when the request body is destroyed during transfer', async function () {
const stream = Readable.from(infiniteIterator())
// Note: this test won't work on `/hang`
const promise = fetchNothing(this.url('/sink'), {
method: 'POST',
body: stream,
})
await once(this.server.events, 'request-received')
stream.destroy()
await expect(promise).to.be.rejectedWith(AbortError)
await wait(80)
await expectRequestAborted(this.server.lastReq)
})
it("doesn't abort the request if the request body ends normally", async function () {
const stream = Readable.from('hello there')
await fetchNothing(this.url('/sink'), { method: 'POST', body: stream })
})
it('handles errors', async function () {
await expect(fetchNothing(this.url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(this.server.lastReq)
})
it('supports abort signals', async function () {
await expect(
abortOnceReceived(
signal => fetchNothing(this.url('/hang'), { signal }),
this.server
)
).to.be.rejectedWith(AbortError)
await expectRequestAborted(this.server.lastReq)
})
it('destroys the request body when an error occurs', async function () {
const stream = Readable.from(infiniteIterator())
await expect(
abortOnceReceived(
signal =>
fetchNothing(this.url('/hang'), {
method: 'POST',
body: stream,
signal,
}),
this.server
)
).to.be.rejectedWith(AbortError)
expect(stream.destroyed).to.be.true
})
})
describe('fetchString', function () {
it('returns a string', async function () {
const body = await fetchString(this.url('/hello'))
expect(body).to.equal('hello')
})
it('handles errors', async function () {
await expect(fetchString(this.url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(this.server.lastReq)
})
})
describe('fetchRedirect', function () {
it('returns the immediate redirect', async function () {
const body = await fetchRedirect(this.url('/redirect/1'))
expect(body).to.equal(this.url('/redirect/2'))
})
it('rejects status 200', async function () {
await expect(fetchRedirect(this.url('/hello'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(this.server.lastReq)
})
it('rejects empty redirect', async function () {
await expect(fetchRedirect(this.url('/redirect/empty-location')))
.to.be.rejectedWith(RequestFailedError)
.and.eventually.have.property('cause')
.and.to.have.property('message')
.to.equal('missing Location response header on 3xx response')
await expectRequestAborted(this.server.lastReq)
})
it('handles errors', async function () {
await expect(fetchRedirect(this.url('/500'))).to.be.rejectedWith(
RequestFailedError
)
await expectRequestAborted(this.server.lastReq)
})
})
describe('CustomHttpAgent', function () {
it('makes an http request successfully', async function () {
const agent = new CustomHttpAgent({ connectTimeout: 100 })
const body = await fetchString(this.url('/hello'), { agent })
expect(body).to.equal('hello')
})
it('times out when accessing a non-routable address', async function () {
const agent = new CustomHttpAgent({ connectTimeout: 10 })
await expect(fetchString('http://10.255.255.255/', { agent }))
.to.be.rejectedWith(FetchError)
.and.eventually.have.property('message')
.and.to.equal(
'request to http://10.255.255.255/ failed, reason: connect timeout'
)
})
})
describe('CustomHttpsAgent', function () {
it('makes an https request successfully', async function () {
const agent = new CustomHttpsAgent({
connectTimeout: 100,
ca: PUBLIC_CERT,
})
const body = await fetchString(this.httpsUrl('/hello'), { agent })
expect(body).to.equal('hello')
})
it('rejects an untrusted server', async function () {
const agent = new CustomHttpsAgent({
connectTimeout: 100,
})
await expect(fetchString(this.httpsUrl('/hello'), { agent }))
.to.be.rejectedWith(FetchError)
.and.eventually.have.property('code')
.and.to.equal('DEPTH_ZERO_SELF_SIGNED_CERT')
})
it('times out when accessing a non-routable address', async function () {
const agent = new CustomHttpsAgent({ connectTimeout: 10 })
await expect(fetchString('https://10.255.255.255/', { agent }))
.to.be.rejectedWith(FetchError)
.and.eventually.have.property('message')
.and.to.equal(
'request to https://10.255.255.255/ failed, reason: connect timeout'
)
})
})
})
async function streamToString(stream) {
let s = ''
for await (const chunk of stream) {
s += chunk
}
return s
}
async function* infiniteIterator() {
let i = 1
while (true) {
yield `chunk ${i++}\n`
}
}
/**
* @param {(signal: AbortSignal) => Promise<any>} func
* @param {TestServer} server
*/
async function abortOnceReceived(func, server) {
const controller = new AbortController()
const promise = func(controller.signal)
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(1)
await once(server.events, 'request-received')
controller.abort()
try {
return await promise
} finally {
expect(events.getEventListeners(controller.signal, 'abort')).to.have.length(
0
)
}
}
async function expectRequestAborted(req) {
if (!req.destroyed) {
try {
await once(req, 'close')
} catch (err) {
// `once` throws if req emits an 'error' event.
// We ignore `Error: aborted` when the request is aborted.
if (err.message !== 'aborted') {
throw err
}
}
}
expect(req.destroyed).to.be.true
}
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
@@ -1,16 +1,15 @@
const express = require('express')
const bodyParser = require('body-parser')
const { EventEmitter } = require('node:events')
const http = require('node:http')
const https = require('node:https')
const { promisify } = require('node:util')
import express from 'express'
import { EventEmitter } from 'node:events'
import http from 'node:http'
import https from 'node:https'
import { promisify } from 'node:util'
class TestServer {
constructor() {
this.app = express()
this.events = new EventEmitter()
this.app.use(bodyParser.json())
this.app.use(express.json())
this.app.use((req, res, next) => {
this.events.emit('request-received')
this.lastReq = req
@@ -40,6 +39,22 @@ class TestServer {
res.sendStatus(500)
})
this.app.get('/400', (req, res) => {
res.status(400).send('boom-400')
})
this.app.get('/409', (req, res) => {
res.status(409).send('boom-409')
})
this.app.get('/413', (req, res) => {
res.status(413).send('boom-413')
})
this.app.get('/422', (req, res) => {
res.status(422).send('boom-422')
})
this.app.post('/sink', (req, res) => {
req.on('data', () => {})
req.on('end', () => {
@@ -136,4 +151,4 @@ class TestServer {
}
}
module.exports = { TestServer }
export { TestServer }
+3
View File
@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.backend.json",
"compilerOptions": {
"noImplicitAny": true
},
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
}
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig, ViteUserConfig } from 'vitest/config'
let reporterOptions: ViteUserConfig['test'] = {}
if (process.env.CI) {
reporterOptions = {
reporters: [
'default',
[
'junit',
{
classnameTemplate: `Unit tests.{filename}`,
},
],
],
outputFile: 'reports/junit-vitest-unit.xml',
}
}
export default defineConfig({
test: {
include: ['test/unit/**/*.test.{js,ts}'],
setupFiles: ['./test/setup.js'],
isolate: false,
...reporterOptions,
},
})
-1
View File
@@ -1 +0,0 @@
24.14.1
-10
View File
@@ -1,10 +0,0 @@
logger
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/logger
--pipeline-owner=32
--public-repo=False
+45
View File
@@ -1,4 +1,5 @@
const Stream = require('node:stream')
const { setTimeout } = require('node:timers/promises')
const bunyan = require('bunyan')
const GCPManager = require('./gcp-manager')
const Serializers = require('./serializers')
@@ -7,8 +8,22 @@ const {
GCEMetadataLogLevelChecker,
} = require('./log-level-checker')
const { setLogger } = require('@overleaf/fetch-utils')
const {
setLogger: setValidationToolsLogger,
} = require('@overleaf/validation-tools')
// Delay (ms) before exiting to allow final logs to be flushed.
const EXIT_DELAY = 500
const LoggingManager = {
/**
* The serializers registered by addSerializer, kept here rather than only on
* the logger so that they survive a later initialize() call. A module that
* registers one as it is imported would otherwise lose it, since a service
* initializes its logger after its imports have run.
*/
customSerializers: {},
/**
* @param {string} name - The name of the logger
*/
@@ -27,15 +42,35 @@ const LoggingManager = {
error: Serializers.err,
req: Serializers.req,
res: Serializers.res,
...this.customSerializers,
},
streams: options.streams ?? [this._getOutputStreamConfig()],
})
this._setupRingBuffer()
this._setupLogLevelChecker()
setLogger(this)
setValidationToolsLogger(this)
return this
},
/**
* Register a serializer, which decides what gets recorded for a named log
* field, e.g. `logger.error({ dropboxResponseError: err }, '...')`. Callers
* add their own on top of the ones initialize() sets up, and keep them across
* a later initialize(), so registering one as a module is imported works.
*
* A serializer takes whatever was logged under that field, so it receives an
* unknown and narrows it itself, and returns the value to record in its
* place, which is only ever JSON encoded.
*
* @param {string} name - the log field the serializer applies to
* @param {(value: unknown) => unknown} serializer
*/
addSerializer(name, serializer) {
this.customSerializers[name] = serializer
this.logger.serializers[name] = serializer
},
/**
* @param {Record<string, any>|string} attributes - Attributes to log (nice serialization for err, req, res)
* @param {string} [message] - Optional message
@@ -96,6 +131,16 @@ const LoggingManager = {
this.logger.fatal(attributes, message)
},
/**
* Exit after a delay to ensure that log messages are flushed
* @param {number} code
*/
async exit(code) {
await setTimeout(EXIT_DELAY)
process.exit(code)
},
_getOutputStreamConfig() {
switch (process.env.LOGGING_FORMAT) {
case 'gke': {
+8 -1
View File
@@ -7,11 +7,15 @@
"url": "https://github.com/overleaf/overleaf"
},
"main": "index.js",
"exports": {
".": "./index.js",
"./*": "./*"
},
"license": "AGPL-3.0-only",
"version": "3.1.1",
"scripts": {
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
@@ -21,10 +25,13 @@
"@google-cloud/logging-bunyan": "^5.1.0",
"@overleaf/fetch-utils": "workspace:*",
"@overleaf/o-error": "workspace:*",
"@overleaf/validation-tools": "workspace:*",
"bunyan": "^1.8.14"
},
"devDependencies": {
"@types/node": "^24.5.2",
"chai": "^4.3.6",
"eslint": "^10.4.0",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
+9 -5
View File
@@ -1,4 +1,5 @@
const OError = require('@overleaf/o-error')
const { getRawReqInput } = require('@overleaf/validation-tools')
function errSerializer(err) {
if (!err) {
@@ -36,11 +37,14 @@ function reqSerializer(req) {
'content-length': headers['content-length'],
},
}
if (req.params) {
const projectId =
req.params.projectId || req.params.project_id || req.params.Project_id
const userId = req.params.userId || req.params.user_id
const docId = req.params.docId || req.params.doc_id
// raw access justified: case 2 (final error-handler logging) — this
// serializer must not throw while extracting log context, and bunyan
// silently drops the whole `req` field (with a stderr warning) if it did
const { params } = getRawReqInput(req)
if (params) {
const projectId = params.projectId || params.project_id || params.Project_id
const userId = params.userId || params.user_id
const docId = params.docId || params.doc_id
if (projectId) {
entry.projectId = projectId
}
@@ -1,7 +1,7 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
mochaFile: `reports/junit-mocha-${process.env.TEST_NAME_PATTERN}.xml`,
includePending: true,
jenkinsMode: true,
output: true,
+5
View File
@@ -7,6 +7,11 @@ chai.should()
chai.use(sinonChai)
SandboxedModule.configure({
requires: {
// pulls in mongodb (via zod ObjectId helpers), which does not compile in
// the sandbox (missing TextEncoder)
'@overleaf/validation-tools': require('@overleaf/validation-tools'),
},
globals: { Buffer, JSON, console, process },
sourceTransformers: {
removeNodePrefix: function (source) {
@@ -17,6 +17,7 @@ describe('LoggingManager', function () {
info: sinon.stub(),
level: sinon.stub(),
warn: sinon.stub(),
serializers: {},
}
this.Bunyan = {
createLogger: sinon.stub().returns(this.bunyanLogger),
@@ -46,10 +47,14 @@ describe('LoggingManager', function () {
this.fetchUtils = {
setLogger: sinon.stub(),
}
this.validationTools = {
setLogger: sinon.stub(),
}
this.LoggingManager = SandboxedModule.require(MODULE_PATH, {
requires: {
bunyan: this.Bunyan,
'@overleaf/fetch-utils': this.fetchUtils,
'@overleaf/validation-tools': this.validationTools,
'./log-level-checker': this.LogLevelChecker,
},
})
@@ -122,6 +127,37 @@ describe('LoggingManager', function () {
})
})
describe('addSerializer', function () {
beforeEach(function () {
this.serializer = sinon.stub()
})
it('registers the serializer on the current logger', function () {
this.LoggingManager.addSerializer('custom', this.serializer)
expect(this.bunyanLogger.serializers.custom).to.equal(this.serializer)
})
it('keeps the serializer through a later initialize', function () {
// A module registering a serializer as it is imported does so before the
// service initializes its own logger, which replaces the bunyan instance.
this.LoggingManager.addSerializer('custom', this.serializer)
this.Bunyan.createLogger.reset()
this.LoggingManager.initialize(this.loggerName)
expect(
this.Bunyan.createLogger.firstCall.args[0].serializers.custom
).to.equal(this.serializer)
})
it('keeps the serializers initialize sets up', function () {
this.LoggingManager.addSerializer('custom', this.serializer)
this.Bunyan.createLogger.reset()
this.LoggingManager.initialize(this.loggerName)
expect(
this.Bunyan.createLogger.firstCall.args[0].serializers
).to.include.keys('err', 'error', 'req', 'res')
})
})
describe('bunyan logging', function () {
beforeEach(function () {
this.logArgs = [{ foo: 'bar' }, 'foo', 'bar']
@@ -0,0 +1,137 @@
const path = require('node:path')
const { expect } = require('chai')
const MODULE_PATH = path.join(__dirname, '../../serializers.js')
const serializers = require(MODULE_PATH)
// Mirrors the symbols the patched express (.yarn/patches/express-npm-*.patch)
// installs on req in REQ_LOCKDOWN_MODE=warn|throw, and how
// @overleaf/validation-tools' getRawReqInput() reads them.
const RAW_BODY = Symbol.for('overleaf.lockdown.rawBody')
const RAW_QUERY = Symbol.for('overleaf.lockdown.rawQuery')
const RAW_PARAMS = Symbol.for('overleaf.lockdown.rawParams')
const INSTALLED = Symbol.for('overleaf.lockdown.installed')
// Replicates what the patched express does to a request in throw mode:
// parsed input lives in symbol-keyed fields, the public `params` getter
// throws instead of returning it.
function lockedRequest({ params, ...rest }) {
const req = { ...rest }
req[INSTALLED] = true
req[RAW_PARAMS] = params
req[RAW_QUERY] = {}
req[RAW_BODY] = undefined
Object.defineProperty(req, 'params', {
configurable: true,
enumerable: false,
get() {
throw new Error('raw request input is forbidden (req.params)')
},
})
return req
}
describe('reqSerializer', function () {
it('returns undefined/falsy req untouched', function () {
expect(serializers.req(null)).to.equal(null)
expect(serializers.req(undefined)).to.equal(undefined)
})
it('extracts method, url, headers and remote address', function () {
const req = {
method: 'GET',
originalUrl: '/project/123',
headers: {
referer: 'https://example.com',
'user-agent': 'test-agent',
'content-length': '42',
},
ip: '127.0.0.1',
params: {},
}
expect(serializers.req(req)).to.deep.equal({
method: 'GET',
url: '/project/123',
remoteAddress: '127.0.0.1',
headers: {
referer: 'https://example.com',
'user-agent': 'test-agent',
'content-length': '42',
},
})
})
it('extracts projectId/userId/docId from ordinary, readable req.params', function () {
const req = {
method: 'GET',
url: '/',
headers: {},
params: {
project_id: 'project-1',
user_id: 'user-1',
doc_id: 'doc-1',
},
}
const entry = serializers.req(req)
expect(entry.projectId).to.equal('project-1')
expect(entry.userId).to.equal('user-1')
expect(entry.docId).to.equal('doc-1')
})
it('prefers the camelCase param name when both spellings are present', function () {
const req = {
method: 'GET',
url: '/',
headers: {},
params: {
projectId: 'camel-project',
project_id: 'snake-project',
userId: 'camel-user',
user_id: 'snake-user',
docId: 'camel-doc',
doc_id: 'snake-doc',
},
}
const entry = serializers.req(req)
expect(entry.projectId).to.equal('camel-project')
expect(entry.userId).to.equal('camel-user')
expect(entry.docId).to.equal('camel-doc')
})
it('omits projectId/userId/docId when req.params has none of the recognised fields', function () {
const req = { method: 'GET', url: '/', headers: {}, params: {} }
const entry = serializers.req(req)
expect(entry).to.not.have.property('projectId')
expect(entry).to.not.have.property('userId')
expect(entry).to.not.have.property('docId')
})
it('does not throw when req.params is a lockdown-installed throwing getter, and still extracts the ids via the raw accessor', function () {
const req = lockedRequest({
method: 'GET',
url: '/project/123',
headers: {},
params: {
project_id: 'project-1',
user_id: 'user-1',
doc_id: 'doc-1',
},
})
// Sanity check: touching req.params directly really does throw, the way
// it would under REQ_LOCKDOWN_MODE=throw.
expect(() => req.params).to.throw(/raw request input is forbidden/)
let entry
expect(() => {
entry = serializers.req(req)
}).to.not.throw()
expect(entry.projectId).to.equal('project-1')
expect(entry.userId).to.equal('user-1')
expect(entry.docId).to.equal('doc-1')
})
})
-1
View File
@@ -1 +0,0 @@
24.14.1
-10
View File
@@ -1,10 +0,0 @@
metrics
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/metrics
--pipeline-owner=32
--public-repo=False
+6 -5
View File
@@ -34,14 +34,15 @@ function initializeOpenTelemetryInstrumentation() {
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node')
const { Resource } = require('@opentelemetry/resources')
const { resourceFromAttributes } = require('@opentelemetry/resources')
const {
SemanticResourceAttributes,
ATTR_SERVICE_NAME,
ATTR_SERVICE_NAMESPACE,
} = require('@opentelemetry/semantic-conventions')
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: APP_NAME,
[SemanticResourceAttributes.SERVICE_NAMESPACE]: 'Overleaf',
const resource = resourceFromAttributes({
[ATTR_SERVICE_NAME]: APP_NAME,
[ATTR_SERVICE_NAMESPACE]: 'Overleaf',
'host.type': 'VM',
})
+10 -7
View File
@@ -9,20 +9,23 @@
"main": "index.js",
"dependencies": {
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
"@google-cloud/profiler": "^6.0.4",
"@google-cloud/profiler": "^7.0.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/auto-instrumentations-node": "^0.76.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-node": "^0.218.0",
"@opentelemetry/auto-instrumentations-node": "^0.78.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
"@opentelemetry/resources": "^2.9.0",
"@opentelemetry/sdk-node": "^0.220.0",
"@opentelemetry/semantic-conventions": "^1.41.1",
"compression": "^1.7.4",
"prom-client": "^14.1.1",
"yn": "^3.1.1"
},
"devDependencies": {
"@overleaf/logger": "workspace:*",
"@types/node": "^24.5.2",
"bunyan": "^1.0.0",
"chai": "^4.3.6",
"eslint": "^10.4.0",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
@@ -31,10 +34,10 @@
"typescript": "^5.0.4"
},
"scripts": {
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"test:acceptance": "mocha --recursive --exit --grep=${MOCHA_GREP:-} test/acceptance",
"test:acceptance": "mocha --recursive --exit --grep=${TEST_NAME_PATTERN:-} test/acceptance",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
@@ -1,7 +1,7 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
mochaFile: `reports/junit-mocha-${process.env.TEST_NAME_PATTERN}.xml`,
includePending: true,
jenkinsMode: true,
output: true,
-1
View File
@@ -1 +0,0 @@
24.14.1
-11
View File
@@ -1,11 +0,0 @@
mongo-utils
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/mongo-utils
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True
+7 -1
View File
@@ -3,10 +3,14 @@
"version": "0.0.1",
"description": "utilities to help working with mongo",
"main": "index.js",
"exports": {
".": "./index.js",
"./*": "./*"
},
"scripts": {
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
@@ -18,7 +22,9 @@
"mongodb-legacy": "6.1.3"
},
"devDependencies": {
"@types/node": "^24.5.2",
"chai": "^4.3.6",
"eslint": "^10.4.0",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
-1
View File
@@ -1 +0,0 @@
24.14.1
-10
View File
@@ -1,10 +0,0 @@
o-error
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/o-error
--pipeline-owner=32
--public-repo=False
+2 -1
View File
@@ -18,7 +18,7 @@
],
"scripts": {
"build": "yarn run --silent test",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
@@ -32,6 +32,7 @@
"@types/chai": "^4.3.0",
"@types/node": "^24.5.2",
"chai": "^4.3.6",
"eslint": "^10.4.0",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
@@ -1,7 +1,7 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
mochaFile: `reports/junit-mocha-${process.env.TEST_NAME_PATTERN}.xml`,
includePending: true,
jenkinsMode: true,
output: true,
-1
View File
@@ -1 +0,0 @@
24.14.1
@@ -1,11 +0,0 @@
object-persistor
--dependencies=None
--env-add=
--env-pass-through=
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/object-persistor
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True
+8 -1
View File
@@ -3,10 +3,14 @@
"version": "1.0.2",
"description": "Module for storing objects in multiple backends, with fallback on 404 to assist migration between them",
"main": "index.js",
"exports": {
".": "./index.js",
"./*": "./*"
},
"scripts": {
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
@@ -33,8 +37,11 @@
"tiny-async-pool": "^1.1.0"
},
"devDependencies": {
"@types/lodash": "^4.17.24",
"@types/node": "^24.5.2",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"eslint": "^10.4.0",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
@@ -1,7 +1,7 @@
module.exports = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: `reports/junit-mocha-${process.env.MOCHA_GREP}.xml`,
mochaFile: `reports/junit-mocha-${process.env.TEST_NAME_PATTERN}.xml`,
includePending: true,
jenkinsMode: true,
output: true,
-1
View File
@@ -1 +0,0 @@
24.14.1

Some files were not shown because too many files have changed in this diff Show More