@jagilber-org/index-server 1.22.1 → 1.26.1

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.
Files changed (189) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/CODE_OF_CONDUCT.md +2 -0
  3. package/CONTRIBUTING.md +32 -2
  4. package/README.md +82 -19
  5. package/SECURITY.md +17 -5
  6. package/dist/config/dashboardConfig.d.ts +3 -0
  7. package/dist/config/dashboardConfig.js +3 -0
  8. package/dist/config/defaultValues.d.ts +1 -1
  9. package/dist/config/defaultValues.js +1 -1
  10. package/dist/config/featureConfig.d.ts +2 -0
  11. package/dist/config/featureConfig.js +6 -1
  12. package/dist/config/runtimeConfig.d.ts +1 -1
  13. package/dist/config/runtimeConfig.js +8 -9
  14. package/dist/dashboard/client/admin.html +170 -53
  15. package/dist/dashboard/client/css/admin.css +132 -0
  16. package/dist/dashboard/client/js/admin.auth.js +25 -11
  17. package/dist/dashboard/client/js/admin.config.js +1 -1
  18. package/dist/dashboard/client/js/admin.feedback.js +328 -0
  19. package/dist/dashboard/client/js/admin.graph.js +120 -18
  20. package/dist/dashboard/client/js/admin.instructions.js +27 -13
  21. package/dist/dashboard/client/js/admin.logs.js +1 -5
  22. package/dist/dashboard/client/js/admin.maintenance.js +53 -8
  23. package/dist/dashboard/client/js/admin.messaging.js +1 -4
  24. package/dist/dashboard/client/js/admin.overview.js +5 -1
  25. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  26. package/dist/dashboard/client/js/admin.utils.js +43 -1
  27. package/dist/dashboard/client/js/mermaid.min.js +813 -537
  28. package/dist/dashboard/export/DataExporter.js +2 -1
  29. package/dist/dashboard/server/AdminPanel.d.ts +3 -0
  30. package/dist/dashboard/server/AdminPanel.js +132 -35
  31. package/dist/dashboard/server/ApiRoutes.js +40 -9
  32. package/dist/dashboard/server/DashboardServer.js +1 -1
  33. package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
  34. package/dist/dashboard/server/FileMetricsStorage.js +52 -5
  35. package/dist/dashboard/server/HttpTransport.js +6 -0
  36. package/dist/dashboard/server/InstanceManager.js +7 -2
  37. package/dist/dashboard/server/KnowledgeStore.js +7 -2
  38. package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
  39. package/dist/dashboard/server/MetricsCollector.js +113 -17
  40. package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
  41. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
  42. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
  43. package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
  44. package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
  45. package/dist/dashboard/server/routes/admin.routes.js +35 -27
  46. package/dist/dashboard/server/routes/alerts.routes.js +4 -3
  47. package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
  48. package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
  49. package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
  50. package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
  51. package/dist/dashboard/server/routes/graph.routes.js +10 -13
  52. package/dist/dashboard/server/routes/index.d.ts +1 -0
  53. package/dist/dashboard/server/routes/index.js +74 -39
  54. package/dist/dashboard/server/routes/instances.routes.js +2 -1
  55. package/dist/dashboard/server/routes/instructions.routes.js +46 -27
  56. package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
  57. package/dist/dashboard/server/routes/logs.routes.js +5 -4
  58. package/dist/dashboard/server/routes/messaging.routes.js +15 -14
  59. package/dist/dashboard/server/routes/metrics.routes.js +14 -13
  60. package/dist/dashboard/server/routes/scripts.routes.js +6 -3
  61. package/dist/dashboard/server/routes/status.routes.js +5 -4
  62. package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
  63. package/dist/dashboard/server/routes/usage.routes.js +2 -1
  64. package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
  65. package/dist/dashboard/server/utils/escapeHtml.js +11 -0
  66. package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
  67. package/dist/dashboard/server/utils/pathContainment.js +15 -0
  68. package/dist/dashboard/server/wsInit.js +2 -2
  69. package/dist/lib/mcpStdioLogging.d.ts +165 -0
  70. package/dist/lib/mcpStdioLogging.js +287 -0
  71. package/dist/schemas/index.d.ts +37 -2
  72. package/dist/schemas/index.js +27 -3
  73. package/dist/server/backgroundServicesStartup.d.ts +7 -1
  74. package/dist/server/backgroundServicesStartup.js +25 -8
  75. package/dist/server/certInit.d.ts +97 -0
  76. package/dist/server/certInit.js +359 -0
  77. package/dist/server/certInit.types.d.ts +92 -0
  78. package/dist/server/certInit.types.js +34 -0
  79. package/dist/server/handshake/fallbackFrames.d.ts +31 -0
  80. package/dist/server/handshake/fallbackFrames.js +38 -0
  81. package/dist/server/handshake/initializeDetector.d.ts +31 -0
  82. package/dist/server/handshake/initializeDetector.js +88 -0
  83. package/dist/server/handshake/protocol.d.ts +15 -0
  84. package/dist/server/handshake/protocol.js +37 -0
  85. package/dist/server/handshake/readyEmitter.d.ts +6 -0
  86. package/dist/server/handshake/readyEmitter.js +88 -0
  87. package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
  88. package/dist/server/handshake/safetyFallbacks.js +134 -0
  89. package/dist/server/handshake/stdinSniffer.d.ts +1 -0
  90. package/dist/server/handshake/stdinSniffer.js +260 -0
  91. package/dist/server/handshake/tracing.d.ts +16 -0
  92. package/dist/server/handshake/tracing.js +95 -0
  93. package/dist/server/handshakeManager.d.ts +23 -23
  94. package/dist/server/handshakeManager.js +36 -466
  95. package/dist/server/index-server.d.ts +23 -0
  96. package/dist/server/index-server.js +194 -9
  97. package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
  98. package/dist/server/mcpReadOnlySurfaces.js +297 -0
  99. package/dist/server/sdkServer.js +69 -7
  100. package/dist/server/transport.d.ts +5 -6
  101. package/dist/server/transport.js +46 -64
  102. package/dist/server/transportFactory.d.ts +3 -9
  103. package/dist/server/transportFactory.js +18 -380
  104. package/dist/services/atomicFs.d.ts +3 -0
  105. package/dist/services/atomicFs.js +171 -13
  106. package/dist/services/auditLog.d.ts +17 -2
  107. package/dist/services/auditLog.js +75 -14
  108. package/dist/services/bootstrapGating.js +1 -1
  109. package/dist/services/categoryRules.d.ts +10 -0
  110. package/dist/services/categoryRules.js +17 -0
  111. package/dist/services/classificationService.js +7 -5
  112. package/dist/services/embeddingService.d.ts +27 -11
  113. package/dist/services/embeddingService.js +51 -14
  114. package/dist/services/feedbackStorage.d.ts +39 -0
  115. package/dist/services/feedbackStorage.js +88 -0
  116. package/dist/services/handlers/instructions.add.js +429 -317
  117. package/dist/services/handlers/instructions.groom.js +128 -31
  118. package/dist/services/handlers/instructions.import.js +56 -23
  119. package/dist/services/handlers/instructions.patch.js +43 -32
  120. package/dist/services/handlers/instructions.query.js +20 -29
  121. package/dist/services/handlers/instructions.shared.d.ts +54 -0
  122. package/dist/services/handlers/instructions.shared.js +126 -1
  123. package/dist/services/handlers.activation.js +83 -81
  124. package/dist/services/handlers.dashboardConfig.d.ts +2 -2
  125. package/dist/services/handlers.dashboardConfig.js +1 -2
  126. package/dist/services/handlers.diagnostics.js +75 -54
  127. package/dist/services/handlers.feedback.d.ts +4 -11
  128. package/dist/services/handlers.feedback.js +11 -333
  129. package/dist/services/handlers.gates.js +69 -37
  130. package/dist/services/handlers.graph.js +2 -2
  131. package/dist/services/handlers.help.js +2 -2
  132. package/dist/services/handlers.instructionSchema.js +4 -2
  133. package/dist/services/handlers.integrity.js +42 -22
  134. package/dist/services/handlers.messaging.js +1 -1
  135. package/dist/services/handlers.metrics.js +51 -6
  136. package/dist/services/handlers.prompt.js +10 -2
  137. package/dist/services/handlers.search.js +94 -44
  138. package/dist/services/handlers.trace.js +1 -1
  139. package/dist/services/handlers.usage.js +38 -7
  140. package/dist/services/indexContext.d.ts +21 -1
  141. package/dist/services/indexContext.js +263 -78
  142. package/dist/services/indexLoader.d.ts +1 -0
  143. package/dist/services/indexLoader.js +28 -8
  144. package/dist/services/instructionRecordValidation.d.ts +39 -0
  145. package/dist/services/instructionRecordValidation.js +388 -0
  146. package/dist/services/instructions.dispatcher.js +4 -4
  147. package/dist/services/loaderSchemaValidator.d.ts +15 -0
  148. package/dist/services/loaderSchemaValidator.js +69 -0
  149. package/dist/services/logger.js +11 -2
  150. package/dist/services/mcpLogBridge.d.ts +49 -0
  151. package/dist/services/mcpLogBridge.js +83 -0
  152. package/dist/services/ownershipService.js +18 -8
  153. package/dist/services/performanceBaseline.js +23 -22
  154. package/dist/services/promptReviewService.d.ts +3 -1
  155. package/dist/services/promptReviewService.js +41 -13
  156. package/dist/services/regexSafety.d.ts +6 -0
  157. package/dist/services/regexSafety.js +46 -0
  158. package/dist/services/seedBootstrap.js +1 -1
  159. package/dist/services/storage/factory.d.ts +14 -1
  160. package/dist/services/storage/factory.js +61 -1
  161. package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
  162. package/dist/services/storage/jsonEmbeddingStore.js +83 -0
  163. package/dist/services/storage/jsonFileStore.d.ts +3 -1
  164. package/dist/services/storage/jsonFileStore.js +8 -6
  165. package/dist/services/storage/migrationEngine.d.ts +13 -0
  166. package/dist/services/storage/migrationEngine.js +31 -0
  167. package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
  168. package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
  169. package/dist/services/storage/sqliteStore.d.ts +3 -1
  170. package/dist/services/storage/sqliteStore.js +2 -2
  171. package/dist/services/storage/types.d.ts +48 -1
  172. package/dist/services/toolRegistry.js +77 -67
  173. package/dist/services/toolRegistry.zod.js +89 -86
  174. package/dist/services/tracing.js +5 -4
  175. package/dist/utils/envUtils.d.ts +4 -0
  176. package/dist/utils/envUtils.js +7 -0
  177. package/dist/utils/memoryMonitor.js +11 -10
  178. package/package.json +11 -4
  179. package/schemas/instruction.schema.json +38 -1
  180. package/scripts/copy-dashboard-assets.mjs +1 -1
  181. package/scripts/dist/README.md +1 -1
  182. package/scripts/setup-wizard.mjs +781 -0
  183. package/server.json +1 -0
  184. package/dist/externalClientLib.d.ts +0 -1
  185. package/dist/externalClientLib.js +0 -2
  186. package/dist/portableClientWrapper.d.ts +0 -1
  187. package/dist/portableClientWrapper.js +0 -2
  188. package/dist/services/indexingService.d.ts +0 -1
  189. 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
+ }