@cyanheads/mcp-ts-core 0.13.6 → 0.13.8
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/AGENTS.md +5 -5
- package/CLAUDE.md +5 -5
- package/README.md +57 -52
- package/biome.json +1 -1
- package/changelog/0.13.x/0.13.7.md +77 -0
- package/changelog/0.13.x/0.13.8.md +101 -0
- package/config/tsconfig.base.json +2 -2
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +61 -11
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +35 -6
- package/dist/core/app.js.map +1 -1
- package/dist/core/context.d.ts +9 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +17 -16
- package/dist/core/context.js.map +1 -1
- package/dist/core/worker.d.ts +2 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +9 -1
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/enrichment-rules.d.ts +3 -2
- package/dist/linter/rules/enrichment-rules.d.ts.map +1 -1
- package/dist/linter/rules/enrichment-rules.js +9 -2
- package/dist/linter/rules/enrichment-rules.js.map +1 -1
- package/dist/linter/rules/handler-body-rules.d.ts.map +1 -1
- package/dist/linter/rules/handler-body-rules.js +10 -4
- package/dist/linter/rules/handler-body-rules.js.map +1 -1
- package/dist/linter/rules/schema-rules.d.ts +5 -0
- package/dist/linter/rules/schema-rules.d.ts.map +1 -1
- package/dist/linter/rules/schema-rules.js +44 -17
- package/dist/linter/rules/schema-rules.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +6 -0
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +3 -0
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/outputContract.d.ts +33 -0
- package/dist/mcp-server/outputContract.d.ts.map +1 -0
- package/dist/mcp-server/outputContract.js +43 -0
- package/dist/mcp-server/outputContract.js.map +1 -0
- package/dist/mcp-server/prompts/prompt-registration.d.ts.map +1 -1
- package/dist/mcp-server/prompts/prompt-registration.js +6 -3
- package/dist/mcp-server/prompts/prompt-registration.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +10 -2
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +16 -5
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +70 -14
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js +4 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +1 -1
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +2 -5
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +15 -5
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/mcp-server/transports/http/sessionStore.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/sessionStore.js +2 -2
- package/dist/mcp-server/transports/http/sessionStore.js.map +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js +1 -1
- package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
- package/dist/services/canvas/core/DataCanvas.d.ts.map +1 -1
- package/dist/services/canvas/core/DataCanvas.js +7 -5
- package/dist/services/canvas/core/DataCanvas.js.map +1 -1
- package/dist/services/canvas/core/canvasFactory.d.ts.map +1 -1
- package/dist/services/canvas/core/canvasFactory.js +2 -2
- package/dist/services/canvas/core/canvasFactory.js.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +25 -16
- package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
- package/dist/services/llm/providers/openrouter.provider.js +1 -1
- package/dist/services/llm/providers/openrouter.provider.js.map +1 -1
- package/dist/services/speech/providers/elevenlabs.provider.js +3 -3
- package/dist/services/speech/providers/elevenlabs.provider.js.map +1 -1
- package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
- package/dist/services/speech/providers/whisper.provider.js +5 -5
- package/dist/services/speech/providers/whisper.provider.js.map +1 -1
- package/dist/storage/core/IStorageProvider.d.ts +5 -2
- package/dist/storage/core/IStorageProvider.d.ts.map +1 -1
- package/dist/storage/core/StorageService.d.ts.map +1 -1
- package/dist/storage/core/StorageService.js +3 -6
- package/dist/storage/core/StorageService.js.map +1 -1
- package/dist/storage/core/providerHelpers.d.ts +29 -8
- package/dist/storage/core/providerHelpers.d.ts.map +1 -1
- package/dist/storage/core/providerHelpers.js +49 -11
- package/dist/storage/core/providerHelpers.js.map +1 -1
- package/dist/storage/core/storageFactory.d.ts.map +1 -1
- package/dist/storage/core/storageFactory.js +12 -15
- package/dist/storage/core/storageFactory.js.map +1 -1
- package/dist/storage/core/storageValidation.d.ts +13 -13
- package/dist/storage/core/storageValidation.d.ts.map +1 -1
- package/dist/storage/core/storageValidation.js +49 -125
- package/dist/storage/core/storageValidation.js.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/d1Provider.js +9 -7
- package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.d.ts +2 -0
- package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/kvProvider.js +12 -10
- package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
- package/dist/storage/providers/cloudflare/r2Provider.js +11 -8
- package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts +1 -0
- package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
- package/dist/storage/providers/fileSystem/fileSystemProvider.js +14 -12
- package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts +6 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
- package/dist/storage/providers/inMemory/inMemoryProvider.js +15 -10
- package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.d.ts.map +1 -1
- package/dist/storage/providers/supabase/supabaseProvider.js +5 -1
- package/dist/storage/providers/supabase/supabaseProvider.js.map +1 -1
- package/dist/testing/fuzz.d.ts.map +1 -1
- package/dist/testing/fuzz.js +7 -1
- package/dist/testing/fuzz.js.map +1 -1
- package/dist/testing/index.d.ts +21 -6
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +57 -10
- package/dist/testing/index.js.map +1 -1
- package/dist/types-global/errors.d.ts +7 -4
- package/dist/types-global/errors.d.ts.map +1 -1
- package/dist/types-global/errors.js.map +1 -1
- package/dist/utils/formatting/codeSpan.d.ts +27 -0
- package/dist/utils/formatting/codeSpan.d.ts.map +1 -0
- package/dist/utils/formatting/codeSpan.js +42 -0
- package/dist/utils/formatting/codeSpan.js.map +1 -0
- package/dist/utils/formatting/diffFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/diffFormatter.js +7 -15
- package/dist/utils/formatting/diffFormatter.js.map +1 -1
- package/dist/utils/formatting/markdownBuilder.d.ts +12 -5
- package/dist/utils/formatting/markdownBuilder.d.ts.map +1 -1
- package/dist/utils/formatting/markdownBuilder.js +14 -2
- package/dist/utils/formatting/markdownBuilder.js.map +1 -1
- package/dist/utils/formatting/partialResult.d.ts +28 -2
- package/dist/utils/formatting/partialResult.d.ts.map +1 -1
- package/dist/utils/formatting/partialResult.js +46 -2
- package/dist/utils/formatting/partialResult.js.map +1 -1
- package/dist/utils/formatting/tableFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/tableFormatter.js +5 -9
- package/dist/utils/formatting/tableFormatter.js.map +1 -1
- package/dist/utils/formatting/treeFormatter.d.ts.map +1 -1
- package/dist/utils/formatting/treeFormatter.js +5 -9
- package/dist/utils/formatting/treeFormatter.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +21 -8
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +70 -38
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/mappings.d.ts +18 -1
- package/dist/utils/internal/error-handler/mappings.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/mappings.js +23 -1
- package/dist/utils/internal/error-handler/mappings.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +2 -0
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/logger.d.ts +75 -3
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +181 -52
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/performance.d.ts +16 -1
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +59 -20
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/network/fetchWithTimeout.d.ts +11 -5
- package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
- package/dist/utils/network/fetchWithTimeout.js +50 -23
- package/dist/utils/network/fetchWithTimeout.js.map +1 -1
- package/dist/utils/network/retry.d.ts +16 -8
- package/dist/utils/network/retry.d.ts.map +1 -1
- package/dist/utils/network/retry.js +19 -8
- package/dist/utils/network/retry.js.map +1 -1
- package/dist/utils/overflow/outlineOnOverflow.d.ts +18 -2
- package/dist/utils/overflow/outlineOnOverflow.d.ts.map +1 -1
- package/dist/utils/overflow/outlineOnOverflow.js +28 -3
- package/dist/utils/overflow/outlineOnOverflow.js.map +1 -1
- package/dist/utils/pagination/pagination.d.ts +3 -1
- package/dist/utils/pagination/pagination.d.ts.map +1 -1
- package/dist/utils/pagination/pagination.js +10 -2
- package/dist/utils/pagination/pagination.js.map +1 -1
- package/dist/utils/parsing/csvParser.d.ts.map +1 -1
- package/dist/utils/parsing/csvParser.js +4 -2
- package/dist/utils/parsing/csvParser.js.map +1 -1
- package/dist/utils/parsing/htmlExtractor.js +1 -1
- package/dist/utils/parsing/htmlExtractor.js.map +1 -1
- package/dist/utils/parsing/jsonParser.d.ts.map +1 -1
- package/dist/utils/parsing/jsonParser.js +3 -1
- package/dist/utils/parsing/jsonParser.js.map +1 -1
- package/dist/utils/parsing/xmlParser.d.ts.map +1 -1
- package/dist/utils/parsing/xmlParser.js +3 -1
- package/dist/utils/parsing/xmlParser.js.map +1 -1
- package/dist/utils/parsing/yamlParser.d.ts.map +1 -1
- package/dist/utils/parsing/yamlParser.js +3 -1
- package/dist/utils/parsing/yamlParser.js.map +1 -1
- package/dist/utils/security/idGenerator.d.ts.map +1 -1
- package/dist/utils/security/idGenerator.js +20 -4
- package/dist/utils/security/idGenerator.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +46 -15
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +203 -96
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +16 -1
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +16 -1
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/dist/utils/telemetry/instrumentation.d.ts +13 -3
- package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/utils/telemetry/instrumentation.js +104 -17
- package/dist/utils/telemetry/instrumentation.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +12 -18
- package/framework-skills/add-prompt/SKILL.md +3 -1
- package/framework-skills/add-provider/SKILL.md +14 -4
- package/framework-skills/add-resource/SKILL.md +3 -3
- package/framework-skills/add-tool/SKILL.md +22 -7
- package/framework-skills/api-auth/SKILL.md +3 -1
- package/framework-skills/api-canvas/SKILL.md +4 -4
- package/framework-skills/api-config/SKILL.md +9 -5
- package/framework-skills/api-context/SKILL.md +10 -7
- package/framework-skills/api-errors/SKILL.md +20 -11
- package/framework-skills/api-linter/SKILL.md +14 -10
- package/framework-skills/api-telemetry/SKILL.md +38 -13
- package/framework-skills/api-testing/SKILL.md +25 -15
- package/framework-skills/api-utils/SKILL.md +10 -10
- package/framework-skills/api-utils/references/formatting.md +1 -1
- package/framework-skills/api-utils/references/parsing.md +2 -2
- package/framework-skills/api-utils/references/security.md +13 -10
- package/framework-skills/code-simplifier/SKILL.md +31 -18
- package/framework-skills/design-mcp-server/SKILL.md +62 -35
- package/framework-skills/git-wrapup/SKILL.md +19 -10
- package/framework-skills/maintenance/SKILL.md +3 -3
- package/framework-skills/orchestrations/SKILL.md +1 -1
- package/framework-skills/orchestrations/workflows/greenfield-build.md +15 -8
- package/framework-skills/polish-docs-meta/SKILL.md +2 -2
- package/framework-skills/polish-docs-meta/references/package-meta.md +1 -1
- package/framework-skills/polish-docs-meta/references/readme.md +4 -3
- package/framework-skills/release-and-publish/SKILL.md +7 -5
- package/framework-skills/release-pr-review/SKILL.md +18 -1
- package/framework-skills/report-issue-framework/SKILL.md +2 -2
- package/framework-skills/report-issue-local/SKILL.md +3 -3
- package/framework-skills/security-pass/SKILL.md +11 -3
- package/framework-skills/techniques/SKILL.md +1 -1
- package/framework-skills/techniques/references/outline-on-overflow.md +12 -7
- package/framework-skills/tool-defs-analysis/SKILL.md +3 -3
- package/package.json +30 -36
- package/scripts/check-skill-versions.ts +103 -22
- package/scripts/devcheck.ts +4 -3
- package/scripts/lint-packaging.ts +38 -1
- package/templates/.env.example +7 -1
- package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +1 -1
- package/templates/AGENTS.md +2 -2
- package/templates/CLAUDE.md +2 -2
- package/templates/Dockerfile +30 -10
- package/templates/package.json +4 -3
- package/templates/src/mcp-server/prompts/definitions/echo.prompt.ts +2 -4
- package/templates/src/mcp-server/resources/definitions/echo-app-ui.app-resource.ts +51 -14
- package/templates/src/mcp-server/resources/definitions/echo.resource.ts +1 -1
- package/templates/src/mcp-server/tools/definitions/echo-app.app-tool.ts +2 -3
- package/templates/src/mcp-server/tools/definitions/echo.tool.ts +1 -1
- package/dist/utils/telemetry/index.d.ts +0 -12
- package/dist/utils/telemetry/index.d.ts.map +0 -1
- package/dist/utils/telemetry/index.js +0 -12
- package/dist/utils/telemetry/index.js.map +0 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* and numbers, as well as redacting sensitive fields from data intended for logging.
|
|
5
5
|
*
|
|
6
6
|
* Several methods (`sanitizeHtml`, `sanitizeString`, `sanitizeUrl`, `sanitizeNumber`) are
|
|
7
|
-
* **async
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* **async**. HTML handling lazy-loads the optional `sanitize-html` peer on first use and throws
|
|
8
|
+
* a `McpError` with `JsonRpcErrorCode.ConfigurationError` when it is not installed; URL and
|
|
9
|
+
* number validation are built in.
|
|
10
10
|
*
|
|
11
11
|
* Path sanitization (`sanitizePath`) is synchronous but only available in Node.js environments.
|
|
12
12
|
*
|
|
@@ -208,22 +208,21 @@ export declare class Sanitization {
|
|
|
208
208
|
/**
|
|
209
209
|
* Sanitizes a string according to its intended usage context.
|
|
210
210
|
*
|
|
211
|
-
* This method is **async** because
|
|
212
|
-
* depending on the requested context.
|
|
211
|
+
* This method is **async** because the HTML-stripping contexts lazy-load `sanitize-html`.
|
|
213
212
|
*
|
|
214
213
|
* | `context` | Behavior |
|
|
215
214
|
* |----------------|----------|
|
|
216
215
|
* | `'text'` | Strips all HTML tags and attributes (default). |
|
|
217
216
|
* | `'html'` | Runs full HTML sanitization via `sanitizeHtml` (respects `allowedTags`/`allowedAttributes`). |
|
|
218
217
|
* | `'attribute'` | Strips all tags and attributes — safe for use inside an HTML attribute value. |
|
|
219
|
-
* | `'url'` | Validates the URL
|
|
218
|
+
* | `'url'` | Validates the URL as `sanitizeUrl` does (http/https only); returns `''` if invalid. |
|
|
220
219
|
* | `'javascript'` | **Disallowed.** Always throws `McpError`. |
|
|
221
220
|
*
|
|
222
221
|
* @param input - The string to sanitize. Returns `''` immediately if falsy.
|
|
223
222
|
* @param options - Context and optional allowlist overrides.
|
|
224
223
|
* @returns Promise resolving to the sanitized string, or `''` for invalid URLs.
|
|
225
224
|
* @throws {McpError} With `ValidationError` if `context` is `'javascript'`.
|
|
226
|
-
* @throws {McpError} With `ConfigurationError` if
|
|
225
|
+
* @throws {McpError} With `ConfigurationError` if `sanitize-html` is needed and not installed.
|
|
227
226
|
* @example
|
|
228
227
|
* ```ts
|
|
229
228
|
* await sanitization.sanitizeString('<b>hello</b>', { context: 'text' });
|
|
@@ -240,17 +239,19 @@ export declare class Sanitization {
|
|
|
240
239
|
/**
|
|
241
240
|
* Validates and sanitizes a URL string.
|
|
242
241
|
*
|
|
243
|
-
*
|
|
242
|
+
* Async for API stability; validation is built in and needs no peer dependency.
|
|
244
243
|
*
|
|
245
|
-
* Validation requires a protocol and host.
|
|
246
|
-
*
|
|
244
|
+
* Validation requires a protocol and host. Hosts may be domains, single-label names such as
|
|
245
|
+
* `localhost`, IPv4 dotted quads, or bracketed IPv6 literals. Whitespace, `<`, and `>` are
|
|
246
|
+
* rejected anywhere, as are URLs over 2084 characters. Even if a protocol appears in
|
|
247
|
+
* `allowedProtocols`, the pseudo-protocols `javascript:`, `data:`, and `vbscript:` are always
|
|
248
|
+
* rejected.
|
|
247
249
|
*
|
|
248
250
|
* @param input - The URL string to sanitize. Leading/trailing whitespace is trimmed.
|
|
249
251
|
* @param allowedProtocols - URL schemes that are permitted. Defaults to `['http', 'https']`.
|
|
250
252
|
* @returns Promise resolving to the trimmed, validated URL string.
|
|
251
253
|
* @throws {McpError} With `ValidationError` if the URL is invalid, uses a disallowed protocol,
|
|
252
254
|
* or uses a blocked pseudo-protocol (`javascript:`, `data:`, `vbscript:`).
|
|
253
|
-
* @throws {McpError} With `ConfigurationError` if `validator` is not installed.
|
|
254
255
|
* @example
|
|
255
256
|
* ```ts
|
|
256
257
|
* await sanitization.sanitizeUrl('https://example.com/path');
|
|
@@ -324,10 +325,10 @@ export declare class Sanitization {
|
|
|
324
325
|
/**
|
|
325
326
|
* Validates a numeric input and optionally clamps it to a range.
|
|
326
327
|
*
|
|
327
|
-
*
|
|
328
|
-
* dependency, which is lazy-loaded on first call. Numeric inputs bypass the lazy load.
|
|
328
|
+
* Async for API stability; validation is built in and needs no peer dependency.
|
|
329
329
|
*
|
|
330
|
-
* - String inputs: trimmed
|
|
330
|
+
* - String inputs: trimmed, required to be a plain decimal (optional sign and fraction, no
|
|
331
|
+
* exponent or separators), then parsed with `parseFloat`.
|
|
331
332
|
* - Number inputs: used directly.
|
|
332
333
|
* - `NaN` and `Infinity` are always rejected.
|
|
333
334
|
* - If `min` or `max` are provided, the value is silently clamped (a debug log is emitted).
|
|
@@ -337,7 +338,6 @@ export declare class Sanitization {
|
|
|
337
338
|
* @param max - Inclusive upper bound. If the value is above this, it is clamped to `max`.
|
|
338
339
|
* @returns Promise resolving to the validated (and potentially clamped) number.
|
|
339
340
|
* @throws {McpError} With `ValidationError` if the input is not numeric, is `NaN`, or is `Infinity`.
|
|
340
|
-
* @throws {McpError} With `ConfigurationError` if `validator` is not installed (string input only).
|
|
341
341
|
* @example
|
|
342
342
|
* ```ts
|
|
343
343
|
* await sanitization.sanitizeNumber('42.5');
|
|
@@ -378,6 +378,37 @@ export declare class Sanitization {
|
|
|
378
378
|
* ```
|
|
379
379
|
*/
|
|
380
380
|
sanitizeForLogging(input: unknown): unknown;
|
|
381
|
+
/**
|
|
382
|
+
* Redacts, serializes, and caps a value for a log field: {@link sanitizeForLogging},
|
|
383
|
+
* then `JSON.stringify`, then truncation to at most `maxBytes` UTF-8 bytes.
|
|
384
|
+
*
|
|
385
|
+
* Redaction runs first, so a cut can never keep the prefix of a secret the
|
|
386
|
+
* redactor would have replaced. The cut lands on a character boundary — never
|
|
387
|
+
* inside a multi-byte sequence or between the halves of a surrogate pair — so
|
|
388
|
+
* the result is always well-formed. It is a prefix of the whole serialization,
|
|
389
|
+
* so a truncated payload is no longer valid JSON; `truncated` says so.
|
|
390
|
+
*
|
|
391
|
+
* Returned as a string rather than an object because the logger drops values
|
|
392
|
+
* nested past its sanitize depth, which would silently strip the deep parts of
|
|
393
|
+
* a payload. A value `JSON.stringify` rejects (a `bigint`) yields
|
|
394
|
+
* `'[Log Serialization Failed]'` rather than throwing into the caller's path.
|
|
395
|
+
*
|
|
396
|
+
* Key-name redaction only: a secret inside a free-form string value (a query,
|
|
397
|
+
* a message) is serialized as-is.
|
|
398
|
+
*
|
|
399
|
+
* @param value - The value to serialize.
|
|
400
|
+
* @param maxBytes - Maximum UTF-8 byte length of `text`.
|
|
401
|
+
* @returns `text`, the serialized and possibly truncated payload, and `truncated`.
|
|
402
|
+
* @example
|
|
403
|
+
* ```ts
|
|
404
|
+
* sanitization.serializeForLogging({ query: 'q', apiKey: 'sk-1' }, 16_384);
|
|
405
|
+
* // => { text: '{"query":"q","apiKey":"[REDACTED]"}', truncated: false }
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
serializeForLogging(value: unknown, maxBytes: number): {
|
|
409
|
+
text: string;
|
|
410
|
+
truncated: boolean;
|
|
411
|
+
};
|
|
381
412
|
/**
|
|
382
413
|
* Recursively redacts sensitive fields in an object or array in place.
|
|
383
414
|
* @param obj - The object or array to redact.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitization.d.ts","sourceRoot":"","sources":["../../../src/utils/security/sanitization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,YAAY,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"sanitization.d.ts","sourceRoot":"","sources":["../../../src/utils/security/sanitization.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,13 +11,52 @@ async function loadSanitizeHtml() {
|
|
|
11
11
|
})).default;
|
|
12
12
|
return _sanitizeHtmlFn;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const URL_MAX_LENGTH = 2084;
|
|
15
|
+
const URL_FORBIDDEN_CHARS = /[\s<>]/;
|
|
16
|
+
const URL_AUTHORITY = /^[a-z][a-z0-9+.-]*:(?:\/\/)?([^/?#]*)/i;
|
|
17
|
+
const URL_HOST_LABEL = String.raw `[\p{L}\p{N}_](?:[\p{L}\p{M}\p{N}_-]*[\p{L}\p{M}\p{N}_])?`;
|
|
18
|
+
const URL_HOST = new RegExp(String.raw `^(?:${URL_HOST_LABEL}(?:\.${URL_HOST_LABEL})*\.?|\[[0-9a-f:.]+\])$`, 'iu');
|
|
19
|
+
const IPV4_HOST = /^\d+\.\d+\.\d+\.\d+$/;
|
|
20
|
+
/**
|
|
21
|
+
* Validates `input` as an absolute URL with a host, using a scheme from `protocols`.
|
|
22
|
+
*
|
|
23
|
+
* Rejects whitespace, `<`, or `>` anywhere, inputs over 2084 characters, and hosts outside
|
|
24
|
+
* letters, digits, `_`, and non-edge `-` labels (or a bracketed IPv6 literal). The host is
|
|
25
|
+
* checked as written and must match the host the WHATWG parser resolves, so inputs that read
|
|
26
|
+
* as one host but fetch another (a backslash before `@`, an obfuscated IPv4 form such as
|
|
27
|
+
* `2130706433` or `127.1`) are refused. Single-label hosts such as `localhost` are accepted.
|
|
28
|
+
*/
|
|
29
|
+
function isAllowedUrl(input, protocols) {
|
|
30
|
+
if (!input || input.length > URL_MAX_LENGTH || URL_FORBIDDEN_CHARS.test(input))
|
|
31
|
+
return false;
|
|
32
|
+
const url = URL.parse(input);
|
|
33
|
+
if (!url || !protocols.some((protocol) => `${protocol.toLowerCase()}:` === url.protocol)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const authority = URL_AUTHORITY.exec(input)?.[1] ?? '';
|
|
37
|
+
const host = authority.slice(authority.lastIndexOf('@') + 1).replace(/:\d*$/, '');
|
|
38
|
+
if (!URL_HOST.test(host))
|
|
39
|
+
return false;
|
|
40
|
+
// The host as written must be the host the parser resolved — `http://evil.com\@good.com`
|
|
41
|
+
// reads as good.com but parses (and fetches) as evil.com.
|
|
42
|
+
if (URL.parse(`${url.protocol}//${host}`)?.hostname !== url.hostname)
|
|
43
|
+
return false;
|
|
44
|
+
return !IPV4_HOST.test(url.hostname) || url.hostname === host;
|
|
20
45
|
}
|
|
46
|
+
/** A plain decimal: optional sign, optional fraction, no exponent or separators. */
|
|
47
|
+
const NUMERIC_STRING = /^[+-]?([0-9]*\.)?[0-9]+$/;
|
|
48
|
+
/** `data.reason` + recovery shared by every `sanitizeNumber` rejection. */
|
|
49
|
+
const INVALID_NUMBER = {
|
|
50
|
+
reason: 'invalid_number',
|
|
51
|
+
recovery: {
|
|
52
|
+
hint: 'Provide a finite decimal number, such as 42 or -3.5, without an exponent or thousands separators.',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const PATH_REJECTION_HINTS = {
|
|
56
|
+
invalid_path: 'Provide a non-empty path string that contains no null bytes.',
|
|
57
|
+
path_traversal: 'Provide a relative path that stays inside the permitted directory, with no `..` segments climbing out of it.',
|
|
58
|
+
absolute_path_disallowed: 'Provide a relative path instead of an absolute one.',
|
|
59
|
+
};
|
|
21
60
|
// Dynamically import 'path' only in Node.js environments.
|
|
22
61
|
// Top-level await ensures the module is loaded before any sanitizePath call.
|
|
23
62
|
let pathModule;
|
|
@@ -253,22 +292,21 @@ export class Sanitization {
|
|
|
253
292
|
/**
|
|
254
293
|
* Sanitizes a string according to its intended usage context.
|
|
255
294
|
*
|
|
256
|
-
* This method is **async** because
|
|
257
|
-
* depending on the requested context.
|
|
295
|
+
* This method is **async** because the HTML-stripping contexts lazy-load `sanitize-html`.
|
|
258
296
|
*
|
|
259
297
|
* | `context` | Behavior |
|
|
260
298
|
* |----------------|----------|
|
|
261
299
|
* | `'text'` | Strips all HTML tags and attributes (default). |
|
|
262
300
|
* | `'html'` | Runs full HTML sanitization via `sanitizeHtml` (respects `allowedTags`/`allowedAttributes`). |
|
|
263
301
|
* | `'attribute'` | Strips all tags and attributes — safe for use inside an HTML attribute value. |
|
|
264
|
-
* | `'url'` | Validates the URL
|
|
302
|
+
* | `'url'` | Validates the URL as `sanitizeUrl` does (http/https only); returns `''` if invalid. |
|
|
265
303
|
* | `'javascript'` | **Disallowed.** Always throws `McpError`. |
|
|
266
304
|
*
|
|
267
305
|
* @param input - The string to sanitize. Returns `''` immediately if falsy.
|
|
268
306
|
* @param options - Context and optional allowlist overrides.
|
|
269
307
|
* @returns Promise resolving to the sanitized string, or `''` for invalid URLs.
|
|
270
308
|
* @throws {McpError} With `ValidationError` if `context` is `'javascript'`.
|
|
271
|
-
* @throws {McpError} With `ConfigurationError` if
|
|
309
|
+
* @throws {McpError} With `ConfigurationError` if `sanitize-html` is needed and not installed.
|
|
272
310
|
* @example
|
|
273
311
|
* ```ts
|
|
274
312
|
* await sanitization.sanitizeString('<b>hello</b>', { context: 'text' });
|
|
@@ -301,26 +339,21 @@ export class Sanitization {
|
|
|
301
339
|
return sanitizeHtmlFn(input, { allowedTags: [], allowedAttributes: {} });
|
|
302
340
|
}
|
|
303
341
|
case 'url': {
|
|
304
|
-
|
|
305
|
-
if (!v.isURL(input, {
|
|
306
|
-
protocols: ['http', 'https'],
|
|
307
|
-
require_protocol: true,
|
|
308
|
-
require_host: true,
|
|
309
|
-
})) {
|
|
342
|
+
if (!isAllowedUrl(input, ['http', 'https'])) {
|
|
310
343
|
logger.warning('Potentially invalid URL detected during string sanitization (context: url)', requestContextService.createRequestContext({
|
|
311
344
|
operation: 'Sanitization.sanitizeString.urlWarning',
|
|
312
345
|
additionalContext: { invalidUrlAttempt: input },
|
|
313
346
|
}));
|
|
314
347
|
return '';
|
|
315
348
|
}
|
|
316
|
-
return
|
|
349
|
+
return input.trim();
|
|
317
350
|
}
|
|
318
351
|
case 'javascript':
|
|
319
352
|
logger.error('Attempted JavaScript sanitization via sanitizeString, which is disallowed.', requestContextService.createRequestContext({
|
|
320
353
|
operation: 'Sanitization.sanitizeString.jsAttempt',
|
|
321
354
|
additionalContext: { inputSnippet: input.substring(0, 50) },
|
|
322
355
|
}));
|
|
323
|
-
throw validationError('JavaScript sanitization is not supported through sanitizeString due to security risks.');
|
|
356
|
+
throw validationError('JavaScript sanitization is not supported through sanitizeString due to security risks.', { reason: 'unsupported_sanitize_context' });
|
|
324
357
|
default: {
|
|
325
358
|
const sanitizeHtmlFn = await loadSanitizeHtml();
|
|
326
359
|
return sanitizeHtmlFn(input, { allowedTags: [], allowedAttributes: {} });
|
|
@@ -330,17 +363,19 @@ export class Sanitization {
|
|
|
330
363
|
/**
|
|
331
364
|
* Validates and sanitizes a URL string.
|
|
332
365
|
*
|
|
333
|
-
*
|
|
366
|
+
* Async for API stability; validation is built in and needs no peer dependency.
|
|
334
367
|
*
|
|
335
|
-
* Validation requires a protocol and host.
|
|
336
|
-
*
|
|
368
|
+
* Validation requires a protocol and host. Hosts may be domains, single-label names such as
|
|
369
|
+
* `localhost`, IPv4 dotted quads, or bracketed IPv6 literals. Whitespace, `<`, and `>` are
|
|
370
|
+
* rejected anywhere, as are URLs over 2084 characters. Even if a protocol appears in
|
|
371
|
+
* `allowedProtocols`, the pseudo-protocols `javascript:`, `data:`, and `vbscript:` are always
|
|
372
|
+
* rejected.
|
|
337
373
|
*
|
|
338
374
|
* @param input - The URL string to sanitize. Leading/trailing whitespace is trimmed.
|
|
339
375
|
* @param allowedProtocols - URL schemes that are permitted. Defaults to `['http', 'https']`.
|
|
340
376
|
* @returns Promise resolving to the trimmed, validated URL string.
|
|
341
377
|
* @throws {McpError} With `ValidationError` if the URL is invalid, uses a disallowed protocol,
|
|
342
378
|
* or uses a blocked pseudo-protocol (`javascript:`, `data:`, `vbscript:`).
|
|
343
|
-
* @throws {McpError} With `ConfigurationError` if `validator` is not installed.
|
|
344
379
|
* @example
|
|
345
380
|
* ```ts
|
|
346
381
|
* await sanitization.sanitizeUrl('https://example.com/path');
|
|
@@ -353,28 +388,31 @@ export class Sanitization {
|
|
|
353
388
|
* // throws McpError (ValidationError)
|
|
354
389
|
* ```
|
|
355
390
|
*/
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
391
|
+
sanitizeUrl(input, allowedProtocols = ['http', 'https']) {
|
|
392
|
+
return Promise.try(() => {
|
|
393
|
+
try {
|
|
394
|
+
const trimmedInput = input.trim();
|
|
395
|
+
if (!isAllowedUrl(trimmedInput, allowedProtocols)) {
|
|
396
|
+
throw new Error('Invalid URL format or protocol not in allowed list.');
|
|
397
|
+
}
|
|
398
|
+
const lowercasedInput = trimmedInput.toLowerCase();
|
|
399
|
+
if (lowercasedInput.startsWith('javascript:') ||
|
|
400
|
+
lowercasedInput.startsWith('data:') ||
|
|
401
|
+
lowercasedInput.startsWith('vbscript:')) {
|
|
402
|
+
throw new Error('Disallowed pseudo-protocol (javascript:, data:, or vbscript:) in URL.');
|
|
403
|
+
}
|
|
404
|
+
return trimmedInput;
|
|
366
405
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
406
|
+
catch (error) {
|
|
407
|
+
throw validationError(error instanceof Error ? error.message : 'Invalid or unsafe URL provided.', {
|
|
408
|
+
input,
|
|
409
|
+
reason: 'invalid_url',
|
|
410
|
+
recovery: {
|
|
411
|
+
hint: `Provide an absolute URL with a host and no whitespace or angle brackets, using one of these schemes: ${allowedProtocols.join(', ')}.`,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
372
414
|
}
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
throw validationError(error instanceof Error ? error.message : 'Invalid or unsafe URL provided.', { input });
|
|
377
|
-
}
|
|
415
|
+
});
|
|
378
416
|
}
|
|
379
417
|
/**
|
|
380
418
|
* Sanitizes a file path, preventing path traversal attacks and normalizing format.
|
|
@@ -418,11 +456,16 @@ export class Sanitization {
|
|
|
418
456
|
...(resolvedRootDir && { rootDir: resolvedRootDir }),
|
|
419
457
|
};
|
|
420
458
|
let wasAbsoluteInitially = false;
|
|
459
|
+
const rejectPath = (message, reason) => validationError(message, {
|
|
460
|
+
input: originalInput,
|
|
461
|
+
reason,
|
|
462
|
+
recovery: { hint: PATH_REJECTION_HINTS[reason] },
|
|
463
|
+
});
|
|
421
464
|
try {
|
|
422
465
|
if (!input || typeof input !== 'string')
|
|
423
|
-
throw
|
|
466
|
+
throw rejectPath('Invalid path input: must be a non-empty string.', 'invalid_path');
|
|
424
467
|
if (input.includes('\0'))
|
|
425
|
-
throw
|
|
468
|
+
throw rejectPath('Path contains null byte, which is disallowed.', 'invalid_path');
|
|
426
469
|
let normalized = path.normalize(input);
|
|
427
470
|
wasAbsoluteInitially = path.isAbsolute(normalized);
|
|
428
471
|
if (effectiveOptions.toPosix) {
|
|
@@ -432,18 +475,18 @@ export class Sanitization {
|
|
|
432
475
|
if (resolvedRootDir) {
|
|
433
476
|
const fullPath = path.resolve(resolvedRootDir, normalized);
|
|
434
477
|
if (!fullPath.startsWith(resolvedRootDir + path.sep) && fullPath !== resolvedRootDir) {
|
|
435
|
-
throw
|
|
478
|
+
throw rejectPath('Path traversal detected: attempts to escape the defined root directory.', 'path_traversal');
|
|
436
479
|
}
|
|
437
480
|
finalSanitizedPath = path.relative(resolvedRootDir, fullPath);
|
|
438
481
|
finalSanitizedPath = finalSanitizedPath === '' ? '.' : finalSanitizedPath;
|
|
439
482
|
if (path.isAbsolute(finalSanitizedPath) && !effectiveOptions.allowAbsolute) {
|
|
440
|
-
throw
|
|
483
|
+
throw rejectPath('Path resolved to absolute outside root when absolute paths are disallowed.', 'path_traversal');
|
|
441
484
|
}
|
|
442
485
|
}
|
|
443
486
|
else {
|
|
444
487
|
if (path.isAbsolute(normalized)) {
|
|
445
488
|
if (!effectiveOptions.allowAbsolute) {
|
|
446
|
-
throw
|
|
489
|
+
throw rejectPath('Absolute paths are disallowed by current options.', 'absolute_path_disallowed');
|
|
447
490
|
}
|
|
448
491
|
else {
|
|
449
492
|
finalSanitizedPath = normalized;
|
|
@@ -454,7 +497,7 @@ export class Sanitization {
|
|
|
454
497
|
const currentWorkingDir = path.resolve('.');
|
|
455
498
|
if (!resolvedAgainstCwd.startsWith(currentWorkingDir + path.sep) &&
|
|
456
499
|
resolvedAgainstCwd !== currentWorkingDir) {
|
|
457
|
-
throw
|
|
500
|
+
throw rejectPath('Relative path traversal detected (escapes current working directory context).', 'path_traversal');
|
|
458
501
|
}
|
|
459
502
|
finalSanitizedPath = normalized;
|
|
460
503
|
}
|
|
@@ -478,7 +521,9 @@ export class Sanitization {
|
|
|
478
521
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
479
522
|
},
|
|
480
523
|
}));
|
|
481
|
-
|
|
524
|
+
if (error instanceof McpError)
|
|
525
|
+
throw error;
|
|
526
|
+
throw rejectPath(error instanceof Error ? error.message : 'Invalid or unsafe path provided.', 'invalid_path');
|
|
482
527
|
}
|
|
483
528
|
}
|
|
484
529
|
/**
|
|
@@ -525,6 +570,8 @@ export class Sanitization {
|
|
|
525
570
|
throw validationError(`JSON string exceeds maximum allowed size of ${maxSize} bytes.`, {
|
|
526
571
|
actualSize: computeBytes(input),
|
|
527
572
|
maxSize,
|
|
573
|
+
reason: 'json_too_large',
|
|
574
|
+
recovery: { hint: `Send a JSON payload of at most ${maxSize} bytes.` },
|
|
528
575
|
});
|
|
529
576
|
}
|
|
530
577
|
return JSON.parse(input);
|
|
@@ -534,16 +581,18 @@ export class Sanitization {
|
|
|
534
581
|
throw error;
|
|
535
582
|
throw validationError(error instanceof Error ? error.message : 'Invalid JSON format.', {
|
|
536
583
|
inputPreview: input.length > 100 ? `${input.substring(0, 100)}...` : input,
|
|
584
|
+
reason: 'invalid_json',
|
|
585
|
+
recovery: { hint: 'Send the value as a well-formed JSON string.' },
|
|
537
586
|
});
|
|
538
587
|
}
|
|
539
588
|
}
|
|
540
589
|
/**
|
|
541
590
|
* Validates a numeric input and optionally clamps it to a range.
|
|
542
591
|
*
|
|
543
|
-
*
|
|
544
|
-
* dependency, which is lazy-loaded on first call. Numeric inputs bypass the lazy load.
|
|
592
|
+
* Async for API stability; validation is built in and needs no peer dependency.
|
|
545
593
|
*
|
|
546
|
-
* - String inputs: trimmed
|
|
594
|
+
* - String inputs: trimmed, required to be a plain decimal (optional sign and fraction, no
|
|
595
|
+
* exponent or separators), then parsed with `parseFloat`.
|
|
547
596
|
* - Number inputs: used directly.
|
|
548
597
|
* - `NaN` and `Infinity` are always rejected.
|
|
549
598
|
* - If `min` or `max` are provided, the value is silently clamped (a debug log is emitted).
|
|
@@ -553,7 +602,6 @@ export class Sanitization {
|
|
|
553
602
|
* @param max - Inclusive upper bound. If the value is above this, it is clamped to `max`.
|
|
554
603
|
* @returns Promise resolving to the validated (and potentially clamped) number.
|
|
555
604
|
* @throws {McpError} With `ValidationError` if the input is not numeric, is `NaN`, or is `Infinity`.
|
|
556
|
-
* @throws {McpError} With `ConfigurationError` if `validator` is not installed (string input only).
|
|
557
605
|
* @example
|
|
558
606
|
* ```ts
|
|
559
607
|
* await sanitization.sanitizeNumber('42.5');
|
|
@@ -566,50 +614,58 @@ export class Sanitization {
|
|
|
566
614
|
* // throws McpError (ValidationError)
|
|
567
615
|
* ```
|
|
568
616
|
*/
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
617
|
+
sanitizeNumber(input, min, max) {
|
|
618
|
+
return Promise.try(() => {
|
|
619
|
+
let value;
|
|
620
|
+
if (typeof input === 'string') {
|
|
621
|
+
const trimmedInput = input.trim();
|
|
622
|
+
if (!NUMERIC_STRING.test(trimmedInput)) {
|
|
623
|
+
throw validationError('Invalid number format: input is empty or not numeric.', {
|
|
624
|
+
input,
|
|
625
|
+
...INVALID_NUMBER,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
value = parseFloat(trimmedInput);
|
|
576
629
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
630
|
+
else if (typeof input === 'number') {
|
|
631
|
+
value = input;
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
throw validationError('Invalid input type: expected number or string.', {
|
|
635
|
+
input: String(input),
|
|
636
|
+
...INVALID_NUMBER,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (Number.isNaN(value) || !Number.isFinite(value)) {
|
|
640
|
+
throw validationError('Invalid number value (NaN or Infinity).', {
|
|
641
|
+
input,
|
|
642
|
+
...INVALID_NUMBER,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
let clamped = false;
|
|
646
|
+
const originalValueForLog = value;
|
|
647
|
+
if (min !== undefined && value < min) {
|
|
648
|
+
value = min;
|
|
649
|
+
clamped = true;
|
|
650
|
+
}
|
|
651
|
+
if (max !== undefined && value > max) {
|
|
652
|
+
value = max;
|
|
653
|
+
clamped = true;
|
|
654
|
+
}
|
|
655
|
+
if (clamped) {
|
|
656
|
+
logger.debug('Number clamped to range.', requestContextService.createRequestContext({
|
|
657
|
+
operation: 'Sanitization.sanitizeNumber.clamped',
|
|
658
|
+
additionalContext: {
|
|
659
|
+
originalInput: String(input),
|
|
660
|
+
parsedValue: originalValueForLog,
|
|
661
|
+
minValue: min,
|
|
662
|
+
maxValue: max,
|
|
663
|
+
clampedValue: value,
|
|
664
|
+
},
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
return value;
|
|
668
|
+
});
|
|
613
669
|
}
|
|
614
670
|
/**
|
|
615
671
|
* Produces a log-safe deep clone of `input` with sensitive field values replaced by `'[REDACTED]'`.
|
|
@@ -655,6 +711,57 @@ export class Sanitization {
|
|
|
655
711
|
return '[Log Sanitization Failed]';
|
|
656
712
|
}
|
|
657
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Redacts, serializes, and caps a value for a log field: {@link sanitizeForLogging},
|
|
716
|
+
* then `JSON.stringify`, then truncation to at most `maxBytes` UTF-8 bytes.
|
|
717
|
+
*
|
|
718
|
+
* Redaction runs first, so a cut can never keep the prefix of a secret the
|
|
719
|
+
* redactor would have replaced. The cut lands on a character boundary — never
|
|
720
|
+
* inside a multi-byte sequence or between the halves of a surrogate pair — so
|
|
721
|
+
* the result is always well-formed. It is a prefix of the whole serialization,
|
|
722
|
+
* so a truncated payload is no longer valid JSON; `truncated` says so.
|
|
723
|
+
*
|
|
724
|
+
* Returned as a string rather than an object because the logger drops values
|
|
725
|
+
* nested past its sanitize depth, which would silently strip the deep parts of
|
|
726
|
+
* a payload. A value `JSON.stringify` rejects (a `bigint`) yields
|
|
727
|
+
* `'[Log Serialization Failed]'` rather than throwing into the caller's path.
|
|
728
|
+
*
|
|
729
|
+
* Key-name redaction only: a secret inside a free-form string value (a query,
|
|
730
|
+
* a message) is serialized as-is.
|
|
731
|
+
*
|
|
732
|
+
* @param value - The value to serialize.
|
|
733
|
+
* @param maxBytes - Maximum UTF-8 byte length of `text`.
|
|
734
|
+
* @returns `text`, the serialized and possibly truncated payload, and `truncated`.
|
|
735
|
+
* @example
|
|
736
|
+
* ```ts
|
|
737
|
+
* sanitization.serializeForLogging({ query: 'q', apiKey: 'sk-1' }, 16_384);
|
|
738
|
+
* // => { text: '{"query":"q","apiKey":"[REDACTED]"}', truncated: false }
|
|
739
|
+
* ```
|
|
740
|
+
*/
|
|
741
|
+
serializeForLogging(value, maxBytes) {
|
|
742
|
+
let json;
|
|
743
|
+
try {
|
|
744
|
+
json = JSON.stringify(this.sanitizeForLogging(value)) ?? 'undefined';
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return { text: '[Log Serialization Failed]', truncated: false };
|
|
748
|
+
}
|
|
749
|
+
// Counted by hand rather than with `TextEncoder.encodeInto`, whose fill of a
|
|
750
|
+
// short buffer differs by runtime (Node stops up to two bytes early). A
|
|
751
|
+
// high surrogate starts a 4-byte pair and is sliced only with its partner;
|
|
752
|
+
// `JSON.stringify` escapes lone surrogates, so none reach here.
|
|
753
|
+
let bytes = 0;
|
|
754
|
+
for (let i = 0; i < json.length; i++) {
|
|
755
|
+
const unit = json.charCodeAt(i);
|
|
756
|
+
const width = unit < 0x80 ? 1 : unit < 0x800 ? 2 : unit >= 0xd800 && unit < 0xdc00 ? 4 : 3;
|
|
757
|
+
if (bytes + width > maxBytes)
|
|
758
|
+
return { text: json.slice(0, i), truncated: true };
|
|
759
|
+
bytes += width;
|
|
760
|
+
if (width === 4)
|
|
761
|
+
i++;
|
|
762
|
+
}
|
|
763
|
+
return { text: json, truncated: false };
|
|
764
|
+
}
|
|
658
765
|
/**
|
|
659
766
|
* Recursively redacts sensitive fields in an object or array in place.
|
|
660
767
|
* @param obj - The object or array to redact.
|