@optimizely-opal/opal-tool-ocp-sdk 0.0.0-dev.5 → 0.0.0-devmg.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.
Files changed (40) hide show
  1. package/README.md +106 -45
  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/decorator/Decorator.test.js.map +1 -1
  11. package/dist/function/ToolFunction.d.ts +11 -7
  12. package/dist/function/ToolFunction.d.ts.map +1 -1
  13. package/dist/function/ToolFunction.js +53 -10
  14. package/dist/function/ToolFunction.js.map +1 -1
  15. package/dist/function/ToolFunction.test.js +225 -122
  16. package/dist/function/ToolFunction.test.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/service/Service.d.ts +14 -13
  22. package/dist/service/Service.d.ts.map +1 -1
  23. package/dist/service/Service.js +22 -16
  24. package/dist/service/Service.js.map +1 -1
  25. package/dist/service/Service.test.js +53 -36
  26. package/dist/service/Service.test.js.map +1 -1
  27. package/dist/types/Models.d.ts +5 -5
  28. package/dist/types/Models.d.ts.map +1 -1
  29. package/dist/types/Models.js +9 -9
  30. package/dist/types/Models.js.map +1 -1
  31. package/package.json +10 -3
  32. package/src/auth/TokenVerifier.test.ts +152 -0
  33. package/src/auth/TokenVerifier.ts +145 -0
  34. package/src/decorator/Decorator.test.ts +4 -4
  35. package/src/function/ToolFunction.test.ts +251 -128
  36. package/src/function/ToolFunction.ts +60 -11
  37. package/src/index.ts +1 -0
  38. package/src/service/Service.test.ts +55 -37
  39. package/src/service/Service.ts +29 -22
  40. 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
  },
@@ -26,32 +28,37 @@ jest.mock('@zaiusinc/app-sdk', () => ({
26
28
  bodyJSON: data,
27
29
  bodyAsU8Array: new Uint8Array()
28
30
  })),
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
+ },
29
39
  }));
30
40
 
31
41
  // Create a concrete implementation for testing
32
42
  class TestToolFunction extends ToolFunction {
33
- private mockValidateBearerToken: jest.MockedFunction<(token: string) => boolean>;
43
+ private mockReady: jest.MockedFunction<() => Promise<boolean>>;
34
44
 
35
45
  public constructor(request?: any) {
36
- super(request || {}); // Pass the request parameter properly
37
- // Set the request directly without defaulting to empty object
46
+ super(request || {});
38
47
  (this as any).request = request;
39
-
40
- this.mockValidateBearerToken = jest.fn().mockReturnValue(true);
48
+ this.mockReady = jest.fn().mockResolvedValue(true);
41
49
  }
42
50
 
43
- // Override the concrete method with mock implementation for testing
44
- protected validateBearerToken(bearerToken: string): boolean {
45
- return this.mockValidateBearerToken(bearerToken);
51
+ // Override the ready method with mock implementation for testing
52
+ protected ready(): Promise<boolean> {
53
+ return this.mockReady();
46
54
  }
47
55
 
48
- // Expose request and validation mock for testing
49
56
  public getRequest() {
50
57
  return (this as any).request;
51
58
  }
52
59
 
53
- public getMockValidateBearerToken() {
54
- return this.mockValidateBearerToken;
60
+ public getMockReady() {
61
+ return this.mockReady;
55
62
  }
56
63
  }
57
64
 
@@ -60,180 +67,296 @@ describe('ToolFunction', () => {
60
67
  let mockResponse: Response;
61
68
  let toolFunction: TestToolFunction;
62
69
  let mockProcessRequest: jest.MockedFunction<typeof toolsService.processRequest>;
63
- 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
+ }>;
64
75
 
65
76
  beforeEach(() => {
66
77
  jest.clearAllMocks();
67
78
 
68
- // Create mock instances
69
- mockRequest = {
70
- headers: new Map(),
71
- method: 'POST',
72
- path: '/test'
79
+ // Create mock token verifier
80
+ mockTokenVerifier = {
81
+ verify: jest.fn(),
73
82
  };
74
- mockResponse = {} as Response;
75
83
 
76
84
  // Setup the mocks
77
85
  mockProcessRequest = jest.mocked(toolsService.processRequest);
78
- mockExtractBearerToken = jest.mocked(toolsService.extractBearerToken);
86
+ mockGetTokenVerifier = jest.mocked(getTokenVerifier);
87
+ mockGetAppContext = jest.mocked(getAppContext);
79
88
 
80
- // 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;
81
125
  toolFunction = new TestToolFunction(mockRequest);
82
126
  });
83
127
 
84
- describe('bearer token validation', () => {
85
- it('should extract bearer token from headers and validate it', async () => {
86
- // Arrange
87
- const bearerToken = 'valid-token-123';
88
- mockExtractBearerToken.mockReturnValue(bearerToken);
89
- toolFunction.getMockValidateBearerToken().mockReturnValue(true);
90
- mockProcessRequest.mockResolvedValue(mockResponse);
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
+ });
91
143
 
92
- // Act
93
- const result = await toolFunction.perform();
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
+ };
94
153
 
95
- // Assert
96
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
97
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
98
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
99
- expect(result).toBe(mockResponse);
154
+ describe('/ready endpoint', () => {
155
+ beforeEach(() => {
156
+ setupAuthMocks();
100
157
  });
101
158
 
102
- it('should return 403 Forbidden when bearer token validation fails', async () => {
159
+ it('should return ready: true when ready method returns true', async () => {
103
160
  // Arrange
104
- const bearerToken = 'invalid-token-456';
105
- mockExtractBearerToken.mockReturnValue(bearerToken);
106
- toolFunction.getMockValidateBearerToken().mockReturnValue(false);
161
+ const readyRequest = createReadyRequestWithAuth();
162
+
163
+ toolFunction = new TestToolFunction(readyRequest);
164
+ toolFunction.getMockReady().mockResolvedValue(true);
107
165
 
108
166
  // Act
109
167
  const result = await toolFunction.perform();
110
168
 
111
169
  // Assert
112
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
113
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
114
- expect(mockProcessRequest).not.toHaveBeenCalled();
115
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
170
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
171
+ expect(result).toEqual(new Response(200, { ready: true }));
172
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
116
173
  });
117
174
 
118
- it('should handle complex bearer token validation scenarios', async () => {
119
- // Arrange - simulate a token that should be valid based on some complex logic
120
- const complexToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9l' +
121
- 'IiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
122
- mockExtractBearerToken.mockReturnValue(complexToken);
123
- toolFunction.getMockValidateBearerToken().mockReturnValue(true);
124
- mockProcessRequest.mockResolvedValue(mockResponse);
175
+ it('should return ready: false when ready method returns false', async () => {
176
+ // Arrange
177
+ const readyRequest = createReadyRequestWithAuth();
178
+
179
+ toolFunction = new TestToolFunction(readyRequest);
180
+ toolFunction.getMockReady().mockResolvedValue(false);
125
181
 
126
182
  // Act
127
183
  const result = await toolFunction.perform();
128
184
 
129
185
  // Assert
130
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
131
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(complexToken);
132
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
133
- expect(result).toBe(mockResponse);
186
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
187
+ expect(result).toEqual(new Response(200, { ready: false }));
188
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
134
189
  });
135
190
 
136
- it('should handle bearer token validation throwing an error', async () => {
191
+ it('should handle ready method throwing an error', async () => {
137
192
  // Arrange
138
- const bearerToken = 'malformed-token';
139
- mockExtractBearerToken.mockReturnValue(bearerToken);
140
- toolFunction.getMockValidateBearerToken().mockImplementation(() => {
141
- throw new Error('Token validation error');
142
- });
193
+ const readyRequest = createReadyRequestWithAuth();
194
+
195
+ toolFunction = new TestToolFunction(readyRequest);
196
+ toolFunction.getMockReady().mockRejectedValue(new Error('Ready check failed'));
143
197
 
144
198
  // Act & Assert
145
- await expect(toolFunction.perform()).rejects.toThrow('Token validation error');
146
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
147
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
148
- expect(mockProcessRequest).not.toHaveBeenCalled();
199
+ await expect(toolFunction.perform()).rejects.toThrow('Ready check failed');
200
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
201
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
149
202
  });
150
203
 
151
- it('should call validateBearerToken only once per request', async () => {
152
- // Arrange
153
- const bearerToken = 'test-token';
154
- mockExtractBearerToken.mockReturnValue(bearerToken);
155
- toolFunction.getMockValidateBearerToken().mockReturnValue(true);
156
- mockProcessRequest.mockResolvedValue(mockResponse);
157
-
158
- // Act
159
- await toolFunction.perform();
204
+ it('should use default ready implementation when not overridden', async () => {
205
+ // Create a class that doesn't override ready method
206
+ class DefaultReadyToolFunction extends ToolFunction {
207
+ public constructor(request?: any) {
208
+ super(request || {});
209
+ (this as any).request = request;
210
+ }
160
211
 
161
- // Assert
162
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledTimes(1);
163
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
164
- });
212
+ public getRequest() {
213
+ return (this as any).request;
214
+ }
215
+ }
165
216
 
166
- it('should extract bearer token only once per request', async () => {
167
217
  // Arrange
168
- const bearerToken = 'test-token';
169
- mockExtractBearerToken.mockReturnValue(bearerToken);
170
- toolFunction.getMockValidateBearerToken().mockReturnValue(true);
171
- mockProcessRequest.mockResolvedValue(mockResponse);
218
+ const readyRequest = createReadyRequestWithAuth();
219
+ const defaultToolFunction = new DefaultReadyToolFunction(readyRequest);
172
220
 
173
221
  // Act
174
- await toolFunction.perform();
222
+ const result = await defaultToolFunction.perform();
175
223
 
176
- // Assert
177
- expect(mockExtractBearerToken).toHaveBeenCalledTimes(1);
178
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
224
+ // Assert - Default implementation should return true
225
+ expect(result).toEqual(new Response(200, { ready: true }));
226
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
179
227
  });
228
+ });
180
229
 
181
- it('should skip validation when bearer token is undefined', async () => {
182
- // Arrange
183
- mockExtractBearerToken.mockReturnValue(undefined);
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);
184
234
  mockProcessRequest.mockResolvedValue(mockResponse);
185
235
 
186
- // Act
187
236
  const result = await toolFunction.perform();
188
237
 
189
- // Assert
190
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
191
- expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
192
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
193
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);
194
243
  });
195
244
 
196
- it('should skip validation when bearer token is empty string', async () => {
197
- // Arrange
198
- mockExtractBearerToken.mockReturnValue('');
199
- mockProcessRequest.mockResolvedValue(mockResponse);
245
+ it('should return 403 response with invalid token', async () => {
246
+ // Setup mock token verifier to return false
247
+ mockTokenVerifier.verify.mockResolvedValue(false);
200
248
 
201
- // Act
202
249
  const result = await toolFunction.perform();
203
250
 
204
- // Assert
205
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
206
- expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
207
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
208
- expect(result).toBe(mockResponse);
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();
209
255
  });
210
256
 
211
- it('should use default validateBearerToken implementation when not overridden', async () => {
212
- // Create a class that doesn't override validateBearerToken
213
- class DefaultToolFunction extends ToolFunction {
214
- public constructor(request?: any) {
215
- super(request || {});
216
- (this as any).request = request;
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
+ }
217
270
  }
271
+ };
218
272
 
219
- public getRequest() {
220
- return (this as any).request;
273
+ const toolFunctionWithDifferentOrgId = new TestToolFunction(requestWithDifferentOrgId);
274
+
275
+ const result = await toolFunctionWithDifferentOrgId.perform();
276
+
277
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
278
+ expect(mockGetAppContext).toHaveBeenCalled();
279
+ expect(mockProcessRequest).not.toHaveBeenCalled();
280
+ });
281
+
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
+ }
221
295
  }
222
- }
296
+ };
223
297
 
224
- // Arrange
225
- const defaultToolFunction = new DefaultToolFunction(mockRequest);
226
- const bearerToken = 'any-token';
227
- mockExtractBearerToken.mockReturnValue(bearerToken);
228
- mockProcessRequest.mockResolvedValue(mockResponse);
298
+ const toolFunctionWithoutToken = new TestToolFunction(requestWithoutToken);
229
299
 
230
- // Act
231
- const result = await defaultToolFunction.perform();
300
+ const result = await toolFunctionWithoutToken.perform();
232
301
 
233
- // Assert - Default implementation should return true and allow request to proceed
234
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
235
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, defaultToolFunction);
236
- expect(result).toBe(mockResponse);
302
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
303
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
304
+ expect(mockProcessRequest).not.toHaveBeenCalled();
305
+ });
306
+
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
+ };
322
+
323
+ const toolFunctionWithoutCustomerId = new TestToolFunction(requestWithoutCustomerId);
324
+
325
+ const result = await toolFunctionWithoutCustomerId.perform();
326
+
327
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
328
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
329
+ expect(mockProcessRequest).not.toHaveBeenCalled();
330
+ });
331
+
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
339
+ }
340
+ };
341
+
342
+ const toolFunctionWithoutAuth = new TestToolFunction(requestWithoutAuth);
343
+
344
+ const result = await toolFunctionWithoutAuth.perform();
345
+
346
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
347
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
348
+ expect(mockProcessRequest).not.toHaveBeenCalled();
349
+ });
350
+
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();
237
360
  });
238
361
  });
239
362
 
@@ -1,5 +1,7 @@
1
- import { Function, Response } 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
@@ -7,31 +9,78 @@ import { toolsService } from '../service/Service';
7
9
  */
8
10
  export abstract class ToolFunction extends Function {
9
11
 
12
+ /**
13
+ * Override this method to implement any required credentials and/or other configuration
14
+ * exist and are valid. Reasonable caching should be utilized to prevent excessive requests to external resources.
15
+ * @async
16
+ * @returns true if the opal function is ready to use
17
+ */
18
+ protected ready(): Promise<boolean> {
19
+ return Promise.resolve(true);
20
+ }
21
+
10
22
  /**
11
23
  * Process the incoming request using the tools service
12
24
  *
13
25
  * @returns Response as the HTTP response
14
26
  */
15
27
  public async perform(): Promise<Response> {
16
- const bearerToken = toolsService.extractBearerToken(this.request.headers);
17
- if (bearerToken && !this.validateBearerToken(bearerToken)) {
28
+ amendLogContext({ opalThreadId: this.request.headers.get('x-opal-thread-id') || '' });
29
+ if (!(await this.authorizeRequest())) {
18
30
  return new Response(403, { error: 'Forbidden' });
19
31
  }
20
32
 
33
+ if (this.request.path === '/ready') {
34
+ const isReady = await this.ready();
35
+ return new Response(200, { ready: isReady });
36
+ }
21
37
  // Pass 'this' as context so decorated methods can use the existing instance
22
38
  return toolsService.processRequest(this.request, this);
23
39
  }
24
40
 
25
41
  /**
26
- * Validates the bearer token for authorization.
42
+ * Authenticate the incoming request by validating the OptiID token and organization ID
27
43
  *
28
- * This method provides a default implementation that accepts all tokens.
29
- * Subclasses can override this method to implement custom bearer token validation logic.
30
- *
31
- * @param _bearerToken - The bearer token extracted from the Authorization header
32
- * @returns true if the token is valid and the request should proceed, false to return 403 Forbidden
44
+ * @throws true if authentication succeeds
33
45
  */
34
- protected validateBearerToken(_bearerToken: string): boolean {
35
- return true;
46
+ private async authorizeRequest(): Promise<boolean> {
47
+ logger.info('Authorizing request:', this.request.bodyJSON);
48
+ if (this.request.path === '/discovery' || this.request.path === '/ready') {
49
+ return true;
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);
36
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
+ }
84
+ }
85
+
37
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';