diff --git a/MIGRATION.md b/MIGRATION.md index 46d39ebf1085..9c7697ef12a8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -784,13 +784,14 @@ If you [opt out of span streaming](#opting-out-of-span-streaming), span names re The following span names were adjusted: -| Span op | Before | After | -| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none | -| `router` | Framework-specific, sometimes containing the raw URL (`/users/123`, `SvelteKit Route Change`) | The span's `http.route`, or `Router` if the SDK has none | -| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) | -| `resource.*` | The resource URL, relative to the page origin for same-origin resources (`/assets/app.js`) | The resource domain (`cdn.example.com`), or `Resource` if the SDK has none | -| `mcp.notification.client_to_server`, `mcp.notification.server_to_client` | The notification method name (`notifications/tools/list_changed`) | The notification method name, or `MCP notification` if the message carries none | +| Span op | Before | After | +| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none | +| `router` | Framework-specific, sometimes containing the raw URL (`/users/123`, `SvelteKit Route Change`) | The span's `http.route`, or `Router` if the SDK has none | +| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) | +| `resource.*` | The resource URL, relative to the page origin for same-origin resources (`/assets/app.js`) | The resource domain (`cdn.example.com`), or `Resource` if the SDK has none | +| `mcp.server` | The method and its target, including the resource URI (`resources/read file:///docs/api.md`) | The method alone for resource methods (`resources/read`). Tool and prompt names are unchanged (`tools/call get-weather`) | +| `mcp.notification.client_to_server`, `mcp.notification.server_to_client` | The notification method name (`notifications/tools/list_changed`) | The notification method name, or `MCP notification` if the message carries none | Resource spans now also carry a `url.domain` attribute holding that domain. The full URL remains available on `url.full`. @@ -802,6 +803,8 @@ Because a low-cardinality name cannot say which part of request processing a spa For the same reason, `useOperationNameForRootSpan` no longer renames the enclosing root span (`GET /graphql` stays `GET /graphql`, instead of becoming `GET /graphql (query GetUser)`). The operations are still recorded on that span's `sentry.graphql.operation` attribute, as long as the option stays enabled (the default). Disabling it skips both, as before. +Resource URIs are unbounded, so they are no longer part of an `mcp.server` span name. The URI remains available on the `mcp.resource.uri` attribute. + Only the Express, Koa and Hapi integrations resolve a route template for `router` spans. Angular, Ember and SvelteKit have none when the span starts, so their router spans are named `Router`. Child spans of a service or root span carry its name in their `sentry.segment.name` attribute, so that changes with it. If you group or filter spans by segment name in dashboards or alerts, update those references. diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts index 08e1e51b1ca2..5fa8e473674c 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/mcp.test.ts @@ -104,7 +104,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = return span.name === 'POST /messages' && getSpanOp(span) === 'http.server' && span.is_segment; }); const resourceSpanPromise = waitForStreamedSpan('node-express-streaming', span => { - return span.name === 'resources/read echo://foobar' && getSpanOp(span) === 'mcp.server' && span.is_segment; + return span.name === 'resources/read' && getSpanOp(span) === 'mcp.server' && span.is_segment; }); const resourceResult = await client.readResource({ @@ -123,6 +123,7 @@ test.skip('Should record streamed spans for mcp handlers', async ({ baseURL }) = expect(resourceSpan).toBeDefined(); expect(getSpanOp(resourceSpan)).toBe('mcp.server'); expect(resourceSpan.attributes['mcp.method.name']?.value).toBe('resources/read'); + expect(resourceSpan.attributes['mcp.resource.uri']?.value).toBe('echo://foobar'); }); await test.step('prompt handler', async () => { @@ -244,7 +245,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp await test.step('resource handler', async () => { const resourceSpanPromise = waitForStreamedSpan('node-express-streaming', span => { return ( - span.name === 'resources/read echo://streamable-test' && + span.name === 'resources/read' && getSpanOp(span) === 'mcp.server' && String(span.attributes['mcp.transport']?.value).includes('StreamableHTTPServerTransport') ); @@ -262,6 +263,7 @@ test('Should record streamed spans for streamable HTTP transport (wrapper transp expect(resourceSpan).toBeDefined(); expect(getSpanOp(resourceSpan)).toBe('mcp.server'); expect(resourceSpan.attributes['mcp.method.name']?.value).toBe('resources/read'); + expect(resourceSpan.attributes['mcp.resource.uri']?.value).toBe('echo://streamable-test'); }); await test.step('prompt handler', async () => { diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 68dc90edb54a..8834ec729437 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -44,6 +44,8 @@ "@langchain/core": "^0.3.80", "@langchain/openai": "^0.5.0", "@langchain/langgraph": "^0.2.32", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "@nestjs/common": "^11", "@nestjs/core": "^11", "@nestjs/platform-express": "^11", diff --git a/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/instrument.mjs new file mode 100644 index 000000000000..46a27dd03b74 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/scenario.mjs new file mode 100644 index 000000000000..53d07d4ffd9d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/scenario.mjs @@ -0,0 +1,26 @@ +import { Client } from '@modelcontextprotocol/client'; +import { InMemoryTransport, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; +import { wrapMcpServerWithSentry } from '@sentry/node'; + +const server = wrapMcpServerWithSentry(new McpServer({ name: 'Echo', version: '1.0.0' })); + +server.registerResource('echo', new ResourceTemplate('echo://{message}', { list: undefined }), {}, async uri => ({ + contents: [{ uri: uri.href, text: 'Resource echo' }], +})); + +server.registerTool('echo', {}, async () => ({ content: [{ type: 'text', text: 'Tool echo' }] })); + +async function run() { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: 'test-client', version: '1.0.0' }); + + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + await client.readResource({ uri: 'echo://foobar' }); + await client.callTool({ name: 'echo', arguments: {} }); + + await client.close(); + await server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/test.ts new file mode 100644 index 000000000000..eb146ca33fbd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/mcp-server-streamed/test.ts @@ -0,0 +1,46 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; + +function mcpSpans(container: SerializedStreamedSpanContainer): SerializedStreamedSpanContainer['items'] { + return container.items.filter(item => item.attributes['sentry.op']?.value === 'mcp.server'); +} + +describe('MCP server spans (streamed)', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('names resource spans after the method alone, keeping the URI on the attribute', async () => { + await createTestRunner() + .expect({ + span: container => { + const resourceSpan = mcpSpans(container).find( + span => span.attributes['mcp.method.name']?.value === 'resources/read', + ); + + expect(resourceSpan?.name).toBe('resources/read'); + expect(resourceSpan?.attributes['mcp.resource.uri']?.value).toBe('echo://foobar'); + }, + }) + .start() + .completed(); + }); + + test('keeps the tool name, which comes from a bounded registry', async () => { + await createTestRunner() + .expect({ + span: container => { + const toolSpan = mcpSpans(container).find( + span => span.attributes['mcp.method.name']?.value === 'tools/call', + ); + + expect(toolSpan?.name).toBe('tools/call echo'); + }, + }) + .start() + .completed(); + }); + }); +}); diff --git a/packages/core/src/integrations/mcp-server/methodConfig.ts b/packages/core/src/integrations/mcp-server/methodConfig.ts index fcaa4bb65621..0153adf49bbc 100644 --- a/packages/core/src/integrations/mcp-server/methodConfig.ts +++ b/packages/core/src/integrations/mcp-server/methodConfig.ts @@ -19,6 +19,7 @@ const METHOD_CONFIGS: Record = { 'tools/call': { targetField: 'name', targetAttribute: MCP_TOOL_NAME_ATTRIBUTE, + targetIsLowCardinality: true, captureArguments: true, argumentsField: 'arguments', }, @@ -38,6 +39,7 @@ const METHOD_CONFIGS: Record = { 'prompts/get': { targetField: 'name', targetAttribute: MCP_PROMPT_NAME_ATTRIBUTE, + targetIsLowCardinality: true, captureName: true, captureArguments: true, argumentsField: 'arguments', @@ -55,11 +57,12 @@ export function extractTargetInfo( params: Record, ): { target?: string; + targetIsLowCardinality: boolean; attributes: Record; } { const config = METHOD_CONFIGS[method]; if (!config) { - return { attributes: {} }; + return { targetIsLowCardinality: false, attributes: {} }; } const target = @@ -69,6 +72,7 @@ export function extractTargetInfo( return { target, + targetIsLowCardinality: !!config.targetIsLowCardinality, attributes: target && config.targetAttribute ? { [config.targetAttribute]: target } : {}, }; } diff --git a/packages/core/src/integrations/mcp-server/spans.ts b/packages/core/src/integrations/mcp-server/spans.ts index 5fb90bc18f5a..60e07b19a11e 100644 --- a/packages/core/src/integrations/mcp-server/spans.ts +++ b/packages/core/src/integrations/mcp-server/spans.ts @@ -9,7 +9,7 @@ import { getClient } from '../../currentScopes'; import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; -import { MCP_NOTIFICATION_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; +import { MCP_NOTIFICATION_SPAN_NAME_FALLBACK, MCP_SERVER_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; import { startSpan } from '../../tracing/trace'; import { buildTransportAttributes, buildTypeSpecificAttributes } from './attributeExtraction'; import { @@ -86,16 +86,18 @@ function createMcpSpan(config: McpSpanConfig): unknown { const { method } = message; const params = message.params; const client = getClient(); + const spanStreamingEnabled = !!client && hasSpanStreamingEnabled(client); // Determine span name based on type and OTEL conventions let spanName: string; if (type === 'request') { const targetInfo = extractTargetInfo(method, params || {}); - spanName = createSpanName(method, targetInfo.target); + const target = spanStreamingEnabled && !targetInfo.targetIsLowCardinality ? undefined : targetInfo.target; + spanName = method ? createSpanName(method, target) : MCP_SERVER_SPAN_NAME_FALLBACK; } else { // For notifications, use method name directly per OpenTelemetry conventions. // With span streaming, span names have to be low cardinality, so a message without a method name gets a static name. - spanName = method || (!!client && hasSpanStreamingEnabled(client) ? MCP_NOTIFICATION_SPAN_NAME_FALLBACK : method); + spanName = method || (spanStreamingEnabled ? MCP_NOTIFICATION_SPAN_NAME_FALLBACK : method); } const rawAttributes: Record = { @@ -189,8 +191,12 @@ export function buildMcpServerSpanConfig( const { method } = jsonRpcMessage; const params = jsonRpcMessage.params; + const client = getClient(); + const spanStreamingEnabled = !!client && hasSpanStreamingEnabled(client); + const targetInfo = extractTargetInfo(method, params || {}); - const spanName = createSpanName(method, targetInfo.target); + const target = spanStreamingEnabled && !targetInfo.targetIsLowCardinality ? undefined : targetInfo.target; + const spanName = method ? createSpanName(method, target) : MCP_SERVER_SPAN_NAME_FALLBACK; const rawAttributes: Record = { ...buildTransportAttributes(transport, extra), @@ -199,7 +205,6 @@ export function buildMcpServerSpanConfig( ...buildSentryAttributes('request'), }; - const client = getClient(); const userInfo = Boolean(client?.getDataCollectionOptions().userInfo); const attributes = filterMcpPiiFromSpanData(rawAttributes, userInfo) as Record; diff --git a/packages/core/src/integrations/mcp-server/types.ts b/packages/core/src/integrations/mcp-server/types.ts index 613d5dd5ad77..8884a98cc72c 100644 --- a/packages/core/src/integrations/mcp-server/types.ts +++ b/packages/core/src/integrations/mcp-server/types.ts @@ -9,6 +9,8 @@ import type { Span } from '../../types/span'; export type MethodConfig = { targetField: string; targetAttribute: string; + /** Whether the target is drawn from a bounded set, and so may appear in a span name. */ + targetIsLowCardinality?: boolean; captureArguments?: boolean; argumentsField?: string; captureUri?: boolean; diff --git a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts index e4ecaf8f303a..6a035c5c61bc 100644 --- a/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/semanticConventions.test.ts @@ -616,4 +616,44 @@ describe('MCP Server Semantic Conventions', () => { expect(lastCall?.[0]?.attributes).not.toHaveProperty('mcp.logging.message'); }); }); + + describe('Span names with span streaming', () => { + let wrappedMcpServer: ReturnType; + let mockTransport: ReturnType; + + beforeEach(() => { + getClientSpy.mockReturnValue(createMockClient(true, undefined, 'stream')); + wrappedMcpServer = wrapMcpServerWithSentry(createMockMcpServer(), { recordInputs: true, recordOutputs: true }); + mockTransport = createMockTransport(); + mockTransport.sessionId = 'test-session-123'; + }); + + it('drops the resource URI from the name, keeping it on the attribute', async () => { + await wrappedMcpServer.connect(mockTransport); + + mockTransport.onmessage?.( + { jsonrpc: '2.0', method: 'resources/read', id: 'req-1', params: { uri: 'file:///docs/api.md' } }, + {}, + ); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'resources/read', + attributes: expect.objectContaining({ 'mcp.resource.uri': 'file:///docs/api.md' }), + }), + ); + }); + + it.each([ + ['tools/call', { name: 'get-weather' }, 'tools/call get-weather'], + ['prompts/get', { name: 'analyze-code' }, 'prompts/get analyze-code'], + ['initialize', {}, 'initialize'], + ])('keeps the %s span name, which is already low cardinality', async (method, params, expected) => { + await wrappedMcpServer.connect(mockTransport); + + mockTransport.onmessage?.({ jsonrpc: '2.0', method, id: 'req-1', params }, {}); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ name: expected })); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 2f28fa056494..be8849254248 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5579,6 +5579,34 @@ resolved "https://registry.yarnpkg.com/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz#577c0c25d8aae9f69a97738b7b0d03d1471cdc49" integrity sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng== +"@modelcontextprotocol/client@2.0.0": + version "2.0.0" + resolved "https://sfw.security.sentry.io/npm/@modelcontextprotocol/client/-/client-2.0.0.tgz#4dc18c983c7acb40040f08442123e3c5b88d6074" + integrity sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw== + dependencies: + "@modelcontextprotocol/core" "2.0.0" + cross-spawn "^7.0.5" + eventsource "^3.0.2" + eventsource-parser "^3.0.0" + jose "^6.1.3" + pkce-challenge "^5.0.0" + zod "^4.2.0" + +"@modelcontextprotocol/core@2.0.0": + version "2.0.0" + resolved "https://sfw.security.sentry.io/npm/@modelcontextprotocol/core/-/core-2.0.0.tgz#c918a4c6aef22a7c59dfb0aabc2a82c187cce157" + integrity sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA== + dependencies: + zod "^4.2.0" + +"@modelcontextprotocol/server@2.0.0": + version "2.0.0" + resolved "https://sfw.security.sentry.io/npm/@modelcontextprotocol/server/-/server-2.0.0.tgz#4fae5ccb8f7925ed9a6bf6595bb3b58617063ffd" + integrity sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw== + dependencies: + "@modelcontextprotocol/core" "2.0.0" + zod "^4.2.0" + "@mongodb-js/saslprep@^1.1.0", "@mongodb-js/saslprep@^1.3.0": version "1.4.6" resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz#2edf5819fa0e69d86059f44d1fe57ae9d7817c12" @@ -13277,7 +13305,7 @@ cross-inspect@1.0.1: dependencies: tslib "^2.4.0" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -15418,10 +15446,17 @@ events@^3.0.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -eventsource-parser@^3.0.8: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.1.0.tgz#4e198eb91cd333d0a8ddcc036502b3618a25f449" - integrity sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg== +eventsource-parser@^3.0.0, eventsource-parser@^3.0.1, eventsource-parser@^3.0.8: + version "3.1.1" + resolved "https://sfw.security.sentry.io/npm/eventsource-parser/-/eventsource-parser-3.1.1.tgz#b96cbb7dace4f3774f58a9e3b1ae9a4a524872e2" + integrity sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ== + +eventsource@^3.0.2: + version "3.0.7" + resolved "https://sfw.security.sentry.io/npm/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" exact-mirror@^0.2.7: version "0.2.7" @@ -18239,6 +18274,11 @@ jiti@^2.4.2, jiti@^2.6.1, jiti@^2.7.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== +jose@^6.1.3: + version "6.2.10" + resolved "https://sfw.security.sentry.io/npm/jose/-/jose-6.2.10.tgz#b70436c920c4b3f97314c28a8b8612c15b1d9e7f" + integrity sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g== + js-md4@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/js-md4/-/js-md4-0.3.2.tgz#cd3b3dc045b0c404556c81ddb5756c23e59d7cf5" @@ -22278,6 +22318,11 @@ piscina@3.2.0: optionalDependencies: nice-napi "^1.0.2" +pkce-challenge@^5.0.0: + version "5.0.1" + resolved "https://sfw.security.sentry.io/npm/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d" + integrity sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ== + pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -28532,10 +28577,10 @@ zod@^3.23.8, zod@^3.24.1, zod@^3.25.32: resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== -zod@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" - integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== +zod@^4.0.0, zod@^4.2.0: + version "4.4.3" + resolved "https://sfw.security.sentry.io/npm/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" + integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== zone.js@^0.12.0: version "0.12.0"