* [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
* 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
* 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
* 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
* [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
* 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
* 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
* 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
* [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