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,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
+ }