Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,11 @@ ALLOW_SOCIAL_LOGIN=false
ALLOW_SOCIAL_REGISTRATION=false
ALLOW_PASSWORD_RESET=false
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
# ALLOW_EMAIL_CHANGE=true # note: enabled by default if omitted/commented out
# `emailChange` in librechat.yaml takes precedence over this, and also sets the link lifetime.
# Multi-node rollouts: keep this false until every node runs this version or newer. Older
# nodes issue and accept password reset links that carry no address binding, so one held by
# a previous address owner can outlive the change and reset the renamed account.
ALLOW_UNVERIFIED_EMAIL_LOGIN=true

SESSION_EXPIRY=1000 * 60 * 15
Expand Down
19 changes: 16 additions & 3 deletions .github/workflows/playwright-mock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CHANGED: ${{ github.event.pull_request.changed_files }}
E2E_SKIP_ARMED: ${{ vars.CODEGRAPH_E2E_SKIP }}
FULL_INCLUDE: '{"include":[{"name":"memory, shard 1/3","stream_store":"memory","redis_image":"","suite":"full","shard":"1/3","artifact":"memory-1-of-3"},{"name":"memory, shard 2/3","stream_store":"memory","redis_image":"","suite":"full","shard":"2/3","artifact":"memory-2-of-3"},{"name":"memory, shard 3/3","stream_store":"memory","redis_image":"","suite":"full","shard":"3/3","artifact":"memory-3-of-3"},{"name":"redis transport","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"","artifact":"redis-transport"}]}'
FULL_INCLUDE: '{"include":[{"name":"memory, shard 1/3","stream_store":"memory","redis_image":"","suite":"full","shard":"1/3","artifact":"memory-1-of-3","email_change":"true"},{"name":"memory, shard 2/3","stream_store":"memory","redis_image":"","suite":"full","shard":"2/3","artifact":"memory-2-of-3"},{"name":"memory, shard 3/3","stream_store":"memory","redis_image":"","suite":"full","shard":"3/3","artifact":"memory-3-of-3"},{"name":"redis transport","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"","artifact":"redis-transport"}]}'
run: |
set +e
note() { echo "$1" >> "$GITHUB_STEP_SUMMARY"; }
Expand Down Expand Up @@ -168,8 +168,8 @@ jobs:
(github.event_name == 'pull_request' && needs.codegraph_select.outputs.e2e_include != '' &&
fromJSON(needs.codegraph_select.outputs.e2e_include)) ||
(github.event_name == 'pull_request' &&
fromJSON('{"include":[{"name":"memory, shard 1/3","stream_store":"memory","redis_image":"","suite":"full","shard":"1/3","artifact":"memory-1-of-3"},{"name":"memory, shard 2/3","stream_store":"memory","redis_image":"","suite":"full","shard":"2/3","artifact":"memory-2-of-3"},{"name":"memory, shard 3/3","stream_store":"memory","redis_image":"","suite":"full","shard":"3/3","artifact":"memory-3-of-3"},{"name":"redis transport","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"","artifact":"redis-transport"}]}')) ||
fromJSON('{"include":[{"name":"memory, shard 1/2","stream_store":"memory","redis_image":"","suite":"full","shard":"1/2","artifact":"memory-1-of-2"},{"name":"memory, shard 2/2","stream_store":"memory","redis_image":"","suite":"full","shard":"2/2","artifact":"memory-2-of-2"},{"name":"redis, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"1/2","artifact":"redis-1-of-2"},{"name":"redis, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"2/2","artifact":"redis-2-of-2"}]}')
fromJSON('{"include":[{"name":"memory, shard 1/3","stream_store":"memory","redis_image":"","suite":"full","shard":"1/3","artifact":"memory-1-of-3","email_change":"true"},{"name":"memory, shard 2/3","stream_store":"memory","redis_image":"","suite":"full","shard":"2/3","artifact":"memory-2-of-3"},{"name":"memory, shard 3/3","stream_store":"memory","redis_image":"","suite":"full","shard":"3/3","artifact":"memory-3-of-3"},{"name":"redis transport","stream_store":"redis","redis_image":"redis:7-alpine","suite":"transport","shard":"","artifact":"redis-transport"}]}')) ||
fromJSON('{"include":[{"name":"memory, shard 1/2","stream_store":"memory","redis_image":"","suite":"full","shard":"1/2","artifact":"memory-1-of-2","email_change":"true"},{"name":"memory, shard 2/2","stream_store":"memory","redis_image":"","suite":"full","shard":"2/2","artifact":"memory-2-of-2"},{"name":"redis, shard 1/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"1/2","artifact":"redis-1-of-2"},{"name":"redis, shard 2/2","stream_store":"redis","redis_image":"redis:7-alpine","suite":"full","shard":"2/2","artifact":"redis-2-of-2"}]}')
}}
services:
redis:
Expand Down Expand Up @@ -360,6 +360,19 @@ jobs:
env:
CI: 'true'

- name: Run registered-email change e2e
if: matrix.email_change == 'true'
run: npx playwright test --config=e2e/playwright.config.email.ts
env:
CI: 'true'

- name: Run disabled registered-email change e2e
if: matrix.email_change == 'true'
run: npx playwright test --config=e2e/playwright.config.email.ts
env:
CI: 'true'
E2E_ALLOW_EMAIL_CHANGE: 'false'

- name: Upload Playwright HTML report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
Expand Down
78 changes: 78 additions & 0 deletions .github/workflows/static-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ on:
- 'package.json'
- 'package-lock.json'
- 'eslint.config.mjs'
# Both spellings: the trigger has to start the workflow for the root
# baseline and for a nested one outside the source trees above, which is
# exactly what the `suppressions` filter below and validateSuppressions()
# claim to cover.
- 'eslint-suppressions.json'
- '**/eslint-suppressions.json'
- '.github/workflows/static-checks.yml'
- '!**.md'

Expand Down Expand Up @@ -54,11 +60,40 @@ jobs:
- 'client/**'
- 'packages/**'
- 'eslint.config.mjs'
- 'eslint-suppressions.json'
- '.github/workflows/static-checks.yml'
- '!**.md'
eslint_config:
- 'eslint.config.mjs'
- '.github/workflows/static-checks.yml'
# The recorded design-rule backlog is data the lint reads, not code it
# lints, so a diff that only edits it matches no source file. Its own
# group runs the baseline validation below; it deliberately stays out
# of `eslint_config`, which gates the full-tree double sweep and could
# learn nothing from a count changing.
suppressions:
- 'eslint-suppressions.json'
- '**/eslint-suppressions.json'
- 'client/src/**'
- 'packages/client/src/**'
# The bundle the rules resolve primitives through is named by the
# library's manifest, produced by its build config and shaped by
# its compiler options.
- 'packages/client/package.json'
- 'packages/client/tsdown.config.mjs'
- 'packages/client/tsconfig.json'
- 'eslint.config.mjs'
# The plugin is a dependency: a bump changes what the rules
# classify and a removal takes them with it, and neither touches a
# source file.
- 'package.json'
- 'package-lock.json'
# The runner is the gate: `validateSuppressions` is what a recorded
# count is measured against, so a change to it has to run against
# the record — the ESLint step above only ever sees sources.
- 'scripts/static-checks.mts'
- '.github/workflows/static-checks.yml'
- '!**.md'
config:
- 'api/**'
- 'config/**'
Expand Down Expand Up @@ -102,6 +137,17 @@ jobs:
continue-on-error: true
run: npm ci

# The @shadcn/lint design rules read each primitive's `cva` variants through
# the @librechat/client entry point, which resolves to packages/client/dist.
# Without it they classify nothing and report strictly fewer violations than
# the same tree does on a machine that built the package — a lint that passes
# for the wrong reason, and a recorded baseline that cannot be trusted.
- name: Build client-package for the design rules
id: design_rules_build
if: always() && (steps.paths.outputs.eslint == 'true' || steps.paths.outputs.suppressions == 'true')
continue-on-error: true
run: npm run build:client-package

# Run ESLint on changed files within the api/, client/, and packages/ directories.
- name: Run ESLint on changed files
id: eslint
Expand Down Expand Up @@ -135,10 +181,15 @@ jobs:
# command into one shell string, and Linux rejects a single argv
# string over 128 KiB (MAX_ARG_STRLEN) — past ~2,200 changed files
# npx dies with exit 249 and no output.
# --pass-on-unpruned-suppressions: the @shadcn/lint design rules run at error
# against a recorded backlog in eslint-suppressions.json. Fixing one of those
# violations leaves its suppression unused, which otherwise fails the PR that
# fixed it; `npm run lint:design:prune` tightens the counts.
node_modules/.bin/eslint --no-error-on-unmatched-pattern \
--config eslint.config.mjs \
--no-warn-ignored \
--max-warnings=0 \
--pass-on-unpruned-suppressions \
-- "${CHANGED_FILES[@]}"

# Run Prettier --check on the same set of changed files to catch
Expand Down Expand Up @@ -242,6 +293,20 @@ jobs:
node_modules/.bin/eslint --config eslint.config.mjs \
api/server/index.js client/src/main.jsx packages/api/src/index.ts

- name: Validate the design-rule suppressions baseline
id: suppressions
if: always() && steps.paths.outputs.suppressions == 'true'
continue-on-error: true
run: |
# The changed-file ESLint step above selects JS/TS sources, so a diff that
# only edits eslint-suppressions.json lints nothing and would otherwise
# merge unread. The runner owns the rules for what the baseline may say.
# --against the PR base, not just the baseline path: the check also asks
# whether the changed files still owe what they record, and that needs
# the diff rather than one file name.
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
node scripts/static-checks.mts --against "$BASE_SHA" --only suppressions

- name: Restore data-provider build cache
if: always() && steps.paths.outputs.config == 'true'
id: cache-data-provider
Expand Down Expand Up @@ -672,6 +737,8 @@ jobs:
INSTALL_DEPENDENCIES_OUTCOME: ${{ steps.install_dependencies.outcome }}
ESLINT_OUTCOME: ${{ steps.eslint.outcome }}
ESLINT_CONFIG_OUTCOME: ${{ steps.eslint_config.outcome }}
DESIGN_RULES_BUILD_OUTCOME: ${{ steps.design_rules_build.outcome }}
SUPPRESSIONS_OUTCOME: ${{ steps.suppressions.outcome }}
PRETTIER_OUTCOME: ${{ steps.prettier.outcome }}
IMPORT_SORT_OUTCOME: ${{ steps.import_sort.outcome }}
RUNNER_OUTCOME: ${{ steps.runner.outcome }}
Expand Down Expand Up @@ -709,6 +776,8 @@ jobs:
record_failure "Dependency installation" "$INSTALL_DEPENDENCIES_OUTCOME"
record_failure "ESLint" "$ESLINT_OUTCOME"
record_failure "ESLint config validation" "$ESLINT_CONFIG_OUTCOME"
record_failure "Client-package build for the design rules" "$DESIGN_RULES_BUILD_OUTCOME"
record_failure "Design-rule suppressions" "$SUPPRESSIONS_OUTCOME"
record_failure "Prettier" "$PRETTIER_OUTCOME"
record_failure "Import sorting" "$IMPORT_SORT_OUTCOME"
record_failure "Local static-checks runner" "$RUNNER_OUTCOME"
Expand Down Expand Up @@ -789,6 +858,15 @@ jobs:
if: steps.paths.outputs.eslint_config == 'true'
run: npm ci

# This job has its own clean checkout, so it needs the primitive metadata
# the design rules read out of packages/client/dist as much as the Static
# checks job does: without it `shadcn/no-restyle` sees no `cva` variants
# and classifies fewer overrides, which would let the full-tree gate pass
# a component-ownership regression.
- name: Build client-package for the design rules
if: steps.paths.outputs.eslint_config == 'true'
run: npm run build:client-package

# Full-tree sweep that gates on regression, not cleanliness: the tree
# carries a pre-existing lint backlog (70 errors at time of wiring), so
# requiring a clean sweep would fail config PRs on unrelated debt.
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,6 @@ scripts/activity-labels/results/
!e2e/lighthouse/librechat.yaml
/.lighthouse/
/e2e/lighthouse/.test-results/
# Chrome user-data directories a Lighthouse run leaves in the repository root when the browser
# is handed a Windows profile path from WSL: the whole path arrives as one directory name.
/*lighthouse.[0-9]*/
27 changes: 25 additions & 2 deletions .husky/lint-staged.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,33 @@ module.exports = {
'*.{js,jsx,ts,tsx}': [
'node scripts/sort-imports.mts',
'prettier --write',
'eslint --fix',
// The design rules read each primitive's `cva` variants through
// @librechat/client, which resolves to packages/client/dist, so the metadata
// has to exist before the fixing pass below reads it: run first, that pass
// would classify against a stale build and report counts neither the runner
// nor CI agrees with — and its failure aborts lint-staged before the runner
// gets to rebuild. tsdown is ~1s when nothing changed.
() => 'npm run --silent build:client-package',
// Same invocation as the Static Checks CI job: warnings are failures there,
// and changed files under config-ignored paths must not trip it.
'eslint --config eslint.config.mjs --no-warn-ignored --max-warnings=0',
// --pass-on-unpruned-suppressions: fixing a design-rule violation recorded in
// eslint-suppressions.json otherwise fails the commit that fixed it. Tighten the
// recorded counts with `npm run lint:design:prune`.
// One invocation, not a bare `eslint --fix` followed by this one: ESLint applies
// suppressions after fixes, so a single run reports the post-fix state, while a
// first run without the flag exits 2 on the suppression the fix just made unused
// and lint-staged never reaches the command that tolerates it.
'eslint --fix --config eslint.config.mjs --no-warn-ignored --max-warnings=0 --pass-on-unpruned-suppressions',
// The runner is the CI mirror — same arguments — and rebuilds the metadata
// itself when a staged change made it stale, so the commit and the lane
// agree on what the rules classify.
'node scripts/static-checks.mts --only eslint',
],
'*.json': ['prettier --write'],
// No group here for the recorded backlog, the design metadata or this gate's
// own source: `.husky/pre-commit` ends by running the runner over the staged
// diff with `--skip eslint,prettier,imports`, and that run selects its groups
// from the same filters the Static Checks lane's paths-filter mirrors. It also
// sees a deletion, which lint-staged does not hand to a task at all. A group
// here would run the whole-record sweep a second time for the same commit.
};
5 changes: 4 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
[ -n "$CI" ] && exit 0

# Formats, sorts and lints the exact staged content of each changed file.
npx lint-staged --config ./.husky/lint-staged.config.js || exit 1
# Serially: two of the groups build packages/client for the design rules, and
# lint-staged runs groups in parallel by default — two tsdown writes into the
# same dist leave the metadata the rules read half-written.
npx lint-staged --concurrent false --config ./.husky/lint-staged.config.js || exit 1

# The rest of the Static Checks CI job, gated on the paths this commit touches.
# Add STATIC_CHECKS_FULL=1 for the slow gates (TypeScript, config migration
Expand Down
11 changes: 9 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,15 @@ feature-local styles. Use semantic theme/Tailwind roles for color and shared app
introduce raw palette utilities, hard-coded colors, or arbitrary theme CSS. If the system cannot
express a reusable design need, deepen the shared primitive or versioned theme-token registry
instead of copying classes into a feature. Keep genuine layout and behavior local, and document
why any new custom CSS cannot be expressed by the shared system. See the detailed policy in
`CLAUDE.md` under “Theming and styling.”
why any new custom CSS cannot be expressed by the shared system.

`npm run lint` enforces this: `@shadcn/lint` reads each primitive's `cva` variants and reports a
`className` that overrides what the primitive owns, naming the variant, size or file to use
instead. Do not reach for `eslint-disable`, and do not widen a file's recorded count in
`eslint-suppressions.json` to land a restyle — that file holds the backlog the rules inherited, so
raising an entry is a reviewable claim that the override is right. After fixing violations, run
`npm run lint:design:prune`; after moving a file that carries suppressions, run
`npm run lint:design:suppress`. See the detailed policy in `CLAUDE.md` under “Theming and styling.”

## Backend auth cache

Expand Down
Loading
Loading