within-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +77 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.js +243 -0
  5. package/dist/middleware.d.ts +40 -0
  6. package/dist/middleware.js +562 -0
  7. package/dist/modules/compatibility.d.ts +18 -0
  8. package/dist/modules/compatibility.js +99 -0
  9. package/dist/modules/constants.d.ts +12 -0
  10. package/dist/modules/constants.js +12 -0
  11. package/dist/modules/context-parameters.d.ts +13 -0
  12. package/dist/modules/context-parameters.js +74 -0
  13. package/dist/modules/diagnostics.d.ts +7 -0
  14. package/dist/modules/diagnostics.js +12 -0
  15. package/dist/modules/eventQueue.d.ts +32 -0
  16. package/dist/modules/eventQueue.js +234 -0
  17. package/dist/modules/exceptions.d.ts +13 -0
  18. package/dist/modules/exceptions.js +730 -0
  19. package/dist/modules/exporters/datadog.d.ts +17 -0
  20. package/dist/modules/exporters/datadog.js +166 -0
  21. package/dist/modules/exporters/otlp.d.ts +13 -0
  22. package/dist/modules/exporters/otlp.js +140 -0
  23. package/dist/modules/exporters/posthog.d.ts +32 -0
  24. package/dist/modules/exporters/posthog.js +272 -0
  25. package/dist/modules/exporters/sentry.d.ts +27 -0
  26. package/dist/modules/exporters/sentry.js +386 -0
  27. package/dist/modules/exporters/trace-context.d.ts +8 -0
  28. package/dist/modules/exporters/trace-context.js +27 -0
  29. package/dist/modules/index.d.ts +8 -0
  30. package/dist/modules/index.js +8 -0
  31. package/dist/modules/internal.d.ts +33 -0
  32. package/dist/modules/internal.js +195 -0
  33. package/dist/modules/logging.d.ts +2 -0
  34. package/dist/modules/logging.js +74 -0
  35. package/dist/modules/mcp-sdk-compat.d.ts +32 -0
  36. package/dist/modules/mcp-sdk-compat.js +111 -0
  37. package/dist/modules/privacy.d.ts +15 -0
  38. package/dist/modules/privacy.js +179 -0
  39. package/dist/modules/redaction.d.ts +11 -0
  40. package/dist/modules/redaction.js +81 -0
  41. package/dist/modules/sanitization.d.ts +9 -0
  42. package/dist/modules/sanitization.js +111 -0
  43. package/dist/modules/session.d.ts +22 -0
  44. package/dist/modules/session.js +109 -0
  45. package/dist/modules/telemetry.d.ts +8 -0
  46. package/dist/modules/telemetry.js +53 -0
  47. package/dist/modules/tools.d.ts +25 -0
  48. package/dist/modules/tools.js +119 -0
  49. package/dist/modules/tracing.d.ts +4 -0
  50. package/dist/modules/tracing.js +263 -0
  51. package/dist/modules/tracingV2.d.ts +2 -0
  52. package/dist/modules/tracingV2.js +300 -0
  53. package/dist/modules/truncation.d.ts +24 -0
  54. package/dist/modules/truncation.js +303 -0
  55. package/dist/modules/validation.d.ts +6 -0
  56. package/dist/modules/validation.js +51 -0
  57. package/dist/network.d.ts +83 -0
  58. package/dist/network.js +411 -0
  59. package/dist/proxy.d.ts +31 -0
  60. package/dist/proxy.js +158 -0
  61. package/dist/thirdparty/ksuid/base-convert-int-array.js +49 -0
  62. package/dist/thirdparty/ksuid/base62.js +24 -0
  63. package/dist/thirdparty/ksuid/index.d.ts +30 -0
  64. package/dist/thirdparty/ksuid/index.js +201 -0
  65. package/dist/types.d.ts +197 -0
  66. package/dist/types.js +5 -0
  67. package/dist/validate.d.ts +53 -0
  68. package/dist/validate.js +139 -0
  69. package/package.json +43 -0
@@ -0,0 +1,386 @@
1
+ import { writeToLog } from "../logging.js";
2
+ import { traceContext } from "./trace-context.js";
3
+ import { WITHIN_SOURCE } from "../constants.js";
4
+ export class SentryExporter {
5
+ endpoint;
6
+ authHeader;
7
+ config;
8
+ parsedDSN;
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.parsedDSN = this.parseDSN(config.dsn);
12
+ // Build envelope endpoint
13
+ this.endpoint = `${this.parsedDSN.protocol}://${this.parsedDSN.host}${this.parsedDSN.port ? `:${this.parsedDSN.port}` : ""}${this.parsedDSN.path}/api/${this.parsedDSN.projectId}/envelope/`;
14
+ // Build auth header
15
+ this.authHeader = `Sentry sentry_version=7, sentry_client=within-sdk/1.0.2, sentry_key=${this.parsedDSN.publicKey}`;
16
+ writeToLog(`SentryExporter: Initialized with endpoint ${this.endpoint}`);
17
+ }
18
+ parseDSN(dsn) {
19
+ // DSN format: protocol://publicKey@host[:port]/path/projectId
20
+ const regex = /^(https?):\/\/([a-f0-9]+)@([\w.-]+)(:\d+)?(\/.*)?\/(\d+)$/;
21
+ const match = dsn.match(regex);
22
+ if (!match) {
23
+ throw new Error(`Invalid Sentry DSN: ${dsn}`);
24
+ }
25
+ return {
26
+ protocol: match[1],
27
+ publicKey: match[2],
28
+ host: match[3],
29
+ port: match[4]?.substring(1), // Remove leading ':'
30
+ path: match[5] || "",
31
+ projectId: match[6],
32
+ };
33
+ }
34
+ async export(event) {
35
+ try {
36
+ // ALWAYS send log
37
+ const log = this.eventToLog(event);
38
+ const logEnvelope = this.createLogEnvelope(log);
39
+ writeToLog(`SentryExporter: Sending log for event ${event.id} to Sentry`);
40
+ const logResponse = await fetch(this.endpoint, {
41
+ method: "POST",
42
+ headers: {
43
+ "X-Sentry-Auth": this.authHeader,
44
+ "Content-Type": "application/x-sentry-envelope",
45
+ },
46
+ body: logEnvelope,
47
+ });
48
+ if (!logResponse.ok) {
49
+ const errorBody = await logResponse.text();
50
+ writeToLog(`Sentry log export failed - Status: ${logResponse.status}, Body: ${errorBody}`);
51
+ }
52
+ else {
53
+ writeToLog(`Sentry log export success - Event: ${event.id}`);
54
+ }
55
+ // OPTIONALLY send transaction for performance monitoring
56
+ if (this.config.enableTracing) {
57
+ const transaction = this.eventToTransaction(event);
58
+ const transactionEnvelope = this.createTransactionEnvelope(transaction);
59
+ writeToLog(`SentryExporter: Sending transaction ${transaction.event_id} to Sentry`);
60
+ const transactionResponse = await fetch(this.endpoint, {
61
+ method: "POST",
62
+ headers: {
63
+ "X-Sentry-Auth": this.authHeader,
64
+ "Content-Type": "application/x-sentry-envelope",
65
+ },
66
+ body: transactionEnvelope,
67
+ });
68
+ if (!transactionResponse.ok) {
69
+ const errorBody = await transactionResponse.text();
70
+ writeToLog(`Sentry transaction export failed - Status: ${transactionResponse.status}, Body: ${errorBody}`);
71
+ }
72
+ else {
73
+ writeToLog(`Sentry transaction export success - Event: ${event.id}`);
74
+ }
75
+ }
76
+ // ALWAYS send error event for Issue creation if this is an error
77
+ if (event.isError) {
78
+ // Use transaction if available for better context, otherwise create minimal error event
79
+ const errorEvent = this.config.enableTracing
80
+ ? this.eventToErrorEvent(event, this.eventToTransaction(event))
81
+ : this.eventToErrorEvent(event);
82
+ const errorEnvelope = this.createErrorEnvelope(errorEvent);
83
+ writeToLog(`SentryExporter: Sending error event ${errorEvent.event_id} to Sentry for Issue creation`);
84
+ const errorResponse = await fetch(this.endpoint, {
85
+ method: "POST",
86
+ headers: {
87
+ "X-Sentry-Auth": this.authHeader,
88
+ "Content-Type": "application/x-sentry-envelope",
89
+ },
90
+ body: errorEnvelope,
91
+ });
92
+ if (!errorResponse.ok) {
93
+ const errorBody = await errorResponse.text();
94
+ writeToLog(`Sentry error export failed - Status: ${errorResponse.status}, Body: ${errorBody}`);
95
+ }
96
+ else {
97
+ writeToLog(`Sentry error export success - Event: ${event.id}`);
98
+ }
99
+ }
100
+ }
101
+ catch (error) {
102
+ writeToLog(`Sentry export error: ${error}`);
103
+ }
104
+ }
105
+ eventToLog(event) {
106
+ const timestamp = event.timestamp
107
+ ? new Date(event.timestamp).getTime() / 1000
108
+ : Date.now() / 1000;
109
+ const traceId = traceContext.getTraceId(event.sessionId);
110
+ // Generate deterministic event_id for Sentry
111
+ const eventId = traceContext.getSpanId(event.id) + traceContext.getSpanId(event.id);
112
+ // Build message
113
+ const message = event.resourceName
114
+ ? `MCP ${event.eventType || "event"}: ${event.resourceName}`
115
+ : `MCP ${event.eventType || "event"}`;
116
+ return {
117
+ timestamp,
118
+ trace_id: traceId,
119
+ event_id: eventId,
120
+ level: event.isError ? "error" : "info",
121
+ body: message,
122
+ attributes: this.buildLogAttributes(event),
123
+ };
124
+ }
125
+ buildLogAttributes(event) {
126
+ const attributes = {};
127
+ if (event.eventType) {
128
+ attributes.eventType = { value: event.eventType, type: "string" };
129
+ }
130
+ if (event.resourceName) {
131
+ attributes.resourceName = { value: event.resourceName, type: "string" };
132
+ }
133
+ if (event.serverName) {
134
+ attributes.serverName = { value: event.serverName, type: "string" };
135
+ }
136
+ if (event.clientName) {
137
+ attributes.clientName = { value: event.clientName, type: "string" };
138
+ }
139
+ if (event.sessionId) {
140
+ attributes.sessionId = { value: event.sessionId, type: "string" };
141
+ }
142
+ if (event.projectId) {
143
+ attributes.projectId = { value: event.projectId, type: "string" };
144
+ }
145
+ if (event.duration !== undefined) {
146
+ attributes.duration_ms = { value: event.duration, type: "double" };
147
+ }
148
+ if (event.identifyActorGivenId) {
149
+ attributes.actorId = {
150
+ value: event.identifyActorGivenId,
151
+ type: "string",
152
+ };
153
+ }
154
+ if (event.identifyActorName) {
155
+ attributes.actorName = { value: event.identifyActorName, type: "string" };
156
+ }
157
+ if (event.userIntent) {
158
+ attributes.userIntent = { value: event.userIntent, type: "string" };
159
+ }
160
+ if (event.serverVersion) {
161
+ attributes.serverVersion = { value: event.serverVersion, type: "string" };
162
+ }
163
+ if (event.clientVersion) {
164
+ attributes.clientVersion = { value: event.clientVersion, type: "string" };
165
+ }
166
+ if (event.isError !== undefined) {
167
+ attributes.isError = { value: event.isError, type: "boolean" };
168
+ }
169
+ return attributes;
170
+ }
171
+ createLogEnvelope(log) {
172
+ // Envelope header
173
+ const envelopeHeader = {
174
+ event_id: log.event_id,
175
+ sent_at: new Date().toISOString(),
176
+ };
177
+ // Item header with ALL MANDATORY fields
178
+ const itemHeader = {
179
+ type: "log",
180
+ item_count: 1, // MANDATORY - must match number of logs
181
+ content_type: "application/vnd.sentry.items.log+json", // MANDATORY - exact string
182
+ };
183
+ // Payload with CORRECT key
184
+ const payload = {
185
+ items: [log], // Changed from 'logs' to 'items'
186
+ };
187
+ // Build envelope with TRAILING NEWLINE
188
+ return ([
189
+ JSON.stringify(envelopeHeader),
190
+ JSON.stringify(itemHeader),
191
+ JSON.stringify(payload),
192
+ ].join("\n") + "\n"); // Added required trailing newline
193
+ }
194
+ eventToTransaction(event) {
195
+ // Calculate timestamps
196
+ const endTimestamp = event.timestamp
197
+ ? new Date(event.timestamp).getTime() / 1000
198
+ : Date.now() / 1000;
199
+ const startTimestamp = event.duration
200
+ ? endTimestamp - event.duration / 1000
201
+ : endTimestamp;
202
+ const traceId = traceContext.getTraceId(event.sessionId);
203
+ const spanId = traceContext.getSpanId(event.id);
204
+ // Build transaction name
205
+ const transactionName = event.resourceName
206
+ ? `${event.eventType || "mcp"} - ${event.resourceName}`
207
+ : event.eventType || "mcp.event";
208
+ const transaction = {
209
+ type: "transaction",
210
+ event_id: traceContext.getSpanId(event.id) + traceContext.getSpanId(),
211
+ timestamp: endTimestamp,
212
+ start_timestamp: startTimestamp,
213
+ transaction: transactionName,
214
+ contexts: this.buildContexts(event, {
215
+ trace_id: traceId,
216
+ span_id: spanId,
217
+ op: event.eventType || "mcp.event",
218
+ status: event.isError ? "internal_error" : "ok",
219
+ }),
220
+ tags: this.buildTags(event),
221
+ extra: this.buildExtra(event),
222
+ };
223
+ return transaction;
224
+ }
225
+ buildTags(event) {
226
+ const tags = {
227
+ source: WITHIN_SOURCE,
228
+ };
229
+ if (this.config.environment)
230
+ tags.environment = this.config.environment;
231
+ if (this.config.release)
232
+ tags.release = this.config.release;
233
+ if (event.eventType)
234
+ tags.event_type = event.eventType;
235
+ if (event.resourceName)
236
+ tags.resource = event.resourceName;
237
+ if (event.serverName)
238
+ tags.server_name = event.serverName;
239
+ if (event.clientName)
240
+ tags.client_name = event.clientName;
241
+ if (event.identifyActorGivenId)
242
+ tags.actor_id = event.identifyActorGivenId;
243
+ // Add customer-defined tags (namespaced to avoid collisions with Sentry reserved fields)
244
+ if (event.tags) {
245
+ for (const [key, value] of Object.entries(event.tags)) {
246
+ tags[`within.${key}`] = value;
247
+ }
248
+ }
249
+ return tags;
250
+ }
251
+ buildExtra(event) {
252
+ const extra = {};
253
+ if (event.sessionId)
254
+ extra.session_id = event.sessionId;
255
+ if (event.projectId)
256
+ extra.project_id = event.projectId;
257
+ if (event.userIntent)
258
+ extra.user_intent = event.userIntent;
259
+ if (event.identifyActorName)
260
+ extra.actor_name = event.identifyActorName;
261
+ if (event.serverVersion)
262
+ extra.server_version = event.serverVersion;
263
+ if (event.clientVersion)
264
+ extra.client_version = event.clientVersion;
265
+ if (event.duration !== undefined)
266
+ extra.duration_ms = event.duration;
267
+ if (event.error)
268
+ extra.error = event.error;
269
+ return extra;
270
+ }
271
+ buildContexts(event, traceCtx) {
272
+ const contexts = {
273
+ trace: traceCtx,
274
+ };
275
+ // Add customer-defined properties as a custom context
276
+ if (event.properties) {
277
+ contexts.within = event.properties;
278
+ }
279
+ return contexts;
280
+ }
281
+ eventToErrorEvent(event, transaction) {
282
+ // Extract error message
283
+ let errorMessage = "Unknown error";
284
+ let errorType = "ToolCallError";
285
+ if (event.error) {
286
+ if (typeof event.error === "string") {
287
+ errorMessage = event.error;
288
+ }
289
+ else if (typeof event.error === "object" && event.error !== null) {
290
+ if ("message" in event.error) {
291
+ errorMessage = String(event.error.message);
292
+ }
293
+ else {
294
+ errorMessage = JSON.stringify(event.error);
295
+ }
296
+ if ("type" in event.error) {
297
+ errorType = String(event.error.type);
298
+ }
299
+ }
300
+ }
301
+ // Use same trace context as the transaction for correlation (if available)
302
+ const traceId = transaction
303
+ ? transaction.contexts.trace.trace_id
304
+ : traceContext.getTraceId(event.sessionId);
305
+ const spanId = traceContext.getSpanId(event.id);
306
+ const timestamp = transaction
307
+ ? transaction.timestamp
308
+ : event.timestamp
309
+ ? new Date(event.timestamp).getTime() / 1000
310
+ : Date.now() / 1000;
311
+ const errorEvent = {
312
+ type: "event",
313
+ event_id: traceContext.getSpanId(event.id) + traceContext.getSpanId(),
314
+ timestamp,
315
+ level: "error",
316
+ exception: {
317
+ values: [
318
+ {
319
+ type: errorType,
320
+ value: errorMessage,
321
+ mechanism: {
322
+ type: "mcp_tool_call",
323
+ handled: false,
324
+ },
325
+ },
326
+ ],
327
+ },
328
+ contexts: {
329
+ ...this.buildContexts(event, {
330
+ trace_id: traceId,
331
+ span_id: spanId,
332
+ parent_span_id: transaction?.contexts.trace.span_id,
333
+ op: transaction?.contexts.trace.op || event.eventType || "mcp.event",
334
+ }),
335
+ mcp: {
336
+ resource_name: event.resourceName,
337
+ session_id: event.sessionId,
338
+ event_type: event.eventType,
339
+ user_intent: event.userIntent,
340
+ },
341
+ },
342
+ tags: this.buildTags(event),
343
+ extra: this.buildExtra(event),
344
+ transaction: transaction?.transaction ||
345
+ (event.resourceName
346
+ ? `${event.eventType || "mcp"} - ${event.resourceName}`
347
+ : event.eventType || "mcp.event"), // Generate transaction name if not available
348
+ };
349
+ return errorEvent;
350
+ }
351
+ createTransactionEnvelope(transaction) {
352
+ // Envelope header
353
+ const envelopeHeader = {
354
+ event_id: transaction.event_id,
355
+ sent_at: new Date().toISOString(),
356
+ };
357
+ // Item header for transaction
358
+ const itemHeader = {
359
+ type: "transaction",
360
+ };
361
+ // Build envelope (newline-separated JSON)
362
+ return [
363
+ JSON.stringify(envelopeHeader),
364
+ JSON.stringify(itemHeader),
365
+ JSON.stringify(transaction),
366
+ ].join("\n");
367
+ }
368
+ createErrorEnvelope(errorEvent) {
369
+ // Envelope header
370
+ const envelopeHeader = {
371
+ event_id: errorEvent.event_id,
372
+ sent_at: new Date().toISOString(),
373
+ };
374
+ // Item header for error event
375
+ const itemHeader = {
376
+ type: "event",
377
+ content_type: "application/json",
378
+ };
379
+ // Build envelope (newline-separated JSON)
380
+ return [
381
+ JSON.stringify(envelopeHeader),
382
+ JSON.stringify(itemHeader),
383
+ JSON.stringify(errorEvent),
384
+ ].join("\n");
385
+ }
386
+ }
@@ -0,0 +1,8 @@
1
+ declare class TraceContext {
2
+ getTraceId(sessionId?: string): string;
3
+ getSpanId(eventId?: string): string;
4
+ getDatadogTraceId(sessionId?: string): string;
5
+ getDatadogSpanId(eventId?: string): string;
6
+ }
7
+ export declare const traceContext: TraceContext;
8
+ export {};
@@ -0,0 +1,27 @@
1
+ import { createHash, randomBytes } from "crypto";
2
+ class TraceContext {
3
+ getTraceId(sessionId) {
4
+ if (!sessionId) {
5
+ return randomBytes(16).toString("hex");
6
+ }
7
+ return createHash("sha256")
8
+ .update(sessionId)
9
+ .digest("hex")
10
+ .substring(0, 32);
11
+ }
12
+ getSpanId(eventId) {
13
+ if (!eventId) {
14
+ return randomBytes(8).toString("hex");
15
+ }
16
+ return createHash("sha256").update(eventId).digest("hex").substring(0, 16);
17
+ }
18
+ getDatadogTraceId(sessionId) {
19
+ const hex = this.getTraceId(sessionId);
20
+ return BigInt("0x" + hex.substring(16, 32)).toString();
21
+ }
22
+ getDatadogSpanId(eventId) {
23
+ const hex = this.getSpanId(eventId);
24
+ return BigInt("0x" + hex).toString();
25
+ }
26
+ }
27
+ export const traceContext = new TraceContext();
@@ -0,0 +1,8 @@
1
+ export * from "./compatibility.js";
2
+ export * from "./context-parameters.js";
3
+ export * from "./exceptions.js";
4
+ export * from "./internal.js";
5
+ export * from "./logging.js";
6
+ export * from "./session.js";
7
+ export * from "./tools.js";
8
+ export * from "./tracing.js";
@@ -0,0 +1,8 @@
1
+ export * from "./compatibility.js";
2
+ export * from "./context-parameters.js";
3
+ export * from "./exceptions.js";
4
+ export * from "./internal.js";
5
+ export * from "./logging.js";
6
+ export * from "./session.js";
7
+ export * from "./tools.js";
8
+ export * from "./tracing.js";
@@ -0,0 +1,33 @@
1
+ import { WithinTrackingData, MCPServerLike, UserIdentity, CompatibleRequestHandlerExtra } from "../types.js";
2
+ export declare function getServerTrackingData(server: MCPServerLike): WithinTrackingData | undefined;
3
+ export declare function setServerTrackingData(server: MCPServerLike, data: WithinTrackingData): void;
4
+ /**
5
+ * Deep comparison of two UserIdentity objects
6
+ */
7
+ export declare function areIdentitiesEqual(a: UserIdentity, b: UserIdentity): boolean;
8
+ /**
9
+ * Merges two UserIdentity objects, overwriting userId and userName,
10
+ * but merging userData fields
11
+ */
12
+ export declare function mergeIdentities(previous: UserIdentity | undefined, next: UserIdentity): UserIdentity;
13
+ /**
14
+ * Handles user identification for a request.
15
+ * Calls the identify function if configured, compares with previous identity,
16
+ * and publishes an identify event only if the identity has changed.
17
+ *
18
+ * @param server - The MCP server instance
19
+ * @param data - The server tracking data
20
+ * @param request - The request object to pass to identify function
21
+ * @param extra - Optional extra parameters containing headers, sessionId, etc.
22
+ */
23
+ export declare function handleIdentify(server: MCPServerLike, data: WithinTrackingData, request: any, extra?: CompatibleRequestHandlerExtra): Promise<void>;
24
+ /**
25
+ * Resolves the eventTags callback, validates the result, and returns validated tags.
26
+ * Returns null if no callback configured, callback returns nullish, or callback throws.
27
+ */
28
+ export declare function resolveEventTags(data: WithinTrackingData, request: any, extra?: CompatibleRequestHandlerExtra): Promise<Record<string, string> | null>;
29
+ /**
30
+ * Resolves the eventProperties callback and returns the result.
31
+ * Returns null if no callback configured, callback returns nullish, or callback throws.
32
+ */
33
+ export declare function resolveEventProperties(data: WithinTrackingData, request: any, extra?: CompatibleRequestHandlerExtra): Promise<Record<string, any> | null>;
@@ -0,0 +1,195 @@
1
+ import { publishEvent } from "./eventQueue.js";
2
+ import { writeToLog } from "./logging.js";
3
+ import { validateTags } from "./validation.js";
4
+ import { WithinEventType } from "./constants.js";
5
+ import { sanitizeIdentity } from "./privacy.js";
6
+ /**
7
+ * Simple LRU cache for session identities.
8
+ * Prevents memory leaks by capping at maxSize entries.
9
+ * This cache persists across server instance restarts.
10
+ */
11
+ class IdentityCache {
12
+ cache;
13
+ maxSize;
14
+ constructor(maxSize = 1000) {
15
+ this.cache = new Map();
16
+ this.maxSize = maxSize;
17
+ }
18
+ get(sessionId) {
19
+ const entry = this.cache.get(sessionId);
20
+ if (entry) {
21
+ // Update timestamp on access (LRU behavior)
22
+ entry.timestamp = Date.now();
23
+ // Move to end (most recently used)
24
+ this.cache.delete(sessionId);
25
+ this.cache.set(sessionId, entry);
26
+ return entry.identity;
27
+ }
28
+ return undefined;
29
+ }
30
+ set(sessionId, identity) {
31
+ // Remove if already exists (to re-add at end)
32
+ this.cache.delete(sessionId);
33
+ // Evict oldest if at capacity
34
+ if (this.cache.size >= this.maxSize) {
35
+ const oldestKey = this.cache.keys().next().value;
36
+ if (oldestKey !== undefined) {
37
+ this.cache.delete(oldestKey);
38
+ }
39
+ }
40
+ this.cache.set(sessionId, { identity, timestamp: Date.now() });
41
+ }
42
+ has(sessionId) {
43
+ return this.cache.has(sessionId);
44
+ }
45
+ size() {
46
+ return this.cache.size;
47
+ }
48
+ }
49
+ // Global identity cache shared across all server instances
50
+ // This prevents duplicate identify events when server objects are recreated
51
+ const _globalIdentityCache = new IdentityCache(1000);
52
+ // Internal tracking storage
53
+ const _serverTracking = new WeakMap();
54
+ export function getServerTrackingData(server) {
55
+ return _serverTracking.get(server);
56
+ }
57
+ export function setServerTrackingData(server, data) {
58
+ _serverTracking.set(server, data);
59
+ }
60
+ /**
61
+ * Deep comparison of two UserIdentity objects
62
+ */
63
+ export function areIdentitiesEqual(a, b) {
64
+ if (a.userId !== b.userId)
65
+ return false;
66
+ if (a.userName !== b.userName)
67
+ return false;
68
+ // Deep compare userData objects
69
+ const aData = a.userData || {};
70
+ const bData = b.userData || {};
71
+ const aKeys = Object.keys(aData);
72
+ const bKeys = Object.keys(bData);
73
+ if (aKeys.length !== bKeys.length)
74
+ return false;
75
+ for (const key of aKeys) {
76
+ if (!(key in bData))
77
+ return false;
78
+ if (JSON.stringify(aData[key]) !== JSON.stringify(bData[key]))
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ /**
84
+ * Merges two UserIdentity objects, overwriting userId and userName,
85
+ * but merging userData fields
86
+ */
87
+ export function mergeIdentities(previous, next) {
88
+ if (!previous) {
89
+ return next;
90
+ }
91
+ return {
92
+ userId: next.userId,
93
+ userName: next.userName,
94
+ userData: {
95
+ ...(previous.userData || {}),
96
+ ...(next.userData || {}),
97
+ },
98
+ };
99
+ }
100
+ /**
101
+ * Handles user identification for a request.
102
+ * Calls the identify function if configured, compares with previous identity,
103
+ * and publishes an identify event only if the identity has changed.
104
+ *
105
+ * @param server - The MCP server instance
106
+ * @param data - The server tracking data
107
+ * @param request - The request object to pass to identify function
108
+ * @param extra - Optional extra parameters containing headers, sessionId, etc.
109
+ */
110
+ export async function handleIdentify(server, data, request, extra) {
111
+ if (!data.options.identify) {
112
+ return;
113
+ }
114
+ const sessionId = data.sessionId;
115
+ let identifyEvent = {
116
+ sessionId: sessionId,
117
+ resourceName: request.params?.name || "Unknown",
118
+ eventType: WithinEventType.identify,
119
+ parameters: {
120
+ request: request,
121
+ extra: extra,
122
+ },
123
+ timestamp: new Date(),
124
+ redactionFn: data.options.redactSensitiveInformation,
125
+ };
126
+ try {
127
+ const rawIdentity = await data.options.identify(request, extra);
128
+ const identityResult = rawIdentity
129
+ ? sanitizeIdentity(data.vendorSlug, rawIdentity, data.options.privacy)
130
+ : null;
131
+ if (identityResult) {
132
+ // Now use the (possibly updated) sessionId for all subsequent operations
133
+ const currentSessionId = data.sessionId;
134
+ // Check global cache first (works across server instance restarts)
135
+ const previousIdentity = _globalIdentityCache.get(currentSessionId);
136
+ // Merge identities (overwrite userId/userName, merge userData)
137
+ const mergedIdentity = mergeIdentities(previousIdentity, identityResult);
138
+ // Only publish if identity has changed
139
+ const hasChanged = !previousIdentity ||
140
+ !areIdentitiesEqual(previousIdentity, mergedIdentity);
141
+ // Update BOTH caches to keep them in sync
142
+ // Global cache: persists across server instances
143
+ _globalIdentityCache.set(currentSessionId, mergedIdentity);
144
+ // Per-server cache: used by getSessionInfo() for fast local access
145
+ data.identifiedSessions.set(data.sessionId, mergedIdentity);
146
+ data.sessionInfo.identifyActorGivenId = mergedIdentity.userId;
147
+ data.sessionInfo.identifyActorName = undefined;
148
+ data.sessionInfo.identifyActorData = mergedIdentity.userData || {};
149
+ setServerTrackingData(server, data);
150
+ if (hasChanged) {
151
+ writeToLog(`Identified session ${currentSessionId} (actor: ${mergedIdentity.userId})`);
152
+ publishEvent(server, identifyEvent);
153
+ }
154
+ }
155
+ else {
156
+ writeToLog(`Warning: Supplied identify function returned null for session ${sessionId}`);
157
+ }
158
+ }
159
+ catch (error) {
160
+ writeToLog(`Error: User supplied identify function threw an error while identifying session ${sessionId} - ${error}`);
161
+ }
162
+ }
163
+ /**
164
+ * Resolves the eventTags callback, validates the result, and returns validated tags.
165
+ * Returns null if no callback configured, callback returns nullish, or callback throws.
166
+ */
167
+ export async function resolveEventTags(data, request, extra) {
168
+ if (!data.options.eventTags)
169
+ return null;
170
+ try {
171
+ const raw = (await data.options.eventTags(request, extra)) ?? null;
172
+ if (!raw)
173
+ return null;
174
+ return validateTags(raw);
175
+ }
176
+ catch (e) {
177
+ writeToLog(`eventTags callback error: ${e}`);
178
+ return null;
179
+ }
180
+ }
181
+ /**
182
+ * Resolves the eventProperties callback and returns the result.
183
+ * Returns null if no callback configured, callback returns nullish, or callback throws.
184
+ */
185
+ export async function resolveEventProperties(data, request, extra) {
186
+ if (!data.options.eventProperties)
187
+ return null;
188
+ try {
189
+ return (await data.options.eventProperties(request, extra)) ?? null;
190
+ }
191
+ catch (e) {
192
+ writeToLog(`eventProperties callback error: ${e}`);
193
+ return null;
194
+ }
195
+ }
@@ -0,0 +1,2 @@
1
+ export declare function setDiagnosticsSink(fn: ((entry: string) => void) | null): void;
2
+ export declare function writeToLog(message: string): void;