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.
- package/LICENSE +1 -0
- package/README.md +77 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +243 -0
- package/dist/middleware.d.ts +40 -0
- package/dist/middleware.js +562 -0
- package/dist/modules/compatibility.d.ts +18 -0
- package/dist/modules/compatibility.js +99 -0
- package/dist/modules/constants.d.ts +12 -0
- package/dist/modules/constants.js +12 -0
- package/dist/modules/context-parameters.d.ts +13 -0
- package/dist/modules/context-parameters.js +74 -0
- package/dist/modules/diagnostics.d.ts +7 -0
- package/dist/modules/diagnostics.js +12 -0
- package/dist/modules/eventQueue.d.ts +32 -0
- package/dist/modules/eventQueue.js +234 -0
- package/dist/modules/exceptions.d.ts +13 -0
- package/dist/modules/exceptions.js +730 -0
- package/dist/modules/exporters/datadog.d.ts +17 -0
- package/dist/modules/exporters/datadog.js +166 -0
- package/dist/modules/exporters/otlp.d.ts +13 -0
- package/dist/modules/exporters/otlp.js +140 -0
- package/dist/modules/exporters/posthog.d.ts +32 -0
- package/dist/modules/exporters/posthog.js +272 -0
- package/dist/modules/exporters/sentry.d.ts +27 -0
- package/dist/modules/exporters/sentry.js +386 -0
- package/dist/modules/exporters/trace-context.d.ts +8 -0
- package/dist/modules/exporters/trace-context.js +27 -0
- package/dist/modules/index.d.ts +8 -0
- package/dist/modules/index.js +8 -0
- package/dist/modules/internal.d.ts +33 -0
- package/dist/modules/internal.js +195 -0
- package/dist/modules/logging.d.ts +2 -0
- package/dist/modules/logging.js +74 -0
- package/dist/modules/mcp-sdk-compat.d.ts +32 -0
- package/dist/modules/mcp-sdk-compat.js +111 -0
- package/dist/modules/privacy.d.ts +15 -0
- package/dist/modules/privacy.js +179 -0
- package/dist/modules/redaction.d.ts +11 -0
- package/dist/modules/redaction.js +81 -0
- package/dist/modules/sanitization.d.ts +9 -0
- package/dist/modules/sanitization.js +111 -0
- package/dist/modules/session.d.ts +22 -0
- package/dist/modules/session.js +109 -0
- package/dist/modules/telemetry.d.ts +8 -0
- package/dist/modules/telemetry.js +53 -0
- package/dist/modules/tools.d.ts +25 -0
- package/dist/modules/tools.js +119 -0
- package/dist/modules/tracing.d.ts +4 -0
- package/dist/modules/tracing.js +263 -0
- package/dist/modules/tracingV2.d.ts +2 -0
- package/dist/modules/tracingV2.js +300 -0
- package/dist/modules/truncation.d.ts +24 -0
- package/dist/modules/truncation.js +303 -0
- package/dist/modules/validation.d.ts +6 -0
- package/dist/modules/validation.js +51 -0
- package/dist/network.d.ts +83 -0
- package/dist/network.js +411 -0
- package/dist/proxy.d.ts +31 -0
- package/dist/proxy.js +158 -0
- package/dist/thirdparty/ksuid/base-convert-int-array.js +49 -0
- package/dist/thirdparty/ksuid/base62.js +24 -0
- package/dist/thirdparty/ksuid/index.d.ts +30 -0
- package/dist/thirdparty/ksuid/index.js +201 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.js +5 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.js +139 -0
- package/package.json +43 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { writeToLog } from "./logging.js";
|
|
2
|
+
import { getServerTrackingData, handleIdentify, resolveEventTags, resolveEventProperties, } from "./internal.js";
|
|
3
|
+
import { getServerSessionId } from "./session.js";
|
|
4
|
+
import { WithinEventType } from "./constants.js";
|
|
5
|
+
import { publishEvent } from "./eventQueue.js";
|
|
6
|
+
import { handleReportMissing } from "./tools.js";
|
|
7
|
+
import { setupInitializeTracing, setupListToolsTracing } from "./tracing.js";
|
|
8
|
+
import { captureException } from "./exceptions.js";
|
|
9
|
+
import { getToolFunction, hasToolFunction, createWrappedTool, getObjectShape, getLiteralValue, } from "./mcp-sdk-compat.js";
|
|
10
|
+
// WeakMap to track which callbacks have already been wrapped
|
|
11
|
+
const wrappedCallbacks = new WeakMap();
|
|
12
|
+
// Symbol to mark tools that have already been processed
|
|
13
|
+
const WITHIN_PROCESSED = Symbol("__within_processed__");
|
|
14
|
+
function isToolResultError(result) {
|
|
15
|
+
return result && typeof result === "object" && result.isError === true;
|
|
16
|
+
}
|
|
17
|
+
function addTracingToToolRegistry(tools, server) {
|
|
18
|
+
return Object.fromEntries(Object.entries(tools).map(([name, tool]) => [
|
|
19
|
+
name,
|
|
20
|
+
addTracingToToolCallbackInternal(tool, name, server),
|
|
21
|
+
]));
|
|
22
|
+
}
|
|
23
|
+
function setupListenerToRegisteredTools(server) {
|
|
24
|
+
try {
|
|
25
|
+
const data = getServerTrackingData(server.server);
|
|
26
|
+
if (!data) {
|
|
27
|
+
writeToLog("Warning: Cannot setup listener - no tracking data found");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Create a proxy handler for the _registeredTools object
|
|
31
|
+
const handler = {
|
|
32
|
+
set(target, property, value) {
|
|
33
|
+
try {
|
|
34
|
+
// Check if this is a tool being registered (has callback or handler property)
|
|
35
|
+
if (typeof property === "string" &&
|
|
36
|
+
value &&
|
|
37
|
+
typeof value === "object" &&
|
|
38
|
+
hasToolFunction(value)) {
|
|
39
|
+
// Check if tool has already been processed
|
|
40
|
+
if (value[WITHIN_PROCESSED]) {
|
|
41
|
+
writeToLog(`Tool ${String(property)} already processed, skipping proxy wrapping`);
|
|
42
|
+
// Just set the value without processing
|
|
43
|
+
return Reflect.set(target, property, value);
|
|
44
|
+
}
|
|
45
|
+
// Check if callback/handler is already wrapped
|
|
46
|
+
if (wrappedCallbacks.has(getToolFunction(value))) {
|
|
47
|
+
writeToLog(`Tool ${String(property)} callback already wrapped, skipping proxy wrapping`);
|
|
48
|
+
// Just set the value without processing
|
|
49
|
+
return Reflect.set(target, property, value);
|
|
50
|
+
}
|
|
51
|
+
// Apply tracing to the callback (context injection happens in setupListToolsTracing)
|
|
52
|
+
value = addTracingToToolCallbackInternal(value, property, server);
|
|
53
|
+
// After adding a tool, try to set up list tools tracing
|
|
54
|
+
// This handles the case where track() is called before tools are registered
|
|
55
|
+
setupListToolsTracing(server);
|
|
56
|
+
// If the tool has an update method, wrap it to handle callback updates
|
|
57
|
+
if (typeof value.update === "function") {
|
|
58
|
+
const originalUpdate = value.update;
|
|
59
|
+
value.update = function (...updateArgs) {
|
|
60
|
+
// If callback is being updated, wrap the new callback
|
|
61
|
+
// Note: MCP SDK's update() method API uses "callback" property in its interface
|
|
62
|
+
if (updateArgs[0]) {
|
|
63
|
+
const updateObj = updateArgs[0];
|
|
64
|
+
if (updateObj.callback &&
|
|
65
|
+
typeof updateObj.callback === "function") {
|
|
66
|
+
const wrappedTool = addTracingToToolCallbackInternal({ callback: updateObj.callback }, property, server);
|
|
67
|
+
updateObj.callback = getToolFunction(wrappedTool);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return originalUpdate.apply(this, updateArgs);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Use Reflect to perform the actual property set
|
|
75
|
+
return Reflect.set(target, property, value);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
writeToLog(`Warning: Error in proxy set handler for tool ${String(property)} - ${error}`);
|
|
79
|
+
// Fall back to default behavior on error
|
|
80
|
+
return Reflect.set(target, property, value);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
get(target, property) {
|
|
84
|
+
return Reflect.get(target, property);
|
|
85
|
+
},
|
|
86
|
+
deleteProperty(target, property) {
|
|
87
|
+
return Reflect.deleteProperty(target, property);
|
|
88
|
+
},
|
|
89
|
+
has(target, property) {
|
|
90
|
+
return Reflect.has(target, property);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
// Replace _registeredTools with a proxied version
|
|
94
|
+
const originalTools = server._registeredTools || {};
|
|
95
|
+
server._registeredTools = new Proxy(originalTools, handler);
|
|
96
|
+
writeToLog("Successfully set up listener for new tool registrations");
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
writeToLog(`Warning: Failed to setup listener for registered tools - ${error}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function addTracingToToolCallbackInternal(tool, toolName, _server) {
|
|
103
|
+
const originalCallback = getToolFunction(tool);
|
|
104
|
+
if (wrappedCallbacks.has(originalCallback)) {
|
|
105
|
+
writeToLog(`Tool ${toolName} callback already wrapped, skipping re-wrap`);
|
|
106
|
+
return tool;
|
|
107
|
+
}
|
|
108
|
+
if (tool[WITHIN_PROCESSED]) {
|
|
109
|
+
writeToLog(`Tool ${toolName} already processed, skipping re-wrap`);
|
|
110
|
+
return tool;
|
|
111
|
+
}
|
|
112
|
+
const wrappedCallback = async function (...params) {
|
|
113
|
+
let args;
|
|
114
|
+
let extra;
|
|
115
|
+
if (params.length === 2) {
|
|
116
|
+
args = params[0];
|
|
117
|
+
extra = params[1];
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
args = undefined;
|
|
121
|
+
extra = params[0];
|
|
122
|
+
}
|
|
123
|
+
const removeContextFromArgs = (args) => {
|
|
124
|
+
if (args && typeof args === "object" && "context" in args) {
|
|
125
|
+
const { context: _context, ...argsWithoutContext } = args;
|
|
126
|
+
return argsWithoutContext;
|
|
127
|
+
}
|
|
128
|
+
return args;
|
|
129
|
+
};
|
|
130
|
+
const cleanedArgs = toolName === "get_more_tools" ? args : removeContextFromArgs(args);
|
|
131
|
+
try {
|
|
132
|
+
if (cleanedArgs === undefined) {
|
|
133
|
+
const handler = originalCallback;
|
|
134
|
+
return await handler(extra);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const handler = originalCallback;
|
|
138
|
+
return await handler(cleanedArgs, extra);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof Error) {
|
|
143
|
+
extra.__within_error = error;
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
// Mark the original callback as wrapped
|
|
149
|
+
wrappedCallbacks.set(originalCallback, true);
|
|
150
|
+
// Mark the wrapped callback as well (in case it gets re-wrapped)
|
|
151
|
+
wrappedCallbacks.set(wrappedCallback, true);
|
|
152
|
+
// Create a new tool object with the wrapped callback, preserving the property name
|
|
153
|
+
const wrappedTool = createWrappedTool(tool, wrappedCallback);
|
|
154
|
+
// Mark the tool as processed
|
|
155
|
+
wrappedTool[WITHIN_PROCESSED] = true;
|
|
156
|
+
return wrappedTool;
|
|
157
|
+
}
|
|
158
|
+
function setupToolsCallHandlerWrapping(server) {
|
|
159
|
+
const lowLevelServer = server.server;
|
|
160
|
+
// Check if tools/call handler already exists
|
|
161
|
+
const existingHandler = lowLevelServer._requestHandlers.get("tools/call");
|
|
162
|
+
if (existingHandler) {
|
|
163
|
+
const wrappedHandler = createToolsCallWrapper(existingHandler, lowLevelServer);
|
|
164
|
+
lowLevelServer._requestHandlers.set("tools/call", wrappedHandler);
|
|
165
|
+
}
|
|
166
|
+
// Intercept future calls to setRequestHandler for tools registered after track()
|
|
167
|
+
const originalSetRequestHandler = lowLevelServer.setRequestHandler.bind(lowLevelServer);
|
|
168
|
+
lowLevelServer.setRequestHandler = function (requestSchema, handler) {
|
|
169
|
+
const shape = getObjectShape(requestSchema);
|
|
170
|
+
const method = shape?.method ? getLiteralValue(shape.method) : undefined;
|
|
171
|
+
// Only wrap tools/call handler
|
|
172
|
+
if (method === "tools/call") {
|
|
173
|
+
const wrappedHandler = createToolsCallWrapper(handler, lowLevelServer);
|
|
174
|
+
return originalSetRequestHandler(requestSchema, wrappedHandler);
|
|
175
|
+
}
|
|
176
|
+
// Pass through all other handlers unchanged
|
|
177
|
+
return originalSetRequestHandler(requestSchema, handler);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function createToolsCallWrapper(originalHandler, server) {
|
|
181
|
+
return async (request, extra) => {
|
|
182
|
+
const startTime = new Date();
|
|
183
|
+
let shouldPublishEvent = false;
|
|
184
|
+
let event = null;
|
|
185
|
+
try {
|
|
186
|
+
const data = getServerTrackingData(server);
|
|
187
|
+
if (!data) {
|
|
188
|
+
writeToLog("Warning: Within SDK is unable to find server tracking data. Please ensure you have called track(server, options) before using tool calls.");
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
shouldPublishEvent = true;
|
|
192
|
+
const sessionId = getServerSessionId(server, extra);
|
|
193
|
+
event = {
|
|
194
|
+
sessionId,
|
|
195
|
+
resourceName: request.params?.name || "Unknown Tool",
|
|
196
|
+
parameters: { request, extra },
|
|
197
|
+
eventType: WithinEventType.mcpToolsCall,
|
|
198
|
+
timestamp: startTime,
|
|
199
|
+
redactionFn: data.options.redactSensitiveInformation,
|
|
200
|
+
};
|
|
201
|
+
const context = request.params?.arguments?.context;
|
|
202
|
+
if (data.options.enableToolCallContext &&
|
|
203
|
+
typeof context === "string") {
|
|
204
|
+
event.userIntent = context;
|
|
205
|
+
}
|
|
206
|
+
// Identify user session
|
|
207
|
+
await handleIdentify(server, data, request, extra);
|
|
208
|
+
event.sessionId = data.sessionId;
|
|
209
|
+
const resolvedTags = await resolveEventTags(data, request, extra);
|
|
210
|
+
if (resolvedTags)
|
|
211
|
+
event.tags = resolvedTags;
|
|
212
|
+
const resolvedProperties = await resolveEventProperties(data, request, extra);
|
|
213
|
+
if (resolvedProperties)
|
|
214
|
+
event.properties = resolvedProperties;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
// If tracing setup fails, log it but continue with tool execution
|
|
219
|
+
writeToLog(`Warning: Within tracing failed for tool ${request.params?.name}, falling back to original handler - ${error}`);
|
|
220
|
+
}
|
|
221
|
+
// If this is get_more_tools, handle it directly without relying on server registration
|
|
222
|
+
if (request?.params?.name === "get_more_tools") {
|
|
223
|
+
try {
|
|
224
|
+
const result = await handleReportMissing({
|
|
225
|
+
context: request?.params?.arguments?.context,
|
|
226
|
+
});
|
|
227
|
+
if (event && shouldPublishEvent) {
|
|
228
|
+
event.userIntent = request?.params?.arguments?.context;
|
|
229
|
+
event.response = result;
|
|
230
|
+
event.duration = new Date().getTime() - startTime.getTime();
|
|
231
|
+
publishEvent(server, event);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (event && shouldPublishEvent) {
|
|
237
|
+
event.isError = true;
|
|
238
|
+
event.error = captureException(error);
|
|
239
|
+
event.duration = new Date().getTime() - startTime.getTime();
|
|
240
|
+
publishEvent(server, event);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Execute other tools (even if tracing setup failed)
|
|
246
|
+
try {
|
|
247
|
+
const result = await originalHandler(request, extra);
|
|
248
|
+
if (event && shouldPublishEvent) {
|
|
249
|
+
// Check for execution errors (SDK converts them to CallToolResult)
|
|
250
|
+
if (isToolResultError(result)) {
|
|
251
|
+
event.isError = true;
|
|
252
|
+
// Check if callback captured the original error (has full stack)
|
|
253
|
+
const capturedError = extra.__within_error;
|
|
254
|
+
if (capturedError) {
|
|
255
|
+
// Use captured error from callback
|
|
256
|
+
event.error = captureException(capturedError);
|
|
257
|
+
delete extra.__within_error; // Cleanup
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// SDK 1.21.0+ converted error (no stack trace available)
|
|
261
|
+
event.error = captureException(result);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
event.response = result;
|
|
265
|
+
event.duration = new Date().getTime() - startTime.getTime();
|
|
266
|
+
publishEvent(server, event);
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
// Validation errors, unknown tool, disabled tool
|
|
272
|
+
if (event && shouldPublishEvent) {
|
|
273
|
+
event.isError = true;
|
|
274
|
+
event.error = captureException(error);
|
|
275
|
+
event.duration = new Date().getTime() - startTime.getTime();
|
|
276
|
+
publishEvent(server, event);
|
|
277
|
+
}
|
|
278
|
+
// Re-throw so Protocol converts to JSONRPC error response
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export function setupTracking(server) {
|
|
284
|
+
try {
|
|
285
|
+
const _withinData = getServerTrackingData(server.server);
|
|
286
|
+
// Setup handler wrapping before any tools are registered
|
|
287
|
+
setupToolsCallHandlerWrapping(server);
|
|
288
|
+
setupInitializeTracing(server);
|
|
289
|
+
// Modify existing callbacks to include tracing and publishing events
|
|
290
|
+
// This now includes get_more_tools if it was added
|
|
291
|
+
server._registeredTools = addTracingToToolRegistry(server._registeredTools, server);
|
|
292
|
+
setupListToolsTracing(server);
|
|
293
|
+
// Proxy the high level server's registered tools to ensure new tools are injected with tracing
|
|
294
|
+
// Note: Context parameter injection now happens in setupListToolsTracing (after JSON Schema conversion)
|
|
295
|
+
setupListenerToRegisteredTools(server);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
writeToLog(`Warning: Failed to setup tool call tracing - ${error}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Event, UnredactedEvent } from "../types.js";
|
|
2
|
+
export declare const MAX_DEPTH = 10;
|
|
3
|
+
export declare const MAX_BREADTH = 100;
|
|
4
|
+
export declare const MAX_STRING_LENGTH = 32768;
|
|
5
|
+
export declare const MAX_EVENT_BYTES = 102400;
|
|
6
|
+
/**
|
|
7
|
+
* Recursively normalizes a value, handling:
|
|
8
|
+
* - String truncation (> MAX_STRING_LENGTH)
|
|
9
|
+
* - Non-serializable values (functions, symbols, undefined, BigInt, NaN, Infinity)
|
|
10
|
+
* - Date objects -> ISO string
|
|
11
|
+
* - Circular reference detection
|
|
12
|
+
* - Depth limiting
|
|
13
|
+
* - Breadth limiting
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalize(input: unknown, depth?: number, maxBreadth?: number, maxStringLength?: number): unknown;
|
|
16
|
+
/**
|
|
17
|
+
* Applies layered truncation to an event:
|
|
18
|
+
* 1. Field-level string limits (userIntent, resourceName, metadata fields, error.message)
|
|
19
|
+
* 2. Error frame limiting (first 25 + last 25 if > 50)
|
|
20
|
+
* 3. Response content text limits (32KB per text block)
|
|
21
|
+
* 4. Recursive normalization on user-controlled fields
|
|
22
|
+
* 5. Size-targeted truncation (progressive depth reduction + last-resort string truncation)
|
|
23
|
+
*/
|
|
24
|
+
export declare function truncateEvent<T extends Event | UnredactedEvent>(event: T): T;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// --- Constants ---
|
|
2
|
+
export const MAX_DEPTH = 10;
|
|
3
|
+
export const MAX_BREADTH = 100;
|
|
4
|
+
export const MAX_STRING_LENGTH = 32_768; // 32KB
|
|
5
|
+
export const MAX_EVENT_BYTES = 102_400; // 100KB
|
|
6
|
+
// --- Field-level limit constants ---
|
|
7
|
+
const MAX_USER_INTENT_LENGTH = 2_048;
|
|
8
|
+
const MAX_ERROR_MESSAGE_LENGTH = 2_048;
|
|
9
|
+
const MAX_RESOURCE_NAME_LENGTH = 256;
|
|
10
|
+
const MAX_METADATA_LENGTH = 256;
|
|
11
|
+
const MAX_STACK_FRAMES = 50;
|
|
12
|
+
const MAX_CONTENT_TEXT_LENGTH = 32_768;
|
|
13
|
+
// --- Truncation markers ---
|
|
14
|
+
const TRUNCATION_SUFFIX = "...";
|
|
15
|
+
/**
|
|
16
|
+
* Recursively normalizes a value, handling:
|
|
17
|
+
* - String truncation (> MAX_STRING_LENGTH)
|
|
18
|
+
* - Non-serializable values (functions, symbols, undefined, BigInt, NaN, Infinity)
|
|
19
|
+
* - Date objects -> ISO string
|
|
20
|
+
* - Circular reference detection
|
|
21
|
+
* - Depth limiting
|
|
22
|
+
* - Breadth limiting
|
|
23
|
+
*/
|
|
24
|
+
export function normalize(input, depth = MAX_DEPTH, maxBreadth = MAX_BREADTH, maxStringLength = MAX_STRING_LENGTH) {
|
|
25
|
+
const memo = new WeakSet();
|
|
26
|
+
return visit(input, depth, maxBreadth, maxStringLength, memo);
|
|
27
|
+
}
|
|
28
|
+
function visit(value, remainingDepth, maxBreadth, maxStringLength, memo) {
|
|
29
|
+
// null
|
|
30
|
+
if (value === null)
|
|
31
|
+
return null;
|
|
32
|
+
// undefined
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return "[undefined]";
|
|
35
|
+
// boolean
|
|
36
|
+
if (typeof value === "boolean")
|
|
37
|
+
return value;
|
|
38
|
+
// number (including NaN, Infinity)
|
|
39
|
+
if (typeof value === "number") {
|
|
40
|
+
if (Number.isNaN(value))
|
|
41
|
+
return "[NaN]";
|
|
42
|
+
if (!Number.isFinite(value))
|
|
43
|
+
return value > 0 ? "[Infinity]" : "[-Infinity]";
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
// bigint
|
|
47
|
+
if (typeof value === "bigint")
|
|
48
|
+
return `[BigInt: ${value}]`;
|
|
49
|
+
// string
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
if (value.length > maxStringLength) {
|
|
52
|
+
return value.slice(0, maxStringLength) + TRUNCATION_SUFFIX;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
// symbol
|
|
57
|
+
if (typeof value === "symbol") {
|
|
58
|
+
const desc = value.description;
|
|
59
|
+
return desc ? `[Symbol(${desc})]` : "[Symbol()]";
|
|
60
|
+
}
|
|
61
|
+
// function
|
|
62
|
+
if (typeof value === "function") {
|
|
63
|
+
const name = value.name || "<anonymous>";
|
|
64
|
+
return `[Function: ${name}]`;
|
|
65
|
+
}
|
|
66
|
+
// Date
|
|
67
|
+
if (value instanceof Date) {
|
|
68
|
+
return Number.isNaN(value.getTime())
|
|
69
|
+
? "[Invalid Date]"
|
|
70
|
+
: value.toISOString();
|
|
71
|
+
}
|
|
72
|
+
// Objects and arrays from here — need depth/breadth/circular checks
|
|
73
|
+
if (typeof value === "object") {
|
|
74
|
+
// Circular reference detection
|
|
75
|
+
if (memo.has(value))
|
|
76
|
+
return "[Circular ~]";
|
|
77
|
+
// Depth limit
|
|
78
|
+
if (remainingDepth <= 0) {
|
|
79
|
+
return Array.isArray(value) ? "[Array]" : "[Object]";
|
|
80
|
+
}
|
|
81
|
+
memo.add(value);
|
|
82
|
+
let result;
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
result = visitArray(value, remainingDepth - 1, maxBreadth, maxStringLength, memo);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
result = visitObject(value, remainingDepth - 1, maxBreadth, maxStringLength, memo);
|
|
88
|
+
}
|
|
89
|
+
memo.delete(value);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
// Fallback: coerce to string
|
|
93
|
+
return String(value);
|
|
94
|
+
}
|
|
95
|
+
function visitArray(arr, remainingDepth, maxBreadth, maxStringLength, memo) {
|
|
96
|
+
const result = [];
|
|
97
|
+
for (let i = 0; i < arr.length; i++) {
|
|
98
|
+
if (i >= maxBreadth) {
|
|
99
|
+
result.push("[MaxProperties ~]");
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
result.push(visit(arr[i], remainingDepth, maxBreadth, maxStringLength, memo));
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function visitObject(obj, remainingDepth, maxBreadth, maxStringLength, memo) {
|
|
107
|
+
const result = {};
|
|
108
|
+
const keys = Object.keys(obj);
|
|
109
|
+
let count = 0;
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
if (count >= maxBreadth) {
|
|
112
|
+
result["..."] = "[MaxProperties ~]";
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
// Skip undefined values — matches JSON.stringify behavior (omits undefined properties)
|
|
116
|
+
if (obj[key] === undefined)
|
|
117
|
+
continue;
|
|
118
|
+
result[key] = visit(obj[key], remainingDepth, maxBreadth, maxStringLength, memo);
|
|
119
|
+
count++;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
// --- Field-level truncation helpers ---
|
|
124
|
+
function truncateString(str, maxLength) {
|
|
125
|
+
if (str == null)
|
|
126
|
+
return str;
|
|
127
|
+
if (str.length <= maxLength)
|
|
128
|
+
return str;
|
|
129
|
+
return str.slice(0, maxLength) + TRUNCATION_SUFFIX;
|
|
130
|
+
}
|
|
131
|
+
function truncateStackFrames(frames) {
|
|
132
|
+
if (!frames || frames.length <= MAX_STACK_FRAMES)
|
|
133
|
+
return frames;
|
|
134
|
+
const half = Math.floor(MAX_STACK_FRAMES / 2);
|
|
135
|
+
return [...frames.slice(0, half), ...frames.slice(-half)];
|
|
136
|
+
}
|
|
137
|
+
function truncateResponseContent(response) {
|
|
138
|
+
if (response == null || typeof response !== "object")
|
|
139
|
+
return response;
|
|
140
|
+
const result = { ...response };
|
|
141
|
+
if (Array.isArray(result.content)) {
|
|
142
|
+
result.content = result.content.map((block) => {
|
|
143
|
+
if (block?.type === "text" &&
|
|
144
|
+
typeof block.text === "string" &&
|
|
145
|
+
block.text.length > MAX_CONTENT_TEXT_LENGTH) {
|
|
146
|
+
return {
|
|
147
|
+
...block,
|
|
148
|
+
text: block.text.slice(0, MAX_CONTENT_TEXT_LENGTH) + TRUNCATION_SUFFIX,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return block;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Calculates the UTF-8 byte size of a JSON-serialized value.
|
|
158
|
+
*/
|
|
159
|
+
const textEncoder = new TextEncoder();
|
|
160
|
+
function jsonByteSize(value) {
|
|
161
|
+
return textEncoder.encode(JSON.stringify(value)).length;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Finds and truncates the largest string values in an object to fit within a byte budget.
|
|
165
|
+
* Last-resort mechanism when depth reduction alone isn't enough.
|
|
166
|
+
* Iterates until the result fits or no further reduction is possible.
|
|
167
|
+
*/
|
|
168
|
+
function truncateLargestFields(obj, maxBytes) {
|
|
169
|
+
let result = structuredClone(obj);
|
|
170
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
171
|
+
const currentSize = jsonByteSize(result);
|
|
172
|
+
if (currentSize <= maxBytes)
|
|
173
|
+
return result;
|
|
174
|
+
const excess = currentSize - maxBytes;
|
|
175
|
+
// Find all string values and their sizes, sorted largest first
|
|
176
|
+
const stringPaths = [];
|
|
177
|
+
collectStringPaths(result, [], stringPaths);
|
|
178
|
+
stringPaths.sort((a, b) => b.length - a.length);
|
|
179
|
+
if (stringPaths.length === 0)
|
|
180
|
+
break; // no strings left to truncate
|
|
181
|
+
// Distribute the reduction across the largest strings
|
|
182
|
+
let remaining = excess + 200; // buffer for JSON overhead from added "..." suffixes
|
|
183
|
+
let truncated = false;
|
|
184
|
+
for (const { path, length } of stringPaths) {
|
|
185
|
+
if (remaining <= 0)
|
|
186
|
+
break;
|
|
187
|
+
const reduction = Math.min(remaining, Math.floor(length * 0.5));
|
|
188
|
+
if (reduction < 10)
|
|
189
|
+
continue; // not worth truncating tiny strings
|
|
190
|
+
const newLength = length - reduction;
|
|
191
|
+
setNestedValue(result, path, getNestedValue(result, path).slice(0, newLength) + TRUNCATION_SUFFIX);
|
|
192
|
+
remaining -= reduction;
|
|
193
|
+
truncated = true;
|
|
194
|
+
}
|
|
195
|
+
if (!truncated)
|
|
196
|
+
break; // no progress possible
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
function collectStringPaths(obj, currentPath, results) {
|
|
201
|
+
if (typeof obj === "string" && obj.length > 100) {
|
|
202
|
+
results.push({ path: [...currentPath], length: obj.length });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(obj)) {
|
|
206
|
+
obj.forEach((item, i) => collectStringPaths(item, [...currentPath, String(i)], results));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (obj != null && typeof obj === "object") {
|
|
210
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
211
|
+
collectStringPaths(value, [...currentPath, key], results);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function getNestedValue(obj, path) {
|
|
216
|
+
let current = obj;
|
|
217
|
+
for (const key of path)
|
|
218
|
+
current = current[key];
|
|
219
|
+
return current;
|
|
220
|
+
}
|
|
221
|
+
function setNestedValue(obj, path, value) {
|
|
222
|
+
let current = obj;
|
|
223
|
+
for (let i = 0; i < path.length - 1; i++)
|
|
224
|
+
current = current[path[i]];
|
|
225
|
+
current[path[path.length - 1]] = value;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Ensures an event fits within MAX_EVENT_BYTES by progressively reducing
|
|
229
|
+
* normalization depth, then truncating largest string fields as a last resort.
|
|
230
|
+
*/
|
|
231
|
+
function truncateToSize(event) {
|
|
232
|
+
// Check if already within budget
|
|
233
|
+
if (jsonByteSize(event) <= MAX_EVENT_BYTES)
|
|
234
|
+
return event;
|
|
235
|
+
// Progressive depth reduction
|
|
236
|
+
for (let depth = MAX_DEPTH - 1; depth >= 1; depth--) {
|
|
237
|
+
const reduced = { ...event };
|
|
238
|
+
if (reduced.parameters != null)
|
|
239
|
+
reduced.parameters = normalize(reduced.parameters, depth);
|
|
240
|
+
if (reduced.response != null)
|
|
241
|
+
reduced.response = normalize(reduced.response, depth);
|
|
242
|
+
if (reduced.identifyActorData != null)
|
|
243
|
+
reduced.identifyActorData = normalize(reduced.identifyActorData, depth);
|
|
244
|
+
if (reduced.error != null)
|
|
245
|
+
reduced.error = normalize(reduced.error, depth);
|
|
246
|
+
if (jsonByteSize(reduced) <= MAX_EVENT_BYTES)
|
|
247
|
+
return reduced;
|
|
248
|
+
}
|
|
249
|
+
// Last resort: truncate largest string fields
|
|
250
|
+
const minimal = { ...event };
|
|
251
|
+
if (minimal.parameters != null)
|
|
252
|
+
minimal.parameters = normalize(minimal.parameters, 1);
|
|
253
|
+
if (minimal.response != null)
|
|
254
|
+
minimal.response = normalize(minimal.response, 1);
|
|
255
|
+
if (minimal.identifyActorData != null)
|
|
256
|
+
minimal.identifyActorData = normalize(minimal.identifyActorData, 1);
|
|
257
|
+
if (minimal.error != null)
|
|
258
|
+
minimal.error = normalize(minimal.error, 1);
|
|
259
|
+
return truncateLargestFields(minimal, MAX_EVENT_BYTES);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Applies layered truncation to an event:
|
|
263
|
+
* 1. Field-level string limits (userIntent, resourceName, metadata fields, error.message)
|
|
264
|
+
* 2. Error frame limiting (first 25 + last 25 if > 50)
|
|
265
|
+
* 3. Response content text limits (32KB per text block)
|
|
266
|
+
* 4. Recursive normalization on user-controlled fields
|
|
267
|
+
* 5. Size-targeted truncation (progressive depth reduction + last-resort string truncation)
|
|
268
|
+
*/
|
|
269
|
+
export function truncateEvent(event) {
|
|
270
|
+
const result = { ...event };
|
|
271
|
+
// Layer 1: Field-level string limits
|
|
272
|
+
result.userIntent = truncateString(result.userIntent, MAX_USER_INTENT_LENGTH);
|
|
273
|
+
result.resourceName = truncateString(result.resourceName, MAX_RESOURCE_NAME_LENGTH);
|
|
274
|
+
result.serverName = truncateString(result.serverName, MAX_METADATA_LENGTH);
|
|
275
|
+
result.serverVersion = truncateString(result.serverVersion, MAX_METADATA_LENGTH);
|
|
276
|
+
result.clientName = truncateString(result.clientName, MAX_METADATA_LENGTH);
|
|
277
|
+
result.clientVersion = truncateString(result.clientVersion, MAX_METADATA_LENGTH);
|
|
278
|
+
// Error field limits
|
|
279
|
+
if (result.error != null && typeof result.error === "object") {
|
|
280
|
+
result.error = { ...result.error };
|
|
281
|
+
result.error.message = truncateString(result.error.message, MAX_ERROR_MESSAGE_LENGTH);
|
|
282
|
+
if (result.error.frames !== undefined) {
|
|
283
|
+
result.error.frames = truncateStackFrames(result.error.frames);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Response content text limits
|
|
287
|
+
result.response = truncateResponseContent(result.response);
|
|
288
|
+
// Layer 2: Recursive normalization on user-controlled fields
|
|
289
|
+
if (result.parameters != null) {
|
|
290
|
+
result.parameters = normalize(result.parameters);
|
|
291
|
+
}
|
|
292
|
+
if (result.response != null) {
|
|
293
|
+
result.response = normalize(result.response);
|
|
294
|
+
}
|
|
295
|
+
if (result.identifyActorData != null) {
|
|
296
|
+
result.identifyActorData = normalize(result.identifyActorData);
|
|
297
|
+
}
|
|
298
|
+
if (result.error != null) {
|
|
299
|
+
result.error = normalize(result.error);
|
|
300
|
+
}
|
|
301
|
+
// Layer 3: Size-targeted normalization
|
|
302
|
+
return truncateToSize(result);
|
|
303
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates and filters a tags object against Within tag constraints.
|
|
3
|
+
* Invalid entries are logged as warnings and dropped.
|
|
4
|
+
* Returns null if no valid entries remain.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateTags(tags: Record<string, string>): Record<string, string> | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { writeToLog } from "./logging.js";
|
|
2
|
+
const TAG_KEY_REGEX = /^[a-zA-Z0-9$_.:\- ]+$/;
|
|
3
|
+
const MAX_TAG_KEY_LENGTH = 32;
|
|
4
|
+
const MAX_TAG_VALUE_LENGTH = 200;
|
|
5
|
+
const MAX_TAG_ENTRIES = 50;
|
|
6
|
+
/**
|
|
7
|
+
* Validates and filters a tags object against Within tag constraints.
|
|
8
|
+
* Invalid entries are logged as warnings and dropped.
|
|
9
|
+
* Returns null if no valid entries remain.
|
|
10
|
+
*/
|
|
11
|
+
export function validateTags(tags) {
|
|
12
|
+
const entries = Object.entries(tags);
|
|
13
|
+
if (entries.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const valid = [];
|
|
17
|
+
for (const [key, value] of entries) {
|
|
18
|
+
// Key validation
|
|
19
|
+
if (typeof key !== "string" || !TAG_KEY_REGEX.test(key)) {
|
|
20
|
+
writeToLog(`Dropping invalid tag: "${String(key)}" — key contains invalid characters or is empty`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (key.length > MAX_TAG_KEY_LENGTH) {
|
|
24
|
+
writeToLog(`Dropping invalid tag: "${key}" — key exceeds max length of ${MAX_TAG_KEY_LENGTH}`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
// Value validation
|
|
28
|
+
if (typeof value !== "string") {
|
|
29
|
+
writeToLog(`Dropping invalid tag: "${key}" — non-string value (got ${typeof value})`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (value.length > MAX_TAG_VALUE_LENGTH) {
|
|
33
|
+
writeToLog(`Dropping invalid tag: "${key}" — value exceeds max length of ${MAX_TAG_VALUE_LENGTH}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (value.includes("\n")) {
|
|
37
|
+
writeToLog(`Dropping invalid tag: "${key}" — value contains newline character`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
valid.push([key, value]);
|
|
41
|
+
}
|
|
42
|
+
if (valid.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (valid.length > MAX_TAG_ENTRIES) {
|
|
46
|
+
const dropped = valid.length - MAX_TAG_ENTRIES;
|
|
47
|
+
writeToLog(`Dropping ${dropped} tag(s) — exceeds maximum of ${MAX_TAG_ENTRIES} entries per event`);
|
|
48
|
+
valid.length = MAX_TAG_ENTRIES;
|
|
49
|
+
}
|
|
50
|
+
return Object.fromEntries(valid);
|
|
51
|
+
}
|