@optimizely-opal/opal-tool-ocp-sdk 0.0.0-beta.7 → 0.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 (38) hide show
  1. package/README.md +9 -43
  2. package/dist/auth/TokenVerifier.d.ts +31 -0
  3. package/dist/auth/TokenVerifier.d.ts.map +1 -0
  4. package/dist/auth/TokenVerifier.js +127 -0
  5. package/dist/auth/TokenVerifier.js.map +1 -0
  6. package/dist/auth/TokenVerifier.test.d.ts +2 -0
  7. package/dist/auth/TokenVerifier.test.d.ts.map +1 -0
  8. package/dist/auth/TokenVerifier.test.js +114 -0
  9. package/dist/auth/TokenVerifier.test.js.map +1 -0
  10. package/dist/function/ToolFunction.d.ts +4 -7
  11. package/dist/function/ToolFunction.d.ts.map +1 -1
  12. package/dist/function/ToolFunction.js +38 -10
  13. package/dist/function/ToolFunction.js.map +1 -1
  14. package/dist/function/ToolFunction.test.js +177 -196
  15. package/dist/function/ToolFunction.test.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/service/Service.d.ts +7 -7
  21. package/dist/service/Service.d.ts.map +1 -1
  22. package/dist/service/Service.js +22 -16
  23. package/dist/service/Service.js.map +1 -1
  24. package/dist/service/Service.test.js +8 -3
  25. package/dist/service/Service.test.js.map +1 -1
  26. package/dist/types/Models.d.ts +5 -5
  27. package/dist/types/Models.d.ts.map +1 -1
  28. package/dist/types/Models.js +9 -9
  29. package/dist/types/Models.js.map +1 -1
  30. package/package.json +5 -3
  31. package/src/auth/TokenVerifier.test.ts +152 -0
  32. package/src/auth/TokenVerifier.ts +145 -0
  33. package/src/function/ToolFunction.test.ts +194 -214
  34. package/src/function/ToolFunction.ts +45 -11
  35. package/src/index.ts +1 -0
  36. package/src/service/Service.test.ts +8 -3
  37. package/src/service/Service.ts +22 -17
  38. package/src/types/Models.ts +4 -4
@@ -1,21 +1,23 @@
1
1
  import { ToolFunction } from './ToolFunction';
2
2
  import { toolsService } from '../service/Service';
3
- import { Response } from '@zaiusinc/app-sdk';
3
+ import { Response, getAppContext } from '@zaiusinc/app-sdk';
4
+ import { getTokenVerifier } from '../auth/TokenVerifier';
4
5
 
5
- // Mock the toolsService
6
+ // Mock the dependencies
6
7
  jest.mock('../service/Service', () => ({
7
8
  toolsService: {
8
9
  processRequest: jest.fn(),
9
- extractBearerToken: jest.fn(),
10
10
  },
11
11
  }));
12
12
 
13
- // Mock the Request and Response classes and Function base class
13
+ jest.mock('../auth/TokenVerifier', () => ({
14
+ getTokenVerifier: jest.fn(),
15
+ }));
16
+
14
17
  jest.mock('@zaiusinc/app-sdk', () => ({
15
18
  Function: class {
16
19
  protected request: any;
17
20
  public constructor(_name?: string) {
18
- // Mock constructor that accepts optional name parameter
19
21
  this.request = {};
20
22
  }
21
23
  },
@@ -27,41 +29,34 @@ jest.mock('@zaiusinc/app-sdk', () => ({
27
29
  bodyAsU8Array: new Uint8Array()
28
30
  })),
29
31
  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
+ },
30
39
  }));
31
40
 
32
41
  // Create a concrete implementation for testing
33
42
  class TestToolFunction extends ToolFunction {
34
- private mockValidateBearerToken: jest.MockedFunction<(token: string) => boolean>;
35
43
  private mockReady: jest.MockedFunction<() => Promise<boolean>>;
36
44
 
37
45
  public constructor(request?: any) {
38
- super(request || {}); // Pass the request parameter properly
39
- // Set the request directly without defaulting to empty object
46
+ super(request || {});
40
47
  (this as any).request = request;
41
-
42
- this.mockValidateBearerToken = jest.fn().mockReturnValue(true);
43
48
  this.mockReady = jest.fn().mockResolvedValue(true);
44
49
  }
45
50
 
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
57
56
  public getRequest() {
58
57
  return (this as any).request;
59
58
  }
60
59
 
61
- public getMockValidateBearerToken() {
62
- return this.mockValidateBearerToken;
63
- }
64
-
65
60
  public getMockReady() {
66
61
  return this.mockReady;
67
62
  }
@@ -72,35 +67,99 @@ describe('ToolFunction', () => {
72
67
  let mockResponse: Response;
73
68
  let toolFunction: TestToolFunction;
74
69
  let mockProcessRequest: jest.MockedFunction<typeof toolsService.processRequest>;
75
- let mockExtractBearerToken: jest.MockedFunction<typeof toolsService.extractBearerToken>;
70
+ let mockGetTokenVerifier: jest.MockedFunction<typeof getTokenVerifier>;
71
+ let mockGetAppContext: jest.MockedFunction<typeof getAppContext>;
72
+ let mockTokenVerifier: jest.Mocked<{
73
+ verify: (token: string) => Promise<any>;
74
+ }>;
76
75
 
77
76
  beforeEach(() => {
78
77
  jest.clearAllMocks();
79
78
 
80
- // Create mock instances
81
- mockRequest = {
82
- headers: new Map(),
83
- method: 'POST',
84
- path: '/test'
79
+ // Create mock token verifier
80
+ mockTokenVerifier = {
81
+ verify: jest.fn(),
85
82
  };
86
- mockResponse = {} as Response;
87
83
 
88
84
  // Setup the mocks
89
85
  mockProcessRequest = jest.mocked(toolsService.processRequest);
90
- mockExtractBearerToken = jest.mocked(toolsService.extractBearerToken);
86
+ mockGetTokenVerifier = jest.mocked(getTokenVerifier);
87
+ mockGetAppContext = jest.mocked(getAppContext);
91
88
 
92
- // Create test instance
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
97
+ mockRequest = {
98
+ headers: new Map(),
99
+ 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
+ }
122
+ };
123
+
124
+ mockResponse = {} as Response;
93
125
  toolFunction = new TestToolFunction(mockRequest);
94
126
  });
95
127
 
128
+ // Helper function to create a ready request with valid auth
129
+ const createReadyRequestWithAuth = () => ({
130
+ headers: new Map(),
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
+ });
143
+
144
+ // Helper function to setup authorization mocks to pass
145
+ const setupAuthMocks = () => {
146
+ mockTokenVerifier.verify.mockResolvedValue(true);
147
+ mockGetAppContext.mockReturnValue({
148
+ account: {
149
+ organizationId: 'app-org-123'
150
+ }
151
+ } as any);
152
+ };
153
+
96
154
  describe('/ready endpoint', () => {
155
+ beforeEach(() => {
156
+ setupAuthMocks();
157
+ });
158
+
97
159
  it('should return ready: true when ready method returns true', async () => {
98
160
  // Arrange
99
- const readyRequest = {
100
- headers: new Map(),
101
- method: 'GET',
102
- path: '/ready'
103
- };
161
+ const readyRequest = createReadyRequestWithAuth();
162
+
104
163
  toolFunction = new TestToolFunction(readyRequest);
105
164
  toolFunction.getMockReady().mockResolvedValue(true);
106
165
 
@@ -115,11 +174,8 @@ describe('ToolFunction', () => {
115
174
 
116
175
  it('should return ready: false when ready method returns false', async () => {
117
176
  // Arrange
118
- const readyRequest = {
119
- headers: new Map(),
120
- method: 'GET',
121
- path: '/ready'
122
- };
177
+ const readyRequest = createReadyRequestWithAuth();
178
+
123
179
  toolFunction = new TestToolFunction(readyRequest);
124
180
  toolFunction.getMockReady().mockResolvedValue(false);
125
181
 
@@ -134,11 +190,8 @@ describe('ToolFunction', () => {
134
190
 
135
191
  it('should handle ready method throwing an error', async () => {
136
192
  // Arrange
137
- const readyRequest = {
138
- headers: new Map(),
139
- method: 'GET',
140
- path: '/ready'
141
- };
193
+ const readyRequest = createReadyRequestWithAuth();
194
+
142
195
  toolFunction = new TestToolFunction(readyRequest);
143
196
  toolFunction.getMockReady().mockRejectedValue(new Error('Ready check failed'));
144
197
 
@@ -148,52 +201,6 @@ describe('ToolFunction', () => {
148
201
  expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
149
202
  });
150
203
 
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
-
197
204
  it('should use default ready implementation when not overridden', async () => {
198
205
  // Create a class that doesn't override ready method
199
206
  class DefaultReadyToolFunction extends ToolFunction {
@@ -208,11 +215,7 @@ describe('ToolFunction', () => {
208
215
  }
209
216
 
210
217
  // Arrange
211
- const readyRequest = {
212
- headers: new Map(),
213
- method: 'GET',
214
- path: '/ready'
215
- };
218
+ const readyRequest = createReadyRequestWithAuth();
216
219
  const defaultToolFunction = new DefaultReadyToolFunction(readyRequest);
217
220
 
218
221
  // Act
@@ -224,159 +227,136 @@ describe('ToolFunction', () => {
224
227
  });
225
228
  });
226
229
 
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);
230
+ describe('perform', () => {
231
+ it('should execute successfully with valid token and matching organization', async () => {
232
+ // Setup mock token verifier to return true for valid token
233
+ mockTokenVerifier.verify.mockResolvedValue(true);
233
234
  mockProcessRequest.mockResolvedValue(mockResponse);
234
235
 
235
- // Act
236
236
  const result = await toolFunction.perform();
237
237
 
238
- // Assert
239
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
240
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
241
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
242
238
  expect(result).toBe(mockResponse);
239
+ expect(mockGetTokenVerifier).toHaveBeenCalled();
240
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
241
+ expect(mockGetAppContext).toHaveBeenCalled();
242
+ expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
243
243
  });
244
244
 
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);
245
+ it('should return 403 response with invalid token', async () => {
246
+ // Setup mock token verifier to return false
247
+ mockTokenVerifier.verify.mockResolvedValue(false);
250
248
 
251
- // Act
252
249
  const result = await toolFunction.perform();
253
250
 
254
- // Assert
255
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
256
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
257
- expect(mockProcessRequest).not.toHaveBeenCalled();
258
251
  expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
252
+ expect(mockGetTokenVerifier).toHaveBeenCalled();
253
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
254
+ expect(mockProcessRequest).not.toHaveBeenCalled();
259
255
  });
260
256
 
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);
268
-
269
- // Act
270
- const result = await toolFunction.perform();
257
+ it('should return 403 response when organization ID does not match', async () => {
258
+ // Update mock request with different customer_id
259
+ const requestWithDifferentOrgId = {
260
+ ...mockRequest,
261
+ bodyJSON: {
262
+ ...mockRequest.bodyJSON,
263
+ auth: {
264
+ ...mockRequest.bodyJSON.auth,
265
+ credentials: {
266
+ ...mockRequest.bodyJSON.auth.credentials,
267
+ customer_id: 'different-org-123'
268
+ }
269
+ }
270
+ }
271
+ };
271
272
 
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
- });
273
+ const toolFunctionWithDifferentOrgId = new TestToolFunction(requestWithDifferentOrgId);
278
274
 
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
- });
275
+ const result = await toolFunctionWithDifferentOrgId.perform();
286
276
 
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);
277
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
278
+ expect(mockGetAppContext).toHaveBeenCalled();
291
279
  expect(mockProcessRequest).not.toHaveBeenCalled();
292
280
  });
293
281
 
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);
300
-
301
- // Act
302
- await toolFunction.perform();
303
-
304
- // Assert
305
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledTimes(1);
306
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
307
- });
282
+ it('should return 403 response when access token is missing', async () => {
283
+ // Create request without access token
284
+ const requestWithoutToken = {
285
+ ...mockRequest,
286
+ bodyJSON: {
287
+ ...mockRequest.bodyJSON,
288
+ auth: {
289
+ ...mockRequest.bodyJSON.auth,
290
+ credentials: {
291
+ ...mockRequest.bodyJSON.auth.credentials,
292
+ access_token: undefined
293
+ }
294
+ }
295
+ }
296
+ };
308
297
 
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);
298
+ const toolFunctionWithoutToken = new TestToolFunction(requestWithoutToken);
315
299
 
316
- // Act
317
- await toolFunction.perform();
300
+ const result = await toolFunctionWithoutToken.perform();
318
301
 
319
- // Assert
320
- expect(mockExtractBearerToken).toHaveBeenCalledTimes(1);
321
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
302
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
303
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
304
+ expect(mockProcessRequest).not.toHaveBeenCalled();
322
305
  });
323
306
 
324
- it('should skip validation when bearer token is undefined', async () => {
325
- // Arrange
326
- mockExtractBearerToken.mockReturnValue(undefined);
327
- mockProcessRequest.mockResolvedValue(mockResponse);
328
-
329
- // Act
330
- const result = await toolFunction.perform();
331
-
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);
337
- });
307
+ it('should return 403 response when organisation id is missing', async () => {
308
+ // Create request without customer_id
309
+ const requestWithoutCustomerId = {
310
+ ...mockRequest,
311
+ bodyJSON: {
312
+ ...mockRequest.bodyJSON,
313
+ auth: {
314
+ ...mockRequest.bodyJSON.auth,
315
+ credentials: {
316
+ ...mockRequest.bodyJSON.auth.credentials,
317
+ customer_id: undefined
318
+ }
319
+ }
320
+ }
321
+ };
338
322
 
339
- it('should skip validation when bearer token is empty string', async () => {
340
- // Arrange
341
- mockExtractBearerToken.mockReturnValue('');
342
- mockProcessRequest.mockResolvedValue(mockResponse);
323
+ const toolFunctionWithoutCustomerId = new TestToolFunction(requestWithoutCustomerId);
343
324
 
344
- // Act
345
- const result = await toolFunction.perform();
325
+ const result = await toolFunctionWithoutCustomerId.perform();
346
326
 
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);
327
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
328
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
329
+ expect(mockProcessRequest).not.toHaveBeenCalled();
352
330
  });
353
331
 
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;
332
+ it('should return 403 response when auth structure is missing', async () => {
333
+ // Create request without auth structure
334
+ const requestWithoutAuth = {
335
+ ...mockRequest,
336
+ bodyJSON: {
337
+ parameters: mockRequest.bodyJSON.parameters,
338
+ environment: mockRequest.bodyJSON.environment
360
339
  }
340
+ };
361
341
 
362
- public getRequest() {
363
- return (this as any).request;
364
- }
365
- }
342
+ const toolFunctionWithoutAuth = new TestToolFunction(requestWithoutAuth);
366
343
 
367
- // Arrange
368
- const defaultToolFunction = new DefaultToolFunction(mockRequest);
369
- const bearerToken = 'any-token';
370
- mockExtractBearerToken.mockReturnValue(bearerToken);
371
- mockProcessRequest.mockResolvedValue(mockResponse);
344
+ const result = await toolFunctionWithoutAuth.perform();
372
345
 
373
- // Act
374
- const result = await defaultToolFunction.perform();
346
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
347
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
348
+ expect(mockProcessRequest).not.toHaveBeenCalled();
349
+ });
375
350
 
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);
351
+ it('should return 403 response when token verifier initialization fails', async () => {
352
+ // Setup mock to fail during token verifier initialization
353
+ mockGetTokenVerifier.mockRejectedValue(new Error('Failed to initialize token verifier'));
354
+
355
+ const result = await toolFunction.perform();
356
+
357
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
358
+ expect(mockGetTokenVerifier).toHaveBeenCalled();
359
+ expect(mockProcessRequest).not.toHaveBeenCalled();
380
360
  });
381
361
  });
382
362
 
@@ -1,5 +1,7 @@
1
- import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
1
+ import { Function, Response, amendLogContext, getAppContext, logger } from '@zaiusinc/app-sdk';
2
2
  import { toolsService } from '../service/Service';
3
+ import { getTokenVerifier } from '../auth/TokenVerifier';
4
+ import { OptiIdAuthData } from '../types/Models';
3
5
 
4
6
  /**
5
7
  * Abstract base class for tool-based function execution
@@ -24,8 +26,7 @@ export abstract class ToolFunction extends Function {
24
26
  */
25
27
  public async perform(): Promise<Response> {
26
28
  amendLogContext({ opalThreadId: this.request.headers.get('x-opal-thread-id') || '' });
27
- const bearerToken = toolsService.extractBearerToken(this.request.headers);
28
- if (bearerToken && !this.validateBearerToken(bearerToken)) {
29
+ if (!(await this.authorizeRequest())) {
29
30
  return new Response(403, { error: 'Forbidden' });
30
31
  }
31
32
 
@@ -38,15 +39,48 @@ export abstract class ToolFunction extends Function {
38
39
  }
39
40
 
40
41
  /**
41
- * Validates the bearer token for authorization.
42
+ * Authenticate the incoming request by validating the OptiID token and organization ID
42
43
  *
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
44
+ * @throws true if authentication succeeds
48
45
  */
49
- protected validateBearerToken(_bearerToken: string): boolean {
50
- return true;
46
+ private async authorizeRequest(): Promise<boolean> {
47
+ if (this.request.path === '/discovery' || this.request.path === '/ready') {
48
+ return true;
49
+ }
50
+
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
+ }
51
84
  }
85
+
52
86
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './function/ToolFunction';
2
2
  export * from './types/Models';
3
3
  export * from './decorator/Decorator';
4
+ export * from './auth/TokenVerifier';
4
5
  export { Tool, Interaction, InteractionResult } from './service/Service';
@@ -122,7 +122,8 @@ 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'
125
+ http_method: 'POST',
126
+ auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
126
127
  });
127
128
  });
128
129
 
@@ -181,7 +182,8 @@ describe('ToolsService', () => {
181
182
  description: mockTool.description,
182
183
  parameters: mockTool.parameters.map((p: Parameter) => p.toJSON()),
183
184
  endpoint: mockTool.endpoint,
184
- http_method: 'POST'
185
+ http_method: 'POST',
186
+ auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
185
187
  });
186
188
 
187
189
  expect(secondFunction).toEqual({
@@ -190,7 +192,10 @@ describe('ToolsService', () => {
190
192
  parameters: [],
191
193
  endpoint: '/second-tool',
192
194
  http_method: 'POST',
193
- auth_requirements: authRequirements.map((auth) => auth.toJSON())
195
+ auth_requirements: [
196
+ { provider: 'oauth2', scope_bundle: 'calendar', required: true },
197
+ { provider: 'OptiID', scope_bundle: 'default', required: true }
198
+ ]
194
199
  });
195
200
  });
196
201
  });