@optimizely-opal/opal-tool-ocp-sdk 0.0.0-beta.1 → 0.0.0-beta.10

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 (46) hide show
  1. package/README.md +106 -51
  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.d.ts +4 -2
  11. package/dist/decorator/Decorator.d.ts.map +1 -1
  12. package/dist/decorator/Decorator.js +26 -4
  13. package/dist/decorator/Decorator.js.map +1 -1
  14. package/dist/decorator/Decorator.test.js +110 -0
  15. package/dist/decorator/Decorator.test.js.map +1 -1
  16. package/dist/function/ToolFunction.d.ts +14 -1
  17. package/dist/function/ToolFunction.d.ts.map +1 -1
  18. package/dist/function/ToolFunction.js +59 -3
  19. package/dist/function/ToolFunction.js.map +1 -1
  20. package/dist/function/ToolFunction.test.js +229 -104
  21. package/dist/function/ToolFunction.test.js.map +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/service/Service.d.ts +14 -13
  27. package/dist/service/Service.d.ts.map +1 -1
  28. package/dist/service/Service.js +25 -19
  29. package/dist/service/Service.js.map +1 -1
  30. package/dist/service/Service.test.js +122 -36
  31. package/dist/service/Service.test.js.map +1 -1
  32. package/dist/types/Models.d.ts +5 -5
  33. package/dist/types/Models.d.ts.map +1 -1
  34. package/dist/types/Models.js +9 -9
  35. package/dist/types/Models.js.map +1 -1
  36. package/package.json +10 -3
  37. package/src/auth/TokenVerifier.test.ts +152 -0
  38. package/src/auth/TokenVerifier.ts +145 -0
  39. package/src/decorator/Decorator.test.ts +126 -0
  40. package/src/decorator/Decorator.ts +32 -4
  41. package/src/function/ToolFunction.test.ts +259 -109
  42. package/src/function/ToolFunction.ts +66 -5
  43. package/src/index.ts +1 -0
  44. package/src/service/Service.test.ts +139 -28
  45. package/src/service/Service.ts +31 -24
  46. 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,33 +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
- // Create a mock implementation of the abstract method
41
- this.mockValidateBearerToken = jest.fn().mockReturnValue(true);
48
+ this.mockReady = jest.fn().mockResolvedValue(true);
42
49
  }
43
50
 
44
- // Implement the abstract method
45
- protected validateBearerToken(bearerToken: string): boolean {
46
- return this.mockValidateBearerToken(bearerToken);
51
+ // Override the ready method with mock implementation for testing
52
+ protected ready(): Promise<boolean> {
53
+ return this.mockReady();
47
54
  }
48
55
 
49
- // Expose request and validation mock for testing
50
56
  public getRequest() {
51
57
  return (this as any).request;
52
58
  }
53
59
 
54
- public getMockValidateBearerToken() {
55
- return this.mockValidateBearerToken;
60
+ public getMockReady() {
61
+ return this.mockReady;
56
62
  }
57
63
  }
58
64
 
@@ -61,152 +67,296 @@ describe('ToolFunction', () => {
61
67
  let mockResponse: Response;
62
68
  let toolFunction: TestToolFunction;
63
69
  let mockProcessRequest: jest.MockedFunction<typeof toolsService.processRequest>;
64
- 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
+ }>;
65
75
 
66
76
  beforeEach(() => {
67
77
  jest.clearAllMocks();
68
78
 
69
- // Create mock instances
70
- mockRequest = {
71
- headers: new Map(),
72
- method: 'POST',
73
- path: '/test'
79
+ // Create mock token verifier
80
+ mockTokenVerifier = {
81
+ verify: jest.fn(),
74
82
  };
75
- mockResponse = {} as Response;
76
83
 
77
84
  // Setup the mocks
78
85
  mockProcessRequest = jest.mocked(toolsService.processRequest);
79
- mockExtractBearerToken = jest.mocked(toolsService.extractBearerToken);
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
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
+ };
80
123
 
81
- // Create test instance
124
+ mockResponse = {} as Response;
82
125
  toolFunction = new TestToolFunction(mockRequest);
83
126
  });
84
127
 
85
- describe('bearer token validation', () => {
86
- it('should extract bearer token from headers and validate it', async () => {
87
- // Arrange
88
- const bearerToken = 'valid-token-123';
89
- mockExtractBearerToken.mockReturnValue(bearerToken);
90
- toolFunction.getMockValidateBearerToken().mockReturnValue(true);
91
- mockProcessRequest.mockResolvedValue(mockResponse);
92
-
93
- // Act
94
- const result = await toolFunction.perform();
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
+ });
95
143
 
96
- // Assert
97
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
98
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
99
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
100
- expect(result).toBe(mockResponse);
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
+
154
+ describe('/ready endpoint', () => {
155
+ beforeEach(() => {
156
+ setupAuthMocks();
101
157
  });
102
158
 
103
- it('should return 403 Forbidden when bearer token validation fails', async () => {
159
+ it('should return ready: true when ready method returns true', async () => {
104
160
  // Arrange
105
- const bearerToken = 'invalid-token-456';
106
- mockExtractBearerToken.mockReturnValue(bearerToken);
107
- toolFunction.getMockValidateBearerToken().mockReturnValue(false);
161
+ const readyRequest = createReadyRequestWithAuth();
162
+
163
+ toolFunction = new TestToolFunction(readyRequest);
164
+ toolFunction.getMockReady().mockResolvedValue(true);
108
165
 
109
166
  // Act
110
167
  const result = await toolFunction.perform();
111
168
 
112
169
  // Assert
113
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
114
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
115
- expect(mockProcessRequest).not.toHaveBeenCalled();
116
- 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
117
173
  });
118
174
 
119
- it('should handle complex bearer token validation scenarios', async () => {
120
- // Arrange - simulate a token that should be valid based on some complex logic
121
- const complexToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9l' +
122
- 'IiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
123
- mockExtractBearerToken.mockReturnValue(complexToken);
124
- toolFunction.getMockValidateBearerToken().mockReturnValue(true);
125
- 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);
126
181
 
127
182
  // Act
128
183
  const result = await toolFunction.perform();
129
184
 
130
185
  // Assert
131
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
132
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(complexToken);
133
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
134
- 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
135
189
  });
136
190
 
137
- it('should handle bearer token validation throwing an error', async () => {
191
+ it('should handle ready method throwing an error', async () => {
138
192
  // Arrange
139
- const bearerToken = 'malformed-token';
140
- mockExtractBearerToken.mockReturnValue(bearerToken);
141
- toolFunction.getMockValidateBearerToken().mockImplementation(() => {
142
- throw new Error('Token validation error');
143
- });
193
+ const readyRequest = createReadyRequestWithAuth();
194
+
195
+ toolFunction = new TestToolFunction(readyRequest);
196
+ toolFunction.getMockReady().mockRejectedValue(new Error('Ready check failed'));
144
197
 
145
198
  // Act & Assert
146
- await expect(toolFunction.perform()).rejects.toThrow('Token validation error');
147
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
148
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
149
- 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
150
202
  });
151
203
 
152
- it('should call validateBearerToken only once per request', async () => {
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
+ }
211
+
212
+ public getRequest() {
213
+ return (this as any).request;
214
+ }
215
+ }
216
+
153
217
  // Arrange
154
- const bearerToken = 'test-token';
155
- mockExtractBearerToken.mockReturnValue(bearerToken);
156
- toolFunction.getMockValidateBearerToken().mockReturnValue(true);
157
- mockProcessRequest.mockResolvedValue(mockResponse);
218
+ const readyRequest = createReadyRequestWithAuth();
219
+ const defaultToolFunction = new DefaultReadyToolFunction(readyRequest);
158
220
 
159
221
  // Act
160
- await toolFunction.perform();
222
+ const result = await defaultToolFunction.perform();
161
223
 
162
- // Assert
163
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledTimes(1);
164
- expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
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
165
227
  });
228
+ });
166
229
 
167
- it('should extract bearer token only once per request', async () => {
168
- // Arrange
169
- const bearerToken = 'test-token';
170
- mockExtractBearerToken.mockReturnValue(bearerToken);
171
- 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);
172
234
  mockProcessRequest.mockResolvedValue(mockResponse);
173
235
 
174
- // Act
175
- await toolFunction.perform();
236
+ const result = await toolFunction.perform();
176
237
 
177
- // Assert
178
- expect(mockExtractBearerToken).toHaveBeenCalledTimes(1);
179
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
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);
180
243
  });
181
244
 
182
- it('should skip validation when bearer token is undefined', async () => {
183
- // Arrange
184
- mockExtractBearerToken.mockReturnValue(undefined);
185
- 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);
186
248
 
187
- // Act
188
249
  const result = await toolFunction.perform();
189
250
 
190
- // Assert
191
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
192
- expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
193
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
194
- 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();
195
255
  });
196
256
 
197
- it('should skip validation when bearer token is empty string', async () => {
198
- // Arrange
199
- mockExtractBearerToken.mockReturnValue('');
200
- mockProcessRequest.mockResolvedValue(mockResponse);
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
+ };
272
+
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
+ }
295
+ }
296
+ };
297
+
298
+ const toolFunctionWithoutToken = new TestToolFunction(requestWithoutToken);
299
+
300
+ const result = await toolFunctionWithoutToken.perform();
301
+
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'));
201
354
 
202
- // Act
203
355
  const result = await toolFunction.perform();
204
356
 
205
- // Assert
206
- expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
207
- expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
208
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
209
- expect(result).toBe(mockResponse);
357
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
358
+ expect(mockGetTokenVerifier).toHaveBeenCalled();
359
+ expect(mockProcessRequest).not.toHaveBeenCalled();
210
360
  });
211
361
  });
212
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,19 +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
 
21
- return toolsService.processRequest(this.request);
33
+ if (this.request.path === '/ready') {
34
+ const isReady = await this.ready();
35
+ return new Response(200, { ready: isReady });
36
+ }
37
+ // Pass 'this' as context so decorated methods can use the existing instance
38
+ return toolsService.processRequest(this.request, this);
39
+ }
40
+
41
+ /**
42
+ * Authenticate the incoming request by validating the OptiID token and organization ID
43
+ *
44
+ * @throws true if authentication succeeds
45
+ */
46
+ private async authorizeRequest(): Promise<boolean> {
47
+ if (this.request.path === '/discovery' || this.request.path === '/ready') {
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
+ }
22
84
  }
23
85
 
24
- protected abstract validateBearerToken(bearerToken: string): boolean;
25
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';