#8109·etherpad

Settings CJS mirror is built before reloadSettings(), so plugin settings hashes are never visible to CJS plugins

Author: mathewcsimsCreated Aug 6, 2026Updated Sep 17, 2026
LabelsBugWaiting on Testingplugin framework

Summary

#7421 added property getters on module.exports so that plugins using require('ep_etherpad-lite/node/utils/Settings') can read settings directly. That works for built-in settings such as toolbar, but not for plugin-specific settings hashes (ep_* keys) supplied via settings.json.

The mirror is built from a snapshot of Object.keys(settings) taken at module-evaluation time, which happens before reloadSettings() parses settings.json and adds the plugin keys. A plugin reading its own configuration the documented way therefore always sees undefined.

Observed on 3.3.3, and the same ordering is present in 2.7.3.

Root cause

In src/node/utils/Settings.ts:

  • line ~912 — the CJS compatibility block from #7421:
    javascript
    if (typeof module !== 'undefined' && module.exports) {
      const currentExports = module.exports;
      for (const key of Object.keys(settings)) {
        if (!(key in currentExports)) {
          Object.defineProperty(currentExports, key, { get, set, ... });
        }
      }
    }
  • line ~1464reloadSettings() is called, which runs storeSettings() and adds ep_* keys from settings.json.

Object.keys(settings) at line 912 contains only the defaults declared in the module. ep_comments_page (or any ep_* hash) is not a default — it appears 550 lines later — so no getter is ever defined for it. The getters themselves are live, so existing keys track correctly; the problem is purely which keys get mirrored.

Reproduction

  1. Add a plugin settings hash to settings.json:
    json
    { "ep_comments_page": { "allowReadonlyComments": true } }
  2. In any plugin, log what the documented accessor returns:
    javascript
    const mod = require('ep_etherpad-lite/node/utils/Settings');
    console.log(mod.ep_comments_page);                       // undefined
    console.log(mod.default && mod.default.ep_comments_page); // the object
    console.log(Object.keys(mod).filter(k => k.startsWith('ep_')));         // []
    console.log(Object.keys(mod.default).filter(k => k.startsWith('ep_'))); // ['ep_comments_page']

Actual probe output from a stock 3.3.3 container with the above settings.json:

json
{"moduleType":"object","hasDefault":true,
 "default_ep_comments_page":{"allowReadonlyComments":true,"displayCommentAsIcon":true},
 "argsSettings_ep":{"allowReadonlyComments":true,"displayCommentAsIcon":true},
 "ep_keys_on_module":[],
 "ep_keys_on_default":["ep_comments_page"]}

No Unknown Setting warning is logged — storeSettings() accepts ep_* keys correctly. The value is loaded; it just isn't reachable through the CJS mirror.

The same happens via the EP__ep_comments_page__allowReadonlyComments=true environment variable, so it isn't specific to how the setting is supplied.

Impact

Any plugin that reads its own configuration with require('ep_etherpad-lite/node/utils/Settings').ep_myplugin silently sees undefined, so every configurable option of that plugin falls back to its default with no error or warning. This is the pattern #7421 was intended to keep working.

Concretely: ep_comments_page's allowReadonlyComments and displayCommentAsIcon cannot be enabled at all on a stock install.

Suggested fix

Either move the CJS mirror block to after reloadSettings(), or replace the key-by-key snapshot with a Proxy that forwards property access to settings, so keys added later are covered without depending on evaluation order.

Workarounds that do work today, for reference: read .default (or use .default || mod), or take args.settings from the loadSettings hook.