@optimizely-opal/opal-tool-ocp-sdk 0.0.0-beta.10 → 0.0.0-beta.11
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 +43 -9
- package/dist/function/ToolFunction.d.ts +7 -4
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +10 -39
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +196 -177
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/service/Service.d.ts +7 -7
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +16 -22
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +3 -8
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +5 -5
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +9 -9
- package/dist/types/Models.js.map +1 -1
- package/package.json +3 -5
- package/src/function/ToolFunction.test.ts +214 -194
- package/src/function/ToolFunction.ts +11 -45
- package/src/index.ts +0 -1
- package/src/service/Service.test.ts +3 -8
- package/src/service/Service.ts +17 -22
- package/src/types/Models.ts +4 -4
- package/dist/auth/TokenVerifier.d.ts +0 -31
- package/dist/auth/TokenVerifier.d.ts.map +0 -1
- package/dist/auth/TokenVerifier.js +0 -127
- package/dist/auth/TokenVerifier.js.map +0 -1
- package/dist/auth/TokenVerifier.test.d.ts +0 -2
- package/dist/auth/TokenVerifier.test.d.ts.map +0 -1
- package/dist/auth/TokenVerifier.test.js +0 -114
- package/dist/auth/TokenVerifier.test.js.map +0 -1
- package/src/auth/TokenVerifier.test.ts +0 -152
- package/src/auth/TokenVerifier.ts +0 -145
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import { ToolFunction } from './ToolFunction';
|
|
2
2
|
import { toolsService } from '../service/Service';
|
|
3
|
-
import { Response
|
|
4
|
-
import { getTokenVerifier } from '../auth/TokenVerifier';
|
|
3
|
+
import { Response } from '@zaiusinc/app-sdk';
|
|
5
4
|
|
|
6
|
-
// Mock the
|
|
5
|
+
// Mock the toolsService
|
|
7
6
|
jest.mock('../service/Service', () => ({
|
|
8
7
|
toolsService: {
|
|
9
8
|
processRequest: jest.fn(),
|
|
9
|
+
extractBearerToken: jest.fn(),
|
|
10
10
|
},
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
getTokenVerifier: jest.fn(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
13
|
+
// Mock the Request and Response classes and Function base class
|
|
17
14
|
jest.mock('@zaiusinc/app-sdk', () => ({
|
|
18
15
|
Function: class {
|
|
19
16
|
protected request: any;
|
|
20
17
|
public constructor(_name?: string) {
|
|
18
|
+
// Mock constructor that accepts optional name parameter
|
|
21
19
|
this.request = {};
|
|
22
20
|
}
|
|
23
21
|
},
|
|
@@ -29,34 +27,41 @@ jest.mock('@zaiusinc/app-sdk', () => ({
|
|
|
29
27
|
bodyAsU8Array: new Uint8Array()
|
|
30
28
|
})),
|
|
31
29
|
amendLogContext: jest.fn(),
|
|
32
|
-
getAppContext: jest.fn(),
|
|
33
|
-
logger: {
|
|
34
|
-
info: jest.fn(),
|
|
35
|
-
error: jest.fn(),
|
|
36
|
-
warn: jest.fn(),
|
|
37
|
-
debug: jest.fn(),
|
|
38
|
-
},
|
|
39
30
|
}));
|
|
40
31
|
|
|
41
32
|
// Create a concrete implementation for testing
|
|
42
33
|
class TestToolFunction extends ToolFunction {
|
|
34
|
+
private mockValidateBearerToken: jest.MockedFunction<(token: string) => boolean>;
|
|
43
35
|
private mockReady: jest.MockedFunction<() => Promise<boolean>>;
|
|
44
36
|
|
|
45
37
|
public constructor(request?: any) {
|
|
46
|
-
super(request || {});
|
|
38
|
+
super(request || {}); // Pass the request parameter properly
|
|
39
|
+
// Set the request directly without defaulting to empty object
|
|
47
40
|
(this as any).request = request;
|
|
41
|
+
|
|
42
|
+
this.mockValidateBearerToken = jest.fn().mockReturnValue(true);
|
|
48
43
|
this.mockReady = jest.fn().mockResolvedValue(true);
|
|
49
44
|
}
|
|
50
45
|
|
|
46
|
+
// Override the concrete method with mock implementation for testing
|
|
47
|
+
protected validateBearerToken(bearerToken: string): boolean {
|
|
48
|
+
return this.mockValidateBearerToken(bearerToken);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
51
|
// Override the ready method with mock implementation for testing
|
|
52
52
|
protected ready(): Promise<boolean> {
|
|
53
53
|
return this.mockReady();
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Expose request and validation mock for testing
|
|
56
57
|
public getRequest() {
|
|
57
58
|
return (this as any).request;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
public getMockValidateBearerToken() {
|
|
62
|
+
return this.mockValidateBearerToken;
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
public getMockReady() {
|
|
61
66
|
return this.mockReady;
|
|
62
67
|
}
|
|
@@ -67,99 +72,35 @@ describe('ToolFunction', () => {
|
|
|
67
72
|
let mockResponse: Response;
|
|
68
73
|
let toolFunction: TestToolFunction;
|
|
69
74
|
let mockProcessRequest: jest.MockedFunction<typeof toolsService.processRequest>;
|
|
70
|
-
let
|
|
71
|
-
let mockGetAppContext: jest.MockedFunction<typeof getAppContext>;
|
|
72
|
-
let mockTokenVerifier: jest.Mocked<{
|
|
73
|
-
verify: (token: string) => Promise<any>;
|
|
74
|
-
}>;
|
|
75
|
+
let mockExtractBearerToken: jest.MockedFunction<typeof toolsService.extractBearerToken>;
|
|
75
76
|
|
|
76
77
|
beforeEach(() => {
|
|
77
78
|
jest.clearAllMocks();
|
|
78
79
|
|
|
79
|
-
// Create mock
|
|
80
|
-
mockTokenVerifier = {
|
|
81
|
-
verify: jest.fn(),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Setup the mocks
|
|
85
|
-
mockProcessRequest = jest.mocked(toolsService.processRequest);
|
|
86
|
-
mockGetTokenVerifier = jest.mocked(getTokenVerifier);
|
|
87
|
-
mockGetAppContext = jest.mocked(getAppContext);
|
|
88
|
-
|
|
89
|
-
mockGetTokenVerifier.mockResolvedValue(mockTokenVerifier as any);
|
|
90
|
-
mockGetAppContext.mockReturnValue({
|
|
91
|
-
account: {
|
|
92
|
-
organizationId: 'app-org-123'
|
|
93
|
-
}
|
|
94
|
-
} as any);
|
|
95
|
-
|
|
96
|
-
// Create mock request with bodyJSON structure
|
|
80
|
+
// Create mock instances
|
|
97
81
|
mockRequest = {
|
|
98
82
|
headers: new Map(),
|
|
99
83
|
method: 'POST',
|
|
100
|
-
path: '/test'
|
|
101
|
-
bodyJSON: {
|
|
102
|
-
parameters: {
|
|
103
|
-
task_id: 'task-123',
|
|
104
|
-
content_id: 'content-456'
|
|
105
|
-
},
|
|
106
|
-
auth: {
|
|
107
|
-
provider: 'OptiID',
|
|
108
|
-
credentials: {
|
|
109
|
-
token_type: 'Bearer',
|
|
110
|
-
access_token: 'valid-access-token',
|
|
111
|
-
org_sso_id: 'org-sso-123',
|
|
112
|
-
user_id: 'user-456',
|
|
113
|
-
instance_id: 'instance-789',
|
|
114
|
-
customer_id: 'app-org-123',
|
|
115
|
-
product_sku: 'OPAL'
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
environment: {
|
|
119
|
-
execution_mode: 'headless'
|
|
120
|
-
}
|
|
121
|
-
}
|
|
84
|
+
path: '/test'
|
|
122
85
|
};
|
|
123
|
-
|
|
124
86
|
mockResponse = {} as Response;
|
|
125
|
-
toolFunction = new TestToolFunction(mockRequest);
|
|
126
|
-
});
|
|
127
87
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
method: 'GET',
|
|
132
|
-
path: '/ready',
|
|
133
|
-
bodyJSON: {
|
|
134
|
-
auth: {
|
|
135
|
-
provider: 'OptiID',
|
|
136
|
-
credentials: {
|
|
137
|
-
access_token: 'valid-token',
|
|
138
|
-
customer_id: 'app-org-123'
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
});
|
|
88
|
+
// Setup the mocks
|
|
89
|
+
mockProcessRequest = jest.mocked(toolsService.processRequest);
|
|
90
|
+
mockExtractBearerToken = jest.mocked(toolsService.extractBearerToken);
|
|
143
91
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
mockGetAppContext.mockReturnValue({
|
|
148
|
-
account: {
|
|
149
|
-
organizationId: 'app-org-123'
|
|
150
|
-
}
|
|
151
|
-
} as any);
|
|
152
|
-
};
|
|
92
|
+
// Create test instance
|
|
93
|
+
toolFunction = new TestToolFunction(mockRequest);
|
|
94
|
+
});
|
|
153
95
|
|
|
154
96
|
describe('/ready endpoint', () => {
|
|
155
|
-
beforeEach(() => {
|
|
156
|
-
setupAuthMocks();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
97
|
it('should return ready: true when ready method returns true', async () => {
|
|
160
98
|
// Arrange
|
|
161
|
-
const readyRequest =
|
|
162
|
-
|
|
99
|
+
const readyRequest = {
|
|
100
|
+
headers: new Map(),
|
|
101
|
+
method: 'GET',
|
|
102
|
+
path: '/ready'
|
|
103
|
+
};
|
|
163
104
|
toolFunction = new TestToolFunction(readyRequest);
|
|
164
105
|
toolFunction.getMockReady().mockResolvedValue(true);
|
|
165
106
|
|
|
@@ -174,8 +115,11 @@ describe('ToolFunction', () => {
|
|
|
174
115
|
|
|
175
116
|
it('should return ready: false when ready method returns false', async () => {
|
|
176
117
|
// Arrange
|
|
177
|
-
const readyRequest =
|
|
178
|
-
|
|
118
|
+
const readyRequest = {
|
|
119
|
+
headers: new Map(),
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: '/ready'
|
|
122
|
+
};
|
|
179
123
|
toolFunction = new TestToolFunction(readyRequest);
|
|
180
124
|
toolFunction.getMockReady().mockResolvedValue(false);
|
|
181
125
|
|
|
@@ -190,8 +134,11 @@ describe('ToolFunction', () => {
|
|
|
190
134
|
|
|
191
135
|
it('should handle ready method throwing an error', async () => {
|
|
192
136
|
// Arrange
|
|
193
|
-
const readyRequest =
|
|
194
|
-
|
|
137
|
+
const readyRequest = {
|
|
138
|
+
headers: new Map(),
|
|
139
|
+
method: 'GET',
|
|
140
|
+
path: '/ready'
|
|
141
|
+
};
|
|
195
142
|
toolFunction = new TestToolFunction(readyRequest);
|
|
196
143
|
toolFunction.getMockReady().mockRejectedValue(new Error('Ready check failed'));
|
|
197
144
|
|
|
@@ -201,6 +148,52 @@ describe('ToolFunction', () => {
|
|
|
201
148
|
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
202
149
|
});
|
|
203
150
|
|
|
151
|
+
it('should handle /ready endpoint after bearer token validation passes', async () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const readyRequest = {
|
|
154
|
+
headers: new Map([['authorization', 'Bearer valid-token']]),
|
|
155
|
+
method: 'GET',
|
|
156
|
+
path: '/ready'
|
|
157
|
+
};
|
|
158
|
+
toolFunction = new TestToolFunction(readyRequest);
|
|
159
|
+
toolFunction.getMockReady().mockResolvedValue(true);
|
|
160
|
+
mockExtractBearerToken.mockReturnValue('valid-token');
|
|
161
|
+
toolFunction.getMockValidateBearerToken().mockReturnValue(true); // Make sure auth passes so we reach /ready
|
|
162
|
+
|
|
163
|
+
// Act
|
|
164
|
+
const result = await toolFunction.perform();
|
|
165
|
+
|
|
166
|
+
// Assert
|
|
167
|
+
expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(result).toEqual(new Response(200, { ready: true }));
|
|
169
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(readyRequest.headers); // Auth is checked first
|
|
170
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith('valid-token'); // Auth validation happens
|
|
171
|
+
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return 403 when bearer token validation fails even for /ready endpoint', async () => {
|
|
175
|
+
// Arrange
|
|
176
|
+
const readyRequest = {
|
|
177
|
+
headers: new Map([['authorization', 'Bearer invalid-token']]),
|
|
178
|
+
method: 'GET',
|
|
179
|
+
path: '/ready'
|
|
180
|
+
};
|
|
181
|
+
toolFunction = new TestToolFunction(readyRequest);
|
|
182
|
+
toolFunction.getMockReady().mockResolvedValue(true);
|
|
183
|
+
mockExtractBearerToken.mockReturnValue('invalid-token');
|
|
184
|
+
toolFunction.getMockValidateBearerToken().mockReturnValue(false); // Auth fails
|
|
185
|
+
|
|
186
|
+
// Act
|
|
187
|
+
const result = await toolFunction.perform();
|
|
188
|
+
|
|
189
|
+
// Assert
|
|
190
|
+
expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
|
|
191
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(readyRequest.headers);
|
|
192
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith('invalid-token');
|
|
193
|
+
expect(toolFunction.getMockReady()).not.toHaveBeenCalled(); // Should not reach ready check
|
|
194
|
+
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
195
|
+
});
|
|
196
|
+
|
|
204
197
|
it('should use default ready implementation when not overridden', async () => {
|
|
205
198
|
// Create a class that doesn't override ready method
|
|
206
199
|
class DefaultReadyToolFunction extends ToolFunction {
|
|
@@ -215,7 +208,11 @@ describe('ToolFunction', () => {
|
|
|
215
208
|
}
|
|
216
209
|
|
|
217
210
|
// Arrange
|
|
218
|
-
const readyRequest =
|
|
211
|
+
const readyRequest = {
|
|
212
|
+
headers: new Map(),
|
|
213
|
+
method: 'GET',
|
|
214
|
+
path: '/ready'
|
|
215
|
+
};
|
|
219
216
|
const defaultToolFunction = new DefaultReadyToolFunction(readyRequest);
|
|
220
217
|
|
|
221
218
|
// Act
|
|
@@ -227,136 +224,159 @@ describe('ToolFunction', () => {
|
|
|
227
224
|
});
|
|
228
225
|
});
|
|
229
226
|
|
|
230
|
-
describe('
|
|
231
|
-
it('should
|
|
232
|
-
//
|
|
233
|
-
|
|
227
|
+
describe('bearer token validation', () => {
|
|
228
|
+
it('should extract bearer token from headers and validate it', async () => {
|
|
229
|
+
// Arrange
|
|
230
|
+
const bearerToken = 'valid-token-123';
|
|
231
|
+
mockExtractBearerToken.mockReturnValue(bearerToken);
|
|
232
|
+
toolFunction.getMockValidateBearerToken().mockReturnValue(true);
|
|
234
233
|
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
235
234
|
|
|
235
|
+
// Act
|
|
236
236
|
const result = await toolFunction.perform();
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
expect(
|
|
240
|
-
expect(
|
|
241
|
-
expect(mockGetAppContext).toHaveBeenCalled();
|
|
238
|
+
// Assert
|
|
239
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
240
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
|
|
242
241
|
expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
|
|
242
|
+
expect(result).toBe(mockResponse);
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
it('should return 403
|
|
246
|
-
//
|
|
247
|
-
|
|
245
|
+
it('should return 403 Forbidden when bearer token validation fails', async () => {
|
|
246
|
+
// Arrange
|
|
247
|
+
const bearerToken = 'invalid-token-456';
|
|
248
|
+
mockExtractBearerToken.mockReturnValue(bearerToken);
|
|
249
|
+
toolFunction.getMockValidateBearerToken().mockReturnValue(false);
|
|
248
250
|
|
|
251
|
+
// Act
|
|
249
252
|
const result = await toolFunction.perform();
|
|
250
253
|
|
|
251
|
-
|
|
252
|
-
expect(
|
|
253
|
-
expect(
|
|
254
|
+
// Assert
|
|
255
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
256
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
|
|
254
257
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
258
|
+
expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
|
|
255
259
|
});
|
|
256
260
|
|
|
257
|
-
it('should
|
|
258
|
-
//
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
...mockRequest.bodyJSON.auth,
|
|
265
|
-
credentials: {
|
|
266
|
-
...mockRequest.bodyJSON.auth.credentials,
|
|
267
|
-
customer_id: 'different-org-123'
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
};
|
|
261
|
+
it('should handle complex bearer token validation scenarios', async () => {
|
|
262
|
+
// Arrange - simulate a token that should be valid based on some complex logic
|
|
263
|
+
const complexToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9l' +
|
|
264
|
+
'IiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
265
|
+
mockExtractBearerToken.mockReturnValue(complexToken);
|
|
266
|
+
toolFunction.getMockValidateBearerToken().mockReturnValue(true);
|
|
267
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
272
268
|
|
|
273
|
-
|
|
269
|
+
// Act
|
|
270
|
+
const result = await toolFunction.perform();
|
|
271
|
+
|
|
272
|
+
// Assert
|
|
273
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
274
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(complexToken);
|
|
275
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
|
|
276
|
+
expect(result).toBe(mockResponse);
|
|
277
|
+
});
|
|
274
278
|
|
|
275
|
-
|
|
279
|
+
it('should handle bearer token validation throwing an error', async () => {
|
|
280
|
+
// Arrange
|
|
281
|
+
const bearerToken = 'malformed-token';
|
|
282
|
+
mockExtractBearerToken.mockReturnValue(bearerToken);
|
|
283
|
+
toolFunction.getMockValidateBearerToken().mockImplementation(() => {
|
|
284
|
+
throw new Error('Token validation error');
|
|
285
|
+
});
|
|
276
286
|
|
|
277
|
-
|
|
278
|
-
expect(
|
|
287
|
+
// Act & Assert
|
|
288
|
+
await expect(toolFunction.perform()).rejects.toThrow('Token validation error');
|
|
289
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
290
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
|
|
279
291
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
280
292
|
});
|
|
281
293
|
|
|
282
|
-
it('should
|
|
283
|
-
//
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
auth: {
|
|
289
|
-
...mockRequest.bodyJSON.auth,
|
|
290
|
-
credentials: {
|
|
291
|
-
...mockRequest.bodyJSON.auth.credentials,
|
|
292
|
-
access_token: undefined
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
const toolFunctionWithoutToken = new TestToolFunction(requestWithoutToken);
|
|
294
|
+
it('should call validateBearerToken only once per request', async () => {
|
|
295
|
+
// Arrange
|
|
296
|
+
const bearerToken = 'test-token';
|
|
297
|
+
mockExtractBearerToken.mockReturnValue(bearerToken);
|
|
298
|
+
toolFunction.getMockValidateBearerToken().mockReturnValue(true);
|
|
299
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
299
300
|
|
|
300
|
-
|
|
301
|
+
// Act
|
|
302
|
+
await toolFunction.perform();
|
|
301
303
|
|
|
302
|
-
|
|
303
|
-
expect(
|
|
304
|
-
expect(
|
|
304
|
+
// Assert
|
|
305
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledTimes(1);
|
|
306
|
+
expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
|
|
305
307
|
});
|
|
306
308
|
|
|
307
|
-
it('should
|
|
308
|
-
//
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
auth: {
|
|
314
|
-
...mockRequest.bodyJSON.auth,
|
|
315
|
-
credentials: {
|
|
316
|
-
...mockRequest.bodyJSON.auth.credentials,
|
|
317
|
-
customer_id: undefined
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const toolFunctionWithoutCustomerId = new TestToolFunction(requestWithoutCustomerId);
|
|
309
|
+
it('should extract bearer token only once per request', async () => {
|
|
310
|
+
// Arrange
|
|
311
|
+
const bearerToken = 'test-token';
|
|
312
|
+
mockExtractBearerToken.mockReturnValue(bearerToken);
|
|
313
|
+
toolFunction.getMockValidateBearerToken().mockReturnValue(true);
|
|
314
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
324
315
|
|
|
325
|
-
|
|
316
|
+
// Act
|
|
317
|
+
await toolFunction.perform();
|
|
326
318
|
|
|
327
|
-
|
|
328
|
-
expect(
|
|
329
|
-
expect(
|
|
319
|
+
// Assert
|
|
320
|
+
expect(mockExtractBearerToken).toHaveBeenCalledTimes(1);
|
|
321
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
330
322
|
});
|
|
331
323
|
|
|
332
|
-
it('should
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
bodyJSON: {
|
|
337
|
-
parameters: mockRequest.bodyJSON.parameters,
|
|
338
|
-
environment: mockRequest.bodyJSON.environment
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
const toolFunctionWithoutAuth = new TestToolFunction(requestWithoutAuth);
|
|
324
|
+
it('should skip validation when bearer token is undefined', async () => {
|
|
325
|
+
// Arrange
|
|
326
|
+
mockExtractBearerToken.mockReturnValue(undefined);
|
|
327
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
343
328
|
|
|
344
|
-
|
|
329
|
+
// Act
|
|
330
|
+
const result = await toolFunction.perform();
|
|
345
331
|
|
|
346
|
-
|
|
347
|
-
expect(
|
|
348
|
-
expect(
|
|
332
|
+
// Assert
|
|
333
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
334
|
+
expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
|
|
335
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
|
|
336
|
+
expect(result).toBe(mockResponse);
|
|
349
337
|
});
|
|
350
338
|
|
|
351
|
-
it('should
|
|
352
|
-
//
|
|
353
|
-
|
|
339
|
+
it('should skip validation when bearer token is empty string', async () => {
|
|
340
|
+
// Arrange
|
|
341
|
+
mockExtractBearerToken.mockReturnValue('');
|
|
342
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
354
343
|
|
|
344
|
+
// Act
|
|
355
345
|
const result = await toolFunction.perform();
|
|
356
346
|
|
|
357
|
-
|
|
358
|
-
expect(
|
|
359
|
-
expect(
|
|
347
|
+
// Assert
|
|
348
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
349
|
+
expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
|
|
350
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
|
|
351
|
+
expect(result).toBe(mockResponse);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should use default validateBearerToken implementation when not overridden', async () => {
|
|
355
|
+
// Create a class that doesn't override validateBearerToken
|
|
356
|
+
class DefaultToolFunction extends ToolFunction {
|
|
357
|
+
public constructor(request?: any) {
|
|
358
|
+
super(request || {});
|
|
359
|
+
(this as any).request = request;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public getRequest() {
|
|
363
|
+
return (this as any).request;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Arrange
|
|
368
|
+
const defaultToolFunction = new DefaultToolFunction(mockRequest);
|
|
369
|
+
const bearerToken = 'any-token';
|
|
370
|
+
mockExtractBearerToken.mockReturnValue(bearerToken);
|
|
371
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
372
|
+
|
|
373
|
+
// Act
|
|
374
|
+
const result = await defaultToolFunction.perform();
|
|
375
|
+
|
|
376
|
+
// Assert - Default implementation should return true and allow request to proceed
|
|
377
|
+
expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
|
|
378
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, defaultToolFunction);
|
|
379
|
+
expect(result).toBe(mockResponse);
|
|
360
380
|
});
|
|
361
381
|
});
|
|
362
382
|
|
|
@@ -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
|
|
@@ -26,7 +24,8 @@ export abstract class ToolFunction extends Function {
|
|
|
26
24
|
*/
|
|
27
25
|
public async perform(): Promise<Response> {
|
|
28
26
|
amendLogContext({ opalThreadId: this.request.headers.get('x-opal-thread-id') || '' });
|
|
29
|
-
|
|
27
|
+
const bearerToken = toolsService.extractBearerToken(this.request.headers);
|
|
28
|
+
if (bearerToken && !this.validateBearerToken(bearerToken)) {
|
|
30
29
|
return new Response(403, { error: 'Forbidden' });
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -39,48 +38,15 @@ export abstract class ToolFunction extends Function {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
/**
|
|
42
|
-
*
|
|
41
|
+
* Validates the bearer token for authorization.
|
|
43
42
|
*
|
|
44
|
-
*
|
|
43
|
+
* This method provides a default implementation that accepts all tokens.
|
|
44
|
+
* Subclasses can override this method to implement custom bearer token validation logic.
|
|
45
|
+
*
|
|
46
|
+
* @param _bearerToken - The bearer token extracted from the Authorization header
|
|
47
|
+
* @returns true if the token is valid and the request should proceed, false to return 403 Forbidden
|
|
45
48
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
logger.debug('Authorizing request:', this.request.bodyJSON);
|
|
51
|
-
const authData = this.request.bodyJSON?.auth as OptiIdAuthData;
|
|
52
|
-
const accessToken = authData?.credentials?.access_token;
|
|
53
|
-
if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
|
|
54
|
-
logger.error('OptiID token is required but not provided');
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const customerId = authData.credentials?.customer_id;
|
|
59
|
-
if (!customerId) {
|
|
60
|
-
logger.error('Organisation ID is required but not provided');
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const appOrganisationId = getAppContext().account.organizationId;
|
|
65
|
-
if (customerId !== appOrganisationId) {
|
|
66
|
-
logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return await this.validateAccessToken(accessToken);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private async validateAccessToken(accessToken: string | undefined): Promise<boolean> {
|
|
74
|
-
try {
|
|
75
|
-
if (!accessToken) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
const tokenVerifier = await getTokenVerifier();
|
|
79
|
-
return await tokenVerifier.verify(accessToken);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
logger.error('OptiID token validation failed:', error);
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
49
|
+
protected validateBearerToken(_bearerToken: string): boolean {
|
|
50
|
+
return true;
|
|
84
51
|
}
|
|
85
|
-
|
|
86
52
|
}
|
package/src/index.ts
CHANGED
|
@@ -122,8 +122,7 @@ describe('ToolsService', () => {
|
|
|
122
122
|
description: mockTool.description,
|
|
123
123
|
parameters: mockTool.parameters.map((p: Parameter) => p.toJSON()),
|
|
124
124
|
endpoint: mockTool.endpoint,
|
|
125
|
-
http_method: 'POST'
|
|
126
|
-
auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
|
|
125
|
+
http_method: 'POST'
|
|
127
126
|
});
|
|
128
127
|
});
|
|
129
128
|
|
|
@@ -182,8 +181,7 @@ describe('ToolsService', () => {
|
|
|
182
181
|
description: mockTool.description,
|
|
183
182
|
parameters: mockTool.parameters.map((p: Parameter) => p.toJSON()),
|
|
184
183
|
endpoint: mockTool.endpoint,
|
|
185
|
-
http_method: 'POST'
|
|
186
|
-
auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
|
|
184
|
+
http_method: 'POST'
|
|
187
185
|
});
|
|
188
186
|
|
|
189
187
|
expect(secondFunction).toEqual({
|
|
@@ -192,10 +190,7 @@ describe('ToolsService', () => {
|
|
|
192
190
|
parameters: [],
|
|
193
191
|
endpoint: '/second-tool',
|
|
194
192
|
http_method: 'POST',
|
|
195
|
-
auth_requirements:
|
|
196
|
-
{ provider: 'oauth2', scope_bundle: 'calendar', required: true },
|
|
197
|
-
{ provider: 'OptiID', scope_bundle: 'default', required: true }
|
|
198
|
-
]
|
|
193
|
+
auth_requirements: authRequirements.map((auth) => auth.toJSON())
|
|
199
194
|
});
|
|
200
195
|
});
|
|
201
196
|
});
|