@daniel.stefan/metalink 1.3.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.
- package/README.md +160 -0
- package/package.json +96 -0
- package/packages/cli/dist/bin/cli.d.ts +3 -0
- package/packages/cli/dist/bin/cli.d.ts.map +1 -0
- package/packages/cli/dist/bin/cli.js +4 -0
- package/packages/cli/dist/bin/cli.js.map +1 -0
- package/packages/cli/dist/commands/config/init.d.ts +9 -0
- package/packages/cli/dist/commands/config/init.d.ts.map +1 -0
- package/packages/cli/dist/commands/config/init.js +38 -0
- package/packages/cli/dist/commands/config/init.js.map +1 -0
- package/packages/cli/dist/commands/config/validate.d.ts +9 -0
- package/packages/cli/dist/commands/config/validate.d.ts.map +1 -0
- package/packages/cli/dist/commands/config/validate.js +34 -0
- package/packages/cli/dist/commands/config/validate.js.map +1 -0
- package/packages/cli/dist/commands/daemon/restart.d.ts +15 -0
- package/packages/cli/dist/commands/daemon/restart.d.ts.map +1 -0
- package/packages/cli/dist/commands/daemon/restart.js +184 -0
- package/packages/cli/dist/commands/daemon/restart.js.map +1 -0
- package/packages/cli/dist/commands/daemon/start.d.ts +7 -0
- package/packages/cli/dist/commands/daemon/start.d.ts.map +1 -0
- package/packages/cli/dist/commands/daemon/start.js +85 -0
- package/packages/cli/dist/commands/daemon/start.js.map +1 -0
- package/packages/cli/dist/commands/daemon/status.d.ts +7 -0
- package/packages/cli/dist/commands/daemon/status.d.ts.map +1 -0
- package/packages/cli/dist/commands/daemon/status.js +69 -0
- package/packages/cli/dist/commands/daemon/status.js.map +1 -0
- package/packages/cli/dist/commands/daemon/stop.d.ts +8 -0
- package/packages/cli/dist/commands/daemon/stop.d.ts.map +1 -0
- package/packages/cli/dist/commands/daemon/stop.js +77 -0
- package/packages/cli/dist/commands/daemon/stop.js.map +1 -0
- package/packages/cli/dist/commands/import/mcpm.d.ts +10 -0
- package/packages/cli/dist/commands/import/mcpm.d.ts.map +1 -0
- package/packages/cli/dist/commands/import/mcpm.js +58 -0
- package/packages/cli/dist/commands/import/mcpm.js.map +1 -0
- package/packages/cli/dist/commands/safety/add-risky-pattern.d.ts +16 -0
- package/packages/cli/dist/commands/safety/add-risky-pattern.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/add-risky-pattern.js +72 -0
- package/packages/cli/dist/commands/safety/add-risky-pattern.js.map +1 -0
- package/packages/cli/dist/commands/safety/add-risky.d.ts +12 -0
- package/packages/cli/dist/commands/safety/add-risky.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/add-risky.js +52 -0
- package/packages/cli/dist/commands/safety/add-risky.js.map +1 -0
- package/packages/cli/dist/commands/safety/add-safe-pattern.d.ts +16 -0
- package/packages/cli/dist/commands/safety/add-safe-pattern.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/add-safe-pattern.js +72 -0
- package/packages/cli/dist/commands/safety/add-safe-pattern.js.map +1 -0
- package/packages/cli/dist/commands/safety/add-safe.d.ts +12 -0
- package/packages/cli/dist/commands/safety/add-safe.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/add-safe.js +52 -0
- package/packages/cli/dist/commands/safety/add-safe.js.map +1 -0
- package/packages/cli/dist/commands/safety/check.d.ts +9 -0
- package/packages/cli/dist/commands/safety/check.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/check.js +36 -0
- package/packages/cli/dist/commands/safety/check.js.map +1 -0
- package/packages/cli/dist/commands/safety/export.d.ts +10 -0
- package/packages/cli/dist/commands/safety/export.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/export.js +48 -0
- package/packages/cli/dist/commands/safety/export.js.map +1 -0
- package/packages/cli/dist/commands/safety/import.d.ts +12 -0
- package/packages/cli/dist/commands/safety/import.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/import.js +78 -0
- package/packages/cli/dist/commands/safety/import.js.map +1 -0
- package/packages/cli/dist/commands/safety/index.d.ts +8 -0
- package/packages/cli/dist/commands/safety/index.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/index.js +46 -0
- package/packages/cli/dist/commands/safety/index.js.map +1 -0
- package/packages/cli/dist/commands/safety/list.d.ts +6 -0
- package/packages/cli/dist/commands/safety/list.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/list.js +77 -0
- package/packages/cli/dist/commands/safety/list.js.map +1 -0
- package/packages/cli/dist/commands/safety/remove.d.ts +9 -0
- package/packages/cli/dist/commands/safety/remove.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/remove.js +46 -0
- package/packages/cli/dist/commands/safety/remove.js.map +1 -0
- package/packages/cli/dist/commands/safety/reset.d.ts +9 -0
- package/packages/cli/dist/commands/safety/reset.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/reset.js +46 -0
- package/packages/cli/dist/commands/safety/reset.js.map +1 -0
- package/packages/cli/dist/commands/safety/validate.d.ts +9 -0
- package/packages/cli/dist/commands/safety/validate.d.ts.map +1 -0
- package/packages/cli/dist/commands/safety/validate.js +51 -0
- package/packages/cli/dist/commands/safety/validate.js.map +1 -0
- package/packages/cli/dist/commands/secret/get.d.ts +9 -0
- package/packages/cli/dist/commands/secret/get.d.ts.map +1 -0
- package/packages/cli/dist/commands/secret/get.js +26 -0
- package/packages/cli/dist/commands/secret/get.js.map +1 -0
- package/packages/cli/dist/commands/secret/set.d.ts +10 -0
- package/packages/cli/dist/commands/secret/set.d.ts.map +1 -0
- package/packages/cli/dist/commands/secret/set.js +22 -0
- package/packages/cli/dist/commands/secret/set.js.map +1 -0
- package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.d.ts +2 -0
- package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.js +234 -0
- package/packages/cli/dist/commands/server/__tests__/server-management-e2e.test.js.map +1 -0
- package/packages/cli/dist/commands/server/add.d.ts +14 -0
- package/packages/cli/dist/commands/server/add.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/add.js +86 -0
- package/packages/cli/dist/commands/server/add.js.map +1 -0
- package/packages/cli/dist/commands/server/available.d.ts +10 -0
- package/packages/cli/dist/commands/server/available.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/available.js +62 -0
- package/packages/cli/dist/commands/server/available.js.map +1 -0
- package/packages/cli/dist/commands/server/debug.d.ts +18 -0
- package/packages/cli/dist/commands/server/debug.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/debug.js +165 -0
- package/packages/cli/dist/commands/server/debug.js.map +1 -0
- package/packages/cli/dist/commands/server/info.d.ts +13 -0
- package/packages/cli/dist/commands/server/info.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/info.js +62 -0
- package/packages/cli/dist/commands/server/info.js.map +1 -0
- package/packages/cli/dist/commands/server/list.d.ts +10 -0
- package/packages/cli/dist/commands/server/list.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/list.js +105 -0
- package/packages/cli/dist/commands/server/list.js.map +1 -0
- package/packages/cli/dist/commands/server/refresh-tools.d.ts +13 -0
- package/packages/cli/dist/commands/server/refresh-tools.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/refresh-tools.js +46 -0
- package/packages/cli/dist/commands/server/refresh-tools.js.map +1 -0
- package/packages/cli/dist/commands/server/remove.d.ts +12 -0
- package/packages/cli/dist/commands/server/remove.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/remove.js +39 -0
- package/packages/cli/dist/commands/server/remove.js.map +1 -0
- package/packages/cli/dist/commands/server/restart.d.ts +9 -0
- package/packages/cli/dist/commands/server/restart.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/restart.js +30 -0
- package/packages/cli/dist/commands/server/restart.js.map +1 -0
- package/packages/cli/dist/commands/server/start.d.ts +9 -0
- package/packages/cli/dist/commands/server/start.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/start.js +37 -0
- package/packages/cli/dist/commands/server/start.js.map +1 -0
- package/packages/cli/dist/commands/server/status.d.ts +9 -0
- package/packages/cli/dist/commands/server/status.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/status.js +30 -0
- package/packages/cli/dist/commands/server/status.js.map +1 -0
- package/packages/cli/dist/commands/server/stop.d.ts +9 -0
- package/packages/cli/dist/commands/server/stop.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/stop.js +31 -0
- package/packages/cli/dist/commands/server/stop.js.map +1 -0
- package/packages/cli/dist/commands/server/validate.d.ts +15 -0
- package/packages/cli/dist/commands/server/validate.d.ts.map +1 -0
- package/packages/cli/dist/commands/server/validate.js +87 -0
- package/packages/cli/dist/commands/server/validate.js.map +1 -0
- package/packages/cli/dist/commands/stdio.d.ts +36 -0
- package/packages/cli/dist/commands/stdio.d.ts.map +1 -0
- package/packages/cli/dist/commands/stdio.js +85 -0
- package/packages/cli/dist/commands/stdio.js.map +1 -0
- package/packages/cli/dist/commands/tool/execute-confirm.d.ts +12 -0
- package/packages/cli/dist/commands/tool/execute-confirm.d.ts.map +1 -0
- package/packages/cli/dist/commands/tool/execute-confirm.js +98 -0
- package/packages/cli/dist/commands/tool/execute-confirm.js.map +1 -0
- package/packages/cli/dist/commands/tool/execute.d.ts +12 -0
- package/packages/cli/dist/commands/tool/execute.d.ts.map +1 -0
- package/packages/cli/dist/commands/tool/execute.js +55 -0
- package/packages/cli/dist/commands/tool/execute.js.map +1 -0
- package/packages/cli/dist/commands/tool/list.d.ts +13 -0
- package/packages/cli/dist/commands/tool/list.d.ts.map +1 -0
- package/packages/cli/dist/commands/tool/list.js +91 -0
- package/packages/cli/dist/commands/tool/list.js.map +1 -0
- package/packages/cli/dist/commands/tool/search.d.ts +12 -0
- package/packages/cli/dist/commands/tool/search.d.ts.map +1 -0
- package/packages/cli/dist/commands/tool/search.js +87 -0
- package/packages/cli/dist/commands/tool/search.js.map +1 -0
- package/packages/cli/dist/index.d.ts +2 -0
- package/packages/cli/dist/index.d.ts.map +1 -0
- package/packages/cli/dist/index.js +9 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/dist/utils/daemon-checker.d.ts +18 -0
- package/packages/cli/dist/utils/daemon-checker.d.ts.map +1 -0
- package/packages/cli/dist/utils/daemon-checker.js +69 -0
- package/packages/cli/dist/utils/daemon-checker.js.map +1 -0
- package/packages/cli/dist/utils/daemon-endpoint.d.ts +7 -0
- package/packages/cli/dist/utils/daemon-endpoint.d.ts.map +1 -0
- package/packages/cli/dist/utils/daemon-endpoint.js +13 -0
- package/packages/cli/dist/utils/daemon-endpoint.js.map +1 -0
- package/packages/cli/dist/utils/get-configured-port.d.ts +43 -0
- package/packages/cli/dist/utils/get-configured-port.d.ts.map +1 -0
- package/packages/cli/dist/utils/get-configured-port.js +141 -0
- package/packages/cli/dist/utils/get-configured-port.js.map +1 -0
- package/packages/cli/dist/utils/stdio-bridge.d.ts +48 -0
- package/packages/cli/dist/utils/stdio-bridge.d.ts.map +1 -0
- package/packages/cli/dist/utils/stdio-bridge.js +181 -0
- package/packages/cli/dist/utils/stdio-bridge.js.map +1 -0
- package/packages/cli/package.json +48 -0
- package/packages/core/dist/config/defaults.d.ts +36 -0
- package/packages/core/dist/config/defaults.d.ts.map +1 -0
- package/packages/core/dist/config/defaults.js +324 -0
- package/packages/core/dist/config/defaults.js.map +1 -0
- package/packages/core/dist/config/index.d.ts +9 -0
- package/packages/core/dist/config/index.d.ts.map +1 -0
- package/packages/core/dist/config/index.js +14 -0
- package/packages/core/dist/config/index.js.map +1 -0
- package/packages/core/dist/config/loader.d.ts +269 -0
- package/packages/core/dist/config/loader.d.ts.map +1 -0
- package/packages/core/dist/config/loader.js +777 -0
- package/packages/core/dist/config/loader.js.map +1 -0
- package/packages/core/dist/config/registry.d.ts +212 -0
- package/packages/core/dist/config/registry.d.ts.map +1 -0
- package/packages/core/dist/config/registry.js +754 -0
- package/packages/core/dist/config/registry.js.map +1 -0
- package/packages/core/dist/config/schema.d.ts +4352 -0
- package/packages/core/dist/config/schema.d.ts.map +1 -0
- package/packages/core/dist/config/schema.js +267 -0
- package/packages/core/dist/config/schema.js.map +1 -0
- package/packages/core/dist/daemon.d.ts +7 -0
- package/packages/core/dist/daemon.d.ts.map +1 -0
- package/packages/core/dist/daemon.js +116 -0
- package/packages/core/dist/daemon.js.map +1 -0
- package/packages/core/dist/http-client-retry.d.ts +67 -0
- package/packages/core/dist/http-client-retry.d.ts.map +1 -0
- package/packages/core/dist/http-client-retry.js +133 -0
- package/packages/core/dist/http-client-retry.js.map +1 -0
- package/packages/core/dist/http-client-updated.d.ts +147 -0
- package/packages/core/dist/http-client-updated.d.ts.map +1 -0
- package/packages/core/dist/http-client-updated.js +452 -0
- package/packages/core/dist/http-client-updated.js.map +1 -0
- package/packages/core/dist/http-client.d.ts +207 -0
- package/packages/core/dist/http-client.d.ts.map +1 -0
- package/packages/core/dist/http-client.js +704 -0
- package/packages/core/dist/http-client.js.map +1 -0
- package/packages/core/dist/index.d.ts +13 -0
- package/packages/core/dist/index.d.ts.map +1 -0
- package/packages/core/dist/index.js +23 -0
- package/packages/core/dist/index.js.map +1 -0
- package/packages/core/dist/logging/index.d.ts +46 -0
- package/packages/core/dist/logging/index.d.ts.map +1 -0
- package/packages/core/dist/logging/index.js +74 -0
- package/packages/core/dist/logging/index.js.map +1 -0
- package/packages/core/dist/metrics/index.d.ts +339 -0
- package/packages/core/dist/metrics/index.d.ts.map +1 -0
- package/packages/core/dist/metrics/index.js +792 -0
- package/packages/core/dist/metrics/index.js.map +1 -0
- package/packages/core/dist/plugins/index.d.ts +49 -0
- package/packages/core/dist/plugins/index.d.ts.map +1 -0
- package/packages/core/dist/plugins/index.js +82 -0
- package/packages/core/dist/plugins/index.js.map +1 -0
- package/packages/core/dist/secrets/index.d.ts +6 -0
- package/packages/core/dist/secrets/index.d.ts.map +1 -0
- package/packages/core/dist/secrets/index.js +5 -0
- package/packages/core/dist/secrets/index.js.map +1 -0
- package/packages/core/dist/secrets/keyring.d.ts +54 -0
- package/packages/core/dist/secrets/keyring.d.ts.map +1 -0
- package/packages/core/dist/secrets/keyring.js +141 -0
- package/packages/core/dist/secrets/keyring.js.map +1 -0
- package/packages/core/dist/server/batch-executor.d.ts +83 -0
- package/packages/core/dist/server/batch-executor.d.ts.map +1 -0
- package/packages/core/dist/server/batch-executor.js +291 -0
- package/packages/core/dist/server/batch-executor.js.map +1 -0
- package/packages/core/dist/server/circuit-breaker.d.ts +215 -0
- package/packages/core/dist/server/circuit-breaker.d.ts.map +1 -0
- package/packages/core/dist/server/circuit-breaker.js +330 -0
- package/packages/core/dist/server/circuit-breaker.js.map +1 -0
- package/packages/core/dist/server/client-detection.d.ts +40 -0
- package/packages/core/dist/server/client-detection.d.ts.map +1 -0
- package/packages/core/dist/server/client-detection.js +242 -0
- package/packages/core/dist/server/client-detection.js.map +1 -0
- package/packages/core/dist/server/client-profiles.d.ts +102 -0
- package/packages/core/dist/server/client-profiles.d.ts.map +1 -0
- package/packages/core/dist/server/client-profiles.js +254 -0
- package/packages/core/dist/server/client-profiles.js.map +1 -0
- package/packages/core/dist/server/http.d.ts +386 -0
- package/packages/core/dist/server/http.d.ts.map +1 -0
- package/packages/core/dist/server/http.js +4253 -0
- package/packages/core/dist/server/http.js.map +1 -0
- package/packages/core/dist/server/index.d.ts +7 -0
- package/packages/core/dist/server/index.d.ts.map +1 -0
- package/packages/core/dist/server/index.js +6 -0
- package/packages/core/dist/server/index.js.map +1 -0
- package/packages/core/dist/server/manager.d.ts +458 -0
- package/packages/core/dist/server/manager.d.ts.map +1 -0
- package/packages/core/dist/server/manager.js +3255 -0
- package/packages/core/dist/server/manager.js.map +1 -0
- package/packages/core/dist/server/managers/HttpConnectionManager.d.ts +69 -0
- package/packages/core/dist/server/managers/HttpConnectionManager.d.ts.map +1 -0
- package/packages/core/dist/server/managers/HttpConnectionManager.js +214 -0
- package/packages/core/dist/server/managers/HttpConnectionManager.js.map +1 -0
- package/packages/core/dist/server/managers/ProcessManager.d.ts +128 -0
- package/packages/core/dist/server/managers/ProcessManager.d.ts.map +1 -0
- package/packages/core/dist/server/managers/ProcessManager.js +443 -0
- package/packages/core/dist/server/managers/ProcessManager.js.map +1 -0
- package/packages/core/dist/server/managers/SchemaCacheManager.d.ts +152 -0
- package/packages/core/dist/server/managers/SchemaCacheManager.d.ts.map +1 -0
- package/packages/core/dist/server/managers/SchemaCacheManager.js +426 -0
- package/packages/core/dist/server/managers/SchemaCacheManager.js.map +1 -0
- package/packages/core/dist/server/managers/index.d.ts +9 -0
- package/packages/core/dist/server/managers/index.d.ts.map +1 -0
- package/packages/core/dist/server/managers/index.js +9 -0
- package/packages/core/dist/server/managers/index.js.map +1 -0
- package/packages/core/dist/server/metrics.d.ts +134 -0
- package/packages/core/dist/server/metrics.d.ts.map +1 -0
- package/packages/core/dist/server/metrics.js +273 -0
- package/packages/core/dist/server/metrics.js.map +1 -0
- package/packages/core/dist/server/prompts.d.ts +58 -0
- package/packages/core/dist/server/prompts.d.ts.map +1 -0
- package/packages/core/dist/server/prompts.js +405 -0
- package/packages/core/dist/server/prompts.js.map +1 -0
- package/packages/core/dist/server/protocol-versions.d.ts +49 -0
- package/packages/core/dist/server/protocol-versions.d.ts.map +1 -0
- package/packages/core/dist/server/protocol-versions.js +173 -0
- package/packages/core/dist/server/protocol-versions.js.map +1 -0
- package/packages/core/dist/server/resources.d.ts +64 -0
- package/packages/core/dist/server/resources.d.ts.map +1 -0
- package/packages/core/dist/server/resources.js +243 -0
- package/packages/core/dist/server/resources.js.map +1 -0
- package/packages/core/dist/server/schema-store.d.ts +84 -0
- package/packages/core/dist/server/schema-store.d.ts.map +1 -0
- package/packages/core/dist/server/schema-store.js +234 -0
- package/packages/core/dist/server/schema-store.js.map +1 -0
- package/packages/core/dist/server/schema-validator.d.ts +51 -0
- package/packages/core/dist/server/schema-validator.d.ts.map +1 -0
- package/packages/core/dist/server/schema-validator.js +208 -0
- package/packages/core/dist/server/schema-validator.js.map +1 -0
- package/packages/core/dist/server/token-calculator.d.ts +44 -0
- package/packages/core/dist/server/token-calculator.d.ts.map +1 -0
- package/packages/core/dist/server/token-calculator.js +53 -0
- package/packages/core/dist/server/token-calculator.js.map +1 -0
- package/packages/core/dist/server/types.d.ts +45 -0
- package/packages/core/dist/server/types.d.ts.map +1 -0
- package/packages/core/dist/server/types.js +5 -0
- package/packages/core/dist/server/types.js.map +1 -0
- package/packages/core/dist/utils/file-lock.d.ts +73 -0
- package/packages/core/dist/utils/file-lock.d.ts.map +1 -0
- package/packages/core/dist/utils/file-lock.js +235 -0
- package/packages/core/dist/utils/file-lock.js.map +1 -0
- package/packages/core/package.json +36 -0
- package/packages/dashboard/dist/assets/index-B7hvkCMu.css +1 -0
- package/packages/dashboard/dist/assets/index-yZhLPpzr.js +1 -0
- package/packages/dashboard/dist/index.html +14 -0
- package/packages/dashboard/package.json +24 -0
- package/packages/shared/dist/version.d.ts +2 -0
- package/packages/shared/dist/version.d.ts.map +1 -0
- package/packages/shared/dist/version.js +2 -0
- package/packages/shared/dist/version.js.map +1 -0
- package/packages/shared/package.json +18 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import { ServerConfigSchema } from './schema.js';
|
|
5
|
+
import { FileLock } from '../utils/file-lock.js';
|
|
6
|
+
/**
|
|
7
|
+
* Security configuration for registry operations.
|
|
8
|
+
* Whitelist of allowed commands to prevent arbitrary command execution.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY NOTE (OWASP CWE-78: OS Command Injection Prevention):
|
|
11
|
+
* - Only package runners and runtimes are whitelisted
|
|
12
|
+
* - 'bash' and 'sh' are EXPLICITLY PROHIBITED to prevent shell command injection
|
|
13
|
+
* - If registry is compromised, attackers cannot execute arbitrary shell commands
|
|
14
|
+
* - All server commands must use absolute paths or whitelisted runners
|
|
15
|
+
*
|
|
16
|
+
* @see https://owasp.org/www-community/attacks/Command_Injection
|
|
17
|
+
*/
|
|
18
|
+
export const ALLOWED_COMMANDS = [
|
|
19
|
+
'npx', // npm package runner - executes specific npm packages
|
|
20
|
+
'uvx', // uv package runner - Python package execution
|
|
21
|
+
'node', // Node.js runtime - requires explicit script path
|
|
22
|
+
'python3', // Python 3 runtime - requires explicit script path
|
|
23
|
+
'python', // Python runtime alias - requires explicit script path
|
|
24
|
+
'mcp-grafana', // Specific MCP server binary (pre-approved)
|
|
25
|
+
// SECURITY: 'bash' and 'sh' REMOVED - shell execution prohibited
|
|
26
|
+
// Rationale: Shell access allows arbitrary command injection if registry is compromised
|
|
27
|
+
// Alternative: Use absolute paths (e.g., /usr/local/bin/myserver) for custom servers
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Patterns that indicate dangerous shell metacharacters or injection attempts.
|
|
31
|
+
* Used by validateServerArguments() to detect malicious input.
|
|
32
|
+
*
|
|
33
|
+
* @see https://owasp.org/www-community/attacks/Command_Injection
|
|
34
|
+
*/
|
|
35
|
+
export const DANGEROUS_PATTERNS = [
|
|
36
|
+
/;/, // Command chaining
|
|
37
|
+
/\|/, // Pipe operator
|
|
38
|
+
/&/, // Background/chaining
|
|
39
|
+
/`/, // Command substitution (backticks)
|
|
40
|
+
/\$\(/, // Command substitution $(...)
|
|
41
|
+
/\$\{/, // Variable expansion ${...}
|
|
42
|
+
/>/, // Output redirection
|
|
43
|
+
/</, // Input redirection
|
|
44
|
+
/\n/, // Newline injection
|
|
45
|
+
/\r/, // Carriage return injection
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* SECURITY (CWE-78): Dangerous flags that could execute arbitrary code.
|
|
49
|
+
* These flags are blocked even for whitelisted commands to prevent
|
|
50
|
+
* command injection via flags like `node --eval "malicious code"`.
|
|
51
|
+
*
|
|
52
|
+
* @see https://owasp.org/www-community/attacks/Command_Injection
|
|
53
|
+
*/
|
|
54
|
+
export const DANGEROUS_FLAGS = {
|
|
55
|
+
'node': [
|
|
56
|
+
'--eval', '-e', // Run inline JavaScript code
|
|
57
|
+
'--print', '-p', // Evaluate and print expression
|
|
58
|
+
'--require', '-r', // Preload module (can run arbitrary code)
|
|
59
|
+
'--import', // ESM module preloading
|
|
60
|
+
'--input-type', // Interpret stdin as module (bypass detection)
|
|
61
|
+
],
|
|
62
|
+
'python': [
|
|
63
|
+
'-c', '--command', // Run inline Python code
|
|
64
|
+
'-m', // Run module as script (can be abused)
|
|
65
|
+
],
|
|
66
|
+
'python3': [
|
|
67
|
+
'-c', '--command', // Run inline Python code
|
|
68
|
+
'-m', // Run module as script (can be abused)
|
|
69
|
+
],
|
|
70
|
+
'npx': [
|
|
71
|
+
'--yes', '-y', // Auto-install without confirmation (malicious packages)
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Check if any argument contains a dangerous flag for the given command.
|
|
76
|
+
* Prevents command injection via flags like `node --eval "malicious code"`.
|
|
77
|
+
*
|
|
78
|
+
* SECURITY (CWE-78): This function blocks dangerous interpreter flags that
|
|
79
|
+
* could be used to run arbitrary code even when the command itself is whitelisted.
|
|
80
|
+
*
|
|
81
|
+
* @param command - The command being validated (e.g., 'node', 'python3')
|
|
82
|
+
* @param args - Array of command arguments
|
|
83
|
+
* @param serverName - Optional server name for audit logging
|
|
84
|
+
* @throws Error if a dangerous flag is detected
|
|
85
|
+
*
|
|
86
|
+
* @see https://owasp.org/www-community/attacks/Command_Injection
|
|
87
|
+
*/
|
|
88
|
+
export function validateCommandFlags(command, args, serverName) {
|
|
89
|
+
const dangerousForCommand = DANGEROUS_FLAGS[command];
|
|
90
|
+
if (!dangerousForCommand)
|
|
91
|
+
return; // No restrictions for this command
|
|
92
|
+
const timestamp = new Date().toISOString();
|
|
93
|
+
for (const arg of args) {
|
|
94
|
+
// Check exact match and prefix match (e.g., --eval=code, -e=code)
|
|
95
|
+
for (const dangerousFlag of dangerousForCommand) {
|
|
96
|
+
if (arg === dangerousFlag || arg.startsWith(`${dangerousFlag}=`)) {
|
|
97
|
+
logSecurityEvent({
|
|
98
|
+
timestamp,
|
|
99
|
+
eventType: 'DANGEROUS_FLAG_BLOCKED',
|
|
100
|
+
severity: 'CRITICAL',
|
|
101
|
+
details: `Dangerous flag '${dangerousFlag}' blocked for command '${command}'. ` +
|
|
102
|
+
`This flag could be used to run arbitrary code.`,
|
|
103
|
+
serverName,
|
|
104
|
+
command,
|
|
105
|
+
// Note: Not logging full args array to avoid leaking potential attack payloads
|
|
106
|
+
});
|
|
107
|
+
throw new Error(`SECURITY VIOLATION: Flag '${dangerousFlag}' is not allowed for command '${command}'. ` +
|
|
108
|
+
`This flag could be used to run arbitrary code and is blocked for security reasons.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Log a security audit event.
|
|
115
|
+
* In production, this should be connected to a SIEM or audit log system.
|
|
116
|
+
*
|
|
117
|
+
* @param entry - Security audit log entry
|
|
118
|
+
*/
|
|
119
|
+
export function logSecurityEvent(entry) {
|
|
120
|
+
const logMessage = `[SECURITY ${entry.severity}] ${entry.eventType}: ${entry.details}`;
|
|
121
|
+
// Always log to console with timestamp
|
|
122
|
+
if (entry.severity === 'CRITICAL' || entry.severity === 'HIGH') {
|
|
123
|
+
console.error(logMessage, {
|
|
124
|
+
timestamp: entry.timestamp,
|
|
125
|
+
serverName: entry.serverName,
|
|
126
|
+
command: entry.command,
|
|
127
|
+
// Intentionally not logging args for security (may contain sensitive data)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.warn(logMessage, {
|
|
132
|
+
timestamp: entry.timestamp,
|
|
133
|
+
serverName: entry.serverName,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Validate server command against security rules.
|
|
139
|
+
* Throws an error if command is not whitelisted or contains dangerous patterns.
|
|
140
|
+
*
|
|
141
|
+
* SECURITY: This is a critical function for preventing command injection.
|
|
142
|
+
*
|
|
143
|
+
* @param command - The command to execute (e.g., 'npx', 'node')
|
|
144
|
+
* @param args - Command arguments array
|
|
145
|
+
* @param serverName - Name of the server (for audit logging)
|
|
146
|
+
* @throws Error if validation fails
|
|
147
|
+
*/
|
|
148
|
+
export function validateServerCommand(command, args, serverName) {
|
|
149
|
+
const timestamp = new Date().toISOString();
|
|
150
|
+
// Check 1: Is it a whitelisted command?
|
|
151
|
+
if (!ALLOWED_COMMANDS.includes(command)) {
|
|
152
|
+
// Special handling for shell commands - log as CRITICAL
|
|
153
|
+
if (command === 'bash' || command === 'sh' || command === 'zsh' || command === 'csh') {
|
|
154
|
+
logSecurityEvent({
|
|
155
|
+
timestamp,
|
|
156
|
+
eventType: 'SHELL_COMMAND_BLOCKED',
|
|
157
|
+
severity: 'CRITICAL',
|
|
158
|
+
details: `Shell command '${command}' is prohibited. Use package runners or absolute paths instead.`,
|
|
159
|
+
serverName,
|
|
160
|
+
command,
|
|
161
|
+
});
|
|
162
|
+
throw new Error(`SECURITY VIOLATION: Shell command '${command}' is not allowed. ` +
|
|
163
|
+
`Shell execution is prohibited to prevent command injection attacks. ` +
|
|
164
|
+
`Use 'npx', 'uvx', 'node', or 'python3' with explicit script paths.`);
|
|
165
|
+
}
|
|
166
|
+
logSecurityEvent({
|
|
167
|
+
timestamp,
|
|
168
|
+
eventType: 'COMMAND_VALIDATION_FAILED',
|
|
169
|
+
severity: 'HIGH',
|
|
170
|
+
details: `Command '${command}' not whitelisted. Allowed: ${ALLOWED_COMMANDS.join(', ')}`,
|
|
171
|
+
serverName,
|
|
172
|
+
command,
|
|
173
|
+
});
|
|
174
|
+
throw new Error(`Command '${command}' not whitelisted. Allowed commands: ${ALLOWED_COMMANDS.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
// Check 2: SECURITY (CWE-78) - Block dangerous flags that could run arbitrary code
|
|
177
|
+
validateCommandFlags(command, args, serverName);
|
|
178
|
+
// Check 3: For node/python - first argument should be an absolute path or package name
|
|
179
|
+
if (['node', 'python3', 'python'].includes(command)) {
|
|
180
|
+
if (args.length === 0) {
|
|
181
|
+
logSecurityEvent({
|
|
182
|
+
timestamp,
|
|
183
|
+
eventType: 'COMMAND_VALIDATION_FAILED',
|
|
184
|
+
severity: 'MEDIUM',
|
|
185
|
+
details: `${command} requires a script path argument`,
|
|
186
|
+
serverName,
|
|
187
|
+
command,
|
|
188
|
+
});
|
|
189
|
+
throw new Error(`${command} requires a script path argument`);
|
|
190
|
+
}
|
|
191
|
+
const scriptPath = args[0];
|
|
192
|
+
// Allow: absolute paths, package names starting with @, relative paths from current dir
|
|
193
|
+
const isAbsolutePath = scriptPath.startsWith('/') || scriptPath.startsWith('~');
|
|
194
|
+
const isPackageName = scriptPath.startsWith('@') || /^[a-z0-9-]+$/i.test(scriptPath);
|
|
195
|
+
const isRelativeFromCurrent = scriptPath.startsWith('./');
|
|
196
|
+
// Reject paths starting with .. (path traversal attempt)
|
|
197
|
+
if (scriptPath.startsWith('..')) {
|
|
198
|
+
logSecurityEvent({
|
|
199
|
+
timestamp,
|
|
200
|
+
eventType: 'PATH_TRAVERSAL_DETECTED',
|
|
201
|
+
severity: 'HIGH',
|
|
202
|
+
details: `Script path starts with '..' which indicates path traversal: ${scriptPath}`,
|
|
203
|
+
serverName,
|
|
204
|
+
command,
|
|
205
|
+
args,
|
|
206
|
+
});
|
|
207
|
+
throw new Error(`Script path must not start with '..'. Use absolute paths or paths relative to current directory (./)`);
|
|
208
|
+
}
|
|
209
|
+
// Warn but allow relative paths from current directory
|
|
210
|
+
if (!isAbsolutePath && !isPackageName && !isRelativeFromCurrent) {
|
|
211
|
+
console.warn(`[SECURITY] Non-absolute script path: ${scriptPath}. Consider using absolute paths for security.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Check 3: No path traversal in any arguments
|
|
215
|
+
for (const arg of args) {
|
|
216
|
+
if (arg.includes('../') || arg.includes('..\\')) {
|
|
217
|
+
logSecurityEvent({
|
|
218
|
+
timestamp,
|
|
219
|
+
eventType: 'PATH_TRAVERSAL_DETECTED',
|
|
220
|
+
severity: 'HIGH',
|
|
221
|
+
details: `Path traversal detected in argument: ${arg}`,
|
|
222
|
+
serverName,
|
|
223
|
+
command,
|
|
224
|
+
args,
|
|
225
|
+
});
|
|
226
|
+
throw new Error(`Path traversal detected in argument: ${arg}`);
|
|
227
|
+
}
|
|
228
|
+
// Check 4: No dangerous shell metacharacters in arguments
|
|
229
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
230
|
+
if (pattern.test(arg)) {
|
|
231
|
+
logSecurityEvent({
|
|
232
|
+
timestamp,
|
|
233
|
+
eventType: 'DANGEROUS_PATTERN_DETECTED',
|
|
234
|
+
severity: 'HIGH',
|
|
235
|
+
details: `Dangerous pattern detected in argument: ${arg}`,
|
|
236
|
+
serverName,
|
|
237
|
+
command,
|
|
238
|
+
args,
|
|
239
|
+
});
|
|
240
|
+
throw new Error(`Dangerous shell metacharacter detected in argument: ${arg}. ` +
|
|
241
|
+
'Arguments must not contain shell operators (;|&`$<>).');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Validate registry integrity - checks all server configurations for security issues.
|
|
248
|
+
* This should be called when loading the registry to detect tampering.
|
|
249
|
+
*
|
|
250
|
+
* @param registry - The registry object containing servers array
|
|
251
|
+
* @returns Object with valid flag and any warnings/errors found
|
|
252
|
+
*/
|
|
253
|
+
export async function validateRegistryIntegrity(registry) {
|
|
254
|
+
const timestamp = new Date().toISOString();
|
|
255
|
+
const warnings = [];
|
|
256
|
+
const errors = [];
|
|
257
|
+
for (const server of registry.servers) {
|
|
258
|
+
// Validate server name (alphanumeric, hyphens, underscores only)
|
|
259
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(server.name)) {
|
|
260
|
+
errors.push(`Server name '${server.name}' contains invalid characters. Use alphanumeric, hyphens, or underscores only.`);
|
|
261
|
+
}
|
|
262
|
+
// Validate command if present (stdio server)
|
|
263
|
+
if (server.command) {
|
|
264
|
+
// Check for shell commands
|
|
265
|
+
if (['bash', 'sh', 'zsh', 'csh'].includes(server.command)) {
|
|
266
|
+
errors.push(`Server '${server.name}' uses prohibited shell command '${server.command}'. ` +
|
|
267
|
+
`Shell commands are blocked to prevent command injection.`);
|
|
268
|
+
logSecurityEvent({
|
|
269
|
+
timestamp,
|
|
270
|
+
eventType: 'SHELL_COMMAND_BLOCKED',
|
|
271
|
+
severity: 'CRITICAL',
|
|
272
|
+
details: `Registry contains shell command for server '${server.name}'`,
|
|
273
|
+
serverName: server.name,
|
|
274
|
+
command: server.command,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// Check command whitelist
|
|
278
|
+
if (!ALLOWED_COMMANDS.includes(server.command)) {
|
|
279
|
+
warnings.push(`Server '${server.name}' uses non-whitelisted command '${server.command}'. ` +
|
|
280
|
+
`Consider using whitelisted commands: ${ALLOWED_COMMANDS.join(', ')}`);
|
|
281
|
+
}
|
|
282
|
+
// Validate arguments
|
|
283
|
+
if (server.args) {
|
|
284
|
+
// SECURITY (CWE-78): Check for dangerous flags that could run arbitrary code
|
|
285
|
+
const dangerousForCommand = DANGEROUS_FLAGS[server.command];
|
|
286
|
+
if (dangerousForCommand) {
|
|
287
|
+
for (const arg of server.args) {
|
|
288
|
+
for (const dangerousFlag of dangerousForCommand) {
|
|
289
|
+
if (arg === dangerousFlag || arg.startsWith(`${dangerousFlag}=`)) {
|
|
290
|
+
errors.push(`Server '${server.name}' uses dangerous flag '${dangerousFlag}' for command '${server.command}'. ` +
|
|
291
|
+
`This flag is blocked to prevent arbitrary code running.`);
|
|
292
|
+
logSecurityEvent({
|
|
293
|
+
timestamp,
|
|
294
|
+
eventType: 'DANGEROUS_FLAG_BLOCKED',
|
|
295
|
+
severity: 'CRITICAL',
|
|
296
|
+
details: `Registry contains dangerous flag '${dangerousFlag}' for server '${server.name}'`,
|
|
297
|
+
serverName: server.name,
|
|
298
|
+
command: server.command,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Check for path traversal and dangerous patterns
|
|
305
|
+
for (const arg of server.args) {
|
|
306
|
+
if (arg.includes('../') || arg.includes('..\\')) {
|
|
307
|
+
errors.push(`Server '${server.name}' has path traversal in argument: ${arg}`);
|
|
308
|
+
}
|
|
309
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
310
|
+
if (pattern.test(arg)) {
|
|
311
|
+
errors.push(`Server '${server.name}' has dangerous pattern in argument: ${arg}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Validate environment variables
|
|
318
|
+
if (server.env) {
|
|
319
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
320
|
+
// Check for path traversal in env values
|
|
321
|
+
if (typeof value === 'string' && (value.includes('../') || value.includes('..\\'))) {
|
|
322
|
+
warnings.push(`Server '${server.name}' has path traversal in env var '${key}'`);
|
|
323
|
+
}
|
|
324
|
+
// Don't log actual env values - they may contain secrets
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const valid = errors.length === 0;
|
|
329
|
+
logSecurityEvent({
|
|
330
|
+
timestamp,
|
|
331
|
+
eventType: valid ? 'INTEGRITY_CHECK_PASSED' : 'INTEGRITY_CHECK_FAILED',
|
|
332
|
+
severity: valid ? 'LOW' : 'HIGH',
|
|
333
|
+
details: `Registry integrity check: ${errors.length} errors, ${warnings.length} warnings`,
|
|
334
|
+
});
|
|
335
|
+
return { valid, warnings, errors };
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* RegistryManager handles atomic read/write operations on mcp-registry.json.
|
|
339
|
+
* Ensures data integrity through file locking and rolling backups.
|
|
340
|
+
*
|
|
341
|
+
* Events emitted:
|
|
342
|
+
* - server:added { server: ServerConfig, timestamp: number }
|
|
343
|
+
* - server:removed { name: string, timestamp: number }
|
|
344
|
+
* - registry:saved { path: string, timestamp: number }
|
|
345
|
+
* - registry:error { error: string, path: string }
|
|
346
|
+
*/
|
|
347
|
+
export class RegistryManager extends EventEmitter {
|
|
348
|
+
/**
|
|
349
|
+
* Get the default persistent registry path (~/.config/metalink/mcp-registry.json)
|
|
350
|
+
* Respects METALINK_CONFIG_DIR environment variable if set
|
|
351
|
+
*/
|
|
352
|
+
static getDefaultRegistryPath() {
|
|
353
|
+
const configDir = process.env.METALINK_CONFIG_DIR ||
|
|
354
|
+
path.join(process.env.HOME || process.env.USERPROFILE || '~', '.config', 'metalink');
|
|
355
|
+
return path.join(configDir, 'mcp-registry.json');
|
|
356
|
+
}
|
|
357
|
+
constructor(registryPath = '') {
|
|
358
|
+
super();
|
|
359
|
+
this.maxBackups = 3;
|
|
360
|
+
// Use persistent config dir by default if no path provided
|
|
361
|
+
const resolvedPath = registryPath || RegistryManager.getDefaultRegistryPath();
|
|
362
|
+
this.registryPath = path.resolve(resolvedPath);
|
|
363
|
+
this.lock = new FileLock(this.registryPath);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Read and parse the registry file.
|
|
367
|
+
* Does not require lock (read-only operation).
|
|
368
|
+
*/
|
|
369
|
+
async readRegistry() {
|
|
370
|
+
try {
|
|
371
|
+
const content = await fs.readFile(this.registryPath, 'utf8');
|
|
372
|
+
const parsed = JSON.parse(content);
|
|
373
|
+
if (!Array.isArray(parsed.servers)) {
|
|
374
|
+
throw new Error('Invalid registry format: servers must be an array');
|
|
375
|
+
}
|
|
376
|
+
return parsed;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (error.code === 'ENOENT') {
|
|
380
|
+
// Return empty registry if file doesn't exist yet
|
|
381
|
+
return { servers: [] };
|
|
382
|
+
}
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get all servers from registry.
|
|
388
|
+
*/
|
|
389
|
+
async getServers() {
|
|
390
|
+
const registry = await this.readRegistry();
|
|
391
|
+
return registry.servers;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get a single server by name.
|
|
395
|
+
*/
|
|
396
|
+
async getServer(name) {
|
|
397
|
+
const servers = await this.getServers();
|
|
398
|
+
return servers.find(s => s.name === name) || null;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Validate a server configuration.
|
|
402
|
+
* Returns { valid: true } or { valid: false, errors: [...] }
|
|
403
|
+
*
|
|
404
|
+
* SECURITY: Uses validateServerCommand() for comprehensive security checks.
|
|
405
|
+
*/
|
|
406
|
+
async validateServer(config, options = {}) {
|
|
407
|
+
// Validate schema
|
|
408
|
+
const schemaResult = ServerConfigSchema.safeParse(config);
|
|
409
|
+
if (!schemaResult.success) {
|
|
410
|
+
const errors = schemaResult.error.errors.map(e => {
|
|
411
|
+
return `${e.path.join('.')}: ${e.message}`;
|
|
412
|
+
});
|
|
413
|
+
return { valid: false, errors };
|
|
414
|
+
}
|
|
415
|
+
const server = schemaResult.data;
|
|
416
|
+
const errors = [];
|
|
417
|
+
// Validate command (only for stdio servers, and unless allowAnyCommand is true)
|
|
418
|
+
if (!options.allowAnyCommand) {
|
|
419
|
+
if (server.transport === 'stdio' || server.transport === undefined) {
|
|
420
|
+
// For stdio servers, validate the command using security-hardened function
|
|
421
|
+
const stdioServer = server;
|
|
422
|
+
// SECURITY: Use validateServerCommand for comprehensive checks
|
|
423
|
+
try {
|
|
424
|
+
validateServerCommand(stdioServer.command, stdioServer.args || [], server.name);
|
|
425
|
+
}
|
|
426
|
+
catch (validationError) {
|
|
427
|
+
errors.push(validationError instanceof Error
|
|
428
|
+
? validationError.message
|
|
429
|
+
: String(validationError));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// For HTTP servers, check if mcp-remote proxy is available
|
|
434
|
+
if (!ALLOWED_COMMANDS.includes('mcp-remote')) {
|
|
435
|
+
errors.push(`HTTP server '${server.name}' requires 'mcp-remote' proxy to be enabled`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Validate environment variables (no path traversal)
|
|
440
|
+
for (const [key, value] of Object.entries(server.env || {})) {
|
|
441
|
+
if (typeof value !== 'string') {
|
|
442
|
+
errors.push(`Environment variable '${key}' must be a string`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (value.includes('../') || value.includes('..\\')) {
|
|
446
|
+
errors.push(`Environment variable '${key}' contains path traversal`);
|
|
447
|
+
}
|
|
448
|
+
// SECURITY: Check for dangerous patterns in env values
|
|
449
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
450
|
+
if (pattern.test(value)) {
|
|
451
|
+
// Only warn for env vars (may have legitimate uses like URLs with &)
|
|
452
|
+
// But log it for security monitoring
|
|
453
|
+
console.warn(`[SECURITY] Server '${server.name}' env var '${key}' contains pattern that may indicate injection`);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Add a new server to the registry.
|
|
462
|
+
* Validates config, checks uniqueness, acquires lock, writes atomically.
|
|
463
|
+
*
|
|
464
|
+
* @throws Error if validation fails, server already exists, or write fails
|
|
465
|
+
*/
|
|
466
|
+
async addServer(config, options = {}) {
|
|
467
|
+
const { allowAnyCommand = false, timeout = 5000 } = options;
|
|
468
|
+
// Validate configuration
|
|
469
|
+
const validation = await this.validateServer(config, { allowAnyCommand });
|
|
470
|
+
if (!validation.valid) {
|
|
471
|
+
const errorMsg = validation.errors.join('; ');
|
|
472
|
+
throw new Error(`Server validation failed: ${errorMsg}`);
|
|
473
|
+
}
|
|
474
|
+
const server = ServerConfigSchema.parse(config);
|
|
475
|
+
// Acquire lock
|
|
476
|
+
await this.lock.acquire(timeout);
|
|
477
|
+
try {
|
|
478
|
+
// Read current registry (double-check uniqueness under lock)
|
|
479
|
+
const registry = await this.readRegistry();
|
|
480
|
+
if (registry.servers.some(s => s.name === server.name)) {
|
|
481
|
+
throw new Error(`Server '${server.name}' already exists in registry`);
|
|
482
|
+
}
|
|
483
|
+
// Add server
|
|
484
|
+
registry.servers.push(server);
|
|
485
|
+
// Write atomically with backup
|
|
486
|
+
await this.writeRegistry(registry);
|
|
487
|
+
// Emit event
|
|
488
|
+
this.emit('server:added', {
|
|
489
|
+
server,
|
|
490
|
+
timestamp: Date.now(),
|
|
491
|
+
});
|
|
492
|
+
return server;
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
await this.lock.release();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Remove a server from the registry by name.
|
|
500
|
+
* Acquires lock, removes server, writes atomically.
|
|
501
|
+
*
|
|
502
|
+
* @throws Error if server not found or write fails
|
|
503
|
+
*/
|
|
504
|
+
async removeServer(name, options = {}) {
|
|
505
|
+
const { timeout = 5000 } = options;
|
|
506
|
+
// Acquire lock
|
|
507
|
+
await this.lock.acquire(timeout);
|
|
508
|
+
try {
|
|
509
|
+
// Read current registry
|
|
510
|
+
const registry = await this.readRegistry();
|
|
511
|
+
const initialCount = registry.servers.length;
|
|
512
|
+
// Remove server
|
|
513
|
+
registry.servers = registry.servers.filter(s => s.name !== name);
|
|
514
|
+
// Check if server was found
|
|
515
|
+
if (registry.servers.length === initialCount) {
|
|
516
|
+
throw new Error(`Server '${name}' not found in registry`);
|
|
517
|
+
}
|
|
518
|
+
// Write atomically with backup
|
|
519
|
+
await this.writeRegistry(registry);
|
|
520
|
+
// Emit event
|
|
521
|
+
this.emit('server:removed', {
|
|
522
|
+
name,
|
|
523
|
+
timestamp: Date.now(),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
await this.lock.release();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Update an existing server configuration.
|
|
532
|
+
* Validates config, replaces server, writes atomically.
|
|
533
|
+
*/
|
|
534
|
+
async updateServer(name, config, options = {}) {
|
|
535
|
+
const { allowAnyCommand = false, timeout = 5000 } = options;
|
|
536
|
+
// Validate new configuration
|
|
537
|
+
const validation = await this.validateServer(config, { allowAnyCommand });
|
|
538
|
+
if (!validation.valid) {
|
|
539
|
+
const errorMsg = validation.errors.join('; ');
|
|
540
|
+
throw new Error(`Server validation failed: ${errorMsg}`);
|
|
541
|
+
}
|
|
542
|
+
const newConfig = ServerConfigSchema.parse(config);
|
|
543
|
+
// Acquire lock
|
|
544
|
+
await this.lock.acquire(timeout);
|
|
545
|
+
try {
|
|
546
|
+
// Read current registry
|
|
547
|
+
const registry = await this.readRegistry();
|
|
548
|
+
const serverIndex = registry.servers.findIndex(s => s.name === name);
|
|
549
|
+
if (serverIndex === -1) {
|
|
550
|
+
throw new Error(`Server '${name}' not found in registry`);
|
|
551
|
+
}
|
|
552
|
+
// Replace server (name must match)
|
|
553
|
+
if (newConfig.name !== name) {
|
|
554
|
+
throw new Error(`Cannot change server name from '${name}' to '${newConfig.name}'`);
|
|
555
|
+
}
|
|
556
|
+
registry.servers[serverIndex] = newConfig;
|
|
557
|
+
// Write atomically with backup
|
|
558
|
+
await this.writeRegistry(registry);
|
|
559
|
+
// Emit event
|
|
560
|
+
this.emit('server:updated', {
|
|
561
|
+
server: newConfig,
|
|
562
|
+
timestamp: Date.now(),
|
|
563
|
+
});
|
|
564
|
+
return newConfig;
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
await this.lock.release();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Internal method to write registry with backup and rollback on failure.
|
|
572
|
+
*/
|
|
573
|
+
async writeRegistry(registry) {
|
|
574
|
+
const content = JSON.stringify(registry, null, 2);
|
|
575
|
+
try {
|
|
576
|
+
// Create backup before write
|
|
577
|
+
await this.createBackup();
|
|
578
|
+
// Atomic write with temp file + rename
|
|
579
|
+
const dir = path.dirname(this.registryPath);
|
|
580
|
+
const tempPath = path.join(dir, `.registry.tmp.${Date.now()}`);
|
|
581
|
+
try {
|
|
582
|
+
// Write to temp file
|
|
583
|
+
await fs.writeFile(tempPath, content, 'utf8');
|
|
584
|
+
// Atomic rename
|
|
585
|
+
await fs.rename(tempPath, this.registryPath);
|
|
586
|
+
// Clean up old backups
|
|
587
|
+
await this.rotateBackups();
|
|
588
|
+
// Emit success event
|
|
589
|
+
this.emit('registry:saved', {
|
|
590
|
+
path: this.registryPath,
|
|
591
|
+
timestamp: Date.now(),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
// Cleanup temp file on error
|
|
596
|
+
try {
|
|
597
|
+
await fs.unlink(tempPath);
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
// Ignore cleanup errors
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
// Emit error event
|
|
607
|
+
this.emit('registry:error', {
|
|
608
|
+
error: error.message,
|
|
609
|
+
path: this.registryPath,
|
|
610
|
+
timestamp: Date.now(),
|
|
611
|
+
});
|
|
612
|
+
throw error;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Create a backup of the current registry.
|
|
617
|
+
* Backups are named: registry.json.backup, registry.json.backup.1, etc.
|
|
618
|
+
*/
|
|
619
|
+
async createBackup() {
|
|
620
|
+
try {
|
|
621
|
+
const backupPath = `${this.registryPath}.backup`;
|
|
622
|
+
await fs.copyFile(this.registryPath, backupPath);
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
if (error.code !== 'ENOENT') {
|
|
626
|
+
// Only log, don't throw - backup failure shouldn't block writes
|
|
627
|
+
console.warn('Failed to create backup:', error);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Rotate backups to keep only the last N backups.
|
|
633
|
+
* Keeps: .backup, .backup.1, .backup.2 (maxBackups = 3)
|
|
634
|
+
*/
|
|
635
|
+
async rotateBackups() {
|
|
636
|
+
const dir = path.dirname(this.registryPath);
|
|
637
|
+
const filename = path.basename(this.registryPath);
|
|
638
|
+
try {
|
|
639
|
+
// Remove oldest backup if we've exceeded max
|
|
640
|
+
const oldestBackup = `${this.registryPath}.backup.${this.maxBackups}`;
|
|
641
|
+
try {
|
|
642
|
+
await fs.unlink(oldestBackup);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
if (error.code !== 'ENOENT') {
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Shift backups: .backup.1 → .backup.2, .backup → .backup.1
|
|
650
|
+
for (let i = this.maxBackups - 1; i >= 1; i--) {
|
|
651
|
+
const oldPath = `${this.registryPath}.backup${i === 1 ? '' : '.' + (i - 1)}`;
|
|
652
|
+
const newPath = `${this.registryPath}.backup.${i}`;
|
|
653
|
+
try {
|
|
654
|
+
await fs.rename(oldPath, newPath);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
if (error.code !== 'ENOENT') {
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
console.warn('Failed to rotate backups:', error);
|
|
665
|
+
// Don't throw - backup rotation failure shouldn't block writes
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Restore registry from backup.
|
|
670
|
+
* Useful for rollback on catastrophic failure.
|
|
671
|
+
*/
|
|
672
|
+
async restoreFromBackup(backupIndex = 0) {
|
|
673
|
+
const backupPath = backupIndex === 0
|
|
674
|
+
? `${this.registryPath}.backup`
|
|
675
|
+
: `${this.registryPath}.backup.${backupIndex}`;
|
|
676
|
+
try {
|
|
677
|
+
await fs.copyFile(backupPath, this.registryPath);
|
|
678
|
+
this.emit('registry:restored', {
|
|
679
|
+
from: backupPath,
|
|
680
|
+
to: this.registryPath,
|
|
681
|
+
timestamp: Date.now(),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (error.code === 'ENOENT') {
|
|
686
|
+
throw new Error(`Backup not found: ${backupPath}`);
|
|
687
|
+
}
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Get backup file info for monitoring/debugging.
|
|
693
|
+
*/
|
|
694
|
+
async getBackupInfo() {
|
|
695
|
+
const backups = [];
|
|
696
|
+
for (let i = 0; i < this.maxBackups; i++) {
|
|
697
|
+
const backupPath = i === 0
|
|
698
|
+
? `${this.registryPath}.backup`
|
|
699
|
+
: `${this.registryPath}.backup.${i}`;
|
|
700
|
+
try {
|
|
701
|
+
const stat = await fs.stat(backupPath);
|
|
702
|
+
backups.push({
|
|
703
|
+
path: backupPath,
|
|
704
|
+
size: stat.size,
|
|
705
|
+
modified: stat.mtime,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
if (error.code !== 'ENOENT') {
|
|
710
|
+
console.warn(`Failed to stat backup ${backupPath}:`, error);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return backups;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Get registry file info.
|
|
718
|
+
*/
|
|
719
|
+
async getRegistryInfo() {
|
|
720
|
+
try {
|
|
721
|
+
const stat = await fs.stat(this.registryPath);
|
|
722
|
+
return {
|
|
723
|
+
path: this.registryPath,
|
|
724
|
+
size: stat.size,
|
|
725
|
+
exists: true,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
if (error.code === 'ENOENT') {
|
|
730
|
+
return {
|
|
731
|
+
path: this.registryPath,
|
|
732
|
+
size: 0,
|
|
733
|
+
exists: false,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Singleton instance for global registry management.
|
|
742
|
+
* Usage: import { registryManager } from './registry';
|
|
743
|
+
*/
|
|
744
|
+
let globalRegistry = null;
|
|
745
|
+
export function getRegistryManager(registryPath) {
|
|
746
|
+
if (!globalRegistry) {
|
|
747
|
+
globalRegistry = new RegistryManager(registryPath);
|
|
748
|
+
}
|
|
749
|
+
return globalRegistry;
|
|
750
|
+
}
|
|
751
|
+
export function resetRegistryManager() {
|
|
752
|
+
globalRegistry = null;
|
|
753
|
+
}
|
|
754
|
+
//# sourceMappingURL=registry.js.map
|