@optimizely-opal/opal-tool-ocp-sdk 1.0.0-OCP-1442.6 → 1.0.0-OCP-1449.1
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 -72
- 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 +1 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +17 -4
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +54 -8
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +1 -1
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +24 -4
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +260 -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.map +1 -1
- package/dist/logging/ToolLogger.js +2 -1
- package/dist/logging/ToolLogger.js.map +1 -1
- package/dist/logging/ToolLogger.test.js +114 -2
- package/dist/logging/ToolLogger.test.js.map +1 -1
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +227 -55
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +464 -36
- 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 +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +54 -8
- package/src/function/GlobalToolFunction.ts +26 -6
- package/src/function/ToolFunction.test.ts +274 -8
- package/src/function/ToolFunction.ts +33 -7
- package/src/index.ts +1 -0
- package/src/logging/ToolLogger.test.ts +118 -2
- package/src/logging/ToolLogger.ts +2 -1
- package/src/service/Service.test.ts +577 -34
- package/src/service/Service.ts +286 -54
- 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
|
-
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: {
|
|
@@ -347,7 +357,13 @@ describe('GlobalToolFunction', () => {
|
|
|
347
357
|
|
|
348
358
|
const result = await globalToolFunction.perform();
|
|
349
359
|
|
|
350
|
-
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
|
+
});
|
|
351
367
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
352
368
|
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
353
369
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
@@ -373,7 +389,13 @@ describe('GlobalToolFunction', () => {
|
|
|
373
389
|
|
|
374
390
|
const result = await globalToolFunctionWithoutToken.perform();
|
|
375
391
|
|
|
376
|
-
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
|
+
});
|
|
377
399
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
378
400
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
379
401
|
});
|
|
@@ -395,7 +417,13 @@ describe('GlobalToolFunction', () => {
|
|
|
395
417
|
|
|
396
418
|
const result = await globalToolFunctionWithDifferentProvider.perform();
|
|
397
419
|
|
|
398
|
-
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
|
+
});
|
|
399
427
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
400
428
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
401
429
|
});
|
|
@@ -414,7 +442,13 @@ describe('GlobalToolFunction', () => {
|
|
|
414
442
|
|
|
415
443
|
const result = await globalToolFunctionWithoutAuth.perform();
|
|
416
444
|
|
|
417
|
-
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
|
+
});
|
|
418
452
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
419
453
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
420
454
|
});
|
|
@@ -425,7 +459,13 @@ describe('GlobalToolFunction', () => {
|
|
|
425
459
|
|
|
426
460
|
const result = await globalToolFunction.perform();
|
|
427
461
|
|
|
428
|
-
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
|
+
});
|
|
429
469
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
430
470
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
431
471
|
});
|
|
@@ -436,7 +476,13 @@ describe('GlobalToolFunction', () => {
|
|
|
436
476
|
|
|
437
477
|
const result = await globalToolFunction.perform();
|
|
438
478
|
|
|
439
|
-
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
|
+
});
|
|
440
486
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
441
487
|
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
442
488
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
@@ -1,7 +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
4
|
import { ToolLogger } from '../logging/ToolLogger';
|
|
5
|
+
import { ToolError } from '../types/ToolError';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Abstract base class for global tool-based function execution
|
|
@@ -44,8 +45,27 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
private async handleRequest(): Promise<Response> {
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
);
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
if (this.request.path === '/ready') {
|
|
@@ -59,9 +79,9 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
59
79
|
/**
|
|
60
80
|
* Authenticate the incoming request by validating only the OptiID token
|
|
61
81
|
*
|
|
62
|
-
* @
|
|
82
|
+
* @throws {ToolError} If authentication fails
|
|
63
83
|
*/
|
|
64
|
-
private async authorizeRequest(): Promise<
|
|
65
|
-
|
|
84
|
+
private async authorizeRequest(): Promise<void> {
|
|
85
|
+
await authenticateGlobalRequest(this.request);
|
|
66
86
|
}
|
|
67
87
|
}
|