@optimizely-opal/opal-tool-ocp-sdk 0.0.0-devmg.13 → 1.0.0-OCP-1441.2

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 (62) hide show
  1. package/README.md +108 -15
  2. package/dist/auth/AuthUtils.d.ts +26 -0
  3. package/dist/auth/AuthUtils.d.ts.map +1 -0
  4. package/dist/auth/AuthUtils.js +109 -0
  5. package/dist/auth/AuthUtils.js.map +1 -0
  6. package/dist/auth/AuthUtils.test.d.ts +2 -0
  7. package/dist/auth/AuthUtils.test.d.ts.map +1 -0
  8. package/dist/auth/AuthUtils.test.js +601 -0
  9. package/dist/auth/AuthUtils.test.js.map +1 -0
  10. package/dist/auth/TokenVerifier.d.ts.map +1 -1
  11. package/dist/auth/TokenVerifier.js +0 -1
  12. package/dist/auth/TokenVerifier.js.map +1 -1
  13. package/dist/auth/TokenVerifier.test.js +9 -0
  14. package/dist/auth/TokenVerifier.test.js.map +1 -1
  15. package/dist/function/GlobalToolFunction.d.ts +27 -0
  16. package/dist/function/GlobalToolFunction.d.ts.map +1 -0
  17. package/dist/function/GlobalToolFunction.js +53 -0
  18. package/dist/function/GlobalToolFunction.js.map +1 -0
  19. package/dist/function/GlobalToolFunction.test.d.ts +2 -0
  20. package/dist/function/GlobalToolFunction.test.d.ts.map +1 -0
  21. package/dist/function/GlobalToolFunction.test.js +425 -0
  22. package/dist/function/GlobalToolFunction.test.js.map +1 -0
  23. package/dist/function/ToolFunction.d.ts +1 -2
  24. package/dist/function/ToolFunction.d.ts.map +1 -1
  25. package/dist/function/ToolFunction.js +2 -35
  26. package/dist/function/ToolFunction.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/service/Service.d.ts +8 -7
  32. package/dist/service/Service.d.ts.map +1 -1
  33. package/dist/service/Service.js +16 -0
  34. package/dist/service/Service.js.map +1 -1
  35. package/dist/service/Service.test.js +89 -4
  36. package/dist/service/Service.test.js.map +1 -1
  37. package/dist/validation/ParameterValidator.d.ts +42 -0
  38. package/dist/validation/ParameterValidator.d.ts.map +1 -0
  39. package/dist/validation/ParameterValidator.js +122 -0
  40. package/dist/validation/ParameterValidator.js.map +1 -0
  41. package/dist/validation/ParameterValidator.test.d.ts +2 -0
  42. package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
  43. package/dist/validation/ParameterValidator.test.js +282 -0
  44. package/dist/validation/ParameterValidator.test.js.map +1 -0
  45. package/package.json +3 -4
  46. package/src/auth/AuthUtils.test.ts +729 -0
  47. package/src/auth/AuthUtils.ts +117 -0
  48. package/src/auth/TokenVerifier.test.ts +11 -0
  49. package/src/auth/TokenVerifier.ts +0 -1
  50. package/src/function/GlobalToolFunction.test.ts +505 -0
  51. package/src/function/GlobalToolFunction.ts +56 -0
  52. package/src/function/ToolFunction.ts +3 -41
  53. package/src/index.ts +1 -0
  54. package/src/service/Service.test.ts +129 -12
  55. package/src/service/Service.ts +50 -9
  56. package/src/validation/ParameterValidator.test.ts +341 -0
  57. package/src/validation/ParameterValidator.ts +153 -0
  58. package/dist/function/ToolFunction.test.d.ts +0 -2
  59. package/dist/function/ToolFunction.test.d.ts.map +0 -1
  60. package/dist/function/ToolFunction.test.js +0 -314
  61. package/dist/function/ToolFunction.test.js.map +0 -1
  62. package/src/function/ToolFunction.test.ts +0 -374
@@ -1,7 +1,5 @@
1
- import { Function, Response, amendLogContext, getAppContext, logger } from '@zaiusinc/app-sdk';
1
+ import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
2
2
  import { toolsService } from '../service/Service';
3
- import { getTokenVerifier } from '../auth/TokenVerifier';
4
- import { OptiIdAuthData } from '../types/Models';
5
3
 
6
4
  /**
7
5
  * Abstract base class for tool-based function execution
@@ -41,45 +39,9 @@ export abstract class ToolFunction extends Function {
41
39
  /**
42
40
  * Authenticate the incoming request by validating the OptiID token and organization ID
43
41
  *
44
- * @throws true if authentication succeeds
42
+ * @returns true if authentication succeeds
45
43
  */
46
44
  private async authorizeRequest(): Promise<boolean> {
47
- if (this.request.path === '/discovery' || this.request.path === '/ready') {
48
- return true;
49
- }
50
- const authData = this.request.bodyJSON?.auth as OptiIdAuthData;
51
- const accessToken = authData?.credentials?.access_token;
52
- if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
53
- logger.error('OptiID token is required but not provided');
54
- return false;
55
- }
56
-
57
- const customerId = authData.credentials?.customer_id;
58
- if (!customerId) {
59
- logger.error('Organisation ID is required but not provided');
60
- return false;
61
- }
62
-
63
- const appOrganisationId = getAppContext().account?.organizationId;
64
- if (customerId !== appOrganisationId) {
65
- logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
66
- return false;
67
- }
68
-
69
- return await this.validateAccessToken(accessToken);
45
+ return true;
70
46
  }
71
-
72
- private async validateAccessToken(accessToken: string | undefined): Promise<boolean> {
73
- try {
74
- if (!accessToken) {
75
- return false;
76
- }
77
- const tokenVerifier = await getTokenVerifier();
78
- return await tokenVerifier.verify(accessToken);
79
- } catch (error) {
80
- logger.error('OptiID token validation failed:', error);
81
- return false;
82
- }
83
- }
84
-
85
47
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './function/ToolFunction';
2
+ export * from './function/GlobalToolFunction';
2
3
  export * from './types/Models';
3
4
  export * from './decorator/Decorator';
4
5
  export * from './auth/TokenVerifier';
@@ -522,15 +522,25 @@ describe('ToolsService', () => {
522
522
 
523
523
  describe('edge cases', () => {
524
524
  it('should handle request with null bodyJSON', async () => {
525
+ // Create a tool without required parameters
526
+ const toolWithoutRequiredParams = {
527
+ name: 'no_required_params_tool',
528
+ description: 'Tool without required parameters',
529
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
530
+ parameters: [], // No parameters defined
531
+ endpoint: '/no-required-params-tool'
532
+ };
533
+
525
534
  toolsService.registerTool(
526
- mockTool.name,
527
- mockTool.description,
528
- mockTool.handler,
529
- mockTool.parameters,
530
- mockTool.endpoint
535
+ toolWithoutRequiredParams.name,
536
+ toolWithoutRequiredParams.description,
537
+ toolWithoutRequiredParams.handler,
538
+ toolWithoutRequiredParams.parameters,
539
+ toolWithoutRequiredParams.endpoint
531
540
  );
532
541
 
533
542
  const requestWithNullBody = createMockRequest({
543
+ path: '/no-required-params-tool',
534
544
  bodyJSON: null,
535
545
  body: null
536
546
  });
@@ -538,19 +548,29 @@ describe('ToolsService', () => {
538
548
  const response = await toolsService.processRequest(requestWithNullBody, mockToolFunction);
539
549
 
540
550
  expect(response.status).toBe(200);
541
- expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
551
+ expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
542
552
  });
543
553
 
544
554
  it('should handle request with undefined bodyJSON', async () => {
555
+ // Create a tool without required parameters
556
+ const toolWithoutRequiredParams = {
557
+ name: 'no_required_params_tool_2',
558
+ description: 'Tool without required parameters',
559
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
560
+ parameters: [], // No parameters defined
561
+ endpoint: '/no-required-params-tool-2'
562
+ };
563
+
545
564
  toolsService.registerTool(
546
- mockTool.name,
547
- mockTool.description,
548
- mockTool.handler,
549
- mockTool.parameters,
550
- mockTool.endpoint
565
+ toolWithoutRequiredParams.name,
566
+ toolWithoutRequiredParams.description,
567
+ toolWithoutRequiredParams.handler,
568
+ toolWithoutRequiredParams.parameters,
569
+ toolWithoutRequiredParams.endpoint
551
570
  );
552
571
 
553
572
  const requestWithUndefinedBody = createMockRequest({
573
+ path: '/no-required-params-tool-2',
554
574
  bodyJSON: undefined,
555
575
  body: undefined
556
576
  });
@@ -558,7 +578,7 @@ describe('ToolsService', () => {
558
578
  const response = await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
559
579
 
560
580
  expect(response.status).toBe(200);
561
- expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
581
+ expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
562
582
  });
563
583
 
564
584
  it('should extract auth data from bodyJSON when body exists', async () => {
@@ -657,5 +677,102 @@ describe('ToolsService', () => {
657
677
  );
658
678
  });
659
679
  });
680
+
681
+ describe('parameter validation', () => {
682
+ beforeEach(() => {
683
+ jest.clearAllMocks();
684
+ });
685
+
686
+ it('should validate parameters and return 400 for invalid types', async () => {
687
+ // Register a tool with specific parameter types
688
+ const toolWithTypedParams = {
689
+ name: 'typed_tool',
690
+ description: 'Tool with typed parameters',
691
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
692
+ parameters: [
693
+ new Parameter('name', ParameterType.String, 'User name', true),
694
+ new Parameter('age', ParameterType.Integer, 'User age', true),
695
+ new Parameter('active', ParameterType.Boolean, 'Is active', false)
696
+ ],
697
+ endpoint: '/typed-tool'
698
+ };
699
+
700
+ toolsService.registerTool(
701
+ toolWithTypedParams.name,
702
+ toolWithTypedParams.description,
703
+ toolWithTypedParams.handler,
704
+ toolWithTypedParams.parameters,
705
+ toolWithTypedParams.endpoint
706
+ );
707
+
708
+ // Send invalid parameter types
709
+ const invalidRequest = createMockRequest({
710
+ path: '/typed-tool',
711
+ bodyJSON: {
712
+ parameters: {
713
+ name: 123, // should be string
714
+ age: '25', // should be integer
715
+ active: 'true' // should be boolean
716
+ }
717
+ }
718
+ });
719
+
720
+ const response = await toolsService.processRequest(invalidRequest, mockToolFunction);
721
+
722
+ expect(response.status).toBe(400);
723
+
724
+ // Expect RFC 9457 Problem Details format
725
+ expect(response.bodyJSON).toHaveProperty('title', 'One or more validation errors occurred.');
726
+ expect(response.bodyJSON).toHaveProperty('status', 400);
727
+ expect(response.bodyJSON).toHaveProperty('detail', 'See \'errors\' field for details.');
728
+ expect(response.bodyJSON).toHaveProperty('errors');
729
+ expect(response.bodyJSON.errors).toHaveLength(3);
730
+
731
+ // Check error structure - field and message
732
+ const errors = response.bodyJSON.errors;
733
+ expect(errors[0]).toHaveProperty('field', 'name');
734
+ expect(errors[0]).toHaveProperty('message', "Parameter 'name' must be a string, but received number");
735
+
736
+ // Verify the handler was not called
737
+ expect(toolWithTypedParams.handler).not.toHaveBeenCalled();
738
+ });
739
+
740
+ it('should skip validation for tools with no parameter definitions', async () => {
741
+ const toolWithoutParams = {
742
+ name: 'no_params_tool',
743
+ description: 'Tool without parameters',
744
+ handler: jest.fn().mockResolvedValue({ result: 'success' }),
745
+ parameters: [], // No parameters defined
746
+ endpoint: '/no-params-tool'
747
+ };
748
+
749
+ toolsService.registerTool(
750
+ toolWithoutParams.name,
751
+ toolWithoutParams.description,
752
+ toolWithoutParams.handler,
753
+ toolWithoutParams.parameters,
754
+ toolWithoutParams.endpoint
755
+ );
756
+
757
+ // Send request with any data (should be ignored)
758
+ const request = createMockRequest({
759
+ path: '/no-params-tool',
760
+ bodyJSON: {
761
+ parameters: {
762
+ unexpected: 'value'
763
+ }
764
+ }
765
+ });
766
+
767
+ const response = await toolsService.processRequest(request, mockToolFunction);
768
+
769
+ expect(response.status).toBe(200);
770
+ expect(toolWithoutParams.handler).toHaveBeenCalledWith(
771
+ mockToolFunction,
772
+ { unexpected: 'value' },
773
+ undefined
774
+ );
775
+ });
776
+ });
660
777
  });
661
778
  });
@@ -3,14 +3,14 @@ import { AuthRequirement, Parameter } from '../types/Models';
3
3
  import * as App from '@zaiusinc/app-sdk';
4
4
  import { logger } from '@zaiusinc/app-sdk';
5
5
  import { ToolFunction } from '../function/ToolFunction';
6
+ import { GlobalToolFunction } from '../function/GlobalToolFunction';
7
+ import { ParameterValidator } from '../validation/ParameterValidator';
6
8
 
7
9
  /**
8
10
  * Default OptiID authentication requirement that will be enforced for all tools
9
11
  */
10
12
  const DEFAULT_OPTIID_AUTH = new AuthRequirement('OptiID', 'default', true);
11
13
 
12
-
13
-
14
14
  /**
15
15
  * Result type for interaction handlers
16
16
  */
@@ -28,7 +28,11 @@ export class Interaction<TAuthData> {
28
28
  public constructor(
29
29
  public name: string,
30
30
  public endpoint: string,
31
- public handler: (functionContext: ToolFunction, data: unknown, authData?: TAuthData) => Promise<InteractionResult>
31
+ public handler: (
32
+ functionContext: ToolFunction | GlobalToolFunction,
33
+ data: unknown,
34
+ authData?: TAuthData
35
+ ) => Promise<InteractionResult>
32
36
  ) {}
33
37
  }
34
38
 
@@ -55,7 +59,11 @@ export class Tool<TAuthData> {
55
59
  public description: string,
56
60
  public parameters: Parameter[],
57
61
  public endpoint: string,
58
- public handler: (functionContext: ToolFunction, params: unknown, authData?: TAuthData) => Promise<unknown>,
62
+ public handler: (
63
+ functionContext: ToolFunction | GlobalToolFunction,
64
+ params: unknown,
65
+ authData?: TAuthData
66
+ ) => Promise<unknown>,
59
67
  public authRequirements: AuthRequirement[] = [DEFAULT_OPTIID_AUTH]
60
68
  ) {}
61
69
 
@@ -108,14 +116,25 @@ export class ToolsService {
108
116
  public registerTool<TAuthData>(
109
117
  name: string,
110
118
  description: string,
111
- handler: (functionContext: ToolFunction, params: unknown, authData?: TAuthData) => Promise<unknown>,
119
+ handler: (
120
+ functionContext: ToolFunction | GlobalToolFunction,
121
+ params: unknown,
122
+ authData?: TAuthData
123
+ ) => Promise<unknown>,
112
124
  parameters: Parameter[],
113
125
  endpoint: string,
114
126
  authRequirements?: AuthRequirement[]
115
127
  ): void {
116
128
  // Enforce OptiID authentication for all tools
117
129
  const enforcedAuthRequirements = this.enforceOptiIdAuth(authRequirements);
118
- const func = new Tool<TAuthData>(name, description, parameters, endpoint, handler, enforcedAuthRequirements);
130
+ const func = new Tool<TAuthData>(
131
+ name,
132
+ description,
133
+ parameters,
134
+ endpoint,
135
+ handler,
136
+ enforcedAuthRequirements
137
+ );
119
138
  this.functions.set(endpoint, func);
120
139
  }
121
140
 
@@ -127,15 +146,21 @@ export class ToolsService {
127
146
  */
128
147
  public registerInteraction<TAuthData>(
129
148
  name: string,
130
- handler: (functionContext: ToolFunction, data: unknown, authData?: TAuthData) => Promise<InteractionResult>,
149
+ handler: (
150
+ functionContext: ToolFunction | GlobalToolFunction,
151
+ data: unknown,
152
+ authData?: TAuthData
153
+ ) => Promise<InteractionResult>,
131
154
  endpoint: string
132
155
  ): void {
133
156
  const func = new Interaction<TAuthData>(name, endpoint, handler);
134
157
  this.interactions.set(endpoint, func);
135
158
  }
136
159
 
137
- public async processRequest(req: App.Request,
138
- functionContext: ToolFunction): Promise<App.Response> {
160
+ public async processRequest(
161
+ req: App.Request,
162
+ functionContext: ToolFunction | GlobalToolFunction
163
+ ): Promise<App.Response> {
139
164
  if (req.path === '/discovery') {
140
165
  return new App.Response(200, { functions: Array.from(this.functions.values()).map((f) => f.toJSON()) });
141
166
  } else {
@@ -149,6 +174,22 @@ export class ToolsService {
149
174
  params = req.bodyJSON;
150
175
  }
151
176
 
177
+ // Validate parameters before calling the handler (only if tool has parameter definitions)
178
+ if (func.parameters && func.parameters.length > 0) {
179
+ const validationResult = ParameterValidator.validate(params, func.parameters);
180
+ if (!validationResult.isValid) {
181
+ return new App.Response(400, {
182
+ title: 'One or more validation errors occurred.',
183
+ status: 400,
184
+ detail: "See 'errors' field for details.",
185
+ errors: validationResult.errors.map((error) => ({
186
+ field: error.field,
187
+ message: error.message
188
+ }))
189
+ });
190
+ }
191
+ }
192
+
152
193
  // Extract auth data from body JSON
153
194
  const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
154
195