@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.1 → 1.0.0-beta.10

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 (74) hide show
  1. package/README.md +169 -3
  2. package/dist/auth/AuthUtils.d.ts +12 -5
  3. package/dist/auth/AuthUtils.d.ts.map +1 -1
  4. package/dist/auth/AuthUtils.js +80 -25
  5. package/dist/auth/AuthUtils.js.map +1 -1
  6. package/dist/auth/AuthUtils.test.js +161 -117
  7. package/dist/auth/AuthUtils.test.js.map +1 -1
  8. package/dist/function/GlobalToolFunction.d.ts +5 -3
  9. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  10. package/dist/function/GlobalToolFunction.js +32 -8
  11. package/dist/function/GlobalToolFunction.js.map +1 -1
  12. package/dist/function/GlobalToolFunction.test.js +73 -12
  13. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  14. package/dist/function/ToolFunction.d.ts +11 -4
  15. package/dist/function/ToolFunction.d.ts.map +1 -1
  16. package/dist/function/ToolFunction.js +45 -9
  17. package/dist/function/ToolFunction.js.map +1 -1
  18. package/dist/function/ToolFunction.test.js +278 -11
  19. package/dist/function/ToolFunction.test.js.map +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/logging/ToolLogger.d.ts +42 -0
  25. package/dist/logging/ToolLogger.d.ts.map +1 -0
  26. package/dist/logging/ToolLogger.js +255 -0
  27. package/dist/logging/ToolLogger.js.map +1 -0
  28. package/dist/logging/ToolLogger.test.d.ts +2 -0
  29. package/dist/logging/ToolLogger.test.d.ts.map +1 -0
  30. package/dist/logging/ToolLogger.test.js +864 -0
  31. package/dist/logging/ToolLogger.test.js.map +1 -0
  32. package/dist/service/Service.d.ts +88 -2
  33. package/dist/service/Service.d.ts.map +1 -1
  34. package/dist/service/Service.js +228 -39
  35. package/dist/service/Service.js.map +1 -1
  36. package/dist/service/Service.test.js +558 -22
  37. package/dist/service/Service.test.js.map +1 -1
  38. package/dist/types/Models.d.ts +7 -1
  39. package/dist/types/Models.d.ts.map +1 -1
  40. package/dist/types/Models.js +5 -1
  41. package/dist/types/Models.js.map +1 -1
  42. package/dist/types/ToolError.d.ts +72 -0
  43. package/dist/types/ToolError.d.ts.map +1 -0
  44. package/dist/types/ToolError.js +107 -0
  45. package/dist/types/ToolError.js.map +1 -0
  46. package/dist/types/ToolError.test.d.ts +2 -0
  47. package/dist/types/ToolError.test.d.ts.map +1 -0
  48. package/dist/types/ToolError.test.js +185 -0
  49. package/dist/types/ToolError.test.js.map +1 -0
  50. package/dist/validation/ParameterValidator.d.ts +31 -0
  51. package/dist/validation/ParameterValidator.d.ts.map +1 -0
  52. package/dist/validation/ParameterValidator.js +129 -0
  53. package/dist/validation/ParameterValidator.js.map +1 -0
  54. package/dist/validation/ParameterValidator.test.d.ts +2 -0
  55. package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
  56. package/dist/validation/ParameterValidator.test.js +323 -0
  57. package/dist/validation/ParameterValidator.test.js.map +1 -0
  58. package/package.json +3 -3
  59. package/src/auth/AuthUtils.test.ts +176 -157
  60. package/src/auth/AuthUtils.ts +96 -33
  61. package/src/function/GlobalToolFunction.test.ts +78 -14
  62. package/src/function/GlobalToolFunction.ts +46 -11
  63. package/src/function/ToolFunction.test.ts +298 -13
  64. package/src/function/ToolFunction.ts +61 -13
  65. package/src/index.ts +2 -1
  66. package/src/logging/ToolLogger.test.ts +1020 -0
  67. package/src/logging/ToolLogger.ts +292 -0
  68. package/src/service/Service.test.ts +712 -28
  69. package/src/service/Service.ts +288 -38
  70. package/src/types/Models.ts +8 -1
  71. package/src/types/ToolError.test.ts +222 -0
  72. package/src/types/ToolError.ts +125 -0
  73. package/src/validation/ParameterValidator.test.ts +371 -0
  74. package/src/validation/ParameterValidator.ts +150 -0
@@ -0,0 +1,292 @@
1
+ import { logger, LogVisibility } from '@zaiusinc/app-sdk';
2
+ import * as App from '@zaiusinc/app-sdk';
3
+
4
+ const MAX_PARAM_LOG_LENGTH = 128;
5
+ const MAX_BODY_LOG_LENGTH = 256;
6
+ const MAX_ARRAY_ITEMS = 2;
7
+
8
+ /**
9
+ * Utility class for logging Opal tool requests and responses with security considerations
10
+ */
11
+ export class ToolLogger {
12
+ private static readonly SENSITIVE_FIELDS = [
13
+ // Authentication / secrets
14
+ 'password',
15
+ 'pass',
16
+ 'secret',
17
+ 'key',
18
+ 'token',
19
+ 'auth',
20
+ 'credentials',
21
+ 'access_token',
22
+ 'refresh_token',
23
+ 'api_key',
24
+ 'private_key',
25
+ 'client_secret',
26
+ 'session_token',
27
+ 'authorization',
28
+
29
+ // Payment-related
30
+ 'card_number',
31
+ 'credit_card',
32
+ 'cvv',
33
+ 'expiry_date',
34
+
35
+ // Personal info
36
+ 'ssn', // social security number
37
+ 'nid', // national ID
38
+ 'passport',
39
+ 'dob', // date of birth
40
+ 'email',
41
+ 'phone',
42
+ 'address',
43
+
44
+ // Misc / environment
45
+ 'otp',
46
+ 'pin',
47
+ 'security_answer',
48
+ 'security_question',
49
+ 'signing_key',
50
+ 'encryption_key',
51
+ 'jwt',
52
+ 'bearer_token'
53
+ ];
54
+
55
+ /**
56
+ * Redacts sensitive data from an object
57
+ */
58
+ private static redactSensitiveDataAndTruncate(data: any, maxDepth = 5, accumulatedLength = 0): any {
59
+ if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
60
+ return '';
61
+ }
62
+ if (maxDepth <= 0) {
63
+ return '[MAX_DEPTH_EXCEEDED]';
64
+ }
65
+
66
+ if (data === null || data === undefined) {
67
+ return data;
68
+ }
69
+
70
+ if (typeof data === 'string') {
71
+ if (data.length > MAX_PARAM_LOG_LENGTH) {
72
+ const lead = data.substring(0, MAX_PARAM_LOG_LENGTH - 10);
73
+ const tail = data.substring(data.length - 10);
74
+ return `${lead}...[${data.length - MAX_PARAM_LOG_LENGTH} truncated]...${tail}`;
75
+ } else {
76
+ return data;
77
+ }
78
+ }
79
+
80
+ if (typeof data === 'number' || typeof data === 'boolean') {
81
+ return data;
82
+ }
83
+
84
+ if (Array.isArray(data)) {
85
+ const truncated = data.slice(0, MAX_ARRAY_ITEMS);
86
+ const result = truncated.map((item) => this.redactSensitiveDataAndTruncate(item, maxDepth, accumulatedLength));
87
+ if (data.length > MAX_ARRAY_ITEMS) {
88
+ result.push(`... (${data.length - MAX_ARRAY_ITEMS} more items truncated)`);
89
+ }
90
+ return result;
91
+ }
92
+
93
+ if (typeof data === 'object') {
94
+ const result: any = {};
95
+ for (const [key, value] of Object.entries(data)) {
96
+ if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
97
+ break;
98
+ }
99
+ // Check if this field contains sensitive data
100
+ const isSensitive = this.isSensitiveField(key);
101
+
102
+ if (isSensitive) {
103
+ result[key] = '[REDACTED]';
104
+ } else {
105
+ result[key] = this.redactSensitiveDataAndTruncate(value, maxDepth - 1, accumulatedLength);
106
+ }
107
+
108
+ if (result[key]) {
109
+ accumulatedLength += JSON.stringify(result[key]).length;
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+
115
+ return data;
116
+ }
117
+
118
+ /**
119
+ * Checks if a field name is considered sensitive
120
+ */
121
+ private static isSensitiveField(fieldName: string): boolean {
122
+ const lowerKey = fieldName.toLowerCase();
123
+ return this.SENSITIVE_FIELDS.some((sensitiveField) =>
124
+ lowerKey.includes(sensitiveField)
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Creates a summary of request parameters
130
+ */
131
+ private static createParameterSummary(params: any): any {
132
+ if (!params) {
133
+ return null;
134
+ }
135
+
136
+ return this.redactSensitiveDataAndTruncate(params);
137
+ }
138
+
139
+ /**
140
+ * Calculates content length of response data
141
+ */
142
+ private static calculateContentLength(response?: App.Response) {
143
+ if (!response) {
144
+ return 0;
145
+ }
146
+
147
+ try {
148
+ return response.bodyAsU8Array?.length || 'unknown';
149
+ } catch {
150
+ return 'unknown';
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Extracts the response body as a string or parsed JSON object
156
+ */
157
+ private static getResponseBody(response?: App.Response): any {
158
+ if (!response) {
159
+ return null;
160
+ }
161
+
162
+ try {
163
+ const contentType = response.headers?.get('content-type') || '';
164
+ const isJson = contentType.includes('application/json') || contentType.includes('application/problem+json');
165
+ const isText = contentType.startsWith('text/');
166
+
167
+ if (!isJson && !isText) {
168
+ return null;
169
+ }
170
+
171
+ // Try to access bodyAsU8Array - this may throw
172
+ const bodyData = response.bodyAsU8Array;
173
+ if (!bodyData) {
174
+ return null;
175
+ }
176
+
177
+ // Convert Uint8Array to string
178
+ const bodyString = Buffer.from(bodyData).toString();
179
+ if (!bodyString) {
180
+ return null;
181
+ }
182
+
183
+ // Try to parse as JSON if content-type indicates JSON
184
+ if (isJson) {
185
+ try {
186
+ return JSON.parse(bodyString);
187
+ } catch {
188
+ // If JSON parsing fails, return as string
189
+ return bodyString;
190
+ }
191
+ }
192
+
193
+ // Return as plain text for non-JSON content types
194
+ return bodyString;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Creates a summary of response body with security redaction and truncation
202
+ * For failed responses (4xx, 5xx): returns full body with redacted sensitive data
203
+ * For successful responses (2xx): returns first 100 chars with redacted sensitive data
204
+ */
205
+ private static createResponseBodySummary(response?: App.Response, success?: boolean): any {
206
+ const body = this.getResponseBody(response);
207
+ if (body === null || body === undefined) {
208
+ return null;
209
+ }
210
+
211
+ // For objects (parsed JSON), apply redaction
212
+ if (typeof body === 'object') {
213
+ // For failed responses, don't truncate strings within the object
214
+ const redactedBody = this.redactSensitiveDataAndTruncate(body, 5);
215
+
216
+ // For successful responses, truncate to first MAX_BODY_LOG_LENGTH chars
217
+ if (success) {
218
+ const bodyString = JSON.stringify(redactedBody);
219
+ if (bodyString.length > MAX_BODY_LOG_LENGTH) {
220
+ const truncated = bodyString.substring(0, MAX_BODY_LOG_LENGTH);
221
+ return `${truncated}... (truncated)`;
222
+ }
223
+ return redactedBody;
224
+ }
225
+
226
+ // For failed responses, return full redacted body
227
+ return redactedBody;
228
+ }
229
+
230
+ // For strings (plain text or unparseable JSON)
231
+ if (typeof body === 'string') {
232
+ // For successful responses, truncate to first 100 chars
233
+ if (success) {
234
+ if (body.length > MAX_BODY_LOG_LENGTH) {
235
+ return `${body.substring(0, MAX_BODY_LOG_LENGTH)}... (truncated)`;
236
+ }
237
+ return body;
238
+ }
239
+
240
+ // For failed responses, return full body
241
+ return body;
242
+ }
243
+
244
+ return body;
245
+ }
246
+
247
+ /**
248
+ * Logs an incoming request
249
+ */
250
+ public static logRequest(
251
+ req: App.Request,
252
+ ): void {
253
+ const params = req.bodyJSON && req.bodyJSON.parameters ? req.bodyJSON.parameters : req.bodyJSON;
254
+ const requestLog = {
255
+ event: 'opal_tool_request',
256
+ path: req.path,
257
+ method: req.method,
258
+ parameters: this.createParameterSummary(params)
259
+ };
260
+
261
+ // Log with Zaius audience so developers only see requests for accounts they have access to
262
+ logger.info(LogVisibility.Zaius, JSON.stringify(requestLog));
263
+ }
264
+
265
+ /**
266
+ * Logs a successful response
267
+ */
268
+ public static logResponse(
269
+ req: App.Request,
270
+ response: App.Response,
271
+ processingTimeMs?: number
272
+ ): void {
273
+ const success = response.status >= 200 && response.status < 300;
274
+ const responseLog: any = {
275
+ event: 'opal_tool_response',
276
+ path: req.path,
277
+ duration: processingTimeMs ? `${processingTimeMs}ms` : undefined,
278
+ status: response.status,
279
+ contentType: response.headers?.get('content-type') || 'unknown',
280
+ contentLength: this.calculateContentLength(response),
281
+ success
282
+ };
283
+
284
+ const responseBodySummary = this.createResponseBodySummary(response, success);
285
+ if (responseBodySummary) {
286
+ responseLog.responseBody = responseBodySummary;
287
+ }
288
+
289
+ // Log with Zaius audience so developers only see requests for accounts they have access to
290
+ logger.info(LogVisibility.Zaius, JSON.stringify(responseLog));
291
+ }
292
+ }