@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.
- package/README.md +114 -0
- package/dist/auth/AuthUtils.d.ts +5 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +53 -25
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +62 -117
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +2 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +25 -4
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +57 -8
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +8 -2
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +31 -5
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +57 -8
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +34 -0
- package/dist/logging/ToolLogger.d.ts.map +1 -0
- package/dist/logging/ToolLogger.js +153 -0
- package/dist/logging/ToolLogger.js.map +1 -0
- package/dist/logging/ToolLogger.test.d.ts +2 -0
- package/dist/logging/ToolLogger.test.d.ts.map +1 -0
- package/dist/logging/ToolLogger.test.js +646 -0
- package/dist/logging/ToolLogger.test.js.map +1 -0
- package/dist/service/Service.d.ts +15 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +43 -17
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +84 -2
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/ToolError.d.ts +59 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +79 -0
- package/dist/types/ToolError.js.map +1 -0
- package/dist/types/ToolError.test.d.ts +2 -0
- package/dist/types/ToolError.test.d.ts.map +1 -0
- package/dist/types/ToolError.test.js +161 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +5 -16
- package/dist/validation/ParameterValidator.d.ts.map +1 -1
- package/dist/validation/ParameterValidator.js +10 -3
- package/dist/validation/ParameterValidator.js.map +1 -1
- package/dist/validation/ParameterValidator.test.js +186 -146
- package/dist/validation/ParameterValidator.test.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/AuthUtils.test.ts +62 -157
- package/src/auth/AuthUtils.ts +66 -32
- package/src/function/GlobalToolFunction.test.ts +57 -8
- package/src/function/GlobalToolFunction.ts +37 -6
- package/src/function/ToolFunction.test.ts +57 -8
- package/src/function/ToolFunction.ts +45 -7
- package/src/index.ts +1 -0
- package/src/logging/ToolLogger.test.ts +753 -0
- package/src/logging/ToolLogger.ts +177 -0
- package/src/service/Service.test.ts +103 -2
- package/src/service/Service.ts +45 -17
- package/src/types/ToolError.test.ts +192 -0
- package/src/types/ToolError.ts +95 -0
- package/src/validation/ParameterValidator.test.ts +185 -158
- package/src/validation/ParameterValidator.ts +17 -20
package/src/auth/AuthUtils.ts
CHANGED
|
@@ -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
|
-
* @
|
|
10
|
+
* @throws {ToolError} If token validation fails
|
|
10
11
|
*/
|
|
11
|
-
async function validateAccessToken(accessToken: string | undefined): Promise<
|
|
12
|
+
async function validateAccessToken(accessToken: string | undefined): Promise<void> {
|
|
12
13
|
try {
|
|
13
14
|
if (!accessToken) {
|
|
14
|
-
|
|
15
|
+
throw new ToolError('Forbidden', 403, 'OptiID access token is required');
|
|
15
16
|
}
|
|
16
17
|
const tokenVerifier = await getTokenVerifier();
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
86
|
+
* @throws {ToolError} If organization ID is invalid or missing
|
|
45
87
|
*/
|
|
46
|
-
function validateOrganizationId(customerId: string | undefined):
|
|
88
|
+
function validateOrganizationId(customerId: string | undefined): void {
|
|
47
89
|
if (!customerId) {
|
|
48
90
|
logger.error('Organisation ID is required but not provided');
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
116
|
+
* @throws {ToolError} If authentication fails
|
|
77
117
|
*/
|
|
78
|
-
async function authenticateRequest(request: any, validateOrg: boolean): Promise<
|
|
118
|
+
async function authenticateRequest(request: any, validateOrg: boolean): Promise<void> {
|
|
79
119
|
if (shouldSkipAuth(request)) {
|
|
80
|
-
return
|
|
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 } =
|
|
123
|
+
const { authData, accessToken } = extractAndValidateAuthData(request);
|
|
90
124
|
|
|
91
125
|
// Validate organization ID if required
|
|
92
|
-
if (validateOrg
|
|
93
|
-
|
|
126
|
+
if (validateOrg) {
|
|
127
|
+
validateOrganizationId(authData.credentials?.customer_id);
|
|
94
128
|
}
|
|
95
129
|
|
|
96
|
-
|
|
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
|
-
* @
|
|
137
|
+
* @throws {ToolError} If authentication or authorization fails
|
|
104
138
|
*/
|
|
105
|
-
export async function authenticateRegularRequest(request: any): Promise<
|
|
106
|
-
|
|
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
|
-
* @
|
|
147
|
+
* @throws {ToolError} If authentication fails
|
|
114
148
|
*/
|
|
115
|
-
export async function authenticateGlobalRequest(request: any): Promise<
|
|
116
|
-
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
* @
|
|
82
|
+
* @throws {ToolError} If authentication fails
|
|
52
83
|
*/
|
|
53
|
-
private async authorizeRequest(): Promise<
|
|
54
|
-
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
* @
|
|
81
|
+
* @throws {ToolError} If authentication fails
|
|
44
82
|
*/
|
|
45
|
-
private async authorizeRequest(): Promise<
|
|
46
|
-
|
|
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';
|