@jagilber-org/index-server 1.22.1 → 1.26.4
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 +91 -2
- package/CODE_OF_CONDUCT.md +2 -0
- package/CONTRIBUTING.md +32 -2
- package/README.md +82 -19
- package/SECURITY.md +17 -5
- package/dist/config/dashboardConfig.d.ts +3 -0
- package/dist/config/dashboardConfig.js +3 -0
- package/dist/config/defaultValues.d.ts +1 -1
- package/dist/config/defaultValues.js +1 -1
- package/dist/config/featureConfig.d.ts +2 -0
- package/dist/config/featureConfig.js +6 -1
- package/dist/config/runtimeConfig.d.ts +1 -1
- package/dist/config/runtimeConfig.js +8 -9
- package/dist/dashboard/client/admin.html +170 -53
- package/dist/dashboard/client/css/admin.css +132 -0
- package/dist/dashboard/client/js/admin.auth.js +25 -11
- package/dist/dashboard/client/js/admin.config.js +1 -1
- package/dist/dashboard/client/js/admin.feedback.js +328 -0
- package/dist/dashboard/client/js/admin.graph.js +120 -18
- package/dist/dashboard/client/js/admin.instructions.js +27 -13
- package/dist/dashboard/client/js/admin.logs.js +1 -5
- package/dist/dashboard/client/js/admin.maintenance.js +53 -8
- package/dist/dashboard/client/js/admin.messaging.js +1 -4
- package/dist/dashboard/client/js/admin.overview.js +5 -1
- package/dist/dashboard/client/js/admin.sessions.js +1 -1
- package/dist/dashboard/client/js/admin.utils.js +43 -1
- package/dist/dashboard/client/js/mermaid.min.js +813 -537
- package/dist/dashboard/export/DataExporter.js +2 -1
- package/dist/dashboard/server/AdminPanel.d.ts +3 -0
- package/dist/dashboard/server/AdminPanel.js +132 -35
- package/dist/dashboard/server/ApiRoutes.js +40 -9
- package/dist/dashboard/server/DashboardServer.js +1 -1
- package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
- package/dist/dashboard/server/FileMetricsStorage.js +52 -5
- package/dist/dashboard/server/HttpTransport.js +6 -0
- package/dist/dashboard/server/InstanceManager.js +7 -2
- package/dist/dashboard/server/KnowledgeStore.js +7 -2
- package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
- package/dist/dashboard/server/MetricsCollector.js +113 -17
- package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
- package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
- package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
- package/dist/dashboard/server/routes/admin.routes.js +35 -27
- package/dist/dashboard/server/routes/alerts.routes.js +4 -3
- package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
- package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
- package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
- package/dist/dashboard/server/routes/graph.routes.js +10 -13
- package/dist/dashboard/server/routes/index.d.ts +1 -0
- package/dist/dashboard/server/routes/index.js +74 -39
- package/dist/dashboard/server/routes/instances.routes.js +2 -1
- package/dist/dashboard/server/routes/instructions.routes.js +46 -27
- package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
- package/dist/dashboard/server/routes/logs.routes.js +5 -4
- package/dist/dashboard/server/routes/messaging.routes.js +15 -14
- package/dist/dashboard/server/routes/metrics.routes.js +14 -13
- package/dist/dashboard/server/routes/scripts.routes.js +6 -3
- package/dist/dashboard/server/routes/status.routes.js +5 -4
- package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
- package/dist/dashboard/server/routes/usage.routes.js +2 -1
- package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
- package/dist/dashboard/server/utils/escapeHtml.js +11 -0
- package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
- package/dist/dashboard/server/utils/pathContainment.js +15 -0
- package/dist/dashboard/server/wsInit.js +2 -2
- package/dist/lib/mcpStdioLogging.d.ts +165 -0
- package/dist/lib/mcpStdioLogging.js +287 -0
- package/dist/schemas/index.d.ts +37 -2
- package/dist/schemas/index.js +27 -3
- package/dist/server/backgroundServicesStartup.d.ts +7 -1
- package/dist/server/backgroundServicesStartup.js +25 -8
- package/dist/server/certInit.d.ts +97 -0
- package/dist/server/certInit.js +359 -0
- package/dist/server/certInit.types.d.ts +92 -0
- package/dist/server/certInit.types.js +34 -0
- package/dist/server/handshake/fallbackFrames.d.ts +31 -0
- package/dist/server/handshake/fallbackFrames.js +38 -0
- package/dist/server/handshake/initializeDetector.d.ts +31 -0
- package/dist/server/handshake/initializeDetector.js +88 -0
- package/dist/server/handshake/protocol.d.ts +15 -0
- package/dist/server/handshake/protocol.js +37 -0
- package/dist/server/handshake/readyEmitter.d.ts +6 -0
- package/dist/server/handshake/readyEmitter.js +88 -0
- package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
- package/dist/server/handshake/safetyFallbacks.js +134 -0
- package/dist/server/handshake/stdinSniffer.d.ts +1 -0
- package/dist/server/handshake/stdinSniffer.js +260 -0
- package/dist/server/handshake/tracing.d.ts +16 -0
- package/dist/server/handshake/tracing.js +95 -0
- package/dist/server/handshakeManager.d.ts +23 -23
- package/dist/server/handshakeManager.js +36 -466
- package/dist/server/index-server.d.ts +23 -0
- package/dist/server/index-server.js +194 -9
- package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
- package/dist/server/mcpReadOnlySurfaces.js +297 -0
- package/dist/server/sdkServer.js +69 -7
- package/dist/server/transport.d.ts +5 -6
- package/dist/server/transport.js +46 -64
- package/dist/server/transportFactory.d.ts +3 -9
- package/dist/server/transportFactory.js +18 -380
- package/dist/services/atomicFs.d.ts +3 -0
- package/dist/services/atomicFs.js +171 -13
- package/dist/services/auditLog.d.ts +17 -2
- package/dist/services/auditLog.js +75 -14
- package/dist/services/bootstrapGating.js +1 -1
- package/dist/services/categoryRules.d.ts +10 -0
- package/dist/services/categoryRules.js +17 -0
- package/dist/services/classificationService.js +7 -5
- package/dist/services/embeddingService.d.ts +27 -11
- package/dist/services/embeddingService.js +51 -14
- package/dist/services/feedbackStorage.d.ts +39 -0
- package/dist/services/feedbackStorage.js +88 -0
- package/dist/services/handlers/instructions.add.js +429 -317
- package/dist/services/handlers/instructions.groom.js +128 -31
- package/dist/services/handlers/instructions.import.js +56 -23
- package/dist/services/handlers/instructions.patch.js +43 -32
- package/dist/services/handlers/instructions.query.js +20 -29
- package/dist/services/handlers/instructions.shared.d.ts +54 -0
- package/dist/services/handlers/instructions.shared.js +126 -1
- package/dist/services/handlers.activation.js +83 -81
- package/dist/services/handlers.dashboardConfig.d.ts +2 -2
- package/dist/services/handlers.dashboardConfig.js +1 -2
- package/dist/services/handlers.diagnostics.js +75 -54
- package/dist/services/handlers.feedback.d.ts +4 -11
- package/dist/services/handlers.feedback.js +11 -333
- package/dist/services/handlers.gates.js +69 -37
- package/dist/services/handlers.graph.js +2 -2
- package/dist/services/handlers.help.js +2 -2
- package/dist/services/handlers.instructionSchema.js +4 -2
- package/dist/services/handlers.integrity.js +42 -22
- package/dist/services/handlers.messaging.js +1 -1
- package/dist/services/handlers.metrics.js +51 -6
- package/dist/services/handlers.prompt.js +10 -2
- package/dist/services/handlers.search.js +94 -44
- package/dist/services/handlers.trace.js +1 -1
- package/dist/services/handlers.usage.js +38 -7
- package/dist/services/indexContext.d.ts +21 -1
- package/dist/services/indexContext.js +263 -78
- package/dist/services/indexLoader.d.ts +1 -0
- package/dist/services/indexLoader.js +28 -8
- package/dist/services/instructionRecordValidation.d.ts +39 -0
- package/dist/services/instructionRecordValidation.js +388 -0
- package/dist/services/instructions.dispatcher.js +4 -4
- package/dist/services/loaderSchemaValidator.d.ts +15 -0
- package/dist/services/loaderSchemaValidator.js +69 -0
- package/dist/services/logger.js +11 -2
- package/dist/services/mcpLogBridge.d.ts +49 -0
- package/dist/services/mcpLogBridge.js +83 -0
- package/dist/services/ownershipService.js +18 -8
- package/dist/services/performanceBaseline.js +23 -22
- package/dist/services/promptReviewService.d.ts +3 -1
- package/dist/services/promptReviewService.js +41 -13
- package/dist/services/regexSafety.d.ts +6 -0
- package/dist/services/regexSafety.js +46 -0
- package/dist/services/seedBootstrap.js +1 -1
- package/dist/services/storage/factory.d.ts +14 -1
- package/dist/services/storage/factory.js +61 -1
- package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
- package/dist/services/storage/jsonEmbeddingStore.js +83 -0
- package/dist/services/storage/jsonFileStore.d.ts +3 -1
- package/dist/services/storage/jsonFileStore.js +8 -6
- package/dist/services/storage/migrationEngine.d.ts +13 -0
- package/dist/services/storage/migrationEngine.js +31 -0
- package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
- package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
- package/dist/services/storage/sqliteStore.d.ts +3 -1
- package/dist/services/storage/sqliteStore.js +2 -2
- package/dist/services/storage/types.d.ts +48 -1
- package/dist/services/toolRegistry.js +77 -67
- package/dist/services/toolRegistry.zod.js +89 -86
- package/dist/services/tracing.js +5 -4
- package/dist/utils/envUtils.d.ts +4 -0
- package/dist/utils/envUtils.js +7 -0
- package/dist/utils/memoryMonitor.js +11 -10
- package/package.json +12 -4
- package/schemas/instruction.schema.json +38 -1
- package/scripts/copy-dashboard-assets.mjs +1 -1
- package/scripts/dist/README.md +1 -1
- package/scripts/generate-certs.mjs +201 -0
- package/scripts/setup-wizard.mjs +781 -0
- package/server.json +20 -0
- package/dist/externalClientLib.d.ts +0 -1
- package/dist/externalClientLib.js +0 -2
- package/dist/portableClientWrapper.d.ts +0 -1
- package/dist/portableClientWrapper.js +0 -2
- package/dist/services/indexingService.d.ts +0 -1
- package/dist/services/indexingService.js +0 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Certificate bootstrap module for the `--init-cert` CLI switch.
|
|
4
|
+
*
|
|
5
|
+
* Public surface (all exports carry JSDoc per CQ-7):
|
|
6
|
+
* - {@link validateOptions} : merge defaults, validate, resolve absolute paths
|
|
7
|
+
* - {@link parseSan} : split + validate SAN entries
|
|
8
|
+
* - {@link buildOpenSslArgs}: build argv for `openssl req -x509`
|
|
9
|
+
* - {@link preflightOpenssl}: verify openssl is callable on PATH
|
|
10
|
+
* - {@link formatPrintEnv} : produce env-var lines for the operator
|
|
11
|
+
* - {@link runCertInit} : end-to-end pipeline
|
|
12
|
+
*
|
|
13
|
+
* Constitution refs:
|
|
14
|
+
* - SH-4 : every output path is `path.resolve`d and asserted to live under
|
|
15
|
+
* the resolved `certDir`.
|
|
16
|
+
* - SH-6 : this module never disables TLS verification anywhere.
|
|
17
|
+
* - CQ-1 : lives in its own file so `index-server.ts` stays under budget.
|
|
18
|
+
* - CQ-6 : every catch surfaces a typed error; no swallowed exceptions.
|
|
19
|
+
* - OB-3 : failures throw {@link CertInitError} with a stable `code`.
|
|
20
|
+
* - OB-5 : success and skip paths log at INFO; failures log at ERROR.
|
|
21
|
+
*/
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.validateOptions = validateOptions;
|
|
27
|
+
exports.parseSan = parseSan;
|
|
28
|
+
exports.buildOpenSslArgs = buildOpenSslArgs;
|
|
29
|
+
exports.preflightOpenssl = preflightOpenssl;
|
|
30
|
+
exports.formatPrintEnv = formatPrintEnv;
|
|
31
|
+
exports.runCertInit = runCertInit;
|
|
32
|
+
const child_process_1 = require("child_process");
|
|
33
|
+
const fs_1 = __importDefault(require("fs"));
|
|
34
|
+
const os_1 = __importDefault(require("os"));
|
|
35
|
+
const path_1 = __importDefault(require("path"));
|
|
36
|
+
const logger_1 = require("../services/logger");
|
|
37
|
+
const certInit_types_1 = require("./certInit.types");
|
|
38
|
+
// ── Defaults ──────────────────────────────────────────────────────────────
|
|
39
|
+
/** Default RSA key size when the caller does not specify `--key-bits`. */
|
|
40
|
+
const DEFAULT_KEY_BITS = 2048;
|
|
41
|
+
/** Default validity period in days. */
|
|
42
|
+
const DEFAULT_DAYS = 365;
|
|
43
|
+
/** Default subject CommonName. */
|
|
44
|
+
const DEFAULT_CN = 'localhost';
|
|
45
|
+
/** Default SAN list, covering loopback HTTPS scenarios out of the box. */
|
|
46
|
+
const DEFAULT_SAN = 'DNS:localhost,IP:127.0.0.1';
|
|
47
|
+
/** Default cert filename within `certDir`. */
|
|
48
|
+
const DEFAULT_CERT_FILENAME = 'index-server.crt';
|
|
49
|
+
/** Default key filename within `certDir`. */
|
|
50
|
+
const DEFAULT_KEY_FILENAME = 'index-server.key';
|
|
51
|
+
/** Inclusive minimum value for `--days`. */
|
|
52
|
+
const MIN_DAYS = 1;
|
|
53
|
+
/** Inclusive maximum value for `--days`. */
|
|
54
|
+
const MAX_DAYS = 3650;
|
|
55
|
+
/**
|
|
56
|
+
* Compute the default cert directory path: `<homedir>/.index-server/certs`.
|
|
57
|
+
* Used when the caller does not specify `--cert-dir`.
|
|
58
|
+
*
|
|
59
|
+
* @returns Absolute path to the default cert directory.
|
|
60
|
+
*/
|
|
61
|
+
function defaultCertDir() {
|
|
62
|
+
return path_1.default.join(os_1.default.homedir(), '.index-server', 'certs');
|
|
63
|
+
}
|
|
64
|
+
// ── validateOptions ───────────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Validate a partial options bag and return a fully-resolved
|
|
67
|
+
* {@link CertInitOptions} suitable for {@link runCertInit}.
|
|
68
|
+
*
|
|
69
|
+
* Defaults are applied for any field omitted by the caller. All paths are
|
|
70
|
+
* `path.resolve`d. The function rejects values that violate the v1 contract:
|
|
71
|
+
* days out of `[MIN_DAYS, MAX_DAYS]`, key-bits not in `{2048, 4096}`, SAN
|
|
72
|
+
* entries without `DNS:` or `IP:` prefix, empty CN, or output paths that
|
|
73
|
+
* escape `certDir` (SH-4).
|
|
74
|
+
*
|
|
75
|
+
* @param input Partial options as parsed from the CLI.
|
|
76
|
+
* @returns Fully-resolved {@link CertInitOptions}.
|
|
77
|
+
* @throws {@link CertInitError} with codes `INVALID_DAYS`,
|
|
78
|
+
* `INVALID_KEY_BITS`, `INVALID_SAN`, `INVALID_CN`, or
|
|
79
|
+
* `PATH_OUTSIDE_CERT_DIR`.
|
|
80
|
+
*/
|
|
81
|
+
function validateOptions(input) {
|
|
82
|
+
const certDir = path_1.default.resolve(input.certDir ?? defaultCertDir());
|
|
83
|
+
const certFile = path_1.default.resolve(input.certFile ?? path_1.default.join(certDir, DEFAULT_CERT_FILENAME));
|
|
84
|
+
const keyFile = path_1.default.resolve(input.keyFile ?? path_1.default.join(certDir, DEFAULT_KEY_FILENAME));
|
|
85
|
+
// SH-4: every output path must resolve under the cert dir.
|
|
86
|
+
assertUnderDir(certFile, certDir, 'certFile');
|
|
87
|
+
assertUnderDir(keyFile, certDir, 'keyFile');
|
|
88
|
+
const days = input.days ?? DEFAULT_DAYS;
|
|
89
|
+
if (!Number.isInteger(days) || days < MIN_DAYS || days > MAX_DAYS) {
|
|
90
|
+
throw new certInit_types_1.CertInitError('INVALID_DAYS', `days must be an integer in [${MIN_DAYS}, ${MAX_DAYS}], received ${String(days)}`);
|
|
91
|
+
}
|
|
92
|
+
const keyBits = input.keyBits ?? DEFAULT_KEY_BITS;
|
|
93
|
+
if (keyBits !== 2048 && keyBits !== 4096) {
|
|
94
|
+
throw new certInit_types_1.CertInitError('INVALID_KEY_BITS', `keyBits must be 2048 or 4096, received ${String(keyBits)}`);
|
|
95
|
+
}
|
|
96
|
+
const cn = (input.cn ?? DEFAULT_CN).trim();
|
|
97
|
+
if (cn.length === 0) {
|
|
98
|
+
throw new certInit_types_1.CertInitError('INVALID_CN', 'cn (CommonName) must not be empty');
|
|
99
|
+
}
|
|
100
|
+
const san = input.san ?? DEFAULT_SAN;
|
|
101
|
+
// Validate SAN early so callers see a clean error before openssl runs.
|
|
102
|
+
parseSan(san);
|
|
103
|
+
return {
|
|
104
|
+
certDir,
|
|
105
|
+
certFile,
|
|
106
|
+
keyFile,
|
|
107
|
+
cn,
|
|
108
|
+
san,
|
|
109
|
+
days,
|
|
110
|
+
keyBits,
|
|
111
|
+
force: input.force ?? false,
|
|
112
|
+
printEnv: input.printEnv ?? false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Assert that `target` resolves to a path strictly inside `dir`. Used by the
|
|
117
|
+
* SH-4 path-traversal guard in {@link validateOptions}.
|
|
118
|
+
*
|
|
119
|
+
* @param target Absolute path to check.
|
|
120
|
+
* @param dir Absolute directory path that must contain `target`.
|
|
121
|
+
* @param label Human-readable name used in the error message.
|
|
122
|
+
* @throws {@link CertInitError} `PATH_OUTSIDE_CERT_DIR` when `target`
|
|
123
|
+
* escapes `dir`.
|
|
124
|
+
*/
|
|
125
|
+
function assertUnderDir(target, dir, label) {
|
|
126
|
+
const rel = path_1.default.relative(dir, target);
|
|
127
|
+
// SH-4: BOTH conditions are required. Do NOT collapse this to a single check.
|
|
128
|
+
// rel.startsWith('..') catches same-drive parent-dir escape
|
|
129
|
+
// (e.g. C:\certs\..\..\evil -> '..\..\evil').
|
|
130
|
+
// path.isAbsolute(rel) catches Windows cross-drive paths (D:\evil) and
|
|
131
|
+
// UNC shares (\\server\share\evil), where
|
|
132
|
+
// path.relative() returns an *absolute* path
|
|
133
|
+
// rather than a '..\..'-prefixed relative one.
|
|
134
|
+
// Removing either clause silently reintroduces a path-traversal bypass.
|
|
135
|
+
if (rel.startsWith('..') || path_1.default.isAbsolute(rel)) {
|
|
136
|
+
throw new certInit_types_1.CertInitError('PATH_OUTSIDE_CERT_DIR', `${label} (${target}) must resolve under cert dir (${dir})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ── parseSan ──────────────────────────────────────────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Parse a comma-separated SAN string into individual entries, validating that
|
|
142
|
+
* each entry has a recognized `DNS:` or `IP:` prefix.
|
|
143
|
+
*
|
|
144
|
+
* @param raw Raw SAN string from the CLI (e.g. `"DNS:host,IP:127.0.0.1"`).
|
|
145
|
+
* @returns Array of validated SAN entries with surrounding whitespace
|
|
146
|
+
* trimmed.
|
|
147
|
+
* @throws {@link CertInitError} `INVALID_SAN` for empty input, trailing
|
|
148
|
+
* commas, or entries missing a recognized prefix.
|
|
149
|
+
*/
|
|
150
|
+
function parseSan(raw) {
|
|
151
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
152
|
+
throw new certInit_types_1.CertInitError('INVALID_SAN', 'san must be a non-empty string');
|
|
153
|
+
}
|
|
154
|
+
const parts = raw.split(',');
|
|
155
|
+
const entries = [];
|
|
156
|
+
for (const p of parts) {
|
|
157
|
+
const trimmed = p.trim();
|
|
158
|
+
if (trimmed.length === 0) {
|
|
159
|
+
throw new certInit_types_1.CertInitError('INVALID_SAN', `san contains an empty entry (check for trailing/leading commas in "${raw}")`);
|
|
160
|
+
}
|
|
161
|
+
if (!trimmed.startsWith('DNS:') && !trimmed.startsWith('IP:')) {
|
|
162
|
+
throw new certInit_types_1.CertInitError('INVALID_SAN', `san entry "${trimmed}" must start with "DNS:" or "IP:"`);
|
|
163
|
+
}
|
|
164
|
+
entries.push(trimmed);
|
|
165
|
+
}
|
|
166
|
+
return entries;
|
|
167
|
+
}
|
|
168
|
+
// ── buildOpenSslArgs ──────────────────────────────────────────────────────
|
|
169
|
+
/**
|
|
170
|
+
* Build the argument array for the OpenSSL `req -x509` invocation that
|
|
171
|
+
* generates a self-signed certificate. The returned array is suitable for
|
|
172
|
+
* `child_process.execFile('openssl', args)` — no shell metacharacters are
|
|
173
|
+
* inserted, and inputs are not interpolated into a command string.
|
|
174
|
+
*
|
|
175
|
+
* @param opts Fully-resolved cert-init options (use {@link validateOptions}
|
|
176
|
+
* to produce these).
|
|
177
|
+
* @returns Argument array for `openssl`.
|
|
178
|
+
*/
|
|
179
|
+
function buildOpenSslArgs(opts) {
|
|
180
|
+
return [
|
|
181
|
+
'req',
|
|
182
|
+
'-x509',
|
|
183
|
+
'-newkey', `rsa:${opts.keyBits}`,
|
|
184
|
+
'-nodes',
|
|
185
|
+
'-keyout', opts.keyFile,
|
|
186
|
+
'-out', opts.certFile,
|
|
187
|
+
'-days', String(opts.days),
|
|
188
|
+
'-subj', `/CN=${opts.cn}`,
|
|
189
|
+
'-addext', `subjectAltName=${opts.san}`,
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
// ── preflightOpenssl ──────────────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Verify that `openssl` is callable on PATH. Used as a preflight before any
|
|
195
|
+
* generation work so that the failure surfaces with a stable
|
|
196
|
+
* `OPENSSL_NOT_FOUND` code rather than a downstream spawn error.
|
|
197
|
+
*
|
|
198
|
+
* @returns The reported version string when openssl is callable.
|
|
199
|
+
* @throws {@link CertInitError} `OPENSSL_NOT_FOUND` otherwise.
|
|
200
|
+
*/
|
|
201
|
+
function preflightOpenssl() {
|
|
202
|
+
let result;
|
|
203
|
+
try {
|
|
204
|
+
result = (0, child_process_1.spawnSync)('openssl', ['version'], { stdio: 'pipe', timeout: 5000 });
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
throw new certInit_types_1.CertInitError('OPENSSL_NOT_FOUND', 'openssl was not found on PATH or could not be invoked. Install OpenSSL and retry. ' +
|
|
208
|
+
'See https://www.openssl.org/source/ for downloads.', e);
|
|
209
|
+
}
|
|
210
|
+
if (!result || result.status !== 0) {
|
|
211
|
+
const stderr = result?.stderr?.toString().trim() ?? '';
|
|
212
|
+
throw new certInit_types_1.CertInitError('OPENSSL_NOT_FOUND', `openssl probe failed (status=${String(result?.status)}): ${stderr || 'no output'}. Install OpenSSL and retry.`);
|
|
213
|
+
}
|
|
214
|
+
return result.stdout.toString().trim();
|
|
215
|
+
}
|
|
216
|
+
// ── formatPrintEnv ────────────────────────────────────────────────────────
|
|
217
|
+
/**
|
|
218
|
+
* Format the env-var lines an operator can paste into their shell after a
|
|
219
|
+
* successful generation, pointing the dashboard at the new cert/key.
|
|
220
|
+
*
|
|
221
|
+
* @param opts Resolved cert-init options.
|
|
222
|
+
* @param format Output format. `'auto'` picks `'powershell'` on Win32,
|
|
223
|
+
* `'posix'` elsewhere.
|
|
224
|
+
* @returns Multi-line string ending with a trailing newline.
|
|
225
|
+
*/
|
|
226
|
+
function formatPrintEnv(opts, format = 'auto') {
|
|
227
|
+
const resolved = format === 'auto'
|
|
228
|
+
? (process.platform === 'win32' ? 'powershell' : 'posix')
|
|
229
|
+
: format;
|
|
230
|
+
const posix = [
|
|
231
|
+
`export INDEX_SERVER_DASHBOARD_TLS=1`,
|
|
232
|
+
`export INDEX_SERVER_DASHBOARD_TLS_CERT="${opts.certFile}"`,
|
|
233
|
+
`export INDEX_SERVER_DASHBOARD_TLS_KEY="${opts.keyFile}"`,
|
|
234
|
+
].join('\n') + '\n';
|
|
235
|
+
const ps = [
|
|
236
|
+
`$env:INDEX_SERVER_DASHBOARD_TLS="1"`,
|
|
237
|
+
`$env:INDEX_SERVER_DASHBOARD_TLS_CERT="${opts.certFile}"`,
|
|
238
|
+
`$env:INDEX_SERVER_DASHBOARD_TLS_KEY="${opts.keyFile}"`,
|
|
239
|
+
].join('\n') + '\n';
|
|
240
|
+
if (resolved === 'powershell')
|
|
241
|
+
return ps;
|
|
242
|
+
if (resolved === 'posix')
|
|
243
|
+
return posix;
|
|
244
|
+
// both
|
|
245
|
+
return `# POSIX\n${posix}\n# PowerShell\n${ps}`;
|
|
246
|
+
}
|
|
247
|
+
// ── runCertInit ───────────────────────────────────────────────────────────
|
|
248
|
+
/**
|
|
249
|
+
* Execute the full cert-init pipeline: validate options, preflight openssl,
|
|
250
|
+
* create `certDir` if missing, run openssl, set restrictive permissions on
|
|
251
|
+
* POSIX, and emit a structured log line via the existing logger.
|
|
252
|
+
*
|
|
253
|
+
* Idempotency: if `certFile` OR `keyFile` already exists and `force` is false,
|
|
254
|
+
* no openssl invocation is made and a `kind: 'skipped'` result is returned.
|
|
255
|
+
* Treating only the both-exist case as "skip" would clobber a surviving cert
|
|
256
|
+
* when the operator deleted/rotated only the key (or vice versa) — `--force`
|
|
257
|
+
* is required to overwrite *any* existing file.
|
|
258
|
+
*
|
|
259
|
+
* @param input Caller options (typically from the CLI parser). Re-validated
|
|
260
|
+
* internally so direct callers do not need to pre-validate.
|
|
261
|
+
* @returns A {@link CertInitResult} indicating generated vs skipped.
|
|
262
|
+
* @throws {@link CertInitError} for any validation, preflight, or
|
|
263
|
+
* execution failure (codes documented on each helper above).
|
|
264
|
+
*/
|
|
265
|
+
async function runCertInit(input) {
|
|
266
|
+
const opts = validateOptions(input);
|
|
267
|
+
// Skip if EITHER file exists without --force. Treating only the both-exist
|
|
268
|
+
// case as "skip" would clobber a surviving cert when the operator deleted /
|
|
269
|
+
// rotated only the key (or vice versa) — the principle of least surprise is
|
|
270
|
+
// that --force is required to overwrite *any* existing file on disk.
|
|
271
|
+
const certExists = fs_1.default.existsSync(opts.certFile);
|
|
272
|
+
const keyExists = fs_1.default.existsSync(opts.keyFile);
|
|
273
|
+
const bothExist = certExists && keyExists;
|
|
274
|
+
const anyExists = certExists || keyExists;
|
|
275
|
+
if (anyExists && !opts.force) {
|
|
276
|
+
const reason = bothExist
|
|
277
|
+
? 'cert and key files already exist; pass --force to overwrite'
|
|
278
|
+
: `partial state on disk (cert=${certExists}, key=${keyExists}); pass --force to overwrite`;
|
|
279
|
+
(0, logger_1.logInfo)('[certInit] skip (files exist; use --force to overwrite)', {
|
|
280
|
+
cert: opts.certFile,
|
|
281
|
+
certExists,
|
|
282
|
+
key: opts.keyFile,
|
|
283
|
+
keyExists,
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
kind: 'skipped',
|
|
287
|
+
reason,
|
|
288
|
+
certFile: opts.certFile,
|
|
289
|
+
keyFile: opts.keyFile,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Preflight openssl AFTER the skip-when-exists check so an idempotent
|
|
293
|
+
// re-run on a host without openssl (where the cert was generated elsewhere)
|
|
294
|
+
// still succeeds with a `skipped` result.
|
|
295
|
+
preflightOpenssl();
|
|
296
|
+
// Ensure cert dir exists.
|
|
297
|
+
try {
|
|
298
|
+
fs_1.default.mkdirSync(opts.certDir, { recursive: true });
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
throw new certInit_types_1.CertInitError('MKDIR_FAILED', `failed to create cert directory ${opts.certDir}: ${(e instanceof Error) ? e.message : String(e)}`, e);
|
|
302
|
+
}
|
|
303
|
+
const args = buildOpenSslArgs(opts);
|
|
304
|
+
// TOCTOU mitigation: openssl writes the key with the process umask (commonly
|
|
305
|
+
// 0o022 -> mode 0o644). We narrow the umask to 0o077 around the execFile so
|
|
306
|
+
// the key is created mode 0o600 from the start, eliminating the world-
|
|
307
|
+
// readable window between key creation and our explicit chmod below. We
|
|
308
|
+
// restore the previous umask in `finally` regardless of outcome. On Windows
|
|
309
|
+
// umask is effectively a no-op (NTFS ACLs are unaffected), so this is safe
|
|
310
|
+
// cross-platform. See docs/cert_init.md for the residual multi-user note
|
|
311
|
+
// (defense-in-depth: dedicated user, 0o700 parent dir).
|
|
312
|
+
const prevUmask = process.umask(0o077);
|
|
313
|
+
try {
|
|
314
|
+
(0, child_process_1.execFileSync)('openssl', args, { stdio: 'pipe', timeout: 30000 });
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
const stderr = e.stderr?.toString().trim() ?? '';
|
|
318
|
+
const status = e.status;
|
|
319
|
+
const errorMsg = `openssl req failed (status=${String(status)}): ${stderr || (e instanceof Error ? e.message : String(e))}`;
|
|
320
|
+
(0, logger_1.logError)('[certInit] openssl invocation failed', { args: args.join(' '), stderr });
|
|
321
|
+
throw new certInit_types_1.CertInitError('OPENSSL_FAILED', errorMsg, e);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
process.umask(prevUmask);
|
|
325
|
+
}
|
|
326
|
+
// Belt-and-braces: explicitly narrow key permissions to 0o600 even though
|
|
327
|
+
// umask 0o077 above should already have produced that mode. Handles the case
|
|
328
|
+
// where openssl ignores umask on some platforms or where the file pre-existed
|
|
329
|
+
// (force overwrite). No-op on Windows where chmod semantics differ
|
|
330
|
+
// (POSIX-mode bits are ignored by NTFS ACLs).
|
|
331
|
+
if (process.platform !== 'win32') {
|
|
332
|
+
try {
|
|
333
|
+
fs_1.default.chmodSync(opts.keyFile, 0o600);
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
// Non-fatal: log a warning but do not fail the whole operation.
|
|
337
|
+
// The key was still written successfully.
|
|
338
|
+
(0, logger_1.logError)('[certInit] failed to chmod private key to 0600', {
|
|
339
|
+
key: opts.keyFile,
|
|
340
|
+
error: (e instanceof Error) ? e.message : String(e),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
(0, logger_1.logInfo)('[certInit] generated certificate', {
|
|
345
|
+
cert: opts.certFile,
|
|
346
|
+
key: opts.keyFile,
|
|
347
|
+
cn: opts.cn,
|
|
348
|
+
san: opts.san,
|
|
349
|
+
days: opts.days,
|
|
350
|
+
keyBits: opts.keyBits,
|
|
351
|
+
overwritten: bothExist,
|
|
352
|
+
});
|
|
353
|
+
return {
|
|
354
|
+
kind: 'generated',
|
|
355
|
+
certFile: opts.certFile,
|
|
356
|
+
keyFile: opts.keyFile,
|
|
357
|
+
overwritten: bothExist,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the `--init-cert` certificate bootstrap subsystem.
|
|
3
|
+
*
|
|
4
|
+
* These types are deliberately split out from `certInit.ts` so that test
|
|
5
|
+
* fixtures and external callers can import the option/result shapes without
|
|
6
|
+
* pulling in the OpenSSL invocation surface.
|
|
7
|
+
*
|
|
8
|
+
* Constitution refs: CQ-7 (JSDoc on public types), AR-1 (smallest type surface
|
|
9
|
+
* sufficient for the v1 feature).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Output format for `--print-env` helper.
|
|
13
|
+
*
|
|
14
|
+
* - `posix` — emit `export NAME="value"` lines (bash, zsh, sh).
|
|
15
|
+
* - `powershell` — emit `$env:NAME="value"` lines (Windows PowerShell, pwsh).
|
|
16
|
+
* - `both` — emit both, separated by a blank line and platform headers.
|
|
17
|
+
* - `auto` — pick `powershell` on Win32, `posix` elsewhere.
|
|
18
|
+
*/
|
|
19
|
+
export type PrintEnvFormat = 'posix' | 'powershell' | 'both' | 'auto';
|
|
20
|
+
/**
|
|
21
|
+
* Resolved options for a single `runCertInit` invocation.
|
|
22
|
+
*
|
|
23
|
+
* All paths must be absolute by the time they reach `runCertInit`. Validation
|
|
24
|
+
* (path-traversal guard per SH-4, numeric range checks) happens via
|
|
25
|
+
* `validateOptions` before any filesystem or `openssl` work occurs.
|
|
26
|
+
*/
|
|
27
|
+
export interface CertInitOptions {
|
|
28
|
+
/** Absolute directory that contains the cert + key (and optionally CA). */
|
|
29
|
+
certDir: string;
|
|
30
|
+
/** Absolute path to the server certificate file (PEM). Must resolve under `certDir`. */
|
|
31
|
+
certFile: string;
|
|
32
|
+
/** Absolute path to the server private key file (PEM). Must resolve under `certDir`. */
|
|
33
|
+
keyFile: string;
|
|
34
|
+
/** Subject CommonName (CN) for the generated certificate. */
|
|
35
|
+
cn: string;
|
|
36
|
+
/**
|
|
37
|
+
* Comma-separated SAN entries, e.g. `DNS:localhost,IP:127.0.0.1`.
|
|
38
|
+
* Each entry MUST start with a `DNS:` or `IP:` prefix.
|
|
39
|
+
*/
|
|
40
|
+
san: string;
|
|
41
|
+
/** Validity period in days. Range: 1..3650 inclusive. */
|
|
42
|
+
days: number;
|
|
43
|
+
/** RSA key size in bits. Allowed values: 2048, 4096. */
|
|
44
|
+
keyBits: 2048 | 4096;
|
|
45
|
+
/** When true, overwrite existing cert/key files. */
|
|
46
|
+
force: boolean;
|
|
47
|
+
/** When true, the dispatcher will print env-var lines after generation. */
|
|
48
|
+
printEnv: boolean | PrintEnvFormat;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Result of `runCertInit`. Distinguishes generated vs skipped vs failed paths
|
|
52
|
+
* so the dispatcher in `index-server.ts` can decide whether to exit, continue,
|
|
53
|
+
* or surface an error.
|
|
54
|
+
*/
|
|
55
|
+
export type CertInitResult = {
|
|
56
|
+
kind: 'generated';
|
|
57
|
+
/** Absolute path to the written certificate file. */
|
|
58
|
+
certFile: string;
|
|
59
|
+
/** Absolute path to the written private key file. */
|
|
60
|
+
keyFile: string;
|
|
61
|
+
/** True when files were overwritten because `--force` was given. */
|
|
62
|
+
overwritten: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'skipped';
|
|
65
|
+
/** Reason the operation was skipped (e.g. files already present). */
|
|
66
|
+
reason: string;
|
|
67
|
+
/** Absolute path to the existing certificate file. */
|
|
68
|
+
certFile: string;
|
|
69
|
+
/** Absolute path to the existing private key file. */
|
|
70
|
+
keyFile: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Structured error thrown by `runCertInit` and its helpers. The `code` field
|
|
74
|
+
* is stable and machine-readable; tests assert on `code`, not on `message`
|
|
75
|
+
* wording (per OB-3 / TS-12 robustness).
|
|
76
|
+
*/
|
|
77
|
+
export declare class CertInitError extends Error {
|
|
78
|
+
readonly code: CertInitErrorCode;
|
|
79
|
+
readonly cause?: unknown | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* @param code Stable machine-readable identifier for the failure class.
|
|
82
|
+
* @param message Human-readable explanation; safe to surface to operators.
|
|
83
|
+
* @param cause Optional underlying cause (e.g. a spawn error or fs error).
|
|
84
|
+
*/
|
|
85
|
+
constructor(code: CertInitErrorCode, message: string, cause?: unknown | undefined);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Stable error codes emitted by the cert-init subsystem. New codes may be
|
|
89
|
+
* appended; existing codes MUST NOT change meaning (semver guarantee for
|
|
90
|
+
* downstream tooling and test assertions).
|
|
91
|
+
*/
|
|
92
|
+
export type CertInitErrorCode = 'OPENSSL_NOT_FOUND' | 'OPENSSL_FAILED' | 'INVALID_DAYS' | 'INVALID_KEY_BITS' | 'INVALID_SAN' | 'INVALID_CN' | 'PATH_OUTSIDE_CERT_DIR' | 'WRITE_FAILED' | 'MKDIR_FAILED';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Type definitions for the `--init-cert` certificate bootstrap subsystem.
|
|
4
|
+
*
|
|
5
|
+
* These types are deliberately split out from `certInit.ts` so that test
|
|
6
|
+
* fixtures and external callers can import the option/result shapes without
|
|
7
|
+
* pulling in the OpenSSL invocation surface.
|
|
8
|
+
*
|
|
9
|
+
* Constitution refs: CQ-7 (JSDoc on public types), AR-1 (smallest type surface
|
|
10
|
+
* sufficient for the v1 feature).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.CertInitError = void 0;
|
|
14
|
+
/**
|
|
15
|
+
* Structured error thrown by `runCertInit` and its helpers. The `code` field
|
|
16
|
+
* is stable and machine-readable; tests assert on `code`, not on `message`
|
|
17
|
+
* wording (per OB-3 / TS-12 robustness).
|
|
18
|
+
*/
|
|
19
|
+
class CertInitError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
cause;
|
|
22
|
+
/**
|
|
23
|
+
* @param code Stable machine-readable identifier for the failure class.
|
|
24
|
+
* @param message Human-readable explanation; safe to surface to operators.
|
|
25
|
+
* @param cause Optional underlying cause (e.g. a spawn error or fs error).
|
|
26
|
+
*/
|
|
27
|
+
constructor(code, message, cause) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.cause = cause;
|
|
31
|
+
this.name = 'CertInitError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.CertInitError = CertInitError;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JSON-RPC frame builders used by the handshake fallback paths. No I/O,
|
|
3
|
+
* no mutable state — these can be safely unit tested in isolation.
|
|
4
|
+
*/
|
|
5
|
+
export interface ForcedInitFrame {
|
|
6
|
+
jsonrpc: '2.0';
|
|
7
|
+
id: number;
|
|
8
|
+
result: {
|
|
9
|
+
protocolVersion: string;
|
|
10
|
+
capabilities: Record<string, unknown>;
|
|
11
|
+
instructions: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface SyntheticInitRequest {
|
|
15
|
+
jsonrpc: '2.0';
|
|
16
|
+
id: number;
|
|
17
|
+
method: 'initialize';
|
|
18
|
+
params: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a JSON-RPC `initialize` *response* frame used when the handshake
|
|
22
|
+
* detection paths conclude the client sent (or should have sent) initialize
|
|
23
|
+
* but the SDK never produced a result. The label is appended in parens so
|
|
24
|
+
* operators can identify which fallback fabricated the frame.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildForcedInitResultFrame(negotiatedVersion: string, label: 'forced-init-fallback' | 'unconditional-init-fallback', id?: number): ForcedInitFrame;
|
|
27
|
+
/**
|
|
28
|
+
* Build a synthetic `initialize` *request* used to nudge the SDK request
|
|
29
|
+
* dispatcher when stdin parsing succeeded but framing failed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildSyntheticInitRequest(id?: number): SyntheticInitRequest;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure JSON-RPC frame builders used by the handshake fallback paths. No I/O,
|
|
4
|
+
* no mutable state — these can be safely unit tested in isolation.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.buildForcedInitResultFrame = buildForcedInitResultFrame;
|
|
8
|
+
exports.buildSyntheticInitRequest = buildSyntheticInitRequest;
|
|
9
|
+
const BASE_INSTRUCTIONS = 'Use initialize -> tools/list -> tools/call { name, arguments }.';
|
|
10
|
+
/**
|
|
11
|
+
* Build a JSON-RPC `initialize` *response* frame used when the handshake
|
|
12
|
+
* detection paths conclude the client sent (or should have sent) initialize
|
|
13
|
+
* but the SDK never produced a result. The label is appended in parens so
|
|
14
|
+
* operators can identify which fallback fabricated the frame.
|
|
15
|
+
*/
|
|
16
|
+
function buildForcedInitResultFrame(negotiatedVersion, label, id = 1) {
|
|
17
|
+
return {
|
|
18
|
+
jsonrpc: '2.0',
|
|
19
|
+
id,
|
|
20
|
+
result: {
|
|
21
|
+
protocolVersion: negotiatedVersion,
|
|
22
|
+
capabilities: {},
|
|
23
|
+
instructions: `${BASE_INSTRUCTIONS} (${label})`,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build a synthetic `initialize` *request* used to nudge the SDK request
|
|
29
|
+
* dispatcher when stdin parsing succeeded but framing failed.
|
|
30
|
+
*/
|
|
31
|
+
function buildSyntheticInitRequest(id = 1) {
|
|
32
|
+
return {
|
|
33
|
+
jsonrpc: '2.0',
|
|
34
|
+
id,
|
|
35
|
+
method: 'initialize',
|
|
36
|
+
params: {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure detector for the MCP `initialize` JSON-RPC method in a buffered
|
|
3
|
+
* stdin stream. Handles three increasingly tolerant strategies used by the
|
|
4
|
+
* legacy stdin sniffer:
|
|
5
|
+
*
|
|
6
|
+
* - direct : exact `"method":"initialize"` substring match
|
|
7
|
+
* - fuzzy : reconstruct `initialize` near a `"method"` sentinel allowing
|
|
8
|
+
* up to 3 character gaps between target letters
|
|
9
|
+
* - subseq : letters-only subsequence match across the whole window
|
|
10
|
+
*
|
|
11
|
+
* The detector is intentionally side-effect free; callers own logging.
|
|
12
|
+
*/
|
|
13
|
+
export type InitializeDetectMode = 'direct' | 'fuzzy' | 'subseq';
|
|
14
|
+
export interface InitializeDetectResult {
|
|
15
|
+
mode: InitializeDetectMode | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Detect an `initialize` request method anywhere in `buffer`.
|
|
19
|
+
*
|
|
20
|
+
* @param buffer raw stdin bytes decoded as utf8
|
|
21
|
+
* @param fallbackSliceEnabled when true, also probe the trailing 2KB if no
|
|
22
|
+
* `"method"` sentinel was found (enables aggressive scanning during
|
|
23
|
+
* `INDEX_SERVER_TRACE=healthMixed`)
|
|
24
|
+
*/
|
|
25
|
+
export declare function detectInitializeMethod(buffer: string, fallbackSliceEnabled?: boolean): InitializeDetectResult;
|
|
26
|
+
/**
|
|
27
|
+
* Extract a numeric `id` from a JSON-RPC frame embedded in `buffer`. Returns
|
|
28
|
+
* the matched integer or `null` when no plausible id is present. Limited to
|
|
29
|
+
* 6 digits to match legacy behavior.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractRequestId(buffer: string): number | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure detector for the MCP `initialize` JSON-RPC method in a buffered
|
|
4
|
+
* stdin stream. Handles three increasingly tolerant strategies used by the
|
|
5
|
+
* legacy stdin sniffer:
|
|
6
|
+
*
|
|
7
|
+
* - direct : exact `"method":"initialize"` substring match
|
|
8
|
+
* - fuzzy : reconstruct `initialize` near a `"method"` sentinel allowing
|
|
9
|
+
* up to 3 character gaps between target letters
|
|
10
|
+
* - subseq : letters-only subsequence match across the whole window
|
|
11
|
+
*
|
|
12
|
+
* The detector is intentionally side-effect free; callers own logging.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.detectInitializeMethod = detectInitializeMethod;
|
|
16
|
+
exports.extractRequestId = extractRequestId;
|
|
17
|
+
const TARGET = 'initialize';
|
|
18
|
+
const FUZZY_MAX_GAPS = 3;
|
|
19
|
+
const FUZZY_SLICE_LEN = 1200;
|
|
20
|
+
const FALLBACK_SLICE_LEN = 2000;
|
|
21
|
+
function tryFuzzy(slice) {
|
|
22
|
+
let ti = 0;
|
|
23
|
+
let gaps = 0;
|
|
24
|
+
for (let i = 0; i < slice.length && ti < TARGET.length; i++) {
|
|
25
|
+
const ch = slice[i];
|
|
26
|
+
if (ch.toLowerCase?.() === TARGET[ti]) {
|
|
27
|
+
ti++;
|
|
28
|
+
gaps = 0;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (gaps < FUZZY_MAX_GAPS) {
|
|
32
|
+
gaps++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
ti = 0;
|
|
36
|
+
gaps = 0;
|
|
37
|
+
if (ch.toLowerCase?.() === TARGET[ti]) {
|
|
38
|
+
ti++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return ti === TARGET.length;
|
|
42
|
+
}
|
|
43
|
+
function trySubseq(buffer) {
|
|
44
|
+
const letters = buffer.replace(/[^a-zA-Z]/g, '').toLowerCase();
|
|
45
|
+
let ti = 0;
|
|
46
|
+
for (let i = 0; i < letters.length && ti < TARGET.length; i++) {
|
|
47
|
+
if (letters[i] === TARGET[ti])
|
|
48
|
+
ti++;
|
|
49
|
+
}
|
|
50
|
+
return ti === TARGET.length;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Detect an `initialize` request method anywhere in `buffer`.
|
|
54
|
+
*
|
|
55
|
+
* @param buffer raw stdin bytes decoded as utf8
|
|
56
|
+
* @param fallbackSliceEnabled when true, also probe the trailing 2KB if no
|
|
57
|
+
* `"method"` sentinel was found (enables aggressive scanning during
|
|
58
|
+
* `INDEX_SERVER_TRACE=healthMixed`)
|
|
59
|
+
*/
|
|
60
|
+
function detectInitializeMethod(buffer, fallbackSliceEnabled = false) {
|
|
61
|
+
if (/"method"\s*:\s*"initialize"/.test(buffer)) {
|
|
62
|
+
return { mode: 'direct' };
|
|
63
|
+
}
|
|
64
|
+
const methodIdx = buffer.indexOf('"method"');
|
|
65
|
+
const sliceA = methodIdx !== -1 ? buffer.slice(methodIdx, methodIdx + FUZZY_SLICE_LEN) : '';
|
|
66
|
+
const trySlices = sliceA ? [sliceA] : [];
|
|
67
|
+
if (!sliceA && fallbackSliceEnabled)
|
|
68
|
+
trySlices.push(buffer.slice(-FALLBACK_SLICE_LEN));
|
|
69
|
+
for (const slice of trySlices) {
|
|
70
|
+
if (tryFuzzy(slice))
|
|
71
|
+
return { mode: 'fuzzy' };
|
|
72
|
+
}
|
|
73
|
+
if (trySubseq(buffer))
|
|
74
|
+
return { mode: 'subseq' };
|
|
75
|
+
return { mode: null };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Extract a numeric `id` from a JSON-RPC frame embedded in `buffer`. Returns
|
|
79
|
+
* the matched integer or `null` when no plausible id is present. Limited to
|
|
80
|
+
* 6 digits to match legacy behavior.
|
|
81
|
+
*/
|
|
82
|
+
function extractRequestId(buffer) {
|
|
83
|
+
const m = /"id"\s*:\s*(\d{1,6})/.exec(buffer);
|
|
84
|
+
if (!m)
|
|
85
|
+
return null;
|
|
86
|
+
const n = parseInt(m[1], 10);
|
|
87
|
+
return Number.isFinite(n) ? n : null;
|
|
88
|
+
}
|