@jagilber-org/index-server 1.26.11 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,7 +6,31 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [1.26.11] - 2026-04-30
9
+ ## [1.27.0] - 2026-04-30
10
+
11
+ ### Changed (BREAKING)
12
+
13
+ - **Rate limiting is now opt-in** and consolidated behind a single environment variable, `INDEX_SERVER_RATE_LIMIT` (#270).
14
+ - `INDEX_SERVER_RATE_LIMIT=0` (default, or unset) — rate limiting is **disabled**.
15
+ - `INDEX_SERVER_RATE_LIMIT=N` (positive integer) — enforces **N requests per minute** with a fixed 60-second window.
16
+ - Bulk import/export/backup/restore routes (`/api/admin/maintenance/normalize`, `/api/admin/maintenance/backup`, `/api/admin/maintenance/backups`, `/api/admin/maintenance/restore`, `/api/charts/export`, `/api/sqlite/backup`, `/api/sqlite/restore`, `/api/sqlite/export`) are **unconditionally exempt**, so dashboard bulk operations no longer trigger 429 responses.
17
+ - The 429 response body shape simplifies to `{ error, message, retryAfterSeconds, timestamp }`. The previous `tier` field (global vs. mutation) has been removed; there is now a single tier.
18
+
19
+ ### Removed (BREAKING)
20
+
21
+ The following environment variables have been **removed with no back-compat aliases**. Replace them with `INDEX_SERVER_RATE_LIMIT`:
22
+
23
+ | Removed variable | Replacement |
24
+ |------------------|-------------|
25
+ | `INDEX_SERVER_DISABLE_RATE_LIMIT` | unset / `INDEX_SERVER_RATE_LIMIT=0` (default) |
26
+ | `INDEX_SERVER_DISABLE_USAGE_RATE_LIMIT` | unset / `INDEX_SERVER_RATE_LIMIT=0` (default) |
27
+ | `INDEX_SERVER_RATE_LIMIT_MAX` | `INDEX_SERVER_RATE_LIMIT=<N>` |
28
+ | `INDEX_SERVER_RATE_LIMIT_WINDOW_MS` | _removed; window is fixed at 60 seconds_ |
29
+ | `INDEX_SERVER_RATE_LIMIT_MUTATION_MAX` | _removed; single tier only_ |
30
+
31
+ Also removed: `DashboardHttpConfig.rateLimitEnabled / rateLimitWindowMs / rateLimitMax / rateLimitMutationMax` (replaced by `rateLimitPerMinute: number`); `ApiRoutesOptions.rateLimit` (replaced by `rateLimitPerMinute?: number`); `AdminConfig.serverSettings.rateLimit` reduced to `{ perMinute: number }`.
32
+
33
+
10
34
 
11
35
  ### Fixed
12
36
 
@@ -15,10 +15,17 @@ interface DashboardHttpConfig {
15
15
  verboseLogging: boolean;
16
16
  mutationEnabled: boolean;
17
17
  adminApiKey?: string;
18
- rateLimitEnabled: boolean;
19
- rateLimitWindowMs: number;
20
- rateLimitMax: number;
21
- rateLimitMutationMax: number;
18
+ /**
19
+ * Dashboard HTTP API rate limit, in requests per minute.
20
+ *
21
+ * - `0` (default) disables rate limiting entirely (HTTP API + usage tracking).
22
+ * - Any positive integer N enforces N requests/min globally with a fixed
23
+ * 60-second window. Bulk import/export/backup/restore routes are
24
+ * unconditionally exempt regardless of this value.
25
+ *
26
+ * Configured via the `INDEX_SERVER_RATE_LIMIT` environment variable.
27
+ */
28
+ rateLimitPerMinute: number;
22
29
  tls: DashboardTlsConfig;
23
30
  }
24
31
  interface DashboardAdminConfig {
@@ -36,10 +36,7 @@ function parseDashboardConfig(mutationEnabled, instructionsBaseDir) {
36
36
  verboseLogging: (0, envUtils_1.getBooleanEnv)('INDEX_SERVER_VERBOSE_LOGGING'),
37
37
  mutationEnabled,
38
38
  adminApiKey: process.env.INDEX_SERVER_ADMIN_API_KEY || undefined,
39
- rateLimitEnabled: !(0, envUtils_1.getBooleanEnv)('INDEX_SERVER_DISABLE_RATE_LIMIT'),
40
- rateLimitWindowMs: Math.max(1, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT_WINDOW_MS', 60_000)),
41
- rateLimitMax: Math.max(0, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT_MAX', 100)),
42
- rateLimitMutationMax: Math.max(0, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT_MUTATION_MAX', 20)),
39
+ rateLimitPerMinute: Math.max(0, (0, configUtils_1.numberFromEnv)('INDEX_SERVER_RATE_LIMIT', 0)),
43
40
  tls: {
44
41
  enabled: (0, envUtils_1.getBooleanEnv)('INDEX_SERVER_DASHBOARD_TLS'),
45
42
  certPath: process.env.INDEX_SERVER_DASHBOARD_TLS_CERT || undefined,
@@ -1,29 +1,29 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta name="dashboard-build-version" content="1.26.11-9badd8dd">
4
+ <meta name="dashboard-build-version" content="1.27.0-9badd8dd">
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>Index Server Admin</title>
8
- <link rel="stylesheet" href="css/admin.css?v=1.26.11-9badd8dd">
9
- <script defer src="js/admin.utils.js?v=1.26.11-9badd8dd"></script>
10
- <script defer src="js/admin.auth.js?v=1.26.11-9badd8dd"></script>
11
- <script defer src="js/admin.overview.js?v=1.26.11-9badd8dd"></script>
12
- <script defer src="js/admin.sessions.js?v=1.26.11-9badd8dd"></script>
13
- <script defer src="js/admin.monitor.js?v=1.26.11-9badd8dd"></script>
14
- <script defer src="js/admin.graph.js?v=1.26.11-9badd8dd"></script>
8
+ <link rel="stylesheet" href="css/admin.css?v=1.27.0-9badd8dd">
9
+ <script defer src="js/admin.utils.js?v=1.27.0-9badd8dd"></script>
10
+ <script defer src="js/admin.auth.js?v=1.27.0-9badd8dd"></script>
11
+ <script defer src="js/admin.overview.js?v=1.27.0-9badd8dd"></script>
12
+ <script defer src="js/admin.sessions.js?v=1.27.0-9badd8dd"></script>
13
+ <script defer src="js/admin.monitor.js?v=1.27.0-9badd8dd"></script>
14
+ <script defer src="js/admin.graph.js?v=1.27.0-9badd8dd"></script>
15
15
  <script defer src="js/marked.umd.js"></script>
16
- <script defer src="js/admin.instructions.js?v=1.26.11-9badd8dd"></script>
17
- <script defer src="js/admin.logs.js?v=1.26.11-9badd8dd"></script>
18
- <script defer src="js/admin.maintenance.js?v=1.26.11-9badd8dd"></script>
19
- <script defer src="js/admin.config.js?v=1.26.11-9badd8dd"></script>
20
- <script defer src="js/admin.performance.js?v=1.26.11-9badd8dd"></script>
21
- <script defer src="js/admin.instances.js?v=1.26.11-9badd8dd"></script>
22
- <script defer src="js/admin.embeddings.js?v=1.26.11-9badd8dd"></script>
23
- <script defer src="js/admin.messaging.js?v=1.26.11-9badd8dd"></script>
24
- <script defer src="js/admin.sqlite.js?v=1.26.11-9badd8dd"></script>
25
- <script defer src="js/admin.boot.js?v=1.26.11-9badd8dd"></script>
26
- <script defer src="js/admin.feedback.js?v=1.26.11-9badd8dd"></script>
16
+ <script defer src="js/admin.instructions.js?v=1.27.0-9badd8dd"></script>
17
+ <script defer src="js/admin.logs.js?v=1.27.0-9badd8dd"></script>
18
+ <script defer src="js/admin.maintenance.js?v=1.27.0-9badd8dd"></script>
19
+ <script defer src="js/admin.config.js?v=1.27.0-9badd8dd"></script>
20
+ <script defer src="js/admin.performance.js?v=1.27.0-9badd8dd"></script>
21
+ <script defer src="js/admin.instances.js?v=1.27.0-9badd8dd"></script>
22
+ <script defer src="js/admin.embeddings.js?v=1.27.0-9badd8dd"></script>
23
+ <script defer src="js/admin.messaging.js?v=1.27.0-9badd8dd"></script>
24
+ <script defer src="js/admin.sqlite.js?v=1.27.0-9badd8dd"></script>
25
+ <script defer src="js/admin.boot.js?v=1.27.0-9badd8dd"></script>
26
+ <script defer src="js/admin.feedback.js?v=1.27.0-9badd8dd"></script>
27
27
  </head>
28
28
  <body>
29
29
  <div class="admin-container admin-root">
@@ -879,10 +879,10 @@
879
879
  }
880
880
  }
881
881
 
882
- // Graph logic was extracted to js/admin.graph.js?v=1.26.11-9badd8dd
882
+ // Graph logic was extracted to js/admin.graph.js?v=1.27.0-9badd8dd
883
883
  // Functions available globally: reloadGraphMermaid, initGraphScopeDefaults, copyMermaidSource, toggleGraphEdit, applyGraphEdit, cancelGraphEdit, refreshDrillCategories, loadDrillInstructions, clearSelections
884
884
 
885
- <!-- overview functions moved to js/admin.overview.js?v=1.26.11-9badd8dd -->
885
+ <!-- overview functions moved to js/admin.overview.js?v=1.27.0-9badd8dd -->
886
886
 
887
887
  // Lightweight overview-level maintenance display (optional)
888
888
  // Intentionally minimal to avoid blocking overview rendering.
@@ -1067,7 +1067,7 @@
1067
1067
  }
1068
1068
 
1069
1069
  // --- Backup / Restore ---
1070
- // Extracted to js/admin.maintenance.js?v=1.26.11-9badd8dd
1070
+ // Extracted to js/admin.maintenance.js?v=1.27.0-9badd8dd
1071
1071
 
1072
1072
  async function performBackup() {
1073
1073
  try {
@@ -1133,7 +1133,7 @@
1133
1133
  }
1134
1134
 
1135
1135
  async function loadConfiguration() {
1136
- // Primary implementation in js/admin.config.js?v=1.26.11-9badd8dd (loaded via defer).
1136
+ // Primary implementation in js/admin.config.js?v=1.27.0-9badd8dd (loaded via defer).
1137
1137
  // This inline fallback only fires if the external script failed to load.
1138
1138
  if (window.__configExternalLoaded) return;
1139
1139
  try {
@@ -1193,10 +1193,10 @@
1193
1193
  return false;
1194
1194
  }
1195
1195
 
1196
- // Monitoring functions moved to js/admin.monitor.js?v=1.26.11-9badd8dd
1196
+ // Monitoring functions moved to js/admin.monitor.js?v=1.27.0-9badd8dd
1197
1197
 
1198
1198
  // ===== Log Viewer =====
1199
- // Extracted to js/admin.logs.js?v=1.26.11-9badd8dd
1199
+ // Extracted to js/admin.logs.js?v=1.27.0-9badd8dd
1200
1200
 
1201
1201
  // ===== Instruction Management =====
1202
1202
  let instructionEditing = null;
@@ -1693,7 +1693,7 @@
1693
1693
  setInterval(fetchResourceTrends, 10000);
1694
1694
  })();
1695
1695
 
1696
- // Instruction management logic extracted to js/admin.instructions.js?v=1.26.11-9badd8dd
1696
+ // Instruction management logic extracted to js/admin.instructions.js?v=1.27.0-9badd8dd
1697
1697
  // Functions exposed globally: loadInstructions, renderInstructionList, editInstruction, saveInstruction, deleteInstruction, etc.
1698
1698
 
1699
1699
  function startAutoRefresh() {
@@ -11,8 +11,7 @@ export interface AdminConfig {
11
11
  enableVerboseLogging: boolean;
12
12
  enableMutation: boolean;
13
13
  rateLimit: {
14
- windowMs: number;
15
- maxRequests: number;
14
+ perMinute: number;
16
15
  };
17
16
  };
18
17
  indexSettings: {
@@ -23,8 +23,7 @@ class AdminPanelConfig {
23
23
  enableVerboseLogging: !!serverHttp?.verboseLogging,
24
24
  enableMutation: runtimeConfig.mutation.enabled,
25
25
  rateLimit: {
26
- windowMs: 60000,
27
- maxRequests: 100
26
+ perMinute: serverHttp?.rateLimitPerMinute ?? 0
28
27
  }
29
28
  },
30
29
  indexSettings: {
@@ -8,10 +8,11 @@
8
8
  import { Router } from 'express';
9
9
  export interface ApiRoutesOptions {
10
10
  enableCors?: boolean;
11
- rateLimit?: {
12
- windowMs: number;
13
- max: number;
14
- };
11
+ /**
12
+ * Optional override for the rate-limit (requests per 60s window).
13
+ * `0` disables. When omitted, falls back to `httpCfg.rateLimitPerMinute`.
14
+ */
15
+ rateLimitPerMinute?: number;
15
16
  }
16
17
  export declare function createApiRoutes(options?: ApiRoutesOptions): Router;
17
18
  export default createApiRoutes;
@@ -49,11 +49,41 @@ const runtimeConfig_js_1 = require("../../config/runtimeConfig.js");
49
49
  const index_js_1 = require("./routes/index.js");
50
50
  const ensureLoadedMiddleware_js_1 = require("./middleware/ensureLoadedMiddleware.js");
51
51
  const logger_js_1 = require("../../services/logger.js");
52
+ /**
53
+ * Path prefixes whose endpoints are *unconditionally* exempt from the dashboard
54
+ * HTTP rate limiter. These are bulk-shaped, operator-driven operations
55
+ * (backup, restore, import/export, normalize) where the size of a single
56
+ * request — not the *frequency* of requests — is the natural cost driver.
57
+ *
58
+ * Matching is done with `req.path.startsWith(prefix)` against the path *as
59
+ * mounted on the /api router* (so prefixes do not include the leading "/api").
60
+ *
61
+ * Note: the MCP tool surface (index_import / index_export / promote_from_repo
62
+ * / restore handlers) is already outside this HTTP limiter, so no entries are
63
+ * needed for those.
64
+ */
65
+ const BULK_EXEMPT_PREFIXES = [
66
+ '/admin/maintenance/normalize',
67
+ '/admin/maintenance/backup', // covers backup, backup/import, backup/:id, backup/:id/export
68
+ '/admin/maintenance/backups', // covers backups, backups/prune
69
+ '/admin/maintenance/restore',
70
+ '/charts/export',
71
+ '/sqlite/backup', // covers /sqlite/backup and /sqlite/backups
72
+ '/sqlite/restore',
73
+ '/sqlite/export',
74
+ ];
75
+ function isBulkExempt(reqPath) {
76
+ for (const prefix of BULK_EXEMPT_PREFIXES) {
77
+ if (reqPath === prefix || reqPath.startsWith(prefix + '/'))
78
+ return true;
79
+ }
80
+ return false;
81
+ }
52
82
  function createApiRoutes(options = {}) {
53
83
  const router = (0, express_1.Router)();
54
84
  const metricsCollector = (0, MetricsCollector_js_1.getMetricsCollector)();
55
85
  const httpCfg = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http;
56
- const rateLimitOpts = options.rateLimit ?? { windowMs: httpCfg.rateLimitWindowMs, max: httpCfg.rateLimitMax };
86
+ const perMinute = options.rateLimitPerMinute ?? httpCfg.rateLimitPerMinute;
57
87
  // CORS middleware (if enabled)
58
88
  // Security: only allow loopback origins (localhost, 127.0.0.1, [::1]) to prevent
59
89
  // cross-origin attacks. No wildcard (*) origins; credentials are not exposed.
@@ -71,53 +101,28 @@ function createApiRoutes(options = {}) {
71
101
  }
72
102
  // JSON middleware
73
103
  router.use(express_1.default.json());
74
- const rateLimitEnabled = httpCfg.rateLimitEnabled;
75
- if (rateLimitEnabled && rateLimitOpts.max > 0 && rateLimitOpts.windowMs > 0) {
76
- // Global rate limit for all routes (reads + mutations)
104
+ // Rate limit (single tier, fixed 60s window). Disabled when perMinute === 0.
105
+ // Bulk routes (backup/restore/import/export/normalize) are unconditionally
106
+ // exempt via BULK_EXEMPT_PREFIXES see `skip` below.
107
+ if (perMinute > 0) {
108
+ const windowMs = 60_000;
77
109
  router.use((0, express_rate_limit_1.default)({
78
- windowMs: rateLimitOpts.windowMs,
79
- max: rateLimitOpts.max,
110
+ windowMs,
111
+ max: perMinute,
80
112
  standardHeaders: true,
81
113
  legacyHeaders: true,
82
114
  validate: { ip: false },
83
- skip: (req) => req.method === 'OPTIONS',
115
+ skip: (req) => req.method === 'OPTIONS' || isBulkExempt(req.path),
84
116
  keyGenerator: (req) => {
85
117
  const clientIp = req.ip || req.socket.remoteAddress;
86
118
  return clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown';
87
119
  },
88
120
  handler: (_req, res) => {
89
- const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(rateLimitOpts.windowMs / 1000));
121
+ const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(windowMs / 1000));
90
122
  res.status(429).json({
91
123
  error: 'Too Many Requests',
92
124
  message: `Rate limit exceeded. Try again in ${retryAfter} second(s).`,
93
125
  retryAfterSeconds: retryAfter,
94
- tier: 'global',
95
- timestamp: Date.now(),
96
- });
97
- },
98
- }));
99
- // Stricter rate limit for mutation endpoints (POST/PUT/PATCH/DELETE).
100
- // Defaults to runtimeConfig httpCfg.rateLimitMutationMax (env-configurable),
101
- // bounded by the global per-window cap so mutation-tier never exceeds global.
102
- const mutationMax = Math.max(1, Math.min(httpCfg.rateLimitMutationMax, rateLimitOpts.max));
103
- router.use((0, express_rate_limit_1.default)({
104
- windowMs: rateLimitOpts.windowMs,
105
- max: mutationMax,
106
- standardHeaders: true,
107
- legacyHeaders: false,
108
- validate: { ip: false },
109
- skip: (req) => req.method === 'OPTIONS' || req.method === 'GET' || req.method === 'HEAD',
110
- keyGenerator: (req) => {
111
- const clientIp = req.ip || req.socket.remoteAddress;
112
- return `mutation:${clientIp ? (0, express_rate_limit_1.ipKeyGenerator)(clientIp) : 'unknown'}`;
113
- },
114
- handler: (_req, res) => {
115
- const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(rateLimitOpts.windowMs / 1000));
116
- res.status(429).json({
117
- error: 'Too Many Requests',
118
- message: `Mutation rate limit exceeded. Try again in ${retryAfter} second(s).`,
119
- retryAfterSeconds: retryAfter,
120
- tier: 'mutation',
121
126
  timestamp: Date.now(),
122
127
  });
123
128
  },
@@ -106,19 +106,21 @@ function renderPanelMarkdownHtml(name, markdown) {
106
106
  * Called once during DashboardServer construction after middleware is set up.
107
107
  */
108
108
  function mountDashboardRoutes(app, ctx) {
109
- // Build a per-route rate limiter using the same dashboard.http.rateLimit*
110
- // configuration as the /api router. Re-uses the same window/max so behavior
111
- // is consistent across the dashboard. Disabled if rateLimitEnabled is false.
109
+ // Build a per-route rate limiter using the same dashboard.http.rateLimit
110
+ // configuration as the /api router. Re-uses the same per-minute cap so
111
+ // behavior is consistent across the dashboard. Disabled when
112
+ // rateLimitPerMinute === 0 (the default).
112
113
  const httpCfg = (0, runtimeConfig_js_1.getRuntimeConfig)().dashboard.http;
114
+ const perMinute = httpCfg.rateLimitPerMinute;
113
115
  const dashboardLimiter = (0, express_rate_limit_1.default)({
114
- windowMs: Math.max(1, httpCfg.rateLimitWindowMs),
115
- max: Math.max(1, httpCfg.rateLimitMax),
116
+ windowMs: 60_000,
117
+ max: perMinute > 0 ? perMinute : 1,
116
118
  standardHeaders: true,
117
119
  legacyHeaders: false,
118
120
  validate: { ip: false },
119
- skip: () => !httpCfg.rateLimitEnabled,
121
+ skip: () => perMinute <= 0,
120
122
  handler: (_req, res) => {
121
- const retryAfter = Number(res.getHeader('Retry-After') || Math.ceil(httpCfg.rateLimitWindowMs / 1000));
123
+ const retryAfter = Number(res.getHeader('Retry-After') || 60);
122
124
  res.status(429).json({
123
125
  error: 'Too Many Requests',
124
126
  message: `Rate limit exceeded. Try again in ${retryAfter} second(s).`,
@@ -222,8 +224,8 @@ function mountDashboardRoutes(app, ctx) {
222
224
  res.status(404).send('Screenshot not found');
223
225
  }
224
226
  });
225
- // API sub-routes (mounted at /api). Rate limits sourced from runtimeConfig
226
- // (INDEX_SERVER_RATE_LIMIT_*) — see ApiRoutes.createApiRoutes.
227
+ // API sub-routes (mounted at /api). Rate limit sourced from runtimeConfig
228
+ // (INDEX_SERVER_RATE_LIMIT) — see ApiRoutes.createApiRoutes.
227
229
  app.use('/api', (0, ApiRoutes_js_1.createApiRoutes)({ enableCors: ctx.enableCors }));
228
230
  // Back-compat: legacy tests expect /tools.json at dashboard root
229
231
  app.get('/tools.json', (_req, res) => {
@@ -46,8 +46,7 @@ exports.FLAG_REGISTRY = [
46
46
  { name: 'INDEX_SERVER_VISIBILITY_DIAG', category: 'tracing', description: 'Force core trace level for visibility diagnostics.', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
47
47
  { name: 'INDEX_SERVER_FILE_TRACE', category: 'tracing', description: 'Promote index file events to trace level (files).', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
48
48
  // Usage & metrics
49
- { name: 'INDEX_SERVER_DISABLE_RATE_LIMIT', category: 'usage', description: 'Disable all rate limiting (dashboard HTTP API and usage tracking). Rate limiting is enabled by default.', stability: 'stable', default: 'off', type: 'boolean', since: '1.1.3' },
50
- { name: 'INDEX_SERVER_DISABLE_USAGE_RATE_LIMIT', category: 'usage', description: '(Deprecated) Legacy per-subsystem override to disable usage rate limit.', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
49
+ { name: 'INDEX_SERVER_RATE_LIMIT', category: 'usage', description: 'Dashboard HTTP API and usage-tracking rate limit, in requests per minute. 0 (default) disables rate limiting. Bulk import/export/backup/restore routes are unconditionally exempt.', stability: 'stable', default: '0', type: 'number', since: '1.27.0' },
51
50
  { name: 'INDEX_SERVER_DISABLE_USAGE_CLAMP', category: 'usage', description: 'Disable initial usage count clamp logic.', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
52
51
  { name: 'INDEX_SERVER_USAGE_FLUSH_MS', category: 'usage', description: 'Override usage flush debounce interval.', stability: 'diagnostic', default: '75', type: 'number', since: '1.1.1' },
53
52
  // Validation / schema
@@ -36,7 +36,6 @@ const features_1 = require("./features");
36
36
  const atomicFs_1 = require("./atomicFs");
37
37
  const classificationService_1 = require("./classificationService");
38
38
  const ownershipService_1 = require("./ownershipService");
39
- const envUtils_1 = require("../utils/envUtils");
40
39
  const runtimeConfig_1 = require("../config/runtimeConfig");
41
40
  const factory_1 = require("./storage/factory");
42
41
  const migrationEngine_1 = require("./storage/migrationEngine");
@@ -202,11 +201,11 @@ function restoreLastUsedInvariant(e) {
202
201
  const USAGE_RATE_LIMIT_PER_SECOND = 10; // max increments per id per second
203
202
  const usageRateLimiter = new Map();
204
203
  function checkUsageRateLimit(id) {
205
- // Rate limiting is enabled by default. Set INDEX_SERVER_DISABLE_RATE_LIMIT=1 to disable all rate limiting.
206
- if ((0, envUtils_1.getBooleanEnv)('INDEX_SERVER_DISABLE_RATE_LIMIT'))
207
- return true;
208
- // Legacy per-subsystem override (backward compat / deterministic tests).
209
- if ((0, envUtils_1.getBooleanEnv)('INDEX_SERVER_DISABLE_USAGE_RATE_LIMIT'))
204
+ // Rate limiting is opt-in. INDEX_SERVER_RATE_LIMIT=0 (default) or unset
205
+ // disables both the dashboard HTTP limiter and this usage limiter.
206
+ // Any positive integer enables both.
207
+ const rl = Number(process.env.INDEX_SERVER_RATE_LIMIT);
208
+ if (!Number.isFinite(rl) || rl <= 0)
210
209
  return true;
211
210
  const now = Date.now();
212
211
  const windowStart = Math.floor(now / 1000) * 1000; // 1-second windows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jagilber-org/index-server",
3
- "version": "1.26.11",
3
+ "version": "1.27.0",
4
4
  "mcpName": "io.github.jagilber-org/index-server",
5
5
  "description": "MCP instruction indexing server for AI assistant governance — search, CRUD, schema validation, usage tracking, and cross-repo knowledge promotion.",
6
6
  "publishConfig": {