@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.8 โ†’ 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/README.md CHANGED
@@ -6,7 +6,7 @@ A TypeScript SDK for building Opal tools in Optimizely Connect Platform. This SD
6
6
 
7
7
  ## Features
8
8
 
9
- - ๐ŸŽฏ **Decorator-based Tool Registration** - Use `@tool` and `@interaction` decorators to easily register functions
9
+ - ๐ŸŽฏ **Decorator-based Tool Registration** - Use `@tool` decorator to easily register functions
10
10
  - ๐ŸŒ **Global and Regular Function Modes** - SDK can be used in either global or organization-scoped mode
11
11
  - ๐Ÿ”ง **Type-safe Development** - Full TypeScript support with comprehensive type definitions
12
12
  - ๐Ÿ—๏ธ **Abstract Base Classes** - Extend `ToolFunction` or `GlobalToolFunction` for standardized request processing
@@ -16,35 +16,51 @@ A TypeScript SDK for building Opal tools in Optimizely Connect Platform. This SD
16
16
  - โœ… **Automatic Validation** - SDK automatically validates parameters and returns RFC 9457 compliant error responses
17
17
  - ๐Ÿงช **Comprehensive Testing** - Fully tested with Jest
18
18
 
19
- ## Installation
19
+ ## Quick Start
20
+
21
+ The SDK extends the functionality of OCP apps. You need to have an OCP app to use this SDK.
22
+ Learn how to get started with OCP app development [here](https://docs.developers.optimizely.com/optimizely-connect-platform/docs/developer-platform-overview-ocp2).
20
23
 
24
+ Start by adding the SDK to your existing OCP app. In your OCP app folder, execute:
21
25
  ```bash
22
- npm install @optimizely-opal/opal-tool-ocp-sdk
26
+ yarn add @optimizely-opal/opal-tool-ocp-sdk
23
27
  ```
24
28
 
25
- or
29
+ To add an Opal too registry to your OCP app,
30
+ add a [function](https://docs.developers.optimizely.com/optimizely-connect-platform/docs/functions-ocp2) to your app manifest and mark it as an Opal tool function:
26
31
 
27
- ```bash
28
- yarn add @optimizely-opal/opal-tool-ocp-sdk
32
+ **`app.yml`**
33
+ ```yaml
34
+ functions:
35
+ opal_tool: # A unique key for your function.
36
+ entry_point: OpalToolFunction # The name of the class implementing your tool.
37
+ description: Opal tool function # A brief description of this function.
38
+ opal_tool: true
29
39
  ```
30
40
 
31
- ## Quick Start
41
+ Next, create and implement function class - both file name and class must match the value of `entry_point` property from app manifest.
42
+ Function class must extend either `ToolFunction` or `GlobalToolFunction` class from `@optimizely-opal/opal-tool-ocp-sdk` (See 'Function modes' section below).
32
43
 
33
- The SDK supports two function modes:
44
+ **`src/functions/OpalToolFunction`**
45
+ ```typescript
46
+ import { ToolFunction } from '@optimizely-opal/opal-tool-ocp-sdk';
34
47
 
35
- - **Regular Functions** (`ToolFunction`) - Organization-scoped functions that validate customer organization IDs
36
- - **Global Functions** (`GlobalToolFunction`) - Platform-wide functions that work across all organizations
48
+ export class OpalToolFunction extends ToolFunction {
37
49
 
38
- ### Regular Tool Function
50
+ }
51
+ ```
39
52
 
40
- Create a tool function class by extending `ToolFunction` for organization-scoped functionality:
53
+ Next, implement tools methods and annotate them with `@tool` decorator. Each such method is a tool in your registry.
54
+ You can define mulitple tool methods in your app.
55
+ Tool methods can be defined either as tool class instance methods or in separate classes.
56
+ If tools are defined in separate classes, these classes need to be imported into the function class.
41
57
 
58
+ **`src/functions/OpalToolFunction`**
42
59
  ```typescript
43
- import { ToolFunction, tool, interaction, ParameterType, InteractionResult, OptiIdAuthData } from '@optimizely-opal/opal-tool-ocp-sdk';
60
+ import { ToolFunction } from '@optimizely-opal/opal-tool-ocp-sdk';
44
61
 
45
- export class MyToolFunction extends ToolFunction {
62
+ export class OpalToolFunction extends ToolFunction {
46
63
 
47
- // Register a simple tool without authentication
48
64
  @tool({
49
65
  name: 'create_task',
50
66
  description: 'Creates a new task in the system',
@@ -71,118 +87,42 @@ export class MyToolFunction extends ToolFunction {
71
87
  priority: params.priority || 'medium'
72
88
  };
73
89
  }
74
-
75
- // Register a tool with OptiID authentication
76
- @tool({
77
- name: 'secure_task',
78
- description: 'Creates a secure task with OptiID authentication',
79
- endpoint: '/secure-task',
80
- parameters: [
81
- {
82
- name: 'title',
83
- type: ParameterType.String,
84
- description: 'The task title',
85
- required: true
86
- }
87
- ],
88
- authRequirements: [
89
- {
90
- provider: 'OptiID',
91
- scopeBundle: 'tasks',
92
- required: true
93
- }
94
- ]
95
- })
96
- async createSecureTask(params: { title: string }, authData?: OptiIdAuthData) {
97
- if (!authData) {
98
- throw new Error('OptiID authentication required');
99
- }
100
-
101
- const { customer_id, instance_id, access_token } = authData.credentials;
102
- return {
103
- id: '456',
104
- title: params.title,
105
- customer_id,
106
- instance_id
107
- };
108
- }
109
-
110
- // Register an interaction
111
- @interaction({
112
- name: 'task_webhook',
113
- endpoint: '/webhook/task'
114
- })
115
- async handleTaskWebhook(data: any): Promise<InteractionResult> {
116
- return new InteractionResult(
117
- `Task ${data.taskId} was updated`,
118
- `https://app.example.com/tasks/${data.taskId}`
119
- );
120
- }
121
90
  }
122
91
  ```
123
92
 
124
- ### Global Tool Function
93
+ The format of `params` attribute matches the parameters defined in `@tool` decorator.
125
94
 
126
- Create a global tool function by extending `GlobalToolFunction` for platform-wide functionality:
95
+ When the function is called by Opal, the SDK automatically:
127
96
 
128
- ```typescript
129
- import { GlobalToolFunction, tool, interaction, ParameterType, InteractionResult, OptiIdAuthData } from '@optimizely-opal/opal-tool-ocp-sdk';
97
+ - **Routes requests** to your registered tools based on endpoints
98
+ - **Handles authentication** and OptiID token validation before calling your methods
99
+ - **Provides discovery** at `/discovery` endpoint for OCP platform integration
100
+ - **Returns proper HTTP responses** with correct status codes and JSON formatting
130
101
 
131
- export class MyGlobalToolFunction extends GlobalToolFunction {
102
+ Optionally, implement `ready` method to define when tool registry can be registered and called by Opal.
103
+ OCP will call this method to show tool readiness in the UI. Only functions marked as `ready` can be registered and called by Opal.
104
+ The value of `reason` property from the response will be displayed in OCP UI to inform users why the tool is not ready to be registered.
105
+ ```typescript
106
+ protected override async ready(): Promise<ReadyResponse> {
107
+ // validation logic
108
+ if (!isValid) {
109
+ return { ready: false, reason: 'Configure the app first.' };
110
+ }
132
111
 
133
- @tool({
134
- name: 'global_utility',
135
- description: 'A utility tool available to all organizations',
136
- endpoint: '/global-utility',
137
- parameters: [
138
- {
139
- name: 'operation',
140
- type: ParameterType.String,
141
- description: 'The operation to perform',
142
- required: true
143
- }
144
- ]
145
- })
146
- async globalUtility(params: { operation: string }, authData?: OptiIdAuthData) {
147
- return {
148
- result: `Performed ${params.operation} globally`,
149
- organizationId: authData?.credentials.customer_id || 'unknown'
150
- };
112
+ return { ready: true };
151
113
  }
152
- }
153
114
  ```
154
115
 
155
- ### Function Modes
156
-
157
- The SDK operates in one of two modes based on the base class you extend:
158
-
159
- - **Regular Function Mode** (`ToolFunction`): All tools are organization-scoped and validate organization IDs
160
- - **Global Function Mode** (`GlobalToolFunction`): All tools are platform-wide.
161
-
162
- The discovery endpoint returns all tools registered within that function mode.
163
-
164
- Your function class inherits a `perform()` method from `ToolFunction` or `GlobalToolFunction` that serves as the main entry point for handling all incoming requests. When called, the SDK automatically:
165
-
166
- - **Routes requests** to your registered tools and interactions based on endpoints
167
- - **Handles authentication** and OptiID token validation before calling your methods
168
- - **Provides discovery** at `/discovery` endpoint for OCP platform integration
169
- - **Returns proper HTTP responses** with correct status codes and JSON formatting
170
-
171
116
  ## Core Concepts
172
117
 
173
118
  ### Tools
174
119
 
175
- Tools are functions that can be discovered and executed through the OCP platform. They:
176
-
177
- - Have a name, description, and endpoint
178
- - Define parameters with types and validation
179
- - Can require authentication
180
- - Return structured responses
181
- - Are automatically registered based on the function mode you choose
120
+ Each OCP app with an Opal too function is a tool registry in Opal. Tool registry constist of one or more tools.
121
+ Each tool have name, description and a list of input parameters.
182
122
 
183
123
  ### Function Modes
184
124
 
185
- #### Regular Functions (`ToolFunction`)
125
+ #### Regular Functions (function extends `ToolFunction` class)
186
126
 
187
127
  Regular functions are scoped to specific organizations and validate that requests come from the same organization:
188
128
 
@@ -191,7 +131,7 @@ Regular functions are scoped to specific organizations and validate that request
191
131
  - **Per-Organization Configuration**: Can implement organization-specific configuration, authentication credentials, and API keys since they're tied to a single organization
192
132
  - **Per-Organization Authentication**: Can store and use organization-specific authentication tokens, connection strings, and other sensitive data securely
193
133
 
194
- #### Global Functions (`GlobalToolFunction`)
134
+ #### Global Functions (function extends `GlobalToolFunction` class)
195
135
 
196
136
  Global functions work across all organizations without organization validation:
197
137
 
@@ -201,14 +141,6 @@ Global functions work across all organizations without organization validation:
201
141
  - **No Per-Organization Authentication**: Cannot store organization-specific credentials or authentication data
202
142
  - **Global Discovery**: Have a global discovery URL that can be used by any organization without requiring them to install the app first
203
143
 
204
- ### Interactions
205
-
206
- Interactions are event handlers that process incoming data (like webhooks):
207
-
208
- - Have a name and endpoint
209
- - Process unstructured data
210
- - Return interaction results with messages and optional links
211
-
212
144
  ### Parameters
213
145
 
214
146
  Supported parameter types:
@@ -226,7 +158,9 @@ enum ParameterType {
226
158
 
227
159
  ### Parameter Validation
228
160
 
229
- The SDK automatically validates all incoming parameters against the parameter definitions you specify for your tools. When Opal sends requests to your tools, the SDK performs validation before calling your handler methods:
161
+ The SDK automatically validates all incoming parameters against the parameter definitions you specify for your tools.
162
+ When Opal sends requests to your tools, the SDK performs validation before calling your handler methods and automatically returns an error message that Opal understands.
163
+ This allow Opal to auto-correct.
230
164
 
231
165
  #### Automatic Validation Features
232
166
 
@@ -235,36 +169,6 @@ The SDK automatically validates all incoming parameters against the parameter de
235
169
  - **Early Error Response**: Returns validation errors immediately without calling your handler if validation fails
236
170
  - **RFC 9457 Compliance**: Error responses follow the RFC 9457 Problem Details for HTTP APIs specification
237
171
 
238
- #### Validation Error Response Format
239
-
240
- When parameter validation fails, the SDK returns a standardized error response with HTTP status 400 and `Content-Type: application/problem+json`:
241
-
242
- ```json
243
- {
244
- "title": "One or more validation errors occurred.",
245
- "status": 400,
246
- "detail": "See 'errors' field for details.",
247
- "instance": "/your-tool-endpoint",
248
- "errors": [
249
- {
250
- "field": "title",
251
- "message": "Parameter 'title' must be a string, but received number"
252
- },
253
- {
254
- "field": "priority",
255
- "message": "Required parameter 'priority' is missing"
256
- }
257
- ]
258
- }
259
- ```
260
-
261
- **Benefits:**
262
-
263
- - **Reduced Boilerplate**: No need to write parameter validation code in your handlers
264
- - **Consistent Error Format**: All validation errors follow the same RFC 9457 standard format
265
- - **Better Developer Experience**: Clear, actionable error messages for API consumers
266
- - **Type Safety**: Validation ensures your handlers receive correctly typed parameters
267
-
268
172
  ### Authentication
269
173
 
270
174
  The SDK supports authentication and authorization mechanisms:
@@ -434,16 +338,16 @@ This will return:
434
338
 
435
339
  ### Handler Function Signatures
436
340
 
437
- All tool and interaction handler methods follow this signature pattern:
341
+ All tool handler methods follow this signature pattern:
438
342
 
439
343
  ```typescript
440
344
  async handlerMethod(
441
- params: TParams, // Tool parameters or interaction data
345
+ params: TParams, // Tool parameters
442
346
  authData?: OptiIdAuthData // OptiID user authentication data (if authenticated)
443
347
  ): Promise<TResult>
444
348
  ```
445
349
 
446
- - **params**: The input parameters for tools, or interaction data for webhooks
350
+ - **params**: The input parameters for tools
447
351
  - **authData**: Available when OptiID user authentication is configured and successful
448
352
 
449
353
 
@@ -463,17 +367,6 @@ interface ToolConfig {
463
367
  }
464
368
  ```
465
369
 
466
- #### `@interaction(config: InteractionConfig)`
467
-
468
- Registers a method as an interaction handler.
469
-
470
- ```typescript
471
- interface InteractionConfig {
472
- name: string;
473
- endpoint: string;
474
- }
475
- ```
476
-
477
370
  ### Base Classes
478
371
 
479
372
  #### `ToolFunction`
@@ -507,10 +400,8 @@ Extend this class for tools that work across organizations. The `perform` method
507
400
  Key model classes with generic type support:
508
401
 
509
402
  - `Tool<TAuthData>` - Represents a registered tool with typed auth data
510
- - `Interaction<TAuthData>` - Represents an interaction handler with typed auth data
511
403
  - `Parameter` - Defines tool parameters
512
404
  - `AuthRequirement` - Defines authentication needs
513
- - `InteractionResult` - Response from interactions
514
405
  - `OptiIdAuthData` - OptiID specific authentication data
515
406
  - `ReadyResponse` - Response type for the ready method containing status and optional reason
516
407
  - `ToolError` - Custom error class for RFC 9457 Problem Details error responses with configurable HTTP status codes
@@ -639,7 +530,7 @@ yarn lint
639
530
  ### Function with Authentication
640
531
 
641
532
  ```typescript
642
- import { ToolFunction, tool, interaction, ParameterType, OptiIdAuthData, InteractionResult } from '@optimizely-opal/opal-ocp-sdk';
533
+ import { ToolFunction, tool, ParameterType, OptiIdAuthData } from '@optimizely-opal/opal-ocp-sdk';
643
534
 
644
535
  export class AuthenticatedFunction extends ToolFunction {
645
536
 
@@ -659,24 +550,6 @@ export class AuthenticatedFunction extends ToolFunction {
659
550
  return { success: true, customer_id };
660
551
  }
661
552
 
662
- // Interaction with authentication example
663
- @interaction({
664
- name: 'authenticated_webhook',
665
- endpoint: '/secure-webhook'
666
- })
667
- async handleSecureWebhook(data: any, authData?: OptiIdAuthData): Promise<InteractionResult> {
668
- if (!authData) {
669
- return new InteractionResult('Authentication required for webhook processing');
670
- }
671
-
672
- const { customer_id } = authData.credentials;
673
-
674
- // Process webhook data with authentication context
675
- return new InteractionResult(
676
- `Webhook processed for customer ${customer_id}: ${data.eventType}`,
677
- `https://app.example.com/events/${data.eventId}`
678
- );
679
- }
680
553
  }
681
554
  ```
682
555
 
@@ -748,7 +621,7 @@ export class TaskTool {
748
621
 
749
622
  **tools/NotificationTool.ts:**
750
623
  ```typescript
751
- import { tool, interaction, ParameterType, InteractionResult, OptiIdAuthData } from '@optimizely-opal/opal-ocp-sdk';
624
+ import { tool, ParameterType, OptiIdAuthData } from '@optimizely-opal/opal-ocp-sdk';
752
625
 
753
626
  export class NotificationTool {
754
627
  @tool({
@@ -779,16 +652,6 @@ export class NotificationTool {
779
652
  };
780
653
  }
781
654
 
782
- @interaction({
783
- name: 'notification_webhook',
784
- endpoint: '/webhook/notification'
785
- })
786
- async handleNotificationWebhook(data: any): Promise<InteractionResult> {
787
- return new InteractionResult(
788
- `Notification ${data.notificationId} was delivered`,
789
- `https://app.example.com/notifications/${data.notificationId}`
790
- );
791
- }
792
655
  }
793
656
  ```
794
657
 
@@ -816,7 +679,7 @@ This approach provides several benefits:
816
679
 
817
680
  ## Decorator Behavior and Instance Context
818
681
 
819
- The `@tool` and `@interaction` decorators provide intelligent instance context management that behaves differently depending on where the decorated methods are defined:
682
+ The `@tool` decorator provide intelligent instance context management that behaves differently depending on where the decorated methods are defined:
820
683
 
821
684
  ### ToolFunction Subclass Context
822
685
 
package/dist/index.d.ts CHANGED
@@ -4,5 +4,5 @@ export * from './types/Models';
4
4
  export * from './types/ToolError';
5
5
  export * from './decorator/Decorator';
6
6
  export * from './auth/TokenVerifier';
7
- export { Tool, Interaction, InteractionResult } from './service/Service';
7
+ export { Tool, Interaction, InteractionResult, NestedInteractions } from './service/Service';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.InteractionResult = exports.Interaction = exports.Tool = void 0;
17
+ exports.NestedInteractions = exports.InteractionResult = exports.Interaction = exports.Tool = void 0;
18
18
  __exportStar(require("./function/ToolFunction"), exports);
19
19
  __exportStar(require("./function/GlobalToolFunction"), exports);
20
20
  __exportStar(require("./types/Models"), exports);
@@ -25,4 +25,5 @@ var Service_1 = require("./service/Service");
25
25
  Object.defineProperty(exports, "Tool", { enumerable: true, get: function () { return Service_1.Tool; } });
26
26
  Object.defineProperty(exports, "Interaction", { enumerable: true, get: function () { return Service_1.Interaction; } });
27
27
  Object.defineProperty(exports, "InteractionResult", { enumerable: true, get: function () { return Service_1.InteractionResult; } });
28
+ Object.defineProperty(exports, "NestedInteractions", { enumerable: true, get: function () { return Service_1.NestedInteractions; } });
28
29
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,0DAAwC;AACxC,gEAA8C;AAC9C,iDAA+B;AAC/B,oDAAkC;AAClC,wDAAsC;AACtC,uDAAqC;AACrC,6CAAyE;AAAhE,+FAAA,IAAI,OAAA;AAAE,sGAAA,WAAW,OAAA;AAAE,4GAAA,iBAAiB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,0DAAwC;AACxC,gEAA8C;AAC9C,iDAA+B;AAC/B,oDAAkC;AAClC,wDAAsC;AACtC,uDAAqC;AACrC,6CAA6F;AAApF,+FAAA,IAAI,OAAA;AAAE,sGAAA,WAAW,OAAA;AAAE,4GAAA,iBAAiB,OAAA;AAAE,6GAAA,kBAAkB,OAAA"}
@@ -4,12 +4,10 @@ import * as App from '@zaiusinc/app-sdk';
4
4
  */
5
5
  export declare class ToolLogger {
6
6
  private static readonly SENSITIVE_FIELDS;
7
- private static readonly MAX_PARAM_LENGTH;
8
- private static readonly MAX_ARRAY_ITEMS;
9
7
  /**
10
8
  * Redacts sensitive data from an object
11
9
  */
12
- private static redactSensitiveData;
10
+ private static redactSensitiveDataAndTruncate;
13
11
  /**
14
12
  * Checks if a field name is considered sensitive
15
13
  */
@@ -22,6 +20,16 @@ export declare class ToolLogger {
22
20
  * Calculates content length of response data
23
21
  */
24
22
  private static calculateContentLength;
23
+ /**
24
+ * Extracts the response body as a string or parsed JSON object
25
+ */
26
+ private static getResponseBody;
27
+ /**
28
+ * Creates a summary of response body with security redaction and truncation
29
+ * For failed responses (4xx, 5xx): returns full body with redacted sensitive data
30
+ * For successful responses (2xx): returns first 100 chars with redacted sensitive data
31
+ */
32
+ private static createResponseBodySummary;
25
33
  /**
26
34
  * Logs an incoming request
27
35
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ToolLogger.d.ts","sourceRoot":"","sources":["../../src/logging/ToolLogger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,mBAAmB,CAAC;AAEzC;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAyCtC;IAEF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAO;IAC/C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAM;IAE7C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IA8ClC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAO/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAQrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAYrC;;OAEG;WACW,UAAU,CACtB,GAAG,EAAE,GAAG,CAAC,OAAO,GACf,IAAI;IAaP;;OAEG;WACW,WAAW,CACvB,GAAG,EAAE,GAAG,CAAC,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,EACtB,gBAAgB,CAAC,EAAE,MAAM,GACxB,IAAI;CAcR"}
1
+ {"version":3,"file":"ToolLogger.d.ts","sourceRoot":"","sources":["../../src/logging/ToolLogger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,mBAAmB,CAAC;AAMzC;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAyCtC;IAEF;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,8BAA8B;IA4D7C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAO/B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAQrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAYrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IA2C9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,yBAAyB;IA0CxC;;OAEG;WACW,UAAU,CACtB,GAAG,EAAE,GAAG,CAAC,OAAO,GACf,IAAI;IAaP;;OAEG;WACW,WAAW,CACvB,GAAG,EAAE,GAAG,CAAC,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,EACtB,gBAAgB,CAAC,EAAE,MAAM,GACxB,IAAI;CAoBR"}
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ToolLogger = void 0;
4
4
  const app_sdk_1 = require("@zaiusinc/app-sdk");
5
+ const MAX_PARAM_LOG_LENGTH = 128;
6
+ const MAX_BODY_LOG_LENGTH = 256;
7
+ const MAX_ARRAY_ITEMS = 2;
5
8
  /**
6
9
  * Utility class for logging Opal tool requests and responses with security considerations
7
10
  */
@@ -45,12 +48,13 @@ class ToolLogger {
45
48
  'jwt',
46
49
  'bearer_token'
47
50
  ];
48
- static MAX_PARAM_LENGTH = 100;
49
- static MAX_ARRAY_ITEMS = 10;
50
51
  /**
51
52
  * Redacts sensitive data from an object
52
53
  */
53
- static redactSensitiveData(data, maxDepth = 5) {
54
+ static redactSensitiveDataAndTruncate(data, maxDepth = 5, accumulatedLength = 0) {
55
+ if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
56
+ return '';
57
+ }
54
58
  if (maxDepth <= 0) {
55
59
  return '[MAX_DEPTH_EXCEEDED]';
56
60
  }
@@ -58,31 +62,42 @@ class ToolLogger {
58
62
  return data;
59
63
  }
60
64
  if (typeof data === 'string') {
61
- return data.length > this.MAX_PARAM_LENGTH
62
- ? `${data.substring(0, this.MAX_PARAM_LENGTH)}... (truncated, ${data.length} chars total)`
63
- : data;
65
+ if (data.length > MAX_PARAM_LOG_LENGTH) {
66
+ const lead = data.substring(0, MAX_PARAM_LOG_LENGTH - 10);
67
+ const tail = data.substring(data.length - 10);
68
+ return `${lead}...[${data.length - MAX_PARAM_LOG_LENGTH} truncated]...${tail}`;
69
+ }
70
+ else {
71
+ return data;
72
+ }
64
73
  }
65
74
  if (typeof data === 'number' || typeof data === 'boolean') {
66
75
  return data;
67
76
  }
68
77
  if (Array.isArray(data)) {
69
- const truncated = data.slice(0, this.MAX_ARRAY_ITEMS);
70
- const result = truncated.map((item) => this.redactSensitiveData(item, maxDepth));
71
- if (data.length > this.MAX_ARRAY_ITEMS) {
72
- result.push(`... (${data.length - this.MAX_ARRAY_ITEMS} more items truncated)`);
78
+ const truncated = data.slice(0, MAX_ARRAY_ITEMS);
79
+ const result = truncated.map((item) => this.redactSensitiveDataAndTruncate(item, maxDepth, accumulatedLength));
80
+ if (data.length > MAX_ARRAY_ITEMS) {
81
+ result.push(`... (${data.length - MAX_ARRAY_ITEMS} more items truncated)`);
73
82
  }
74
83
  return result;
75
84
  }
76
85
  if (typeof data === 'object') {
77
86
  const result = {};
78
87
  for (const [key, value] of Object.entries(data)) {
88
+ if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
89
+ break;
90
+ }
79
91
  // Check if this field contains sensitive data
80
92
  const isSensitive = this.isSensitiveField(key);
81
93
  if (isSensitive) {
82
94
  result[key] = '[REDACTED]';
83
95
  }
84
96
  else {
85
- result[key] = this.redactSensitiveData(value, maxDepth - 1);
97
+ result[key] = this.redactSensitiveDataAndTruncate(value, maxDepth - 1, accumulatedLength);
98
+ }
99
+ if (result[key]) {
100
+ accumulatedLength += JSON.stringify(result[key]).length;
86
101
  }
87
102
  }
88
103
  return result;
@@ -103,7 +118,7 @@ class ToolLogger {
103
118
  if (!params) {
104
119
  return null;
105
120
  }
106
- return this.redactSensitiveData(params);
121
+ return this.redactSensitiveDataAndTruncate(params);
107
122
  }
108
123
  /**
109
124
  * Calculates content length of response data
@@ -119,6 +134,87 @@ class ToolLogger {
119
134
  return 'unknown';
120
135
  }
121
136
  }
137
+ /**
138
+ * Extracts the response body as a string or parsed JSON object
139
+ */
140
+ static getResponseBody(response) {
141
+ if (!response) {
142
+ return null;
143
+ }
144
+ try {
145
+ const contentType = response.headers?.get('content-type') || '';
146
+ const isJson = contentType.includes('application/json') || contentType.includes('application/problem+json');
147
+ const isText = contentType.startsWith('text/');
148
+ if (!isJson && !isText) {
149
+ return null;
150
+ }
151
+ // Try to access bodyAsU8Array - this may throw
152
+ const bodyData = response.bodyAsU8Array;
153
+ if (!bodyData) {
154
+ return null;
155
+ }
156
+ // Convert Uint8Array to string
157
+ const bodyString = Buffer.from(bodyData).toString();
158
+ if (!bodyString) {
159
+ return null;
160
+ }
161
+ // Try to parse as JSON if content-type indicates JSON
162
+ if (isJson) {
163
+ try {
164
+ return JSON.parse(bodyString);
165
+ }
166
+ catch {
167
+ // If JSON parsing fails, return as string
168
+ return bodyString;
169
+ }
170
+ }
171
+ // Return as plain text for non-JSON content types
172
+ return bodyString;
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ /**
179
+ * Creates a summary of response body with security redaction and truncation
180
+ * For failed responses (4xx, 5xx): returns full body with redacted sensitive data
181
+ * For successful responses (2xx): returns first 100 chars with redacted sensitive data
182
+ */
183
+ static createResponseBodySummary(response, success) {
184
+ const body = this.getResponseBody(response);
185
+ if (body === null || body === undefined) {
186
+ return null;
187
+ }
188
+ // For objects (parsed JSON), apply redaction
189
+ if (typeof body === 'object') {
190
+ // For failed responses, don't truncate strings within the object
191
+ const redactedBody = this.redactSensitiveDataAndTruncate(body, 5);
192
+ // For successful responses, truncate to first MAX_BODY_LOG_LENGTH chars
193
+ if (success) {
194
+ const bodyString = JSON.stringify(redactedBody);
195
+ if (bodyString.length > MAX_BODY_LOG_LENGTH) {
196
+ const truncated = bodyString.substring(0, MAX_BODY_LOG_LENGTH);
197
+ return `${truncated}... (truncated)`;
198
+ }
199
+ return redactedBody;
200
+ }
201
+ // For failed responses, return full redacted body
202
+ return redactedBody;
203
+ }
204
+ // For strings (plain text or unparseable JSON)
205
+ if (typeof body === 'string') {
206
+ // For successful responses, truncate to first 100 chars
207
+ if (success) {
208
+ if (body.length > MAX_BODY_LOG_LENGTH) {
209
+ return `${body.substring(0, MAX_BODY_LOG_LENGTH)}... (truncated)`;
210
+ }
211
+ return body;
212
+ }
213
+ // For failed responses, return full body
214
+ return body;
215
+ }
216
+ return body;
217
+ }
122
218
  /**
123
219
  * Logs an incoming request
124
220
  */
@@ -137,6 +233,7 @@ class ToolLogger {
137
233
  * Logs a successful response
138
234
  */
139
235
  static logResponse(req, response, processingTimeMs) {
236
+ const success = response.status >= 200 && response.status < 300;
140
237
  const responseLog = {
141
238
  event: 'opal_tool_response',
142
239
  path: req.path,
@@ -144,8 +241,12 @@ class ToolLogger {
144
241
  status: response.status,
145
242
  contentType: response.headers?.get('content-type') || 'unknown',
146
243
  contentLength: this.calculateContentLength(response),
147
- success: response.status >= 200 && response.status < 300
244
+ success
148
245
  };
246
+ const responseBodySummary = this.createResponseBodySummary(response, success);
247
+ if (responseBodySummary) {
248
+ responseLog.responseBody = responseBodySummary;
249
+ }
149
250
  // Log with Zaius audience so developers only see requests for accounts they have access to
150
251
  app_sdk_1.logger.info(app_sdk_1.LogVisibility.Zaius, JSON.stringify(responseLog));
151
252
  }