Skip to content

🎛️ feat: DB-Backed Per-Principal Configuration Override System#12354

Merged
danny-avila merged 45 commits into
devfrom
claude/zen-cannon
Mar 25, 2026
Merged

🎛️ feat: DB-Backed Per-Principal Configuration Override System#12354
danny-avila merged 45 commits into
devfrom
claude/zen-cannon

Conversation

@danny-avila

@danny-avila danny-avila commented Mar 21, 2026

Copy link
Copy Markdown
Owner

Summary

Implements a full database-backed configuration override system that allows admins to define per-principal (user, group, or role) config overrides that are deep-merged with the YAML base config at request time and served via a short-lived TTL cache.

  • Adds a Config Mongoose schema with a compound unique index enforcing one config document per principal per tenant, with principalId typed as String (not Mixed) and tenant isolation applied via the existing plugin.
  • Implements createConfigMethods with findConfigByPrincipal, listAllConfigs, getApplicableConfigs, upsertConfig, patchConfigFields, unsetConfigField, deleteConfig, and toggleConfigActive, all using atomic $set/$unset/$inc operations to avoid read-modify-write races and configVersion auto-increment on every write.
  • Replaces the old getAppConfig stub in api/server/services/Config/app.js with a createAppConfigService factory that deep-merges active DB overrides into the base config by priority, caches results per-principal with a configurable TTL (default 60s), namespaces cache keys by tenantId for multi-tenant safety, caches empty-result lookups to prevent per-request DB queries, and falls back to the base config on DB error.
  • Adds mergeConfigOverrides and a generic deepMerge<T> in packages/data-schemas that sorts configs by priority ascending, replaces arrays rather than concatenating, and preserves the base config as immutable.
  • Adds a complete admin HTTP API at GET|PUT|PATCH|DELETE /api/admin/config via createAdminConfigHandlers, gated behind ACCESS_ADMIN at the router level and MANAGE_CONFIGS/READ_CONFIGS at the handler level, with a broad capability check before any DB lookup and per-section checks for writes containing content.
  • Guards upsertConfigOverrides with both a broad hasConfigCapability(null, 'manage') check and Array.isArray(overrides) rejection to prevent empty-body or array-body privilege escalation.
  • Returns 201 on first creation (detected via configVersion === 1) and 200 on subsequent updates, eliminating the prior TOCTOU race from a pre-upsert findConfigByPrincipal call.
  • Removes isActive: true from patchConfigFields so patching fields on a deactivated config does not silently reactivate it.
  • Relocates SystemCapabilities, CapabilityImplications, ResourceCapabilityMap, and related utilities from systemCapabilities.ts into packages/data-schemas/src/admin/capabilities.ts, adds a @librechat/data-schemas/capabilities subpath export for browser consumers, and derives BaseSystemCapability directly from (typeof SystemCapabilities)[keyof typeof SystemCapabilities] to eliminate manual string-union drift.
  • Exports CapabilityUser from packages/api/src/middleware/capabilities.ts and imports it in config.ts, removing the duplicate local definition.
  • Updates hasConfigCapability to accept ConfigSection | null, where null triggers a broad-only check, replacing the prior '' as ConfigSection type lie.
  • Adds MAX_PATCH_ENTRIES = 100 cap on the patchConfigField entries array.
  • Switches packages/data-schemas Rollup output to preserveModules for per-module tree-shaking and adds sideEffects: false to package.json.
  • Adds .gitattributes enforcing LF line endings for Husky hooks and shell scripts.

Change Type

  • New feature (non-breaking change which adds functionality)

Testing

92 tests across 5 spec files, all passing:

Spec file Tests Coverage
config.spec.ts (packages/api/src/admin) 7 isValidFieldPath dot-path validation, prototype pollution rejection (__proto__, constructor, prototype)
config.handler.spec.ts (packages/api/src/admin) 36 Auth ordering (403 before DB lookup), 201/200 via configVersion, fieldPath from req.query, unsafe path rejection, Bug 2 regression (empty {} overrides blocked without MANAGE_CONFIGS), invariants across all 5 mutation handlers (markConfigsDirty called, 401 without auth, 403 without capability), invariants across all 3 read handlers (403 without capability)
service.spec.ts (packages/api/src/app) 16 Base config load and caching, refresh bypass, DB query on cache miss, empty-result caching (no re-query on second call), merge with DB overrides, TTL cache hit, per-userId key isolation (Finding 1 regression), userId-without-role key format, tenant key isolation, base-only empty result does not block scoped queries (Bug 1 regression), per-user no-override result does not block other users, DB error fallback, getUserPrincipals call/no-call based on userId presence, clearAppConfigCache forces reload
resolution.spec.ts (packages/data-schemas/src/app) 9 Empty and null/undefined configs return base config, single override deep merge, priority ordering (higher wins), array replacement, immutability of base config, null override values, configs with no overrides skipped, three-level priority merge
config.spec.ts (packages/data-schemas/src/methods) 22 upsertConfig create, idempotency, configVersion increment, ObjectId normalization; findConfigByPrincipal active/inactive/missing; listAllConfigs unfiltered, filtered by isActive, sorted by priority; getApplicableConfigs always includes __base__, base + matching principals, priority sort, skips undefined principalId; patchConfigFields atomic $set, upsert-create; unsetConfigField removes field, returns null for missing; deleteConfig hard delete, null for missing; toggleConfigActive deactivate and reactivate

Test Configuration

Run from their respective workspace directories:

cd packages/api && npx jest src/admin/config --testPathPattern="config"
cd packages/api && npx jest src/admin/config.handler
cd packages/api && npx jest src/app/service
cd packages/data-schemas && npx jest src/app/resolution
cd packages/data-schemas && npx jest src/methods/config

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

Copilot AI review requested due to automatic review settings March 21, 2026 20:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a new persisted “Config override” collection in packages/data-schemas to store per-principal configuration overrides (user/group/role), including schema/model/methods wiring, and adds repo-level line-ending improvements for Husky hooks.

Changes:

  • Add Config types export and a new config Mongoose schema/model.
  • Add createConfigMethods() with CRUD-ish helpers for config overrides and wire into createMethods().
  • Add pre-commit shebang and enforce LF endings via .gitattributes.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/data-schemas/src/types/index.ts Re-exports new config types from the types barrel.
packages/data-schemas/src/types/config.ts Adds Config / IConfig type definitions for principal-scoped overrides.
packages/data-schemas/src/schema/index.ts Exposes configSchema from the schema barrel.
packages/data-schemas/src/schema/config.ts Defines configSchema fields/indexes and configVersion save hook.
packages/data-schemas/src/models/index.ts Registers the new Config model in createModels().
packages/data-schemas/src/models/config.ts Adds createConfigModel() and applies tenant isolation plugin.
packages/data-schemas/src/methods/index.ts Wires ConfigMethods into the global methods factory and types.
packages/data-schemas/src/methods/config.ts Implements config lookup/upsert/delete/toggle helpers.
.husky/pre-commit Adds shebang for hook execution consistency.
.gitattributes Forces LF line endings for hooks/shell scripts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/data-schemas/src/schema/config.ts Outdated
Comment thread packages/data-schemas/src/schema/config.ts
Comment thread packages/data-schemas/src/methods/config.ts
Comment thread packages/data-schemas/src/methods/config.ts Outdated
Comment thread packages/data-schemas/src/methods/config.ts
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87dd054973

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/data-schemas/src/methods/config.ts
Comment thread packages/api/src/admin/config.ts Outdated
Comment thread packages/api/src/admin/config.ts Outdated
…g overrides

Add the database foundation for principal-based configuration overrides
(user, group, role) in data-schemas. Includes schema with tenantId and
tenant isolation, CRUD methods, and barrel exports.
The pre-commit hook was missing #!/bin/sh, and core.autocrlf=true was
converting it to CRLF, both causing "Exec format error" on Windows.
Add .gitattributes to force LF for .husky/* and *.sh files.
Add /api/admin/config endpoints for managing per-principal config
overrides (user, group, role). Handlers in @librechat/api use DI pattern
with section-level hasConfigCapability checks for granular access control.

Supports full overrides replacement, per-field PATCH via dot-paths, field
deletion, toggle active, and listing.
The path-to-regexp wildcard syntax (:fieldPath(*)) is not supported by
the version used in Express. Send fieldPath in the DELETE request body
instead, which also avoids URL-encoding issues with dotted paths.
Add mergeConfigOverrides utility in data-schemas for deep-merging DB
config overrides into base AppConfig by priority order.

Update getAppConfig to query DB for applicable configs when role/userId
is provided, with short-TTL caching and a hasAnyConfigs feature flag
for zero-cost when no DB configs exist.

Also: add unique compound index on Config schema, pass userId from
config middleware, and signal config changes from admin API handlers.
Move override resolution, caching strategy, and signalConfigChange from
api/server/services/Config/app.js into packages/api/src/app/appConfigService.ts
using the DI factory pattern (createAppConfigService). The JS file becomes
a thin wiring layer injecting loadBaseConfig, cache, and DB dependencies.
Move SystemCapabilities, CapabilityImplications, and utility functions
(hasImpliedCapability, expandImplications) from data-schemas to
data-provider so they are available to external consumers like the
admin panel without a data-schemas dependency.

Add API-friendly admin types: TAdminConfig, TAdminSystemGrant,
TAdminAuditLogEntry, TAdminGroup, TAdminMember, TAdminUserSearchResult,
TCapabilityCategory, and CAPABILITY_CATEGORIES.

data-schemas re-exports these from data-provider and extends with
config-schema-derived types (ConfigSection, SystemCapability union).

Bump version to 0.8.500.
…chemas

Add AdminConfig, AdminConfigListResponse, AdminConfigResponse, and
AdminConfigDeleteResponse types so both LibreChat API handlers and the
admin panel can share the same response contract. Bump version to 0.0.41.
…schemas

SystemCapabilities, CapabilityImplications, utility functions,
CAPABILITY_CATEGORIES, and admin API response types should not be in
data-provider as it gets compiled into the frontend bundle, exposing
the capability surface. Moved everything to data-schemas (server-only).

All consumers already import from @librechat/data-schemas, so no
import changes needed elsewhere. Consolidated duplicate AdminConfig
type (was in both config.ts and admin.ts).
Split systemCapabilities.ts following data-schemas conventions:
- Types (BaseSystemCapability, SystemCapability, AdminConfig, etc.)
  → src/types/admin.ts
- Runtime code (SystemCapabilities, CapabilityImplications, utilities)
  → src/admin/capabilities.ts

Revert data-provider version to 0.8.401 (no longer modified).
- Rename app/appConfigService.ts → app/service.ts (directory provides context)
- Fix import order in admin/config.ts, types/admin.ts, types/config.ts
- Add naming convention to AGENTS.md
The three-state flag (false/null/true) was the source of multiple bugs
across review rounds. Every attempt to safely set it to false was
defeated by getApplicableConfigs querying only a subset of principals.

Removed: HAS_DB_CONFIGS_KEY constant, all reads/writes of the flag,
markConfigsDirty (now a no-op concept), notifyChange wrapper, and all
tests that seeded false manually.

The per-user/role TTL cache (overrideCacheTtl, default 60s) is the
sole caching mechanism. On cache miss, getApplicableConfigs queries
the DB. This is one indexed query per user per TTL window — acceptable
for the config override use case.
When getApplicableConfigs returns no configs for a principal, cache
baseConfig under their override key with TTL. Without this, every
user with no per-principal overrides hits MongoDB on every request
after the 60s cache window expires.
- Include tenantId in override cache keys to prevent cross-tenant
  config contamination. Single-tenant deployments (tenantId undefined)
  use '_' as placeholder — no behavior change for them.
- Reject PrincipalType.PUBLIC in admin config validation — PUBLIC has
  no PrincipalModel and is never resolved by getApplicableConfigs,
  so config docs for it would be dead data.
- Config middleware passes req.user.tenantId to getAppConfig.
DB methods (config.ts):
- findConfigByPrincipal accepts { includeInactive } option so admin
  GET can retrieve inactive configs (#5)
- upsertConfig catches E11000 duplicate key on concurrent upserts and
  retries without upsert flag (#2)
- unsetConfigField no longer filters isActive:true, consistent with
  patchConfigFields (#11)
- Typed filter objects replace Record<string, unknown> (#12)

Admin handlers (config.ts):
- patchConfigField: serial broad capability check before Promise.all
  to pre-warm ALS principal cache, preventing N parallel DB calls (#3)
- isValidFieldPath rejects leading/trailing dots and consecutive
  dots (#7)
- Duplicate fieldPaths in patch entries return 400 (#8)
- DEFAULT_PRIORITY named constant replaces hardcoded 10 (#14)
- Admin getConfig and patchConfigField pass includeInactive to
  findConfigByPrincipal (#5)
- Route import uses barrel instead of direct file path (#13)

Resolution (resolution.ts):
- deepMerge has MAX_MERGE_DEPTH=10 guard to prevent stack overflow
  from crafted deeply nested configs (#4)
- Remove ADMIN_PANEL_REMAINING.md (local dev notes with Windows paths)
- Add empty-result caching regression test
- Add tenantId to AdminConfigDeps.getAppConfig type
- Restore exhaustive never check in principalModel switch
- Standardize toggleConfigActive session handling to options pattern
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2161e98a66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/app/service.ts
Comment thread packages/api/src/admin/config.ts
Comment thread packages/api/src/admin/config.ts
Add the same non-negative number validation for priority that
upsertConfigOverrides already has. Without this, invalid priority
values could be stored via PATCH and corrupt merge ordering.
- Remove no-op cache delete lines from regression tests
- Change no-tenant sentinel from '_' to '__default__' to avoid
  collision with a real tenant ID when multi-tenancy is enabled
- Remove unused CONFIG_STORE from AppConfigServiceDeps
@danny-avila danny-avila changed the title db-configs 🎛️ feat: DB-Backed Per-Principal Configuration Override System Mar 25, 2026
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a49c5275e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// ── Helpers ──────────────────────────────────────────────────────────

function overrideCacheKey(role?: string, userId?: string, tenantId?: string): string {
const tenant = tenantId || '__default__';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope override cache keys by tenant context

overrideCacheKey collapses missing tenantId to __default__, so any caller that omits tenantId will share the same cached merged config across tenants. This is reachable today because multiple request paths still call getAppConfig({ role: req.user?.role }) without a tenant (for example in api/server/controllers/UserController.js and api/server/routes/config.js), so the first tenant to populate _OVERRIDE_:__default__:* can serve stale/wrong tenant overrides to other tenants until TTL expiry.

Useful? React with 👍 / 👎.

Comment on lines +21 to +23
result[key] = deepMerge(targetVal as AnyObject, sourceVal as AnyObject, depth + 1);
} else {
result[key] = sourceVal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block prototype-poisoning keys during config merges

deepMerge copies attacker-controlled keys directly into objects, including special keys like __proto__; a JSON override payload such as {"interface":{"__proto__":{"polluted":true}}} will mutate the prototype of the merged config object instead of creating a normal field. Since PUT /admin/config only checks that overrides is an object (no recursive key sanitization), this can introduce inherited properties into runtime config reads and produce security-sensitive behavior differences.

Useful? React with 👍 / 👎.

Skip __proto__, constructor, and prototype keys during config merge
to prevent prototype pollution via PUT /api/admin/config overrides.
@danny-avila danny-avila marked this pull request as ready for review March 25, 2026 23:39
@danny-avila danny-avila merged commit 4b6d68b into dev Mar 25, 2026
13 checks passed
@danny-avila danny-avila deleted the claude/zen-cannon branch March 25, 2026 23:39
tvonment added a commit to Corporate-Software-AG/librechat that referenced this pull request Apr 1, 2026
Add admin portal UI for managing runtime config overrides.
Leverages upstream DB-backed config override API (PR danny-avila#12354).

Data layer:
- api-endpoints.ts: 6 admin config URL builders
- data-service.ts: 8 admin API functions (CRUD + toggle)
- keys.ts: 3 new QueryKeys (adminConfigs, adminBaseConfig, adminConfig)

React Query hooks:
- AdminConfig/queries.ts: useGetAdminConfigs, useGetAdminBaseConfig
- AdminConfig/mutations.ts: usePatchAdminConfigFields, useUpsertAdminConfig,
  useDeleteAdminConfigFields, useDeleteAdminConfig, useToggleAdminConfig

UI components:
- AdminConfigView.tsx: main admin page with section sidebar, admin-role gated
- SectionEditor.tsx: YAML base (read-only) + JSON override editor

Routing & nav:
- Dashboard.tsx: /d/admin route
- AccountSettings.tsx: admin menu item (ShieldEllipsis icon, ADMIN only)

Editable sections: interface, modelSpecs, endpoints, balance, transactions,
registration, speech, rateLimits.
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
* ✨ feat: Add Config schema, model, and methods for role-based DB config overrides

Add the database foundation for principal-based configuration overrides
(user, group, role) in data-schemas. Includes schema with tenantId and
tenant isolation, CRUD methods, and barrel exports.

* 🔧 fix: Add shebang and enforce LF line endings for git hooks

The pre-commit hook was missing #!/bin/sh, and core.autocrlf=true was
converting it to CRLF, both causing "Exec format error" on Windows.
Add .gitattributes to force LF for .husky/* and *.sh files.

* ✨ feat: Add admin config API routes with section-level capability checks

Add /api/admin/config endpoints for managing per-principal config
overrides (user, group, role). Handlers in @librechat/api use DI pattern
with section-level hasConfigCapability checks for granular access control.

Supports full overrides replacement, per-field PATCH via dot-paths, field
deletion, toggle active, and listing.

* 🐛 fix: Move deleteConfigField fieldPath from URL param to request body

The path-to-regexp wildcard syntax (:fieldPath(*)) is not supported by
the version used in Express. Send fieldPath in the DELETE request body
instead, which also avoids URL-encoding issues with dotted paths.

* ✨ feat: Wire config resolution into getAppConfig with override caching

Add mergeConfigOverrides utility in data-schemas for deep-merging DB
config overrides into base AppConfig by priority order.

Update getAppConfig to query DB for applicable configs when role/userId
is provided, with short-TTL caching and a hasAnyConfigs feature flag
for zero-cost when no DB configs exist.

Also: add unique compound index on Config schema, pass userId from
config middleware, and signal config changes from admin API handlers.

* 🔄 refactor: Extract getAppConfig logic into packages/api as TS service

Move override resolution, caching strategy, and signalConfigChange from
api/server/services/Config/app.js into packages/api/src/app/appConfigService.ts
using the DI factory pattern (createAppConfigService). The JS file becomes
a thin wiring layer injecting loadBaseConfig, cache, and DB dependencies.

* 🧹 chore: Rename configResolution.ts to resolution.ts

* ✨ feat: Move admin types & capabilities to librechat-data-provider

Move SystemCapabilities, CapabilityImplications, and utility functions
(hasImpliedCapability, expandImplications) from data-schemas to
data-provider so they are available to external consumers like the
admin panel without a data-schemas dependency.

Add API-friendly admin types: TAdminConfig, TAdminSystemGrant,
TAdminAuditLogEntry, TAdminGroup, TAdminMember, TAdminUserSearchResult,
TCapabilityCategory, and CAPABILITY_CATEGORIES.

data-schemas re-exports these from data-provider and extends with
config-schema-derived types (ConfigSection, SystemCapability union).

Bump version to 0.8.500.

* feat: Add JSON-serializable admin config API response types to data-schemas

Add AdminConfig, AdminConfigListResponse, AdminConfigResponse, and
AdminConfigDeleteResponse types so both LibreChat API handlers and the
admin panel can share the same response contract. Bump version to 0.0.41.

* refactor: Move admin capabilities & types from data-provider to data-schemas

SystemCapabilities, CapabilityImplications, utility functions,
CAPABILITY_CATEGORIES, and admin API response types should not be in
data-provider as it gets compiled into the frontend bundle, exposing
the capability surface. Moved everything to data-schemas (server-only).

All consumers already import from @librechat/data-schemas, so no
import changes needed elsewhere. Consolidated duplicate AdminConfig
type (was in both config.ts and admin.ts).

* chore: Bump @librechat/data-schemas to 0.0.42

* refactor: Reorganize admin capabilities into admin/ and types/admin.ts

Split systemCapabilities.ts following data-schemas conventions:
- Types (BaseSystemCapability, SystemCapability, AdminConfig, etc.)
  → src/types/admin.ts
- Runtime code (SystemCapabilities, CapabilityImplications, utilities)
  → src/admin/capabilities.ts

Revert data-provider version to 0.8.401 (no longer modified).

* chore: Fix import ordering, rename appConfigService to service

- Rename app/appConfigService.ts → app/service.ts (directory provides context)
- Fix import order in admin/config.ts, types/admin.ts, types/config.ts
- Add naming convention to AGENTS.md

* feat: Add DB base config support (role/__base__)

- Add BASE_CONFIG_PRINCIPAL_ID constant for reserved base config doc
- getApplicableConfigs always includes __base__ in queries
- getAppConfig queries DB even without role/userId when DB configs exist
- Bump @librechat/data-schemas to 0.0.43

* fix: Address PR review issues for admin config

- Add listAllConfigs method; listConfigs endpoint returns all active
  configs instead of only __base__
- Normalize principalId to string in all config methods to prevent
  ObjectId vs string mismatch on user/group lookups
- Block __proto__ and all dunder-prefixed segments in field path
  validation to prevent prototype pollution
- Fix configVersion off-by-one: default to 0, guard pre('save') with
  !isNew, use $inc on findOneAndUpdate
- Remove unused getApplicableConfigs from admin handler deps

* fix: Enable tree-shaking for data-schemas, bump packages

- Switch data-schemas Rollup output to preserveModules so each source
  file becomes its own chunk; consumers (admin panel) can now import
  just the modules they need without pulling in winston/mongoose/etc.
- Add sideEffects: false to data-schemas package.json
- Bump data-schemas to 0.0.44, data-provider to 0.8.402

* feat: add capabilities subpath export to data-schemas

Adds `@librechat/data-schemas/capabilities` subpath export so browser
consumers can import BASE_CONFIG_PRINCIPAL_ID and capability constants
without pulling in Node.js-only modules (winston, async_hooks, etc.).

Bump version to 0.0.45.

* fix: include dist/ in data-provider npm package

Add explicit files field so npm includes dist/types/ in the published
package. Without this, the root .gitignore exclusion of dist/ causes
npm to omit type declarations, breaking TypeScript consumers.

* chore: bump librechat-data-provider to 0.8.403

* feat: add GET /api/admin/config/base for raw AppConfig

Returns the full AppConfig (YAML + DB base merged) so the admin panel
can display actual config field values and structure. The startup config
endpoint (/api/config) returns TStartupConfig which is a different shape
meant for the frontend app.

* chore: imports order

* fix: address code review findings for admin config

Critical:
- Fix clearAppConfigCache: was deleting from wrong cache store (CONFIG_STORE
  instead of APP_CONFIG), now clears BASE and HAS_DB_CONFIGS keys
- Eliminate race condition: patchConfigField and deleteConfigField now use
  atomic MongoDB $set/$unset with dot-path notation instead of
  read-modify-write cycles, removing the lost-update bug entirely
- Add patchConfigFields and unsetConfigField atomic DB methods

Major:
- Reorder cache check before principal resolution in getAppConfig so
  getUserPrincipals DB query only fires on cache miss
- Replace '' as ConfigSection with typed BROAD_CONFIG_ACCESS constant
- Parallelize capability checks with Promise.all instead of sequential
  awaits in for loops
- Use loose equality (== null) for cache miss check to handle both null
  and undefined returns from cache implementations
- Set HAS_DB_CONFIGS_KEY to true on successful config fetch

Minor:
- Remove dead pre('save') hook from config schema (all writes use
  findOneAndUpdate which bypasses document hooks)
- Consolidate duplicate type imports in resolution.ts
- Remove dead deepGet/deepSet/deepUnset functions (replaced by atomic ops)
- Add .sort({ priority: 1 }) to getApplicableConfigs query
- Rename _impliedBy to impliedByMap

* fix: self-referencing BROAD_CONFIG_ACCESS constant

* fix: replace type-cast sentinel with proper null parameter

Update hasConfigCapability to accept ConfigSection | null where null
means broad access check (MANAGE_CONFIGS or READ_CONFIGS only).
Removes the '' as ConfigSection type lie from admin config handlers.

* fix: remaining review findings + add tests

- listAllConfigs accepts optional { isActive } filter so admin listing
  can show inactive configs (danny-avila#9)
- Standardize session application to .session(session ?? null) across
  all config DB methods (danny-avila#15)
- Export isValidFieldPath and getTopLevelSection for testability
- Add 38 tests across 3 spec files:
  - config.spec.ts (api): path validation, prototype pollution rejection
  - resolution.spec.ts: deep merge, priority ordering, array replacement
  - config.spec.ts (data-schemas): full CRUD, ObjectId normalization,
    atomic $set/$unset, configVersion increment, toggle, __base__ query

* fix: address second code review findings

- Fix cross-user cache contamination: overrideCacheKey now handles
  userId-without-role case with its own cache key (danny-avila#1)
- Add broad capability check before DB lookup in getConfig to prevent
  config existence enumeration (danny-avila#2/danny-avila#3)
- Move deleteConfigField fieldPath from request body to query parameter
  for proxy/load balancer compatibility (danny-avila#5)
- Derive BaseSystemCapability from SystemCapabilities const instead of
  manual string union (danny-avila#6)
- Return 201 on upsert creation, 200 on update (danny-avila#11)
- Remove inline narration comments per AGENTS.md (danny-avila#12)
- Type overrides as Partial<TCustomConfig> in DB methods and handler
  deps (danny-avila#13)
- Replace double as-unknown-as casts in resolution.ts with generic
  deepMerge<T> (danny-avila#14)
- Make override cache TTL injectable via AppConfigServiceDeps (danny-avila#16)
- Add exhaustive never check in principalModel switch (danny-avila#17)

* fix: remaining review findings — tests, rename, semantics

- Rename signalConfigChange → markConfigsDirty with JSDoc documenting
  the stale-window tradeoff and overrideCacheTtl knob
- Fix DEFAULT_OVERRIDE_CACHE_TTL naming convention
- Add createAppConfigService tests (14 cases): cache behavior, feature
  flag, cross-user key isolation, fallback on error, markConfigsDirty
- Add admin handler integration tests (13 cases): auth ordering,
  201/200 on create/update, fieldPath from query param, markConfigsDirty
  calls, capability checks

* fix: global flag corruption + empty overrides auth bypass

- Remove HAS_DB_CONFIGS_KEY=false optimization: a scoped query returning
  no configs does not mean no configs exist globally. Setting the flag
  false from a per-principal query short-circuited all subsequent users.
- Add broad manage capability check before section checks in
  upsertConfigOverrides: empty overrides {} no longer bypasses auth.

* test: add regression and invariant tests for config system

Regression tests:
- Bug 1: User A's empty result does not short-circuit User B's overrides
- Bug 2: Empty overrides {} returns 403 without MANAGE_CONFIGS

Invariant tests (applied across ALL handlers):
- All 5 mutation handlers call markConfigsDirty on success
- All 5 mutation handlers return 401 without auth
- All 5 mutation handlers return 403 without capability
- All 3 read handlers return 403 without capability

* fix: third review pass — all findings addressed

Service (service.ts):
- Restore HAS_DB_CONFIGS=false for base-only queries (no role/userId)
  so deployments with zero DB configs skip DB queries (danny-avila#1)
- Resolve cache once at factory init instead of per-invocation (danny-avila#8)
- Use BASE_CONFIG_PRINCIPAL_ID constant in overrideCacheKey (danny-avila#10)
- Add JSDoc to clearAppConfigCache documenting stale-window (danny-avila#4)
- Fix log message to not say "from YAML" (danny-avila#14)

Admin handlers (config.ts):
- Use configVersion===1 for 201 vs 200, eliminating TOCTOU race (danny-avila#2)
- Add Array.isArray guard on overrides body (danny-avila#5)
- Import CapabilityUser from capabilities.ts, remove duplicate (danny-avila#6)
- Replace as-unknown-as cast with targeted type assertion (danny-avila#7)
- Add MAX_PATCH_ENTRIES=100 cap on entries array (danny-avila#15)
- Reorder deleteConfigField to validate principalType first (danny-avila#12)
- Export CapabilityUser from middleware/capabilities.ts

DB methods (config.ts):
- Remove isActive:true from patchConfigFields to prevent silent
  reactivation of disabled configs (danny-avila#3)

Schema (config.ts):
- Change principalId from Schema.Types.Mixed to String (danny-avila#11)

Tests:
- Add patchConfigField unsafe fieldPath rejection test (danny-avila#9)
- Add base-only HAS_DB_CONFIGS=false test (danny-avila#1)
- Update 201/200 tests to use configVersion instead of findConfig (danny-avila#2)

* fix: add read handler 401 invariant tests + document flag behavior

- Add invariant: all 3 read handlers return 401 without auth
- Document on markConfigsDirty that HAS_DB_CONFIGS stays true after
  all configs are deleted until clearAppConfigCache or restart

* fix: remove HAS_DB_CONFIGS false optimization entirely

getApplicableConfigs([]) only queries for __base__, not all configs.
A deployment with role/group configs but no __base__ doc gets the
flag poisoned to false by a base-only query, silently ignoring all
scoped overrides. The optimization is not safe without a comprehensive
Config.exists() check, which adds its own DB cost. Removed entirely.

The flag is now write-once-true (set when configs are found or by
markConfigsDirty) and only cleared by clearAppConfigCache/restart.

* chore: reorder import statements in app.js for clarity

* refactor: remove HAS_DB_CONFIGS_KEY machinery entirely

The three-state flag (false/null/true) was the source of multiple bugs
across review rounds. Every attempt to safely set it to false was
defeated by getApplicableConfigs querying only a subset of principals.

Removed: HAS_DB_CONFIGS_KEY constant, all reads/writes of the flag,
markConfigsDirty (now a no-op concept), notifyChange wrapper, and all
tests that seeded false manually.

The per-user/role TTL cache (overrideCacheTtl, default 60s) is the
sole caching mechanism. On cache miss, getApplicableConfigs queries
the DB. This is one indexed query per user per TTL window — acceptable
for the config override use case.

* docs: rewrite admin panel remaining work with current state

* perf: cache empty override results to avoid repeated DB queries

When getApplicableConfigs returns no configs for a principal, cache
baseConfig under their override key with TTL. Without this, every
user with no per-principal overrides hits MongoDB on every request
after the 60s cache window expires.

* fix: add tenantId to cache keys + reject PUBLIC principal type

- Include tenantId in override cache keys to prevent cross-tenant
  config contamination. Single-tenant deployments (tenantId undefined)
  use '_' as placeholder — no behavior change for them.
- Reject PrincipalType.PUBLIC in admin config validation — PUBLIC has
  no PrincipalModel and is never resolved by getApplicableConfigs,
  so config docs for it would be dead data.
- Config middleware passes req.user.tenantId to getAppConfig.

* fix: fourth review pass findings

DB methods (config.ts):
- findConfigByPrincipal accepts { includeInactive } option so admin
  GET can retrieve inactive configs (danny-avila#5)
- upsertConfig catches E11000 duplicate key on concurrent upserts and
  retries without upsert flag (danny-avila#2)
- unsetConfigField no longer filters isActive:true, consistent with
  patchConfigFields (danny-avila#11)
- Typed filter objects replace Record<string, unknown> (danny-avila#12)

Admin handlers (config.ts):
- patchConfigField: serial broad capability check before Promise.all
  to pre-warm ALS principal cache, preventing N parallel DB calls (danny-avila#3)
- isValidFieldPath rejects leading/trailing dots and consecutive
  dots (danny-avila#7)
- Duplicate fieldPaths in patch entries return 400 (danny-avila#8)
- DEFAULT_PRIORITY named constant replaces hardcoded 10 (danny-avila#14)
- Admin getConfig and patchConfigField pass includeInactive to
  findConfigByPrincipal (danny-avila#5)
- Route import uses barrel instead of direct file path (danny-avila#13)

Resolution (resolution.ts):
- deepMerge has MAX_MERGE_DEPTH=10 guard to prevent stack overflow
  from crafted deeply nested configs (danny-avila#4)

* fix: final review cleanup

- Remove ADMIN_PANEL_REMAINING.md (local dev notes with Windows paths)
- Add empty-result caching regression test
- Add tenantId to AdminConfigDeps.getAppConfig type
- Restore exhaustive never check in principalModel switch
- Standardize toggleConfigActive session handling to options pattern

* fix: validate priority in patchConfigField handler

Add the same non-negative number validation for priority that
upsertConfigOverrides already has. Without this, invalid priority
values could be stored via PATCH and corrupt merge ordering.

* chore: remove planning doc from PR

* fix: correct stale cache key strings in service tests

* fix: clean up service tests and harden tenant sentinel

- Remove no-op cache delete lines from regression tests
- Change no-tenant sentinel from '_' to '__default__' to avoid
  collision with a real tenant ID when multi-tenancy is enabled
- Remove unused CONFIG_STORE from AppConfigServiceDeps

* chore: bump @librechat/data-schemas to 0.0.46

* fix: block prototype-poisoning keys in deepMerge

Skip __proto__, constructor, and prototype keys during config merge
to prevent prototype pollution via PUT /api/admin/config overrides.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants