@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 +25 -1
- package/dist/config/dashboardConfig.d.ts +11 -4
- package/dist/config/dashboardConfig.js +1 -4
- package/dist/dashboard/client/admin.html +26 -26
- package/dist/dashboard/server/AdminPanelConfig.d.ts +1 -2
- package/dist/dashboard/server/AdminPanelConfig.js +1 -2
- package/dist/dashboard/server/ApiRoutes.d.ts +5 -4
- package/dist/dashboard/server/ApiRoutes.js +40 -35
- package/dist/dashboard/server/routes/index.js +11 -9
- package/dist/services/handlers.dashboardConfig.js +1 -2
- package/dist/services/indexContext.js +5 -6
- package/package.json +1 -1
- package/schemas/index-server.code-schema.json +7424 -1588
- package/schemas/manifest.json +3 -3
- package/server.json +2 -2
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.
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
9
|
-
<script defer src="js/admin.utils.js?v=1.
|
|
10
|
-
<script defer src="js/admin.auth.js?v=1.
|
|
11
|
-
<script defer src="js/admin.overview.js?v=1.
|
|
12
|
-
<script defer src="js/admin.sessions.js?v=1.
|
|
13
|
-
<script defer src="js/admin.monitor.js?v=1.
|
|
14
|
-
<script defer src="js/admin.graph.js?v=1.
|
|
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.
|
|
17
|
-
<script defer src="js/admin.logs.js?v=1.
|
|
18
|
-
<script defer src="js/admin.maintenance.js?v=1.
|
|
19
|
-
<script defer src="js/admin.config.js?v=1.
|
|
20
|
-
<script defer src="js/admin.performance.js?v=1.
|
|
21
|
-
<script defer src="js/admin.instances.js?v=1.
|
|
22
|
-
<script defer src="js/admin.embeddings.js?v=1.
|
|
23
|
-
<script defer src="js/admin.messaging.js?v=1.
|
|
24
|
-
<script defer src="js/admin.sqlite.js?v=1.
|
|
25
|
-
<script defer src="js/admin.boot.js?v=1.
|
|
26
|
-
<script defer src="js/admin.feedback.js?v=1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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() {
|
|
@@ -23,8 +23,7 @@ class AdminPanelConfig {
|
|
|
23
23
|
enableVerboseLogging: !!serverHttp?.verboseLogging,
|
|
24
24
|
enableMutation: runtimeConfig.mutation.enabled,
|
|
25
25
|
rateLimit: {
|
|
26
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
79
|
-
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(
|
|
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
|
|
111
|
-
// is consistent across the dashboard. Disabled
|
|
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:
|
|
115
|
-
max:
|
|
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: () =>
|
|
121
|
+
skip: () => perMinute <= 0,
|
|
120
122
|
handler: (_req, res) => {
|
|
121
|
-
const retryAfter = Number(res.getHeader('Retry-After') ||
|
|
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
|
|
226
|
-
// (
|
|
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: '
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
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.
|
|
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": {
|