@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.
- package/README.md +169 -3
- package/dist/auth/AuthUtils.d.ts +12 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +80 -25
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +161 -117
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +5 -3
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +32 -8
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +73 -12
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +11 -4
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +45 -9
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +278 -11
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +42 -0
- package/dist/logging/ToolLogger.d.ts.map +1 -0
- package/dist/logging/ToolLogger.js +255 -0
- package/dist/logging/ToolLogger.js.map +1 -0
- package/dist/logging/ToolLogger.test.d.ts +2 -0
- package/dist/logging/ToolLogger.test.d.ts.map +1 -0
- package/dist/logging/ToolLogger.test.js +864 -0
- package/dist/logging/ToolLogger.test.js.map +1 -0
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +228 -39
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +558 -22
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +7 -1
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +5 -1
- package/dist/types/Models.js.map +1 -1
- package/dist/types/ToolError.d.ts +72 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +107 -0
- package/dist/types/ToolError.js.map +1 -0
- package/dist/types/ToolError.test.d.ts +2 -0
- package/dist/types/ToolError.test.d.ts.map +1 -0
- package/dist/types/ToolError.test.js +185 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +31 -0
- package/dist/validation/ParameterValidator.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.js +129 -0
- package/dist/validation/ParameterValidator.js.map +1 -0
- package/dist/validation/ParameterValidator.test.d.ts +2 -0
- package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.test.js +323 -0
- package/dist/validation/ParameterValidator.test.js.map +1 -0
- package/package.json +3 -3
- package/src/auth/AuthUtils.test.ts +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +78 -14
- package/src/function/GlobalToolFunction.ts +46 -11
- package/src/function/ToolFunction.test.ts +298 -13
- package/src/function/ToolFunction.ts +61 -13
- package/src/index.ts +2 -1
- package/src/logging/ToolLogger.test.ts +1020 -0
- package/src/logging/ToolLogger.ts +292 -0
- package/src/service/Service.test.ts +712 -28
- package/src/service/Service.ts +288 -38
- package/src/types/Models.ts +8 -1
- package/src/types/ToolError.test.ts +222 -0
- package/src/types/ToolError.ts +125 -0
- package/src/validation/ParameterValidator.test.ts +371 -0
- 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
|
+
}
|