@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.1 → 1.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.
- package/README.md +169 -3
- package/dist/auth/AuthUtils.d.ts +12 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +80 -25
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +161 -117
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +5 -3
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +32 -8
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +73 -12
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +11 -4
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +45 -9
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +278 -11
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +42 -0
- package/dist/logging/ToolLogger.d.ts.map +1 -0
- package/dist/logging/ToolLogger.js +255 -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 +864 -0
- package/dist/logging/ToolLogger.test.js.map +1 -0
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +228 -39
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +558 -22
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +7 -1
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +5 -1
- package/dist/types/Models.js.map +1 -1
- package/dist/types/ToolError.d.ts +72 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +107 -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 +185 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +31 -0
- package/dist/validation/ParameterValidator.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.js +129 -0
- package/dist/validation/ParameterValidator.js.map +1 -0
- package/dist/validation/ParameterValidator.test.d.ts +2 -0
- package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.test.js +323 -0
- package/dist/validation/ParameterValidator.test.js.map +1 -0
- package/package.json +3 -3
- package/src/auth/AuthUtils.test.ts +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +78 -14
- package/src/function/GlobalToolFunction.ts +46 -11
- package/src/function/ToolFunction.test.ts +298 -13
- package/src/function/ToolFunction.ts +61 -13
- package/src/index.ts +2 -1
- package/src/logging/ToolLogger.test.ts +1020 -0
- package/src/logging/ToolLogger.ts +292 -0
- package/src/service/Service.test.ts +712 -28
- package/src/service/Service.ts +288 -38
- package/src/types/Models.ts +8 -1
- package/src/types/ToolError.test.ts +222 -0
- package/src/types/ToolError.ts +125 -0
- package/src/validation/ParameterValidator.test.ts +371 -0
- package/src/validation/ParameterValidator.ts +150 -0
package/src/auth/AuthUtils.ts
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
import { getAppContext, logger } from '@zaiusinc/app-sdk';
|
|
1
|
+
import { getAppContext, logger, Request } 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,68 @@ 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
|
|
120
|
+
return;
|
|
81
121
|
}
|
|
82
122
|
|
|
83
|
-
const
|
|
84
|
-
if (!authInfo) {
|
|
85
|
-
logger.error('OptiID token is required but not provided');
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const { authData, accessToken } = authInfo;
|
|
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);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Authenticate internal requests using OptiID token from headers
|
|
135
|
+
* @param request The request object
|
|
136
|
+
* @returns true if authentication succeeds
|
|
137
|
+
*/
|
|
138
|
+
export async function authenticateInternalRequest(request: Request): Promise<void> {
|
|
139
|
+
try {
|
|
140
|
+
const headers = request.headers;
|
|
141
|
+
const optiIdToken = headers?.get('Authorization') || headers?.get('authorization');
|
|
142
|
+
|
|
143
|
+
if (!optiIdToken) {
|
|
144
|
+
logger.info('OptiID token is required in Authorization header for internal requests');
|
|
145
|
+
throw new ToolError('Unauthorized', 401, 'OptiID token is required');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate the token using TokenVerifier directly
|
|
149
|
+
const tokenVerifier = await getTokenVerifier();
|
|
150
|
+
const isValidToken = await tokenVerifier.verify(optiIdToken);
|
|
151
|
+
|
|
152
|
+
if (!isValidToken) {
|
|
153
|
+
logger.info('Invalid OptiID token provided for internal request');
|
|
154
|
+
throw new ToolError('Unauthorized', 401, 'Invalid OptiID token');
|
|
155
|
+
}
|
|
156
|
+
} catch (error: any) {
|
|
157
|
+
logger.error('Internal request authentication failed:', error);
|
|
158
|
+
throw new ToolError('Unauthorized', 401, 'Internal request authentication failed');
|
|
159
|
+
}
|
|
97
160
|
}
|
|
98
161
|
|
|
99
162
|
/**
|
|
100
163
|
* Authenticate a request for regular functions (with organization validation)
|
|
101
164
|
*
|
|
102
165
|
* @param request - The incoming request
|
|
103
|
-
* @
|
|
166
|
+
* @throws {ToolError} If authentication or authorization fails
|
|
104
167
|
*/
|
|
105
|
-
export async function authenticateRegularRequest(request: any): Promise<
|
|
106
|
-
|
|
168
|
+
export async function authenticateRegularRequest(request: any): Promise<void> {
|
|
169
|
+
await authenticateRequest(request, true);
|
|
107
170
|
}
|
|
108
171
|
|
|
109
172
|
/**
|
|
110
173
|
* Authenticate a request for global functions (without organization validation)
|
|
111
174
|
*
|
|
112
175
|
* @param request - The incoming request
|
|
113
|
-
* @
|
|
176
|
+
* @throws {ToolError} If authentication fails
|
|
114
177
|
*/
|
|
115
|
-
export async function authenticateGlobalRequest(request: any): Promise<
|
|
116
|
-
|
|
178
|
+
export async function authenticateGlobalRequest(request: any): Promise<void> {
|
|
179
|
+
await authenticateRequest(request, false);
|
|
117
180
|
}
|
|
@@ -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,20 +46,23 @@ 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
|
|
42
55
|
class TestGlobalToolFunction extends GlobalToolFunction {
|
|
43
|
-
private mockReady: jest.MockedFunction<() => Promise<boolean>>;
|
|
56
|
+
private mockReady: jest.MockedFunction<() => Promise<{ ready: boolean; reason?: string }>>;
|
|
44
57
|
|
|
45
58
|
public constructor(request?: any) {
|
|
46
59
|
super(request || {});
|
|
47
60
|
(this as any).request = request;
|
|
48
|
-
this.mockReady = jest.fn().mockResolvedValue(true);
|
|
61
|
+
this.mockReady = jest.fn().mockResolvedValue({ ready: true });
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
// Override the ready method with mock implementation for testing
|
|
52
|
-
protected ready(): Promise<boolean> {
|
|
65
|
+
protected ready(): Promise<{ ready: boolean; reason?: string }> {
|
|
53
66
|
return this.mockReady();
|
|
54
67
|
}
|
|
55
68
|
|
|
@@ -180,7 +193,7 @@ describe('GlobalToolFunction', () => {
|
|
|
180
193
|
// Arrange
|
|
181
194
|
const readyRequest = createReadyRequestWithAuth();
|
|
182
195
|
globalToolFunction = new TestGlobalToolFunction(readyRequest);
|
|
183
|
-
globalToolFunction.getMockReady().mockResolvedValue(true);
|
|
196
|
+
globalToolFunction.getMockReady().mockResolvedValue({ ready: true });
|
|
184
197
|
|
|
185
198
|
// Act
|
|
186
199
|
const result = await globalToolFunction.perform();
|
|
@@ -195,7 +208,7 @@ describe('GlobalToolFunction', () => {
|
|
|
195
208
|
// Arrange
|
|
196
209
|
const readyRequest = createReadyRequestWithAuth();
|
|
197
210
|
globalToolFunction = new TestGlobalToolFunction(readyRequest);
|
|
198
|
-
globalToolFunction.getMockReady().mockResolvedValue(false);
|
|
211
|
+
globalToolFunction.getMockReady().mockResolvedValue({ ready: false });
|
|
199
212
|
|
|
200
213
|
// Act
|
|
201
214
|
const result = await globalToolFunction.perform();
|
|
@@ -206,6 +219,21 @@ describe('GlobalToolFunction', () => {
|
|
|
206
219
|
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
207
220
|
});
|
|
208
221
|
|
|
222
|
+
it('should return ready: false with reason when ready method returns false with reason', async () => {
|
|
223
|
+
// Arrange
|
|
224
|
+
const readyRequest = createReadyRequestWithAuth();
|
|
225
|
+
globalToolFunction = new TestGlobalToolFunction(readyRequest);
|
|
226
|
+
globalToolFunction.getMockReady().mockResolvedValue({ ready: false, reason: 'Missing API credentials' });
|
|
227
|
+
|
|
228
|
+
// Act
|
|
229
|
+
const result = await globalToolFunction.perform();
|
|
230
|
+
|
|
231
|
+
// Assert
|
|
232
|
+
expect(globalToolFunction.getMockReady()).toHaveBeenCalledTimes(1);
|
|
233
|
+
expect(result).toEqual(new Response(200, { ready: false, reason: 'Missing API credentials' }));
|
|
234
|
+
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
235
|
+
});
|
|
236
|
+
|
|
209
237
|
it('should handle ready method throwing an error', async () => {
|
|
210
238
|
// Arrange
|
|
211
239
|
const readyRequest = createReadyRequestWithAuth();
|
|
@@ -251,7 +279,7 @@ describe('GlobalToolFunction', () => {
|
|
|
251
279
|
path: '/ready'
|
|
252
280
|
};
|
|
253
281
|
globalToolFunction = new TestGlobalToolFunction(readyRequestWithoutAuth);
|
|
254
|
-
globalToolFunction.getMockReady().mockResolvedValue(true);
|
|
282
|
+
globalToolFunction.getMockReady().mockResolvedValue({ ready: true });
|
|
255
283
|
|
|
256
284
|
// Act
|
|
257
285
|
const result = await globalToolFunction.perform();
|
|
@@ -344,7 +372,13 @@ describe('GlobalToolFunction', () => {
|
|
|
344
372
|
|
|
345
373
|
const result = await globalToolFunction.perform();
|
|
346
374
|
|
|
347
|
-
expect(result).
|
|
375
|
+
expect(result.status).toBe(403);
|
|
376
|
+
expect(result.bodyJSON).toEqual({
|
|
377
|
+
title: 'Forbidden',
|
|
378
|
+
status: 403,
|
|
379
|
+
detail: 'Invalid OptiID access token',
|
|
380
|
+
instance: '/test'
|
|
381
|
+
});
|
|
348
382
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
349
383
|
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
350
384
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
@@ -370,7 +404,13 @@ describe('GlobalToolFunction', () => {
|
|
|
370
404
|
|
|
371
405
|
const result = await globalToolFunctionWithoutToken.perform();
|
|
372
406
|
|
|
373
|
-
expect(result).
|
|
407
|
+
expect(result.status).toBe(403);
|
|
408
|
+
expect(result.bodyJSON).toEqual({
|
|
409
|
+
title: 'Forbidden',
|
|
410
|
+
status: 403,
|
|
411
|
+
detail: 'OptiID access token is required',
|
|
412
|
+
instance: '/test'
|
|
413
|
+
});
|
|
374
414
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
375
415
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
376
416
|
});
|
|
@@ -392,7 +432,13 @@ describe('GlobalToolFunction', () => {
|
|
|
392
432
|
|
|
393
433
|
const result = await globalToolFunctionWithDifferentProvider.perform();
|
|
394
434
|
|
|
395
|
-
expect(result).
|
|
435
|
+
expect(result.status).toBe(403);
|
|
436
|
+
expect(result.bodyJSON).toEqual({
|
|
437
|
+
title: 'Forbidden',
|
|
438
|
+
status: 403,
|
|
439
|
+
detail: 'Only OptiID authentication provider is supported',
|
|
440
|
+
instance: '/test'
|
|
441
|
+
});
|
|
396
442
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
397
443
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
398
444
|
});
|
|
@@ -411,7 +457,13 @@ describe('GlobalToolFunction', () => {
|
|
|
411
457
|
|
|
412
458
|
const result = await globalToolFunctionWithoutAuth.perform();
|
|
413
459
|
|
|
414
|
-
expect(result).
|
|
460
|
+
expect(result.status).toBe(403);
|
|
461
|
+
expect(result.bodyJSON).toEqual({
|
|
462
|
+
title: 'Forbidden',
|
|
463
|
+
status: 403,
|
|
464
|
+
detail: 'Authentication data is required',
|
|
465
|
+
instance: '/test'
|
|
466
|
+
});
|
|
415
467
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
416
468
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
417
469
|
});
|
|
@@ -422,7 +474,13 @@ describe('GlobalToolFunction', () => {
|
|
|
422
474
|
|
|
423
475
|
const result = await globalToolFunction.perform();
|
|
424
476
|
|
|
425
|
-
expect(result).
|
|
477
|
+
expect(result.status).toBe(403);
|
|
478
|
+
expect(result.bodyJSON).toEqual({
|
|
479
|
+
title: 'Forbidden',
|
|
480
|
+
status: 403,
|
|
481
|
+
detail: 'Token verification failed',
|
|
482
|
+
instance: '/test'
|
|
483
|
+
});
|
|
426
484
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
427
485
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
428
486
|
});
|
|
@@ -433,7 +491,13 @@ describe('GlobalToolFunction', () => {
|
|
|
433
491
|
|
|
434
492
|
const result = await globalToolFunction.perform();
|
|
435
493
|
|
|
436
|
-
expect(result).
|
|
494
|
+
expect(result.status).toBe(403);
|
|
495
|
+
expect(result.bodyJSON).toEqual({
|
|
496
|
+
title: 'Forbidden',
|
|
497
|
+
status: 403,
|
|
498
|
+
detail: 'Token verification failed',
|
|
499
|
+
instance: '/test'
|
|
500
|
+
});
|
|
437
501
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
438
502
|
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
439
503
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
@@ -1,6 +1,9 @@
|
|
|
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';
|
|
6
|
+
import { ReadyResponse } from '../types/Models';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Abstract base class for global tool-based function execution
|
|
@@ -12,10 +15,10 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
12
15
|
* Override this method to implement any required credentials and/or other configuration
|
|
13
16
|
* exist and are valid. Reasonable caching should be utilized to prevent excessive requests to external resources.
|
|
14
17
|
* @async
|
|
15
|
-
* @returns
|
|
18
|
+
* @returns ReadyResponse containing ready status and optional reason when not ready
|
|
16
19
|
*/
|
|
17
|
-
protected ready(): Promise<boolean> {
|
|
18
|
-
return Promise.resolve(true);
|
|
20
|
+
protected ready(): Promise<ReadyResponse | boolean> {
|
|
21
|
+
return Promise.resolve({ ready: true });
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -24,6 +27,8 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
24
27
|
* @returns Response as the HTTP response
|
|
25
28
|
*/
|
|
26
29
|
public async perform(): Promise<Response> {
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
|
|
27
32
|
// Extract customer_id from auth data for global context attribution
|
|
28
33
|
const authInfo = extractAuthData(this.request);
|
|
29
34
|
const customerId = authInfo?.authData?.credentials?.customer_id;
|
|
@@ -33,13 +38,43 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
33
38
|
customerId: customerId || ''
|
|
34
39
|
});
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
ToolLogger.logRequest(this.request);
|
|
42
|
+
|
|
43
|
+
const response = await this.handleRequest();
|
|
44
|
+
ToolLogger.logResponse(this.request, response, Date.now() - startTime);
|
|
45
|
+
return response;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async handleRequest(): Promise<Response> {
|
|
49
|
+
try {
|
|
50
|
+
await this.authorizeRequest();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof ToolError) {
|
|
53
|
+
return new Response(
|
|
54
|
+
error.status,
|
|
55
|
+
error.toProblemDetails(this.request.path),
|
|
56
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
// Fallback for unexpected errors
|
|
60
|
+
return new Response(
|
|
61
|
+
500,
|
|
62
|
+
{
|
|
63
|
+
title: 'Internal Server Error',
|
|
64
|
+
status: 500,
|
|
65
|
+
detail: 'An unexpected error occurred during authentication',
|
|
66
|
+
instance: this.request.path
|
|
67
|
+
},
|
|
68
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
69
|
+
);
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
if (this.request.path === '/ready') {
|
|
41
|
-
const
|
|
42
|
-
|
|
73
|
+
const readyResult = await this.ready();
|
|
74
|
+
const readyResponse = typeof readyResult === 'boolean'
|
|
75
|
+
? { ready: readyResult }
|
|
76
|
+
: readyResult;
|
|
77
|
+
return new Response(200, readyResponse);
|
|
43
78
|
}
|
|
44
79
|
|
|
45
80
|
return toolsService.processRequest(this.request, this);
|
|
@@ -48,9 +83,9 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
48
83
|
/**
|
|
49
84
|
* Authenticate the incoming request by validating only the OptiID token
|
|
50
85
|
*
|
|
51
|
-
* @
|
|
86
|
+
* @throws {ToolError} If authentication fails
|
|
52
87
|
*/
|
|
53
|
-
private async authorizeRequest(): Promise<
|
|
54
|
-
|
|
88
|
+
private async authorizeRequest(): Promise<void> {
|
|
89
|
+
await authenticateGlobalRequest(this.request);
|
|
55
90
|
}
|
|
56
91
|
}
|