@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.
- package/README.md +108 -15
- package/dist/auth/AuthUtils.d.ts +26 -0
- package/dist/auth/AuthUtils.d.ts.map +1 -0
- package/dist/auth/AuthUtils.js +109 -0
- package/dist/auth/AuthUtils.js.map +1 -0
- package/dist/auth/AuthUtils.test.d.ts +2 -0
- package/dist/auth/AuthUtils.test.d.ts.map +1 -0
- package/dist/auth/AuthUtils.test.js +601 -0
- package/dist/auth/AuthUtils.test.js.map +1 -0
- package/dist/auth/TokenVerifier.d.ts.map +1 -1
- package/dist/auth/TokenVerifier.js +0 -1
- package/dist/auth/TokenVerifier.js.map +1 -1
- package/dist/auth/TokenVerifier.test.js +9 -0
- package/dist/auth/TokenVerifier.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +27 -0
- package/dist/function/GlobalToolFunction.d.ts.map +1 -0
- package/dist/function/GlobalToolFunction.js +53 -0
- package/dist/function/GlobalToolFunction.js.map +1 -0
- package/dist/function/GlobalToolFunction.test.d.ts +2 -0
- package/dist/function/GlobalToolFunction.test.d.ts.map +1 -0
- package/dist/function/GlobalToolFunction.test.js +425 -0
- package/dist/function/GlobalToolFunction.test.js.map +1 -0
- package/dist/function/ToolFunction.d.ts +1 -2
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +2 -35
- package/dist/function/ToolFunction.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/service/Service.d.ts +8 -7
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +16 -0
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +89 -4
- package/dist/service/Service.test.js.map +1 -1
- package/dist/validation/ParameterValidator.d.ts +42 -0
- package/dist/validation/ParameterValidator.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.js +122 -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 +282 -0
- package/dist/validation/ParameterValidator.test.js.map +1 -0
- package/package.json +3 -4
- package/src/auth/AuthUtils.test.ts +729 -0
- package/src/auth/AuthUtils.ts +117 -0
- package/src/auth/TokenVerifier.test.ts +11 -0
- package/src/auth/TokenVerifier.ts +0 -1
- package/src/function/GlobalToolFunction.test.ts +505 -0
- package/src/function/GlobalToolFunction.ts +56 -0
- package/src/function/ToolFunction.ts +3 -41
- package/src/index.ts +1 -0
- package/src/service/Service.test.ts +129 -12
- package/src/service/Service.ts +50 -9
- package/src/validation/ParameterValidator.test.ts +341 -0
- package/src/validation/ParameterValidator.ts +153 -0
- package/dist/function/ToolFunction.test.d.ts +0 -2
- package/dist/function/ToolFunction.test.d.ts.map +0 -1
- package/dist/function/ToolFunction.test.js +0 -314
- package/dist/function/ToolFunction.test.js.map +0 -1
- package/src/function/ToolFunction.test.ts +0 -374
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { Function, Response, amendLogContext
|
|
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
|
-
* @
|
|
42
|
+
* @returns true if authentication succeeds
|
|
45
43
|
*/
|
|
46
44
|
private async authorizeRequest(): Promise<boolean> {
|
|
47
|
-
|
|
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
|
@@ -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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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(
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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(
|
|
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
|
});
|
package/src/service/Service.ts
CHANGED
|
@@ -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: (
|
|
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: (
|
|
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: (
|
|
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>(
|
|
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: (
|
|
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(
|
|
138
|
-
|
|
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
|
|