@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.2 → 1.0.0-beta.4

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 (67) hide show
  1. package/README.md +114 -0
  2. package/dist/auth/AuthUtils.d.ts +5 -5
  3. package/dist/auth/AuthUtils.d.ts.map +1 -1
  4. package/dist/auth/AuthUtils.js +53 -25
  5. package/dist/auth/AuthUtils.js.map +1 -1
  6. package/dist/auth/AuthUtils.test.js +62 -117
  7. package/dist/auth/AuthUtils.test.js.map +1 -1
  8. package/dist/function/GlobalToolFunction.d.ts +2 -1
  9. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  10. package/dist/function/GlobalToolFunction.js +25 -4
  11. package/dist/function/GlobalToolFunction.js.map +1 -1
  12. package/dist/function/GlobalToolFunction.test.js +57 -8
  13. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  14. package/dist/function/ToolFunction.d.ts +8 -2
  15. package/dist/function/ToolFunction.d.ts.map +1 -1
  16. package/dist/function/ToolFunction.js +31 -5
  17. package/dist/function/ToolFunction.js.map +1 -1
  18. package/dist/function/ToolFunction.test.js +57 -8
  19. package/dist/function/ToolFunction.test.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/logging/ToolLogger.d.ts +34 -0
  25. package/dist/logging/ToolLogger.d.ts.map +1 -0
  26. package/dist/logging/ToolLogger.js +153 -0
  27. package/dist/logging/ToolLogger.js.map +1 -0
  28. package/dist/logging/ToolLogger.test.d.ts +2 -0
  29. package/dist/logging/ToolLogger.test.d.ts.map +1 -0
  30. package/dist/logging/ToolLogger.test.js +646 -0
  31. package/dist/logging/ToolLogger.test.js.map +1 -0
  32. package/dist/service/Service.d.ts +15 -2
  33. package/dist/service/Service.d.ts.map +1 -1
  34. package/dist/service/Service.js +43 -17
  35. package/dist/service/Service.js.map +1 -1
  36. package/dist/service/Service.test.js +84 -2
  37. package/dist/service/Service.test.js.map +1 -1
  38. package/dist/types/ToolError.d.ts +59 -0
  39. package/dist/types/ToolError.d.ts.map +1 -0
  40. package/dist/types/ToolError.js +79 -0
  41. package/dist/types/ToolError.js.map +1 -0
  42. package/dist/types/ToolError.test.d.ts +2 -0
  43. package/dist/types/ToolError.test.d.ts.map +1 -0
  44. package/dist/types/ToolError.test.js +161 -0
  45. package/dist/types/ToolError.test.js.map +1 -0
  46. package/dist/validation/ParameterValidator.d.ts +5 -16
  47. package/dist/validation/ParameterValidator.d.ts.map +1 -1
  48. package/dist/validation/ParameterValidator.js +10 -3
  49. package/dist/validation/ParameterValidator.js.map +1 -1
  50. package/dist/validation/ParameterValidator.test.js +186 -146
  51. package/dist/validation/ParameterValidator.test.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/auth/AuthUtils.test.ts +62 -157
  54. package/src/auth/AuthUtils.ts +66 -32
  55. package/src/function/GlobalToolFunction.test.ts +57 -8
  56. package/src/function/GlobalToolFunction.ts +37 -6
  57. package/src/function/ToolFunction.test.ts +57 -8
  58. package/src/function/ToolFunction.ts +45 -7
  59. package/src/index.ts +1 -0
  60. package/src/logging/ToolLogger.test.ts +753 -0
  61. package/src/logging/ToolLogger.ts +177 -0
  62. package/src/service/Service.test.ts +103 -2
  63. package/src/service/Service.ts +45 -17
  64. package/src/types/ToolError.test.ts +192 -0
  65. package/src/types/ToolError.ts +95 -0
  66. package/src/validation/ParameterValidator.test.ts +185 -158
  67. package/src/validation/ParameterValidator.ts +17 -20
@@ -1,23 +1,30 @@
1
1
  import { getAppContext, logger } from '@zaiusinc/app-sdk';
2
2
  import { getTokenVerifier } from './TokenVerifier';
3
3
  import { OptiIdAuthData } from '../types/Models';
4
+ import { ToolError } from '../types/ToolError';
4
5
 
5
6
  /**
6
7
  * Validate the OptiID access token
7
8
  *
8
9
  * @param accessToken - The access token to validate
9
- * @returns true if the token is valid
10
+ * @throws {ToolError} If token validation fails
10
11
  */
11
- async function validateAccessToken(accessToken: string | undefined): Promise<boolean> {
12
+ async function validateAccessToken(accessToken: string | undefined): Promise<void> {
12
13
  try {
13
14
  if (!accessToken) {
14
- return false;
15
+ throw new ToolError('Forbidden', 403, 'OptiID access token is required');
15
16
  }
16
17
  const tokenVerifier = await getTokenVerifier();
17
- return await tokenVerifier.verify(accessToken);
18
+ const isValid = await tokenVerifier.verify(accessToken);
19
+ if (!isValid) {
20
+ throw new ToolError('Forbidden', 403, 'Invalid OptiID access token');
21
+ }
18
22
  } catch (error) {
23
+ if (error instanceof ToolError) {
24
+ throw error;
25
+ }
19
26
  logger.error('OptiID token validation failed:', error);
20
- return false;
27
+ throw new ToolError('Forbidden', 403, 'Token verification failed');
21
28
  }
22
29
  }
23
30
 
@@ -25,37 +32,70 @@ async function validateAccessToken(accessToken: string | undefined): Promise<boo
25
32
  * Extract and validate basic OptiID authentication data from request
26
33
  *
27
34
  * @param request - The incoming request
28
- * @returns object with authData and accessToken, or null if invalid
35
+ * @returns object with authData and accessToken, or null if auth is missing
29
36
  */
30
37
  export function extractAuthData(request: any): { authData: OptiIdAuthData; accessToken: string } | null {
31
38
  const authData = request?.bodyJSON?.auth as OptiIdAuthData;
39
+
40
+ if (!authData) {
41
+ return null;
42
+ }
43
+
44
+ if (authData?.provider?.toLowerCase() !== 'optiid') {
45
+ return null;
46
+ }
47
+
32
48
  const accessToken = authData?.credentials?.access_token;
33
- if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
49
+ if (!accessToken) {
34
50
  return null;
35
51
  }
36
52
 
37
53
  return { authData, accessToken };
38
54
  }
39
55
 
56
+ /**
57
+ * Extract and validate auth data with error throwing for authentication flow
58
+ *
59
+ * @param request - The incoming request
60
+ * @returns object with authData and accessToken
61
+ * @throws {ToolError} If auth data is invalid or missing
62
+ */
63
+ function extractAndValidateAuthData(request: any): { authData: OptiIdAuthData; accessToken: string } {
64
+ const authData = request?.bodyJSON?.auth as OptiIdAuthData;
65
+
66
+ if (!authData) {
67
+ throw new ToolError('Forbidden', 403, 'Authentication data is required');
68
+ }
69
+
70
+ if (authData?.provider?.toLowerCase() !== 'optiid') {
71
+ throw new ToolError('Forbidden', 403, 'Only OptiID authentication provider is supported');
72
+ }
73
+
74
+ const accessToken = authData?.credentials?.access_token;
75
+ if (!accessToken) {
76
+ throw new ToolError('Forbidden', 403, 'OptiID access token is required');
77
+ }
78
+
79
+ return { authData, accessToken };
80
+ }
81
+
40
82
  /**
41
83
  * Validate organization ID matches the app context
42
84
  *
43
85
  * @param customerId - The customer ID from the auth data
44
- * @returns true if the organization ID is valid
86
+ * @throws {ToolError} If organization ID is invalid or missing
45
87
  */
46
- function validateOrganizationId(customerId: string | undefined): boolean {
88
+ function validateOrganizationId(customerId: string | undefined): void {
47
89
  if (!customerId) {
48
90
  logger.error('Organisation ID is required but not provided');
49
- return false;
91
+ throw new ToolError('Forbidden', 403, 'Organization ID is required');
50
92
  }
51
93
 
52
94
  const appOrganisationId = getAppContext()?.account?.organizationId;
53
95
  if (customerId !== appOrganisationId) {
54
96
  logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
55
- return false;
97
+ throw new ToolError('Forbidden', 403, 'Organization ID does not match');
56
98
  }
57
-
58
- return true;
59
99
  }
60
100
 
61
101
  /**
@@ -73,45 +113,39 @@ function shouldSkipAuth(request: any): boolean {
73
113
  *
74
114
  * @param request - The incoming request
75
115
  * @param validateOrg - Whether to validate organization ID
76
- * @returns true if authentication succeeds
116
+ * @throws {ToolError} If authentication fails
77
117
  */
78
- async function authenticateRequest(request: any, validateOrg: boolean): Promise<boolean> {
118
+ async function authenticateRequest(request: any, validateOrg: boolean): Promise<void> {
79
119
  if (shouldSkipAuth(request)) {
80
- return true;
81
- }
82
-
83
- const authInfo = extractAuthData(request);
84
- if (!authInfo) {
85
- logger.error('OptiID token is required but not provided');
86
- return false;
120
+ return;
87
121
  }
88
122
 
89
- const { authData, accessToken } = authInfo;
123
+ const { authData, accessToken } = extractAndValidateAuthData(request);
90
124
 
91
125
  // Validate organization ID if required
92
- if (validateOrg && !validateOrganizationId(authData.credentials?.customer_id)) {
93
- return false;
126
+ if (validateOrg) {
127
+ validateOrganizationId(authData.credentials?.customer_id);
94
128
  }
95
129
 
96
- return await validateAccessToken(accessToken);
130
+ await validateAccessToken(accessToken);
97
131
  }
98
132
 
99
133
  /**
100
134
  * Authenticate a request for regular functions (with organization validation)
101
135
  *
102
136
  * @param request - The incoming request
103
- * @returns true if authentication and authorization succeed
137
+ * @throws {ToolError} If authentication or authorization fails
104
138
  */
105
- export async function authenticateRegularRequest(request: any): Promise<boolean> {
106
- return await authenticateRequest(request, true);
139
+ export async function authenticateRegularRequest(request: any): Promise<void> {
140
+ await authenticateRequest(request, true);
107
141
  }
108
142
 
109
143
  /**
110
144
  * Authenticate a request for global functions (without organization validation)
111
145
  *
112
146
  * @param request - The incoming request
113
- * @returns true if authentication succeeds
147
+ * @throws {ToolError} If authentication fails
114
148
  */
115
- export async function authenticateGlobalRequest(request: any): Promise<boolean> {
116
- return await authenticateRequest(request, false);
149
+ export async function authenticateGlobalRequest(request: any): Promise<void> {
150
+ await authenticateRequest(request, false);
117
151
  }
@@ -22,12 +22,22 @@ jest.mock('@zaiusinc/app-sdk', () => ({
22
22
  }
23
23
  },
24
24
  Request: jest.fn().mockImplementation(() => ({})),
25
- Response: jest.fn().mockImplementation((status, data) => ({
25
+ Response: jest.fn().mockImplementation((status, data, headers) => ({
26
26
  status,
27
27
  data,
28
28
  bodyJSON: data,
29
- bodyAsU8Array: new Uint8Array()
29
+ bodyAsU8Array: new Uint8Array(),
30
+ headers
30
31
  })),
32
+ Headers: jest.fn().mockImplementation((entries) => {
33
+ const headers = new Map();
34
+ if (entries) {
35
+ for (const [key, value] of entries) {
36
+ headers.set(key, value);
37
+ }
38
+ }
39
+ return headers;
40
+ }),
31
41
  amendLogContext: jest.fn(),
32
42
  getAppContext: jest.fn(),
33
43
  logger: {
@@ -36,6 +46,9 @@ jest.mock('@zaiusinc/app-sdk', () => ({
36
46
  warn: jest.fn(),
37
47
  debug: jest.fn(),
38
48
  },
49
+ LogVisibility: {
50
+ Zaius: 'zaius'
51
+ },
39
52
  }));
40
53
 
41
54
  // Create a concrete implementation for testing
@@ -344,7 +357,13 @@ describe('GlobalToolFunction', () => {
344
357
 
345
358
  const result = await globalToolFunction.perform();
346
359
 
347
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
360
+ expect(result.status).toBe(403);
361
+ expect(result.bodyJSON).toEqual({
362
+ title: 'Forbidden',
363
+ status: 403,
364
+ detail: 'Invalid OptiID access token',
365
+ instance: '/test'
366
+ });
348
367
  expect(mockGetTokenVerifier).toHaveBeenCalled();
349
368
  expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
350
369
  expect(mockProcessRequest).not.toHaveBeenCalled();
@@ -370,7 +389,13 @@ describe('GlobalToolFunction', () => {
370
389
 
371
390
  const result = await globalToolFunctionWithoutToken.perform();
372
391
 
373
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
392
+ expect(result.status).toBe(403);
393
+ expect(result.bodyJSON).toEqual({
394
+ title: 'Forbidden',
395
+ status: 403,
396
+ detail: 'OptiID access token is required',
397
+ instance: '/test'
398
+ });
374
399
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
375
400
  expect(mockProcessRequest).not.toHaveBeenCalled();
376
401
  });
@@ -392,7 +417,13 @@ describe('GlobalToolFunction', () => {
392
417
 
393
418
  const result = await globalToolFunctionWithDifferentProvider.perform();
394
419
 
395
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
420
+ expect(result.status).toBe(403);
421
+ expect(result.bodyJSON).toEqual({
422
+ title: 'Forbidden',
423
+ status: 403,
424
+ detail: 'Only OptiID authentication provider is supported',
425
+ instance: '/test'
426
+ });
396
427
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
397
428
  expect(mockProcessRequest).not.toHaveBeenCalled();
398
429
  });
@@ -411,7 +442,13 @@ describe('GlobalToolFunction', () => {
411
442
 
412
443
  const result = await globalToolFunctionWithoutAuth.perform();
413
444
 
414
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
445
+ expect(result.status).toBe(403);
446
+ expect(result.bodyJSON).toEqual({
447
+ title: 'Forbidden',
448
+ status: 403,
449
+ detail: 'Authentication data is required',
450
+ instance: '/test'
451
+ });
415
452
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
416
453
  expect(mockProcessRequest).not.toHaveBeenCalled();
417
454
  });
@@ -422,7 +459,13 @@ describe('GlobalToolFunction', () => {
422
459
 
423
460
  const result = await globalToolFunction.perform();
424
461
 
425
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
462
+ expect(result.status).toBe(403);
463
+ expect(result.bodyJSON).toEqual({
464
+ title: 'Forbidden',
465
+ status: 403,
466
+ detail: 'Token verification failed',
467
+ instance: '/test'
468
+ });
426
469
  expect(mockGetTokenVerifier).toHaveBeenCalled();
427
470
  expect(mockProcessRequest).not.toHaveBeenCalled();
428
471
  });
@@ -433,7 +476,13 @@ describe('GlobalToolFunction', () => {
433
476
 
434
477
  const result = await globalToolFunction.perform();
435
478
 
436
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
479
+ expect(result.status).toBe(403);
480
+ expect(result.bodyJSON).toEqual({
481
+ title: 'Forbidden',
482
+ status: 403,
483
+ detail: 'Token verification failed',
484
+ instance: '/test'
485
+ });
437
486
  expect(mockGetTokenVerifier).toHaveBeenCalled();
438
487
  expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
439
488
  expect(mockProcessRequest).not.toHaveBeenCalled();
@@ -1,6 +1,8 @@
1
- import { GlobalFunction, Response, amendLogContext } from '@zaiusinc/app-sdk';
1
+ import { GlobalFunction, Response, Headers, amendLogContext } from '@zaiusinc/app-sdk';
2
2
  import { authenticateGlobalRequest, extractAuthData } from '../auth/AuthUtils';
3
3
  import { toolsService } from '../service/Service';
4
+ import { ToolLogger } from '../logging/ToolLogger';
5
+ import { ToolError } from '../types/ToolError';
4
6
 
5
7
  /**
6
8
  * Abstract base class for global tool-based function execution
@@ -24,6 +26,8 @@ export abstract class GlobalToolFunction extends GlobalFunction {
24
26
  * @returns Response as the HTTP response
25
27
  */
26
28
  public async perform(): Promise<Response> {
29
+ const startTime = Date.now();
30
+
27
31
  // Extract customer_id from auth data for global context attribution
28
32
  const authInfo = extractAuthData(this.request);
29
33
  const customerId = authInfo?.authData?.credentials?.customer_id;
@@ -33,8 +37,35 @@ export abstract class GlobalToolFunction extends GlobalFunction {
33
37
  customerId: customerId || ''
34
38
  });
35
39
 
36
- if (!(await this.authorizeRequest())) {
37
- return new Response(403, { error: 'Forbidden' });
40
+ ToolLogger.logRequest(this.request);
41
+
42
+ const response = await this.handleRequest();
43
+ ToolLogger.logResponse(this.request, response, Date.now() - startTime);
44
+ return response;
45
+ }
46
+
47
+ private async handleRequest(): Promise<Response> {
48
+ try {
49
+ await this.authorizeRequest();
50
+ } catch (error) {
51
+ if (error instanceof ToolError) {
52
+ return new Response(
53
+ error.status,
54
+ error.toProblemDetails(this.request.path),
55
+ new Headers([['content-type', 'application/problem+json']])
56
+ );
57
+ }
58
+ // Fallback for unexpected errors
59
+ return new Response(
60
+ 500,
61
+ {
62
+ title: 'Internal Server Error',
63
+ status: 500,
64
+ detail: 'An unexpected error occurred during authentication',
65
+ instance: this.request.path
66
+ },
67
+ new Headers([['content-type', 'application/problem+json']])
68
+ );
38
69
  }
39
70
 
40
71
  if (this.request.path === '/ready') {
@@ -48,9 +79,9 @@ export abstract class GlobalToolFunction extends GlobalFunction {
48
79
  /**
49
80
  * Authenticate the incoming request by validating only the OptiID token
50
81
  *
51
- * @returns true if authentication succeeds
82
+ * @throws {ToolError} If authentication fails
52
83
  */
53
- private async authorizeRequest(): Promise<boolean> {
54
- return await authenticateGlobalRequest(this.request);
84
+ private async authorizeRequest(): Promise<void> {
85
+ await authenticateGlobalRequest(this.request);
55
86
  }
56
87
  }
@@ -22,12 +22,22 @@ jest.mock('@zaiusinc/app-sdk', () => ({
22
22
  }
23
23
  },
24
24
  Request: jest.fn().mockImplementation(() => ({})),
25
- Response: jest.fn().mockImplementation((status, data) => ({
25
+ Response: jest.fn().mockImplementation((status, data, headers) => ({
26
26
  status,
27
27
  data,
28
28
  bodyJSON: data,
29
- bodyAsU8Array: new Uint8Array()
29
+ bodyAsU8Array: new Uint8Array(),
30
+ headers
30
31
  })),
32
+ Headers: jest.fn().mockImplementation((entries) => {
33
+ const headers = new Map();
34
+ if (entries) {
35
+ for (const [key, value] of entries) {
36
+ headers.set(key, value);
37
+ }
38
+ }
39
+ return headers;
40
+ }),
31
41
  amendLogContext: jest.fn(),
32
42
  getAppContext: jest.fn(),
33
43
  logger: {
@@ -36,6 +46,9 @@ jest.mock('@zaiusinc/app-sdk', () => ({
36
46
  warn: jest.fn(),
37
47
  debug: jest.fn(),
38
48
  },
49
+ LogVisibility: {
50
+ Zaius: 'zaius'
51
+ },
39
52
  }));
40
53
 
41
54
  // Create a concrete implementation for testing
@@ -248,7 +261,13 @@ describe('ToolFunction', () => {
248
261
 
249
262
  const result = await toolFunction.perform();
250
263
 
251
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
264
+ expect(result.status).toBe(403);
265
+ expect(result.bodyJSON).toEqual({
266
+ title: 'Forbidden',
267
+ status: 403,
268
+ detail: 'Invalid OptiID access token',
269
+ instance: '/test'
270
+ });
252
271
  expect(mockGetTokenVerifier).toHaveBeenCalled();
253
272
  expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
254
273
  expect(mockProcessRequest).not.toHaveBeenCalled();
@@ -274,7 +293,13 @@ describe('ToolFunction', () => {
274
293
 
275
294
  const result = await toolFunctionWithDifferentOrgId.perform();
276
295
 
277
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
296
+ expect(result.status).toBe(403);
297
+ expect(result.bodyJSON).toEqual({
298
+ title: 'Forbidden',
299
+ status: 403,
300
+ detail: 'Organization ID does not match',
301
+ instance: '/test'
302
+ });
278
303
  expect(mockGetAppContext).toHaveBeenCalled();
279
304
  expect(mockProcessRequest).not.toHaveBeenCalled();
280
305
  });
@@ -299,7 +324,13 @@ describe('ToolFunction', () => {
299
324
 
300
325
  const result = await toolFunctionWithoutToken.perform();
301
326
 
302
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
327
+ expect(result.status).toBe(403);
328
+ expect(result.bodyJSON).toEqual({
329
+ title: 'Forbidden',
330
+ status: 403,
331
+ detail: 'OptiID access token is required',
332
+ instance: '/test'
333
+ });
303
334
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
304
335
  expect(mockProcessRequest).not.toHaveBeenCalled();
305
336
  });
@@ -324,7 +355,13 @@ describe('ToolFunction', () => {
324
355
 
325
356
  const result = await toolFunctionWithoutCustomerId.perform();
326
357
 
327
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
358
+ expect(result.status).toBe(403);
359
+ expect(result.bodyJSON).toEqual({
360
+ title: 'Forbidden',
361
+ status: 403,
362
+ detail: 'Organization ID is required',
363
+ instance: '/test'
364
+ });
328
365
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
329
366
  expect(mockProcessRequest).not.toHaveBeenCalled();
330
367
  });
@@ -343,7 +380,13 @@ describe('ToolFunction', () => {
343
380
 
344
381
  const result = await toolFunctionWithoutAuth.perform();
345
382
 
346
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
383
+ expect(result.status).toBe(403);
384
+ expect(result.bodyJSON).toEqual({
385
+ title: 'Forbidden',
386
+ status: 403,
387
+ detail: 'Authentication data is required',
388
+ instance: '/test'
389
+ });
347
390
  expect(mockGetTokenVerifier).not.toHaveBeenCalled();
348
391
  expect(mockProcessRequest).not.toHaveBeenCalled();
349
392
  });
@@ -354,7 +397,13 @@ describe('ToolFunction', () => {
354
397
 
355
398
  const result = await toolFunction.perform();
356
399
 
357
- expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
400
+ expect(result.status).toBe(403);
401
+ expect(result.bodyJSON).toEqual({
402
+ title: 'Forbidden',
403
+ status: 403,
404
+ detail: 'Token verification failed',
405
+ instance: '/test'
406
+ });
358
407
  expect(mockGetTokenVerifier).toHaveBeenCalled();
359
408
  expect(mockProcessRequest).not.toHaveBeenCalled();
360
409
  });
@@ -1,6 +1,8 @@
1
- import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
1
+ import { Function, Response, Headers, amendLogContext } from '@zaiusinc/app-sdk';
2
2
  import { authenticateRegularRequest } from '../auth/AuthUtils';
3
3
  import { toolsService } from '../service/Service';
4
+ import { ToolLogger } from '../logging/ToolLogger';
5
+ import { ToolError } from '../types/ToolError';
4
6
 
5
7
  /**
6
8
  * Abstract base class for tool-based function execution
@@ -19,20 +21,56 @@ export abstract class ToolFunction extends Function {
19
21
  }
20
22
 
21
23
  /**
22
- * Process the incoming request using the tools service
24
+ * Process the incoming request with logging
23
25
  *
24
26
  * @returns Response as the HTTP response
25
27
  */
26
28
  public async perform(): Promise<Response> {
29
+ const startTime = Date.now();
27
30
  amendLogContext({ opalThreadId: this.request.headers.get('x-opal-thread-id') || '' });
28
- if (!(await this.authorizeRequest())) {
29
- return new Response(403, { error: 'Forbidden' });
31
+
32
+ ToolLogger.logRequest(this.request);
33
+
34
+ const response = await this.handleRequest();
35
+
36
+ ToolLogger.logResponse(this.request, response, Date.now() - startTime);
37
+ return response;
38
+ }
39
+
40
+ /**
41
+ * Handle the core request processing logic
42
+ *
43
+ * @returns Response as the HTTP response
44
+ */
45
+ private async handleRequest(): Promise<Response> {
46
+ try {
47
+ await this.authorizeRequest();
48
+ } catch (error) {
49
+ if (error instanceof ToolError) {
50
+ return new Response(
51
+ error.status,
52
+ error.toProblemDetails(this.request.path),
53
+ new Headers([['content-type', 'application/problem+json']])
54
+ );
55
+ }
56
+ // Fallback for unexpected errors
57
+ return new Response(
58
+ 500,
59
+ {
60
+ title: 'Internal Server Error',
61
+ status: 500,
62
+ detail: 'An unexpected error occurred during authentication',
63
+ instance: this.request.path
64
+ },
65
+ new Headers([['content-type', 'application/problem+json']])
66
+ );
30
67
  }
31
68
 
32
69
  if (this.request.path === '/ready') {
33
70
  const isReady = await this.ready();
34
71
  return new Response(200, { ready: isReady });
35
72
  }
73
+
36
74
  // Pass 'this' as context so decorated methods can use the existing instance
37
75
  return toolsService.processRequest(this.request, this);
38
76
  }
@@ -40,9 +78,9 @@ export abstract class ToolFunction extends Function {
40
78
  /**
41
79
  * Authenticate the incoming request by validating the OptiID token and organization ID
42
80
  *
43
- * @returns true if authentication succeeds
81
+ * @throws {ToolError} If authentication fails
44
82
  */
45
- private async authorizeRequest(): Promise<boolean> {
46
- return await authenticateRegularRequest(this.request);
83
+ private async authorizeRequest(): Promise<void> {
84
+ await authenticateRegularRequest(this.request);
47
85
  }
48
86
  }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './function/ToolFunction';
2
2
  export * from './function/GlobalToolFunction';
3
3
  export * from './types/Models';
4
+ export * from './types/ToolError';
4
5
  export * from './decorator/Decorator';
5
6
  export * from './auth/TokenVerifier';
6
7
  export { Tool, Interaction, InteractionResult } from './service/Service';