@optimizely-opal/opal-tool-ocp-sdk 1.0.0-OCP-1441.3 → 1.0.0-OCP-1442.1

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 (36) hide show
  1. package/README.md +42 -0
  2. package/dist/function/GlobalToolFunction.d.ts +1 -0
  3. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  4. package/dist/function/GlobalToolFunction.js +8 -0
  5. package/dist/function/GlobalToolFunction.js.map +1 -1
  6. package/dist/function/GlobalToolFunction.test.js +3 -0
  7. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  8. package/dist/function/ToolFunction.d.ts +7 -1
  9. package/dist/function/ToolFunction.d.ts.map +1 -1
  10. package/dist/function/ToolFunction.js +16 -2
  11. package/dist/function/ToolFunction.js.map +1 -1
  12. package/dist/function/ToolFunction.test.d.ts +2 -0
  13. package/dist/function/ToolFunction.test.d.ts.map +1 -0
  14. package/dist/function/ToolFunction.test.js +317 -0
  15. package/dist/function/ToolFunction.test.js.map +1 -0
  16. package/dist/logging/ToolLogger.d.ts +34 -0
  17. package/dist/logging/ToolLogger.d.ts.map +1 -0
  18. package/dist/logging/ToolLogger.js +125 -0
  19. package/dist/logging/ToolLogger.js.map +1 -0
  20. package/dist/logging/ToolLogger.test.d.ts +2 -0
  21. package/dist/logging/ToolLogger.test.d.ts.map +1 -0
  22. package/dist/logging/ToolLogger.test.js +473 -0
  23. package/dist/logging/ToolLogger.test.js.map +1 -0
  24. package/dist/service/Service.js +1 -1
  25. package/dist/service/Service.js.map +1 -1
  26. package/dist/service/Service.test.js +24 -2
  27. package/dist/service/Service.test.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/function/GlobalToolFunction.test.ts +3 -0
  30. package/src/function/GlobalToolFunction.ts +11 -0
  31. package/src/function/ToolFunction.test.ts +377 -0
  32. package/src/function/ToolFunction.ts +21 -2
  33. package/src/logging/ToolLogger.test.ts +617 -0
  34. package/src/logging/ToolLogger.ts +146 -0
  35. package/src/service/Service.test.ts +25 -2
  36. package/src/service/Service.ts +2 -2
@@ -0,0 +1,146 @@
1
+ import { logger, LogVisibility } from '@zaiusinc/app-sdk';
2
+ import * as App from '@zaiusinc/app-sdk';
3
+
4
+ /**
5
+ * Utility class for logging Opal tool requests and responses with security considerations
6
+ */
7
+ export class ToolLogger {
8
+ private static readonly SENSITIVE_FIELDS = [
9
+ 'password',
10
+ 'secret',
11
+ 'key',
12
+ 'token',
13
+ 'auth',
14
+ 'credentials',
15
+ 'access_token',
16
+ 'refresh_token',
17
+ 'api_key',
18
+ 'private_key',
19
+ 'client_secret'
20
+ ];
21
+
22
+ private static readonly MAX_PARAM_LENGTH = 100;
23
+ private static readonly MAX_ARRAY_ITEMS = 10;
24
+
25
+ /**
26
+ * Redacts sensitive data from an object
27
+ */
28
+ private static redactSensitiveData(data: any, maxDepth = 5): any {
29
+ if (maxDepth <= 0 || data === null || data === undefined) {
30
+ return data;
31
+ }
32
+
33
+ if (typeof data === 'string') {
34
+ return data.length > this.MAX_PARAM_LENGTH
35
+ ? `${data.substring(0, this.MAX_PARAM_LENGTH)}... (truncated, ${data.length} chars total)`
36
+ : data;
37
+ }
38
+
39
+ if (typeof data === 'number' || typeof data === 'boolean') {
40
+ return data;
41
+ }
42
+
43
+ if (Array.isArray(data)) {
44
+ const truncated = data.slice(0, this.MAX_ARRAY_ITEMS);
45
+ const result = truncated.map((item) => this.redactSensitiveData(item, maxDepth - 1));
46
+ if (data.length > this.MAX_ARRAY_ITEMS) {
47
+ result.push(`... (${data.length - this.MAX_ARRAY_ITEMS} more items truncated)`);
48
+ }
49
+ return result;
50
+ }
51
+
52
+ if (typeof data === 'object') {
53
+ const result: any = {};
54
+ for (const [key, value] of Object.entries(data)) {
55
+ // Check if this field contains sensitive data
56
+ const isSensitive = this.isSensitiveField(key);
57
+
58
+ if (isSensitive) {
59
+ result[key] = '[REDACTED]';
60
+ } else {
61
+ result[key] = this.redactSensitiveData(value, maxDepth - 1);
62
+ }
63
+ }
64
+ return result;
65
+ }
66
+
67
+ return data;
68
+ }
69
+
70
+ /**
71
+ * Checks if a field name is considered sensitive
72
+ */
73
+ private static isSensitiveField(fieldName: string): boolean {
74
+ const lowerKey = fieldName.toLowerCase();
75
+ return this.SENSITIVE_FIELDS.some((sensitiveField) =>
76
+ lowerKey.includes(sensitiveField)
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Creates a summary of request parameters
82
+ */
83
+ private static createParameterSummary(params: any): any {
84
+ if (!params) {
85
+ return null;
86
+ }
87
+
88
+ return this.redactSensitiveData(params);
89
+ }
90
+
91
+ /**
92
+ * Calculates content length of response data
93
+ */
94
+ private static calculateContentLength(responseData?: any): number | string {
95
+ if (!responseData) {
96
+ return 0;
97
+ }
98
+
99
+ try {
100
+ const serialized = JSON.stringify(responseData);
101
+ return serialized.length;
102
+ } catch {
103
+ return 'unknown';
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Logs an incoming request
109
+ */
110
+ public static logRequest(
111
+ req: App.Request,
112
+ ): void {
113
+ const params = req.bodyJSON && req.bodyJSON.parameters ? req.bodyJSON.parameters : req.bodyJSON;
114
+ const requestLog = {
115
+ event: 'opal_tool_request',
116
+ path: req.path,
117
+ parameters: this.createParameterSummary(params)
118
+ };
119
+
120
+ // Log with Zaius audience so developers only see requests for accounts they have access to
121
+ logger.info(LogVisibility.Zaius, requestLog);
122
+ }
123
+
124
+ /**
125
+ * Logs a successful response
126
+ */
127
+ public static logResponse(
128
+ req: App.Request,
129
+ response: App.Response,
130
+ processingTimeMs?: number
131
+ ): void {
132
+
133
+ const responseLog = {
134
+ event: 'opal_tool_response',
135
+ path: req.path,
136
+ duration: processingTimeMs ? `${processingTimeMs}ms` : undefined,
137
+ status: response.status,
138
+ contentType: response.headers?.get('content-type') || 'unknown',
139
+ contentLength: this.calculateContentLength(response.bodyJSON),
140
+ success: response.status >= 200 && response.status < 300
141
+ };
142
+
143
+ // Log with Zaius audience so developers only see requests for accounts they have access to
144
+ logger.info(LogVisibility.Zaius, responseLog);
145
+ }
146
+ }
@@ -11,11 +11,30 @@ jest.mock('@zaiusinc/app-sdk', () => ({
11
11
  Function: class {
12
12
  public constructor(public request: any) {}
13
13
  },
14
- Response: jest.fn().mockImplementation((status, data) => ({
14
+ Headers: class {
15
+ public constructor(headers: string[][] = []) {
16
+ this.headers = {};
17
+ headers.forEach(([name, value]) => {
18
+ this.headers[name.toLowerCase()] = value;
19
+ });
20
+ }
21
+ public headers: { [key: string]: string };
22
+ public set(name: string, value: string) {
23
+ this.headers[name.toLowerCase()] = value;
24
+ }
25
+ public get(name: string) {
26
+ return this.headers[name.toLowerCase()];
27
+ }
28
+ public has(name: string) {
29
+ return name.toLowerCase() in this.headers;
30
+ }
31
+ },
32
+ Response: jest.fn().mockImplementation((status, data, headers) => ({
15
33
  status,
16
34
  data,
17
35
  bodyJSON: data,
18
- bodyAsU8Array: new Uint8Array()
36
+ bodyAsU8Array: new Uint8Array(),
37
+ headers: headers || { get: jest.fn(), has: jest.fn(), set: jest.fn() }
19
38
  }))
20
39
  }));
21
40
 
@@ -734,6 +753,10 @@ describe('ToolsService', () => {
734
753
  expect(errors[0]).toHaveProperty('field', 'name');
735
754
  expect(errors[0]).toHaveProperty('message', "Parameter 'name' must be a string, but received number");
736
755
 
756
+ // Check that the content type is set to application/problem+json for RFC 9457 compliance
757
+ expect(response.headers).toBeDefined();
758
+ expect(response.headers.get('content-type')).toBe('application/problem+json');
759
+
737
760
  // Verify the handler was not called
738
761
  expect(toolWithTypedParams.handler).not.toHaveBeenCalled();
739
762
  });
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable max-classes-per-file */
2
2
  import { AuthRequirement, Parameter } from '../types/Models';
3
3
  import * as App from '@zaiusinc/app-sdk';
4
- import { logger } from '@zaiusinc/app-sdk';
4
+ import { logger, Headers } from '@zaiusinc/app-sdk';
5
5
  import { ToolFunction } from '../function/ToolFunction';
6
6
  import { GlobalToolFunction } from '../function/GlobalToolFunction';
7
7
  import { ParameterValidator } from '../validation/ParameterValidator';
@@ -187,7 +187,7 @@ export class ToolsService {
187
187
  field: error.field,
188
188
  message: error.message
189
189
  }))
190
- });
190
+ }, new Headers([['content-type', 'application/problem+json']]));
191
191
  }
192
192
  }
193
193