@optimizely-opal/opal-tool-ocp-sdk 1.0.0-OCP-1442.5 → 1.0.0-OCP-1449.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.
- package/README.md +114 -72
- 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 +1 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +17 -4
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +54 -8
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +1 -1
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +24 -4
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +260 -8
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts.map +1 -1
- package/dist/logging/ToolLogger.js +13 -12
- package/dist/logging/ToolLogger.js.map +1 -1
- package/dist/logging/ToolLogger.test.js +171 -0
- package/dist/logging/ToolLogger.test.js.map +1 -1
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +227 -55
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +464 -36
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/ToolError.d.ts +59 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +79 -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 +161 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +5 -16
- package/dist/validation/ParameterValidator.d.ts.map +1 -1
- package/dist/validation/ParameterValidator.js +10 -3
- package/dist/validation/ParameterValidator.js.map +1 -1
- package/dist/validation/ParameterValidator.test.js +186 -146
- package/dist/validation/ParameterValidator.test.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/AuthUtils.test.ts +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +54 -8
- package/src/function/GlobalToolFunction.ts +26 -6
- package/src/function/ToolFunction.test.ts +274 -8
- package/src/function/ToolFunction.ts +33 -7
- package/src/index.ts +1 -0
- package/src/logging/ToolLogger.test.ts +184 -0
- package/src/logging/ToolLogger.ts +13 -12
- package/src/service/Service.test.ts +577 -34
- package/src/service/Service.ts +286 -54
- package/src/types/ToolError.test.ts +192 -0
- package/src/types/ToolError.ts +95 -0
- package/src/validation/ParameterValidator.test.ts +185 -158
- package/src/validation/ParameterValidator.ts +17 -20
|
@@ -22,12 +22,22 @@ jest.mock('@zaiusinc/app-sdk', () => ({
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
Request: jest.fn().mockImplementation(() => ({})),
|
|
25
|
-
Response: jest.fn().mockImplementation((status, data) => ({
|
|
25
|
+
Response: jest.fn().mockImplementation((status, data, headers) => ({
|
|
26
26
|
status,
|
|
27
27
|
data,
|
|
28
28
|
bodyJSON: data,
|
|
29
|
-
bodyAsU8Array: new Uint8Array()
|
|
29
|
+
bodyAsU8Array: new Uint8Array(),
|
|
30
|
+
headers
|
|
30
31
|
})),
|
|
32
|
+
Headers: jest.fn().mockImplementation((entries) => {
|
|
33
|
+
const headers = new Map();
|
|
34
|
+
if (entries) {
|
|
35
|
+
for (const [key, value] of entries) {
|
|
36
|
+
headers.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return headers;
|
|
40
|
+
}),
|
|
31
41
|
amendLogContext: jest.fn(),
|
|
32
42
|
getAppContext: jest.fn(),
|
|
33
43
|
logger: {
|
|
@@ -251,7 +261,13 @@ describe('ToolFunction', () => {
|
|
|
251
261
|
|
|
252
262
|
const result = await toolFunction.perform();
|
|
253
263
|
|
|
254
|
-
expect(result).
|
|
264
|
+
expect(result.status).toBe(403);
|
|
265
|
+
expect(result.bodyJSON).toEqual({
|
|
266
|
+
title: 'Forbidden',
|
|
267
|
+
status: 403,
|
|
268
|
+
detail: 'Invalid OptiID access token',
|
|
269
|
+
instance: '/test'
|
|
270
|
+
});
|
|
255
271
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
256
272
|
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
257
273
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
@@ -277,7 +293,13 @@ describe('ToolFunction', () => {
|
|
|
277
293
|
|
|
278
294
|
const result = await toolFunctionWithDifferentOrgId.perform();
|
|
279
295
|
|
|
280
|
-
expect(result).
|
|
296
|
+
expect(result.status).toBe(403);
|
|
297
|
+
expect(result.bodyJSON).toEqual({
|
|
298
|
+
title: 'Forbidden',
|
|
299
|
+
status: 403,
|
|
300
|
+
detail: 'Organization ID does not match',
|
|
301
|
+
instance: '/test'
|
|
302
|
+
});
|
|
281
303
|
expect(mockGetAppContext).toHaveBeenCalled();
|
|
282
304
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
283
305
|
});
|
|
@@ -302,7 +324,13 @@ describe('ToolFunction', () => {
|
|
|
302
324
|
|
|
303
325
|
const result = await toolFunctionWithoutToken.perform();
|
|
304
326
|
|
|
305
|
-
expect(result).
|
|
327
|
+
expect(result.status).toBe(403);
|
|
328
|
+
expect(result.bodyJSON).toEqual({
|
|
329
|
+
title: 'Forbidden',
|
|
330
|
+
status: 403,
|
|
331
|
+
detail: 'OptiID access token is required',
|
|
332
|
+
instance: '/test'
|
|
333
|
+
});
|
|
306
334
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
307
335
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
308
336
|
});
|
|
@@ -327,7 +355,13 @@ describe('ToolFunction', () => {
|
|
|
327
355
|
|
|
328
356
|
const result = await toolFunctionWithoutCustomerId.perform();
|
|
329
357
|
|
|
330
|
-
expect(result).
|
|
358
|
+
expect(result.status).toBe(403);
|
|
359
|
+
expect(result.bodyJSON).toEqual({
|
|
360
|
+
title: 'Forbidden',
|
|
361
|
+
status: 403,
|
|
362
|
+
detail: 'Organization ID is required',
|
|
363
|
+
instance: '/test'
|
|
364
|
+
});
|
|
331
365
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
332
366
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
333
367
|
});
|
|
@@ -346,7 +380,13 @@ describe('ToolFunction', () => {
|
|
|
346
380
|
|
|
347
381
|
const result = await toolFunctionWithoutAuth.perform();
|
|
348
382
|
|
|
349
|
-
expect(result).
|
|
383
|
+
expect(result.status).toBe(403);
|
|
384
|
+
expect(result.bodyJSON).toEqual({
|
|
385
|
+
title: 'Forbidden',
|
|
386
|
+
status: 403,
|
|
387
|
+
detail: 'Authentication data is required',
|
|
388
|
+
instance: '/test'
|
|
389
|
+
});
|
|
350
390
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
351
391
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
352
392
|
});
|
|
@@ -357,7 +397,13 @@ describe('ToolFunction', () => {
|
|
|
357
397
|
|
|
358
398
|
const result = await toolFunction.perform();
|
|
359
399
|
|
|
360
|
-
expect(result).
|
|
400
|
+
expect(result.status).toBe(403);
|
|
401
|
+
expect(result.bodyJSON).toEqual({
|
|
402
|
+
title: 'Forbidden',
|
|
403
|
+
status: 403,
|
|
404
|
+
detail: 'Token verification failed',
|
|
405
|
+
instance: '/test'
|
|
406
|
+
});
|
|
361
407
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
362
408
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
363
409
|
});
|
|
@@ -374,4 +420,224 @@ describe('ToolFunction', () => {
|
|
|
374
420
|
expect(toolFunction.getRequest()).toBe(mockRequest);
|
|
375
421
|
});
|
|
376
422
|
});
|
|
423
|
+
|
|
424
|
+
describe('internal request authentication', () => {
|
|
425
|
+
beforeEach(() => {
|
|
426
|
+
// Reset mocks before each test
|
|
427
|
+
jest.clearAllMocks();
|
|
428
|
+
setupAuthMocks();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should use internal authentication for /overrides endpoint', async () => {
|
|
432
|
+
const overridesRequest = {
|
|
433
|
+
path: '/overrides',
|
|
434
|
+
method: 'DELETE',
|
|
435
|
+
bodyJSON: {},
|
|
436
|
+
headers: {
|
|
437
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
438
|
+
if (name === 'Authorization' || name === 'authorization') return 'internal-token';
|
|
439
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
440
|
+
return null;
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
446
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
447
|
+
const result = await toolFunctionOverrides.perform();
|
|
448
|
+
|
|
449
|
+
expect(result).toBe(mockResponse);
|
|
450
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should throw ToolError when internal authentication fails for /overrides endpoint', async () => {
|
|
454
|
+
const overridesRequest = {
|
|
455
|
+
path: '/overrides',
|
|
456
|
+
method: 'DELETE',
|
|
457
|
+
bodyJSON: {},
|
|
458
|
+
headers: {
|
|
459
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
460
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
461
|
+
return null; // No Authorization header
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
467
|
+
const result = await toolFunctionOverrides.perform();
|
|
468
|
+
|
|
469
|
+
expect(result.status).toBe(401);
|
|
470
|
+
expect(result.bodyJSON).toEqual({
|
|
471
|
+
title: 'Unauthorized',
|
|
472
|
+
status: 401,
|
|
473
|
+
detail: 'Internal request authentication failed',
|
|
474
|
+
instance: '/overrides'
|
|
475
|
+
});
|
|
476
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should use regular authentication for non-overrides endpoints', async () => {
|
|
480
|
+
const regularRequest = {
|
|
481
|
+
...mockRequest,
|
|
482
|
+
path: '/regular-tool',
|
|
483
|
+
headers: {
|
|
484
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
485
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
486
|
+
return null;
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
492
|
+
const toolFunctionRegular = new TestToolFunction(regularRequest);
|
|
493
|
+
const result = await toolFunctionRegular.perform();
|
|
494
|
+
|
|
495
|
+
expect(result).toBe(mockResponse);
|
|
496
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(regularRequest, toolFunctionRegular);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should handle internal authentication with valid Authorization header', async () => {
|
|
500
|
+
const overridesRequest = {
|
|
501
|
+
path: '/overrides',
|
|
502
|
+
method: 'PATCH',
|
|
503
|
+
bodyJSON: {
|
|
504
|
+
functions: [
|
|
505
|
+
{
|
|
506
|
+
name: 'test_tool',
|
|
507
|
+
description: 'Updated description'
|
|
508
|
+
}
|
|
509
|
+
]
|
|
510
|
+
},
|
|
511
|
+
headers: {
|
|
512
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
513
|
+
if (name === 'Authorization') return 'valid-internal-token';
|
|
514
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
515
|
+
return null;
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
521
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
522
|
+
const result = await toolFunctionOverrides.perform();
|
|
523
|
+
|
|
524
|
+
expect(result).toBe(mockResponse);
|
|
525
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should handle internal authentication with lowercase authorization header', async () => {
|
|
529
|
+
const overridesRequest = {
|
|
530
|
+
path: '/overrides',
|
|
531
|
+
method: 'PATCH',
|
|
532
|
+
bodyJSON: {
|
|
533
|
+
functions: [
|
|
534
|
+
{
|
|
535
|
+
name: 'test_tool',
|
|
536
|
+
description: 'Updated description'
|
|
537
|
+
}
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
headers: {
|
|
541
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
542
|
+
if (name === 'authorization') return 'valid-internal-token';
|
|
543
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
544
|
+
return null;
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
550
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
551
|
+
const result = await toolFunctionOverrides.perform();
|
|
552
|
+
|
|
553
|
+
expect(result).toBe(mockResponse);
|
|
554
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should fail internal authentication when token verification fails', async () => {
|
|
558
|
+
// Mock token verifier to return false for invalid token
|
|
559
|
+
mockTokenVerifier.verify.mockResolvedValue(false);
|
|
560
|
+
|
|
561
|
+
const overridesRequest = {
|
|
562
|
+
path: '/overrides',
|
|
563
|
+
method: 'DELETE',
|
|
564
|
+
bodyJSON: {},
|
|
565
|
+
headers: {
|
|
566
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
567
|
+
if (name === 'Authorization') return 'invalid-token';
|
|
568
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
569
|
+
return null;
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
575
|
+
const result = await toolFunctionOverrides.perform();
|
|
576
|
+
|
|
577
|
+
expect(result.status).toBe(401);
|
|
578
|
+
expect(result.bodyJSON).toEqual({
|
|
579
|
+
title: 'Unauthorized',
|
|
580
|
+
status: 401,
|
|
581
|
+
detail: 'Internal request authentication failed',
|
|
582
|
+
instance: '/overrides'
|
|
583
|
+
});
|
|
584
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should fail internal authentication when token verifier throws error', async () => {
|
|
588
|
+
// Mock token verifier to throw an error
|
|
589
|
+
mockTokenVerifier.verify.mockRejectedValue(new Error('Token service unavailable'));
|
|
590
|
+
|
|
591
|
+
const overridesRequest = {
|
|
592
|
+
path: '/overrides',
|
|
593
|
+
method: 'DELETE',
|
|
594
|
+
bodyJSON: {},
|
|
595
|
+
headers: {
|
|
596
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
597
|
+
if (name === 'Authorization') return 'some-token';
|
|
598
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
599
|
+
return null;
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
605
|
+
const result = await toolFunctionOverrides.perform();
|
|
606
|
+
|
|
607
|
+
expect(result.status).toBe(401);
|
|
608
|
+
expect(result.bodyJSON).toEqual({
|
|
609
|
+
title: 'Unauthorized',
|
|
610
|
+
status: 401,
|
|
611
|
+
detail: 'Internal request authentication failed',
|
|
612
|
+
instance: '/overrides'
|
|
613
|
+
});
|
|
614
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('should fail internal authentication when headers object is missing', async () => {
|
|
618
|
+
const overridesRequest = {
|
|
619
|
+
path: '/overrides',
|
|
620
|
+
method: 'DELETE',
|
|
621
|
+
bodyJSON: {},
|
|
622
|
+
headers: {
|
|
623
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
624
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
625
|
+
return null;
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
631
|
+
const result = await toolFunctionOverrides.perform();
|
|
632
|
+
|
|
633
|
+
expect(result.status).toBe(401);
|
|
634
|
+
expect(result.bodyJSON).toEqual({
|
|
635
|
+
title: 'Unauthorized',
|
|
636
|
+
status: 401,
|
|
637
|
+
detail: 'Internal request authentication failed',
|
|
638
|
+
instance: '/overrides'
|
|
639
|
+
});
|
|
640
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
641
|
+
});
|
|
642
|
+
});
|
|
377
643
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
|
-
import { authenticateRegularRequest } from '../auth/AuthUtils';
|
|
1
|
+
import { Function, Response, Headers, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
|
+
import { authenticateRegularRequest, authenticateInternalRequest } from '../auth/AuthUtils';
|
|
3
3
|
import { toolsService } from '../service/Service';
|
|
4
4
|
import { ToolLogger } from '../logging/ToolLogger';
|
|
5
|
+
import { ToolError } from '../types/ToolError';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Abstract base class for tool-based function execution
|
|
@@ -42,8 +43,27 @@ export abstract class ToolFunction extends Function {
|
|
|
42
43
|
* @returns Response as the HTTP response
|
|
43
44
|
*/
|
|
44
45
|
private async handleRequest(): Promise<Response> {
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
try {
|
|
47
|
+
await this.authorizeRequest();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof ToolError) {
|
|
50
|
+
return new Response(
|
|
51
|
+
error.status,
|
|
52
|
+
error.toProblemDetails(this.request.path),
|
|
53
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
// Fallback for unexpected errors
|
|
57
|
+
return new Response(
|
|
58
|
+
500,
|
|
59
|
+
{
|
|
60
|
+
title: 'Internal Server Error',
|
|
61
|
+
status: 500,
|
|
62
|
+
detail: 'An unexpected error occurred during authentication',
|
|
63
|
+
instance: this.request.path
|
|
64
|
+
},
|
|
65
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
66
|
+
);
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
if (this.request.path === '/ready') {
|
|
@@ -58,9 +78,15 @@ export abstract class ToolFunction extends Function {
|
|
|
58
78
|
/**
|
|
59
79
|
* Authenticate the incoming request by validating the OptiID token and organization ID
|
|
60
80
|
*
|
|
61
|
-
* @
|
|
81
|
+
* @throws {ToolError} If authentication fails
|
|
62
82
|
*/
|
|
63
|
-
private async authorizeRequest(): Promise<
|
|
64
|
-
|
|
83
|
+
private async authorizeRequest(): Promise<void> {
|
|
84
|
+
// Use internal authentication for overrides endpoint (header-based token)
|
|
85
|
+
if (this.request.path === '/overrides') {
|
|
86
|
+
await authenticateInternalRequest(this.request);
|
|
87
|
+
} else {
|
|
88
|
+
// Use regular authentication for other endpoints (body-based token with org validation)
|
|
89
|
+
await authenticateRegularRequest(this.request);
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
92
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './function/ToolFunction';
|
|
2
2
|
export * from './function/GlobalToolFunction';
|
|
3
3
|
export * from './types/Models';
|
|
4
|
+
export * from './types/ToolError';
|
|
4
5
|
export * from './decorator/Decorator';
|
|
5
6
|
export * from './auth/TokenVerifier';
|
|
6
7
|
export { Tool, Interaction, InteractionResult } from './service/Service';
|
|
@@ -32,6 +32,7 @@ describe('ToolLogger', () => {
|
|
|
32
32
|
const createMockRequest = (overrides: any = {}): App.Request => {
|
|
33
33
|
const defaultRequest = {
|
|
34
34
|
path: '/test-tool',
|
|
35
|
+
method: 'POST',
|
|
35
36
|
bodyJSON: {
|
|
36
37
|
parameters: {
|
|
37
38
|
name: 'test',
|
|
@@ -96,6 +97,7 @@ describe('ToolLogger', () => {
|
|
|
96
97
|
const expectedLog = {
|
|
97
98
|
event: 'opal_tool_request',
|
|
98
99
|
path: '/test-tool',
|
|
100
|
+
method: 'POST',
|
|
99
101
|
parameters: {
|
|
100
102
|
name: 'test',
|
|
101
103
|
value: 'data'
|
|
@@ -118,6 +120,7 @@ describe('ToolLogger', () => {
|
|
|
118
120
|
expectJsonLog({
|
|
119
121
|
event: 'opal_tool_request',
|
|
120
122
|
path: '/test-tool',
|
|
123
|
+
method: 'POST',
|
|
121
124
|
parameters: null
|
|
122
125
|
});
|
|
123
126
|
});
|
|
@@ -135,6 +138,7 @@ describe('ToolLogger', () => {
|
|
|
135
138
|
expectJsonLog({
|
|
136
139
|
event: 'opal_tool_request',
|
|
137
140
|
path: '/test-tool',
|
|
141
|
+
method: 'POST',
|
|
138
142
|
parameters: {
|
|
139
143
|
name: 'direct',
|
|
140
144
|
action: 'test'
|
|
@@ -167,6 +171,7 @@ describe('ToolLogger', () => {
|
|
|
167
171
|
expectJsonLog({
|
|
168
172
|
event: 'opal_tool_request',
|
|
169
173
|
path: '/test-tool',
|
|
174
|
+
method: 'POST',
|
|
170
175
|
parameters: {
|
|
171
176
|
username: 'john',
|
|
172
177
|
password: '[REDACTED]',
|
|
@@ -204,6 +209,7 @@ describe('ToolLogger', () => {
|
|
|
204
209
|
expectJsonLog({
|
|
205
210
|
event: 'opal_tool_request',
|
|
206
211
|
path: '/test-tool',
|
|
212
|
+
method: 'POST',
|
|
207
213
|
parameters: {
|
|
208
214
|
PASSWORD: '[REDACTED]',
|
|
209
215
|
API_KEY: '[REDACTED]',
|
|
@@ -232,6 +238,7 @@ describe('ToolLogger', () => {
|
|
|
232
238
|
expectJsonLog({
|
|
233
239
|
event: 'opal_tool_request',
|
|
234
240
|
path: '/test-tool',
|
|
241
|
+
method: 'POST',
|
|
235
242
|
parameters: {
|
|
236
243
|
description: `${'a'.repeat(100)}... (truncated, 150 chars total)`,
|
|
237
244
|
short_field: 'normal'
|
|
@@ -255,6 +262,7 @@ describe('ToolLogger', () => {
|
|
|
255
262
|
expectJsonLog({
|
|
256
263
|
event: 'opal_tool_request',
|
|
257
264
|
path: '/test-tool',
|
|
265
|
+
method: 'POST',
|
|
258
266
|
parameters: {
|
|
259
267
|
items: [
|
|
260
268
|
...largeArray.slice(0, 10),
|
|
@@ -291,6 +299,7 @@ describe('ToolLogger', () => {
|
|
|
291
299
|
expectJsonLog({
|
|
292
300
|
event: 'opal_tool_request',
|
|
293
301
|
path: '/test-tool',
|
|
302
|
+
method: 'POST',
|
|
294
303
|
parameters: {
|
|
295
304
|
user: {
|
|
296
305
|
name: 'John',
|
|
@@ -328,6 +337,7 @@ describe('ToolLogger', () => {
|
|
|
328
337
|
expectJsonLog({
|
|
329
338
|
event: 'opal_tool_request',
|
|
330
339
|
path: '/test-tool',
|
|
340
|
+
method: 'POST',
|
|
331
341
|
parameters: {
|
|
332
342
|
nullValue: null,
|
|
333
343
|
emptyString: '',
|
|
@@ -353,6 +363,7 @@ describe('ToolLogger', () => {
|
|
|
353
363
|
expectJsonLog({
|
|
354
364
|
event: 'opal_tool_request',
|
|
355
365
|
path: '/test-tool',
|
|
366
|
+
method: 'POST',
|
|
356
367
|
parameters: {
|
|
357
368
|
credentials: '[REDACTED]',
|
|
358
369
|
public_list: ['item1', 'item2']
|
|
@@ -381,6 +392,7 @@ describe('ToolLogger', () => {
|
|
|
381
392
|
expectJsonLog({
|
|
382
393
|
event: 'opal_tool_request',
|
|
383
394
|
path: '/test-tool',
|
|
395
|
+
method: 'POST',
|
|
384
396
|
parameters: {
|
|
385
397
|
auth: '[REDACTED]',
|
|
386
398
|
public_config: {
|
|
@@ -445,6 +457,78 @@ describe('ToolLogger', () => {
|
|
|
445
457
|
const level4 = loggedData.parameters.deep.level1.level2.level3.level4;
|
|
446
458
|
expect(level4).toBe('[MAX_DEPTH_EXCEEDED]');
|
|
447
459
|
});
|
|
460
|
+
|
|
461
|
+
it('should handle arrays containing deeply nested objects that exceed max depth', () => {
|
|
462
|
+
// Create a structure where the array is shallow enough to be processed (depth 3),
|
|
463
|
+
// but individual objects within the array exceed the max depth
|
|
464
|
+
const arrayWithDeepObjects = {
|
|
465
|
+
level1: {
|
|
466
|
+
items: [
|
|
467
|
+
{
|
|
468
|
+
level2: {
|
|
469
|
+
level3: {
|
|
470
|
+
level4: {
|
|
471
|
+
level5: {
|
|
472
|
+
password: 'secret-in-deep-array-object',
|
|
473
|
+
credit_card: '1234-5678-9012-3456'
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
'simple-item',
|
|
480
|
+
{
|
|
481
|
+
shallow: 'data'
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
level2: {
|
|
485
|
+
level3: {
|
|
486
|
+
level4: {
|
|
487
|
+
another: 'deep-object'
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const req = createMockRequest({
|
|
497
|
+
bodyJSON: { parameters: arrayWithDeepObjects }
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
ToolLogger.logRequest(req);
|
|
501
|
+
|
|
502
|
+
// Verify the array structure and depth handling
|
|
503
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
504
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
505
|
+
|
|
506
|
+
const items = loggedData.parameters.level1.items;
|
|
507
|
+
|
|
508
|
+
// First item: deeply nested object with inner parts replaced by placeholder
|
|
509
|
+
expect(items[0]).toEqual({
|
|
510
|
+
level2: {
|
|
511
|
+
level3: {
|
|
512
|
+
level4: '[MAX_DEPTH_EXCEEDED]'
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Second item: simple string should remain unchanged
|
|
518
|
+
expect(items[1]).toBe('simple-item');
|
|
519
|
+
|
|
520
|
+
// Third item: shallow object should be processed normally
|
|
521
|
+
expect(items[2]).toEqual({ shallow: 'data' });
|
|
522
|
+
|
|
523
|
+
// Fourth item: another deeply nested object with inner parts replaced by placeholder
|
|
524
|
+
expect(items[3]).toEqual({
|
|
525
|
+
level2: {
|
|
526
|
+
level3: {
|
|
527
|
+
level4: '[MAX_DEPTH_EXCEEDED]'
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
});
|
|
448
532
|
});
|
|
449
533
|
|
|
450
534
|
describe('logResponse', () => {
|
|
@@ -626,6 +710,7 @@ describe('ToolLogger', () => {
|
|
|
626
710
|
expectJsonLog({
|
|
627
711
|
event: 'opal_tool_request',
|
|
628
712
|
path: '/test-tool',
|
|
713
|
+
method: 'POST',
|
|
629
714
|
parameters: {}
|
|
630
715
|
});
|
|
631
716
|
});
|
|
@@ -644,6 +729,7 @@ describe('ToolLogger', () => {
|
|
|
644
729
|
expectJsonLog({
|
|
645
730
|
event: 'opal_tool_request',
|
|
646
731
|
path: '/test-tool',
|
|
732
|
+
method: 'POST',
|
|
647
733
|
parameters: {
|
|
648
734
|
field: 'value'
|
|
649
735
|
}
|
|
@@ -670,6 +756,7 @@ describe('ToolLogger', () => {
|
|
|
670
756
|
expectJsonLog({
|
|
671
757
|
event: 'opal_tool_request',
|
|
672
758
|
path: '/test-tool',
|
|
759
|
+
method: 'POST',
|
|
673
760
|
parameters: {
|
|
674
761
|
string: 'text',
|
|
675
762
|
number: 42,
|
|
@@ -681,5 +768,102 @@ describe('ToolLogger', () => {
|
|
|
681
768
|
}
|
|
682
769
|
});
|
|
683
770
|
});
|
|
771
|
+
|
|
772
|
+
it('should handle tool override request with enhanced parameter descriptions', () => {
|
|
773
|
+
const overrideRequest = {
|
|
774
|
+
tools: [
|
|
775
|
+
{
|
|
776
|
+
name: 'calculate_experiment_runtime',
|
|
777
|
+
description: 'OVERRIDDEN: Enhanced experiment runtime calculator with advanced features',
|
|
778
|
+
parameters: [
|
|
779
|
+
{
|
|
780
|
+
name: 'BCR',
|
|
781
|
+
type: 'number',
|
|
782
|
+
description: 'OVERRIDDEN: Enhanced baseline conversion rate with validation',
|
|
783
|
+
required: true
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: 'MDE',
|
|
787
|
+
type: 'number',
|
|
788
|
+
description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
|
|
789
|
+
required: true
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: 'sigLevel',
|
|
793
|
+
type: 'number',
|
|
794
|
+
description: 'OVERRIDDEN: Enhanced statistical significance level',
|
|
795
|
+
required: true
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: 'numVariations',
|
|
799
|
+
type: 'number',
|
|
800
|
+
description: 'OVERRIDDEN: Enhanced number of variations handling',
|
|
801
|
+
required: true
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: 'dailyVisitors',
|
|
805
|
+
type: 'number',
|
|
806
|
+
description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
|
|
807
|
+
required: true
|
|
808
|
+
}
|
|
809
|
+
]
|
|
810
|
+
}
|
|
811
|
+
// Note: NOT including calculate_sample_size in override
|
|
812
|
+
]
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const req = createMockRequest({
|
|
816
|
+
path: '/overrides',
|
|
817
|
+
bodyJSON: overrideRequest
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
ToolLogger.logRequest(req);
|
|
821
|
+
|
|
822
|
+
expectJsonLog({
|
|
823
|
+
event: 'opal_tool_request',
|
|
824
|
+
path: '/overrides',
|
|
825
|
+
method: 'POST',
|
|
826
|
+
parameters: {
|
|
827
|
+
tools: [
|
|
828
|
+
{
|
|
829
|
+
name: 'calculate_experiment_runtime',
|
|
830
|
+
description: 'OVERRIDDEN: Enhanced experiment runtime calculator with advanced features',
|
|
831
|
+
parameters: [
|
|
832
|
+
{
|
|
833
|
+
name: 'BCR',
|
|
834
|
+
type: 'number',
|
|
835
|
+
description: 'OVERRIDDEN: Enhanced baseline conversion rate with validation',
|
|
836
|
+
required: true
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: 'MDE',
|
|
840
|
+
type: 'number',
|
|
841
|
+
description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
|
|
842
|
+
required: true
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: 'sigLevel',
|
|
846
|
+
type: 'number',
|
|
847
|
+
description: 'OVERRIDDEN: Enhanced statistical significance level',
|
|
848
|
+
required: true
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
name: 'numVariations',
|
|
852
|
+
type: 'number',
|
|
853
|
+
description: 'OVERRIDDEN: Enhanced number of variations handling',
|
|
854
|
+
required: true
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
name: 'dailyVisitors',
|
|
858
|
+
type: 'number',
|
|
859
|
+
description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
|
|
860
|
+
required: true
|
|
861
|
+
}
|
|
862
|
+
]
|
|
863
|
+
}
|
|
864
|
+
]
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
});
|
|
684
868
|
});
|
|
685
869
|
});
|