@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.7 → 1.0.0-beta.9

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 (58) hide show
  1. package/dist/auth/AuthUtils.d.ts +7 -0
  2. package/dist/auth/AuthUtils.d.ts.map +1 -1
  3. package/dist/auth/AuthUtils.js +27 -0
  4. package/dist/auth/AuthUtils.js.map +1 -1
  5. package/dist/auth/AuthUtils.test.js +99 -0
  6. package/dist/auth/AuthUtils.test.js.map +1 -1
  7. package/dist/function/GlobalToolFunction.d.ts +1 -1
  8. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  9. package/dist/function/GlobalToolFunction.js +4 -1
  10. package/dist/function/GlobalToolFunction.js.map +1 -1
  11. package/dist/function/ToolFunction.d.ts +1 -1
  12. package/dist/function/ToolFunction.d.ts.map +1 -1
  13. package/dist/function/ToolFunction.js +12 -2
  14. package/dist/function/ToolFunction.js.map +1 -1
  15. package/dist/function/ToolFunction.test.js +206 -0
  16. package/dist/function/ToolFunction.test.js.map +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/logging/ToolLogger.d.ts.map +1 -1
  22. package/dist/logging/ToolLogger.js +2 -1
  23. package/dist/logging/ToolLogger.js.map +1 -1
  24. package/dist/logging/ToolLogger.test.js +114 -2
  25. package/dist/logging/ToolLogger.test.js.map +1 -1
  26. package/dist/service/Service.d.ts +73 -0
  27. package/dist/service/Service.d.ts.map +1 -1
  28. package/dist/service/Service.js +188 -42
  29. package/dist/service/Service.js.map +1 -1
  30. package/dist/service/Service.test.js +380 -34
  31. package/dist/service/Service.test.js.map +1 -1
  32. package/dist/types/Models.d.ts +3 -1
  33. package/dist/types/Models.d.ts.map +1 -1
  34. package/dist/types/Models.js +5 -1
  35. package/dist/types/Models.js.map +1 -1
  36. package/dist/types/ToolError.d.ts +13 -0
  37. package/dist/types/ToolError.d.ts.map +1 -1
  38. package/dist/types/ToolError.js +30 -2
  39. package/dist/types/ToolError.js.map +1 -1
  40. package/dist/types/ToolError.test.js +27 -3
  41. package/dist/types/ToolError.test.js.map +1 -1
  42. package/dist/validation/ParameterValidator.test.js +2 -1
  43. package/dist/validation/ParameterValidator.test.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/auth/AuthUtils.test.ts +115 -1
  46. package/src/auth/AuthUtils.ts +30 -1
  47. package/src/function/GlobalToolFunction.ts +5 -2
  48. package/src/function/ToolFunction.test.ts +220 -0
  49. package/src/function/ToolFunction.ts +13 -4
  50. package/src/index.ts +1 -1
  51. package/src/logging/ToolLogger.test.ts +118 -2
  52. package/src/logging/ToolLogger.ts +2 -1
  53. package/src/service/Service.test.ts +474 -32
  54. package/src/service/Service.ts +245 -41
  55. package/src/types/Models.ts +3 -1
  56. package/src/types/ToolError.test.ts +33 -3
  57. package/src/types/ToolError.ts +32 -2
  58. package/src/validation/ParameterValidator.test.ts +4 -1
@@ -436,4 +436,224 @@ describe('ToolFunction', () => {
436
436
  expect(toolFunction.getRequest()).toBe(mockRequest);
437
437
  });
438
438
  });
439
+
440
+ describe('internal request authentication', () => {
441
+ beforeEach(() => {
442
+ // Reset mocks before each test
443
+ jest.clearAllMocks();
444
+ setupAuthMocks();
445
+ });
446
+
447
+ it('should use internal authentication for /overrides endpoint', async () => {
448
+ const overridesRequest = {
449
+ path: '/overrides',
450
+ method: 'DELETE',
451
+ bodyJSON: {},
452
+ headers: {
453
+ get: jest.fn().mockImplementation((name: string) => {
454
+ if (name === 'Authorization' || name === 'authorization') return 'internal-token';
455
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
456
+ return null;
457
+ })
458
+ }
459
+ };
460
+
461
+ mockProcessRequest.mockResolvedValue(mockResponse);
462
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
463
+ const result = await toolFunctionOverrides.perform();
464
+
465
+ expect(result).toBe(mockResponse);
466
+ expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
467
+ });
468
+
469
+ it('should throw ToolError when internal authentication fails for /overrides endpoint', async () => {
470
+ const overridesRequest = {
471
+ path: '/overrides',
472
+ method: 'DELETE',
473
+ bodyJSON: {},
474
+ headers: {
475
+ get: jest.fn().mockImplementation((name: string) => {
476
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
477
+ return null; // No Authorization header
478
+ })
479
+ }
480
+ };
481
+
482
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
483
+ const result = await toolFunctionOverrides.perform();
484
+
485
+ expect(result.status).toBe(401);
486
+ expect(result.bodyJSON).toEqual({
487
+ title: 'Unauthorized',
488
+ status: 401,
489
+ detail: 'Internal request authentication failed',
490
+ instance: '/overrides'
491
+ });
492
+ expect(mockProcessRequest).not.toHaveBeenCalled();
493
+ });
494
+
495
+ it('should use regular authentication for non-overrides endpoints', async () => {
496
+ const regularRequest = {
497
+ ...mockRequest,
498
+ path: '/regular-tool',
499
+ headers: {
500
+ get: jest.fn().mockImplementation((name: string) => {
501
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
502
+ return null;
503
+ })
504
+ }
505
+ };
506
+
507
+ mockProcessRequest.mockResolvedValue(mockResponse);
508
+ const toolFunctionRegular = new TestToolFunction(regularRequest);
509
+ const result = await toolFunctionRegular.perform();
510
+
511
+ expect(result).toBe(mockResponse);
512
+ expect(mockProcessRequest).toHaveBeenCalledWith(regularRequest, toolFunctionRegular);
513
+ });
514
+
515
+ it('should handle internal authentication with valid Authorization header', async () => {
516
+ const overridesRequest = {
517
+ path: '/overrides',
518
+ method: 'PATCH',
519
+ bodyJSON: {
520
+ functions: [
521
+ {
522
+ name: 'test_tool',
523
+ description: 'Updated description'
524
+ }
525
+ ]
526
+ },
527
+ headers: {
528
+ get: jest.fn().mockImplementation((name: string) => {
529
+ if (name === 'Authorization') return 'valid-internal-token';
530
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
531
+ return null;
532
+ })
533
+ }
534
+ };
535
+
536
+ mockProcessRequest.mockResolvedValue(mockResponse);
537
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
538
+ const result = await toolFunctionOverrides.perform();
539
+
540
+ expect(result).toBe(mockResponse);
541
+ expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
542
+ });
543
+
544
+ it('should handle internal authentication with lowercase authorization header', async () => {
545
+ const overridesRequest = {
546
+ path: '/overrides',
547
+ method: 'PATCH',
548
+ bodyJSON: {
549
+ functions: [
550
+ {
551
+ name: 'test_tool',
552
+ description: 'Updated description'
553
+ }
554
+ ]
555
+ },
556
+ headers: {
557
+ get: jest.fn().mockImplementation((name: string) => {
558
+ if (name === 'authorization') return 'valid-internal-token';
559
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
560
+ return null;
561
+ })
562
+ }
563
+ };
564
+
565
+ mockProcessRequest.mockResolvedValue(mockResponse);
566
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
567
+ const result = await toolFunctionOverrides.perform();
568
+
569
+ expect(result).toBe(mockResponse);
570
+ expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
571
+ });
572
+
573
+ it('should fail internal authentication when token verification fails', async () => {
574
+ // Mock token verifier to return false for invalid token
575
+ mockTokenVerifier.verify.mockResolvedValue(false);
576
+
577
+ const overridesRequest = {
578
+ path: '/overrides',
579
+ method: 'DELETE',
580
+ bodyJSON: {},
581
+ headers: {
582
+ get: jest.fn().mockImplementation((name: string) => {
583
+ if (name === 'Authorization') return 'invalid-token';
584
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
585
+ return null;
586
+ })
587
+ }
588
+ };
589
+
590
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
591
+ const result = await toolFunctionOverrides.perform();
592
+
593
+ expect(result.status).toBe(401);
594
+ expect(result.bodyJSON).toEqual({
595
+ title: 'Unauthorized',
596
+ status: 401,
597
+ detail: 'Internal request authentication failed',
598
+ instance: '/overrides'
599
+ });
600
+ expect(mockProcessRequest).not.toHaveBeenCalled();
601
+ });
602
+
603
+ it('should fail internal authentication when token verifier throws error', async () => {
604
+ // Mock token verifier to throw an error
605
+ mockTokenVerifier.verify.mockRejectedValue(new Error('Token service unavailable'));
606
+
607
+ const overridesRequest = {
608
+ path: '/overrides',
609
+ method: 'DELETE',
610
+ bodyJSON: {},
611
+ headers: {
612
+ get: jest.fn().mockImplementation((name: string) => {
613
+ if (name === 'Authorization') return 'some-token';
614
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
615
+ return null;
616
+ })
617
+ }
618
+ };
619
+
620
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
621
+ const result = await toolFunctionOverrides.perform();
622
+
623
+ expect(result.status).toBe(401);
624
+ expect(result.bodyJSON).toEqual({
625
+ title: 'Unauthorized',
626
+ status: 401,
627
+ detail: 'Internal request authentication failed',
628
+ instance: '/overrides'
629
+ });
630
+ expect(mockProcessRequest).not.toHaveBeenCalled();
631
+ });
632
+
633
+ it('should fail internal authentication when headers object is missing', async () => {
634
+ const overridesRequest = {
635
+ path: '/overrides',
636
+ method: 'DELETE',
637
+ bodyJSON: {},
638
+ headers: {
639
+ get: jest.fn().mockImplementation((name: string) => {
640
+ if (name === 'x-opal-thread-id') return 'test-thread-id';
641
+ return null;
642
+ })
643
+ }
644
+ };
645
+
646
+ const toolFunctionOverrides = new TestToolFunction(overridesRequest);
647
+ const result = await toolFunctionOverrides.perform();
648
+
649
+ expect(result.status).toBe(401);
650
+ expect(result.bodyJSON).toEqual({
651
+ title: 'Unauthorized',
652
+ status: 401,
653
+ detail: 'Internal request authentication failed',
654
+ instance: '/overrides'
655
+ });
656
+ expect(mockProcessRequest).not.toHaveBeenCalled();
657
+ });
658
+ });
439
659
  });
@@ -1,5 +1,5 @@
1
1
  import { Function, Response, Headers, amendLogContext } from '@zaiusinc/app-sdk';
2
- import { authenticateRegularRequest } from '../auth/AuthUtils';
2
+ import { authenticateRegularRequest, authenticateInternalRequest } from '../auth/AuthUtils';
3
3
  import { toolsService } from '../service/Service';
4
4
  import { ToolLogger } from '../logging/ToolLogger';
5
5
  import { ToolError } from '../types/ToolError';
@@ -17,7 +17,7 @@ export abstract class ToolFunction extends Function {
17
17
  * @async
18
18
  * @returns ReadyResponse containing ready status and optional reason when not ready
19
19
  */
20
- protected ready(): Promise<ReadyResponse> {
20
+ protected ready(): Promise<ReadyResponse | boolean> {
21
21
  return Promise.resolve({ ready: true });
22
22
  }
23
23
 
@@ -68,7 +68,10 @@ export abstract class ToolFunction extends Function {
68
68
  }
69
69
 
70
70
  if (this.request.path === '/ready') {
71
- const readyResponse = await this.ready();
71
+ const readyResult = await this.ready();
72
+ const readyResponse = typeof readyResult === 'boolean'
73
+ ? { ready: readyResult }
74
+ : readyResult;
72
75
  return new Response(200, readyResponse);
73
76
  }
74
77
 
@@ -82,6 +85,12 @@ export abstract class ToolFunction extends Function {
82
85
  * @throws {ToolError} If authentication fails
83
86
  */
84
87
  private async authorizeRequest(): Promise<void> {
85
- await authenticateRegularRequest(this.request);
88
+ // Use internal authentication for overrides endpoint (header-based token)
89
+ if (this.request.path === '/overrides') {
90
+ await authenticateInternalRequest(this.request);
91
+ } else {
92
+ // Use regular authentication for other endpoints (body-based token with org validation)
93
+ await authenticateRegularRequest(this.request);
94
+ }
86
95
  }
87
96
  }
package/src/index.ts CHANGED
@@ -4,4 +4,4 @@ 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';
@@ -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: {
@@ -496,7 +508,9 @@ describe('ToolLogger', () => {
496
508
  // First item: deeply nested object with inner parts replaced by placeholder
497
509
  expect(items[0]).toEqual({
498
510
  level2: {
499
- level3: '[MAX_DEPTH_EXCEEDED]'
511
+ level3: {
512
+ level4: '[MAX_DEPTH_EXCEEDED]'
513
+ }
500
514
  }
501
515
  });
502
516
 
@@ -509,7 +523,9 @@ describe('ToolLogger', () => {
509
523
  // Fourth item: another deeply nested object with inner parts replaced by placeholder
510
524
  expect(items[3]).toEqual({
511
525
  level2: {
512
- level3: '[MAX_DEPTH_EXCEEDED]'
526
+ level3: {
527
+ level4: '[MAX_DEPTH_EXCEEDED]'
528
+ }
513
529
  }
514
530
  });
515
531
  });
@@ -694,6 +710,7 @@ describe('ToolLogger', () => {
694
710
  expectJsonLog({
695
711
  event: 'opal_tool_request',
696
712
  path: '/test-tool',
713
+ method: 'POST',
697
714
  parameters: {}
698
715
  });
699
716
  });
@@ -712,6 +729,7 @@ describe('ToolLogger', () => {
712
729
  expectJsonLog({
713
730
  event: 'opal_tool_request',
714
731
  path: '/test-tool',
732
+ method: 'POST',
715
733
  parameters: {
716
734
  field: 'value'
717
735
  }
@@ -738,6 +756,7 @@ describe('ToolLogger', () => {
738
756
  expectJsonLog({
739
757
  event: 'opal_tool_request',
740
758
  path: '/test-tool',
759
+ method: 'POST',
741
760
  parameters: {
742
761
  string: 'text',
743
762
  number: 42,
@@ -749,5 +768,102 @@ describe('ToolLogger', () => {
749
768
  }
750
769
  });
751
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
+ });
752
868
  });
753
869
  });
@@ -75,7 +75,7 @@ export class ToolLogger {
75
75
 
76
76
  if (Array.isArray(data)) {
77
77
  const truncated = data.slice(0, this.MAX_ARRAY_ITEMS);
78
- const result = truncated.map((item) => this.redactSensitiveData(item, maxDepth - 1));
78
+ const result = truncated.map((item) => this.redactSensitiveData(item, maxDepth));
79
79
  if (data.length > this.MAX_ARRAY_ITEMS) {
80
80
  result.push(`... (${data.length - this.MAX_ARRAY_ITEMS} more items truncated)`);
81
81
  }
@@ -146,6 +146,7 @@ export class ToolLogger {
146
146
  const requestLog = {
147
147
  event: 'opal_tool_request',
148
148
  path: req.path,
149
+ method: req.method,
149
150
  parameters: this.createParameterSummary(params)
150
151
  };
151
152