@optimizely-opal/opal-tool-ocp-sdk 0.0.0-OCP-1487.6 → 0.0.0-OCP-1487.8

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 (35) hide show
  1. package/README.md +56 -11
  2. package/dist/auth/AuthUtils.d.ts +15 -4
  3. package/dist/auth/AuthUtils.d.ts.map +1 -1
  4. package/dist/auth/AuthUtils.js +8 -7
  5. package/dist/auth/AuthUtils.js.map +1 -1
  6. package/dist/auth/AuthUtils.test.js +39 -39
  7. package/dist/auth/AuthUtils.test.js.map +1 -1
  8. package/dist/decorator/Decorator.d.ts +6 -1
  9. package/dist/decorator/Decorator.d.ts.map +1 -1
  10. package/dist/decorator/Decorator.js +32 -2
  11. package/dist/decorator/Decorator.js.map +1 -1
  12. package/dist/decorator/Decorator.test.js +182 -10
  13. package/dist/decorator/Decorator.test.js.map +1 -1
  14. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  15. package/dist/function/GlobalToolFunction.js +8 -2
  16. package/dist/function/GlobalToolFunction.js.map +1 -1
  17. package/dist/function/ToolFunction.js +1 -1
  18. package/dist/function/ToolFunction.js.map +1 -1
  19. package/dist/service/Service.d.ts +11 -1
  20. package/dist/service/Service.d.ts.map +1 -1
  21. package/dist/service/Service.js +22 -8
  22. package/dist/service/Service.js.map +1 -1
  23. package/dist/utils/ImportUtils.d.ts +15 -0
  24. package/dist/utils/ImportUtils.d.ts.map +1 -0
  25. package/dist/utils/ImportUtils.js +77 -0
  26. package/dist/utils/ImportUtils.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/auth/AuthUtils.test.ts +40 -40
  29. package/src/auth/AuthUtils.ts +6 -6
  30. package/src/decorator/Decorator.test.ts +265 -12
  31. package/src/decorator/Decorator.ts +43 -3
  32. package/src/function/GlobalToolFunction.ts +10 -3
  33. package/src/function/ToolFunction.ts +2 -2
  34. package/src/service/Service.ts +25 -8
  35. package/src/utils/ImportUtils.ts +45 -0
@@ -9,6 +9,9 @@ jest.mock('../service/Service', () => ({
9
9
  registerInteraction: jest.fn()
10
10
  }
11
11
  }));
12
+
13
+ // Mock base class for testing auto-detection
14
+ class GlobalToolFunction {}
12
15
  describe('Decorators', () => {
13
16
  beforeEach(() => {
14
17
  jest.clearAllMocks();
@@ -657,17 +660,15 @@ describe('Decorators', () => {
657
660
  });
658
661
 
659
662
  describe('global tool registration', () => {
660
- it('should register a global tool with isGlobal: true', () => {
661
- const config = {
662
- name: 'globalTool',
663
- description: 'A global tool',
663
+ it('should auto-detect global tools when class extends GlobalToolFunction', () => {
664
+ const config: ToolConfig = {
665
+ name: 'autoGlobalTool',
666
+ description: 'An auto-detected global tool',
664
667
  parameters: [],
665
- endpoint: '/global-tool',
666
- authRequirements: [],
667
- isGlobal: true
668
+ endpoint: '/auto-global-tool'
668
669
  };
669
670
 
670
- class GlobalTestClass {
671
+ class TestGlobalClass extends GlobalToolFunction {
671
672
  @tool(config)
672
673
  public async testTool(): Promise<{ result: string }> {
673
674
  return { result: 'global-tool-result' };
@@ -675,16 +676,268 @@ describe('Decorators', () => {
675
676
  }
676
677
 
677
678
  expect(toolsService.registerTool).toHaveBeenCalledWith(
678
- 'globalTool',
679
- 'A global tool',
679
+ 'autoGlobalTool',
680
+ 'An auto-detected global tool',
681
+ expect.any(Function),
682
+ [],
683
+ '/auto-global-tool',
684
+ [],
685
+ true
686
+ );
687
+
688
+ expect(TestGlobalClass).toBeDefined();
689
+ });
690
+
691
+ it('should register regular tools when class does not extend GlobalToolFunction', () => {
692
+ const config: ToolConfig = {
693
+ name: 'regularTool',
694
+ description: 'A regular tool',
695
+ parameters: [],
696
+ endpoint: '/regular-tool'
697
+ };
698
+
699
+ class TestRegularClass {
700
+ @tool(config)
701
+ public async testTool(): Promise<{ result: string }> {
702
+ return { result: 'regular-tool-result' };
703
+ }
704
+ }
705
+
706
+ expect(toolsService.registerTool).toHaveBeenCalledWith(
707
+ 'regularTool',
708
+ 'A regular tool',
709
+ expect.any(Function),
710
+ [],
711
+ '/regular-tool',
712
+ [],
713
+ false
714
+ );
715
+
716
+ expect(TestRegularClass).toBeDefined();
717
+ });
718
+
719
+ it('should register tools as global when import context is set', () => {
720
+ // Set global import context
721
+ globalThis.__IMPORT_CONTEXT = 'global';
722
+
723
+ const config: ToolConfig = {
724
+ name: 'importedGlobalTool',
725
+ description: 'A tool imported as global',
726
+ parameters: [],
727
+ endpoint: '/imported-global-tool'
728
+ };
729
+
730
+ class TestImportedClass {
731
+ @tool(config)
732
+ public async testTool(): Promise<{ result: string }> {
733
+ return { result: 'imported-global-tool-result' };
734
+ }
735
+ }
736
+
737
+ expect(toolsService.registerTool).toHaveBeenCalledWith(
738
+ 'importedGlobalTool',
739
+ 'A tool imported as global',
680
740
  expect.any(Function),
681
741
  [],
682
- '/global-tool',
742
+ '/imported-global-tool',
683
743
  [],
684
744
  true
685
745
  );
686
746
 
687
- expect(GlobalTestClass).toBeDefined();
747
+ // Clean up
748
+ globalThis.__IMPORT_CONTEXT = undefined;
749
+ expect(TestImportedClass).toBeDefined();
750
+ });
751
+
752
+ it('should register tools as regular when import context is set to regular', () => {
753
+ // Set regular import context
754
+ globalThis.__IMPORT_CONTEXT = 'regular';
755
+
756
+ const config: ToolConfig = {
757
+ name: 'importedRegularTool',
758
+ description: 'A tool imported as regular',
759
+ parameters: [],
760
+ endpoint: '/imported-regular-tool'
761
+ };
762
+
763
+ class TestImportedClass {
764
+ @tool(config)
765
+ public async testTool(): Promise<{ result: string }> {
766
+ return { result: 'imported-regular-tool-result' };
767
+ }
768
+ }
769
+
770
+ expect(toolsService.registerTool).toHaveBeenCalledWith(
771
+ 'importedRegularTool',
772
+ 'A tool imported as regular',
773
+ expect.any(Function),
774
+ [],
775
+ '/imported-regular-tool',
776
+ [],
777
+ false
778
+ );
779
+
780
+ // Clean up
781
+ globalThis.__IMPORT_CONTEXT = undefined;
782
+ expect(TestImportedClass).toBeDefined();
783
+ });
784
+
785
+ it('should handle mixed imports - both regular and global tools together', () => {
786
+ // Test importing regular tools first
787
+ globalThis.__IMPORT_CONTEXT = 'regular';
788
+
789
+ const regularConfig: ToolConfig = {
790
+ name: 'mixedRegularTool',
791
+ description: 'A regular tool in mixed scenario',
792
+ parameters: [],
793
+ endpoint: '/mixed-regular-tool'
794
+ };
795
+
796
+ class RegularToolClass {
797
+ @tool(regularConfig)
798
+ public async regularTool(): Promise<{ result: string }> {
799
+ return { result: 'mixed-regular-result' };
800
+ }
801
+ }
802
+
803
+ // Switch to global context
804
+ globalThis.__IMPORT_CONTEXT = 'global';
805
+
806
+ const globalConfig: ToolConfig = {
807
+ name: 'mixedGlobalTool',
808
+ description: 'A global tool in mixed scenario',
809
+ parameters: [],
810
+ endpoint: '/mixed-global-tool'
811
+ };
812
+
813
+ class GlobalToolClass {
814
+ @tool(globalConfig)
815
+ public async globalTool(): Promise<{ result: string }> {
816
+ return { result: 'mixed-global-result' };
817
+ }
818
+ }
819
+
820
+ // Clear context
821
+ globalThis.__IMPORT_CONTEXT = undefined;
822
+
823
+ // Verify regular tool was registered as regular
824
+ expect(toolsService.registerTool).toHaveBeenCalledWith(
825
+ 'mixedRegularTool',
826
+ 'A regular tool in mixed scenario',
827
+ expect.any(Function),
828
+ [],
829
+ '/mixed-regular-tool',
830
+ [],
831
+ false
832
+ );
833
+
834
+ // Verify global tool was registered as global
835
+ expect(toolsService.registerTool).toHaveBeenCalledWith(
836
+ 'mixedGlobalTool',
837
+ 'A global tool in mixed scenario',
838
+ expect.any(Function),
839
+ [],
840
+ '/mixed-global-tool',
841
+ [],
842
+ true
843
+ );
844
+
845
+ expect(RegularToolClass).toBeDefined();
846
+ expect(GlobalToolClass).toBeDefined();
847
+ });
848
+
849
+ it('should handle context switching correctly within same test', () => {
850
+ let callCount = 0;
851
+ const originalRegisterTool = jest.mocked(toolsService.registerTool);
852
+
853
+ // Track call order and contexts
854
+ originalRegisterTool.mockImplementation(() => {
855
+ callCount++;
856
+ // Store the isGlobal flag from each call for verification
857
+ });
858
+
859
+ // First: Import as regular
860
+ globalThis.__IMPORT_CONTEXT = 'regular';
861
+
862
+ class FirstClass {
863
+ @tool({
864
+ name: 'contextTest1',
865
+ description: 'First context test',
866
+ parameters: [],
867
+ endpoint: '/context-test-1'
868
+ })
869
+ public async tool1() {
870
+ return { result: 'test1' };
871
+ }
872
+ }
873
+
874
+ // Second: Switch to global
875
+ globalThis.__IMPORT_CONTEXT = 'global';
876
+
877
+ class SecondClass {
878
+ @tool({
879
+ name: 'contextTest2',
880
+ description: 'Second context test',
881
+ parameters: [],
882
+ endpoint: '/context-test-2'
883
+ })
884
+ public async tool2() {
885
+ return { result: 'test2' };
886
+ }
887
+ }
888
+
889
+ // Third: Clear context (should fallback to class inheritance)
890
+ globalThis.__IMPORT_CONTEXT = undefined;
891
+
892
+ class ThirdClass {
893
+ @tool({
894
+ name: 'contextTest3',
895
+ description: 'Third context test',
896
+ parameters: [],
897
+ endpoint: '/context-test-3'
898
+ })
899
+ public async tool3() {
900
+ return { result: 'test3' };
901
+ }
902
+ }
903
+
904
+ // Verify the sequence of calls
905
+ expect(toolsService.registerTool).toHaveBeenNthCalledWith(
906
+ callCount - 2, // First call
907
+ 'contextTest1',
908
+ 'First context test',
909
+ expect.any(Function),
910
+ [],
911
+ '/context-test-1',
912
+ [],
913
+ false // Should be regular
914
+ );
915
+
916
+ expect(toolsService.registerTool).toHaveBeenNthCalledWith(
917
+ callCount - 1, // Second call
918
+ 'contextTest2',
919
+ 'Second context test',
920
+ expect.any(Function),
921
+ [],
922
+ '/context-test-2',
923
+ [],
924
+ true // Should be global
925
+ );
926
+
927
+ expect(toolsService.registerTool).toHaveBeenNthCalledWith(
928
+ callCount, // Third call
929
+ 'contextTest3',
930
+ 'Third context test',
931
+ expect.any(Function),
932
+ [],
933
+ '/context-test-3',
934
+ [],
935
+ false // Should be regular (fallback to class check, ThirdClass doesn't extend GlobalToolFunction)
936
+ );
937
+
938
+ expect(FirstClass).toBeDefined();
939
+ expect(SecondClass).toBeDefined();
940
+ expect(ThirdClass).toBeDefined();
688
941
  });
689
942
  });
690
943
  });
@@ -1,6 +1,44 @@
1
1
  import { Parameter, AuthRequirement, ParameterType } from '../types/Models';
2
2
  import { toolsService } from '../service/Service';
3
3
 
4
+ /**
5
+ * Global context flag to track import context
6
+ */
7
+ declare global {
8
+ var __IMPORT_CONTEXT: 'global' | 'regular' | undefined;
9
+ }
10
+
11
+ /**
12
+ * Helper function to check if a class extends GlobalToolFunction
13
+ */
14
+ function isGlobalToolFunction(constructor: any): boolean {
15
+ // Walk up the prototype chain to check for GlobalToolFunction
16
+ let currentConstructor = constructor;
17
+ while (currentConstructor) {
18
+ if (currentConstructor.name === 'GlobalToolFunction') {
19
+ return true;
20
+ }
21
+ currentConstructor = Object.getPrototypeOf(currentConstructor);
22
+ }
23
+ return false;
24
+ }
25
+
26
+ /**
27
+ * Determine if tool should be registered as global based on definition context or import context
28
+ */
29
+ function shouldRegisterAsGlobal(targetConstructor: any): boolean {
30
+ // First check if there's an active import context
31
+ if (globalThis.__IMPORT_CONTEXT === 'global') {
32
+ return true;
33
+ }
34
+ if (globalThis.__IMPORT_CONTEXT === 'regular') {
35
+ return false;
36
+ }
37
+
38
+ // Fallback to checking where the tool is defined
39
+ return isGlobalToolFunction(targetConstructor);
40
+ }
41
+
4
42
  /**
5
43
  * Configuration for @tool decorator
6
44
  */
@@ -10,7 +48,6 @@ export interface ToolConfig {
10
48
  parameters: ParameterConfig[];
11
49
  authRequirements?: AuthRequirementConfig[];
12
50
  endpoint: string;
13
- isGlobal?: boolean;
14
51
  }
15
52
 
16
53
  /**
@@ -70,7 +107,10 @@ export function tool(config: ToolConfig) {
70
107
  return originalMethod.call(instance, params, authData);
71
108
  };
72
109
 
73
- // Immediately register with global ToolsService
110
+ // Determine if this tool should be registered as global based on definition or import context
111
+ const isGlobal = shouldRegisterAsGlobal(target.constructor);
112
+
113
+ // Register tool with global flag based on where it's defined or imported
74
114
  toolsService.registerTool(
75
115
  config.name,
76
116
  config.description,
@@ -78,7 +118,7 @@ export function tool(config: ToolConfig) {
78
118
  parameters,
79
119
  config.endpoint,
80
120
  authRequirements,
81
- config.isGlobal || false
121
+ isGlobal
82
122
  );
83
123
  };
84
124
  }
@@ -1,5 +1,5 @@
1
1
  import { GlobalFunction, Response, amendLogContext } from '@zaiusinc/app-sdk';
2
- import { authorizeGlobalRequest } from '../auth/AuthUtils';
2
+ import { authenticateGlobalRequest, extractAuthData } from '../auth/AuthUtils';
3
3
  import { toolsService } from '../service/Service';
4
4
 
5
5
  /**
@@ -24,7 +24,14 @@ export abstract class GlobalToolFunction extends GlobalFunction {
24
24
  * @returns Response as the HTTP response
25
25
  */
26
26
  public async perform(): Promise<Response> {
27
- amendLogContext({ opalThreadId: this.request.headers.get('x-opal-thread-id') || '' });
27
+ // Extract customer_id from auth data for global context attribution
28
+ const authInfo = extractAuthData(this.request);
29
+ const customerId = authInfo?.authData?.credentials?.customer_id;
30
+
31
+ amendLogContext({
32
+ opalThreadId: this.request.headers.get('x-opal-thread-id') || '',
33
+ customerId: customerId || ''
34
+ });
28
35
 
29
36
  if (!(await this.authorizeRequest())) {
30
37
  return new Response(403, { error: 'Forbidden' });
@@ -44,6 +51,6 @@ export abstract class GlobalToolFunction extends GlobalFunction {
44
51
  * @returns true if authentication succeeds
45
52
  */
46
53
  private async authorizeRequest(): Promise<boolean> {
47
- return await authorizeGlobalRequest(this.request);
54
+ return await authenticateGlobalRequest(this.request);
48
55
  }
49
56
  }
@@ -1,5 +1,5 @@
1
1
  import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
2
- import { authorizeRegularRequest } from '../auth/AuthUtils';
2
+ import { authenticateRegularRequest } from '../auth/AuthUtils';
3
3
  import { toolsService } from '../service/Service';
4
4
 
5
5
  /**
@@ -43,6 +43,6 @@ export abstract class ToolFunction extends Function {
43
43
  * @returns true if authentication succeeds
44
44
  */
45
45
  private async authorizeRequest(): Promise<boolean> {
46
- return await authorizeRegularRequest(this.request);
46
+ return await authenticateRegularRequest(this.request);
47
47
  }
48
48
  }
@@ -89,6 +89,21 @@ export class ToolsService {
89
89
  private functions: Map<string, Tool<any>> = new Map();
90
90
  private interactions: Map<string, Interaction<any>> = new Map();
91
91
 
92
+ /**
93
+ * Set the import context for tools that will be imported
94
+ * @param context The context type ('global' | 'regular')
95
+ */
96
+ public setImportContext(context: 'global' | 'regular'): void {
97
+ globalThis.__IMPORT_CONTEXT = context;
98
+ }
99
+
100
+ /**
101
+ * Clear the import context after import is complete
102
+ */
103
+ public clearImportContext(): void {
104
+ globalThis.__IMPORT_CONTEXT = undefined;
105
+ }
106
+
92
107
  /**
93
108
  * Enforce OptiID authentication for tools by ensuring OptiID auth requirement is present
94
109
  * @param authRequirements Original authentication requirements
@@ -113,6 +128,7 @@ export class ToolsService {
113
128
  * @param parameters List of parameters for the tool
114
129
  * @param endpoint API endpoint for the tool
115
130
  * @param authRequirements Authentication requirements (optional)
131
+ * @param isGlobal Whether the tool is global
116
132
  */
117
133
  public registerTool<TAuthData>(
118
134
  name: string,
@@ -217,17 +233,18 @@ export class ToolsService {
217
233
  /**
218
234
  * Handle discovery endpoint requests with context-aware filtering
219
235
  * @param functionContext The function context making the request
220
- * @returns Response with filtered functions based on context type
236
+ * @returns Response with filtered functions based on where tools are defined/imported
221
237
  */
222
238
  private handleDiscovery(functionContext: ToolFunction | GlobalToolFunction): App.Response {
223
- // Filter tools based on the function context type
224
- const isGlobalFunction = functionContext instanceof GlobalToolFunction;
225
- const availableFunctions = Array.from(this.functions.values()).filter((f) => {
226
- if (isGlobalFunction) {
227
- // Global functions can only see global tools
228
- return f.isGlobal;
239
+ const isGlobalContext = functionContext instanceof GlobalToolFunction;
240
+
241
+ const availableFunctions = Array.from(this.functions.values()).filter((tool) => {
242
+ if (isGlobalContext) {
243
+ // Global context can only see global tools (defined in or imported into GlobalToolFunction)
244
+ return tool.isGlobal;
229
245
  } else {
230
- return !f.isGlobal;
246
+ // Regular context can only see regular tools (defined in or imported into ToolFunction)
247
+ return !tool.isGlobal;
231
248
  }
232
249
  });
233
250
 
@@ -0,0 +1,45 @@
1
+ import { toolsService } from '../service/Service';
2
+
3
+ /**
4
+ * Import a module as global tools
5
+ * @param modulePath The path to the module to import
6
+ * @param baseUrl The base URL for resolving relative imports (optional)
7
+ * @returns The imported module
8
+ */
9
+ export async function importAsGlobal<T = any>(
10
+ modulePath: string,
11
+ baseUrl?: string
12
+ ): Promise<T> {
13
+ try {
14
+ toolsService.setImportContext('global');
15
+ // If baseUrl is provided, resolve the module path relative to it
16
+ // Otherwise, use the modulePath as-is
17
+ const resolvedPath = baseUrl ? new URL(modulePath, baseUrl).href : modulePath;
18
+ const module = await import(resolvedPath);
19
+ return module;
20
+ } finally {
21
+ toolsService.clearImportContext();
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Import a module as regular tools
27
+ * @param modulePath The path to the module to import
28
+ * @param baseUrl The base URL for resolving relative imports (optional)
29
+ * @returns The imported module
30
+ */
31
+ export async function importAsRegular<T = any>(
32
+ modulePath: string,
33
+ baseUrl?: string
34
+ ): Promise<T> {
35
+ try {
36
+ toolsService.setImportContext('regular');
37
+ // If baseUrl is provided, resolve the module path relative to it
38
+ // Otherwise, use the modulePath as-is
39
+ const resolvedPath = baseUrl ? new URL(modulePath, baseUrl).href : modulePath;
40
+ const module = await import(resolvedPath);
41
+ return module;
42
+ } finally {
43
+ toolsService.clearImportContext();
44
+ }
45
+ }