@optimizely-opal/opal-tool-ocp-sdk 0.0.0-OCP-1487.4 → 0.0.0-OCP-1487.6
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 +103 -14
- package/dist/auth/AuthUtils.d.ts +12 -28
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +94 -50
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +535 -403
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/decorator/Decorator.d.ts +1 -0
- package/dist/decorator/Decorator.d.ts.map +1 -1
- package/dist/decorator/Decorator.js +1 -1
- package/dist/decorator/Decorator.js.map +1 -1
- package/dist/decorator/Decorator.test.js +33 -11
- package/dist/decorator/Decorator.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +0 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +1 -10
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +0 -1
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +1 -13
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/AuthUtils.test.ts +638 -495
- package/src/auth/AuthUtils.ts +99 -48
- package/src/decorator/Decorator.test.ts +52 -10
- package/src/decorator/Decorator.ts +3 -1
- package/src/function/GlobalToolFunction.ts +2 -14
- package/src/function/ToolFunction.ts +2 -18
- package/src/service/Service.ts +4 -2
package/src/auth/AuthUtils.ts
CHANGED
|
@@ -3,64 +3,115 @@ import { getTokenVerifier } from './TokenVerifier';
|
|
|
3
3
|
import { OptiIdAuthData } from '../types/Models';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Validate the OptiID access token
|
|
7
|
+
*
|
|
8
|
+
* @param accessToken - The access token to validate
|
|
9
|
+
* @returns true if the token is valid
|
|
7
10
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* Validate the OptiID access token
|
|
12
|
-
*
|
|
13
|
-
* @param accessToken - The access token to validate
|
|
14
|
-
* @returns true if the token is valid
|
|
15
|
-
*/
|
|
16
|
-
public static async validateAccessToken(accessToken: string | undefined): Promise<boolean> {
|
|
17
|
-
try {
|
|
18
|
-
if (!accessToken) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
const tokenVerifier = await getTokenVerifier();
|
|
22
|
-
return await tokenVerifier.verify(accessToken);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
logger.error('OptiID token validation failed:', error);
|
|
11
|
+
async function validateAccessToken(accessToken: string | undefined): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
if (!accessToken) {
|
|
25
14
|
return false;
|
|
26
15
|
}
|
|
16
|
+
const tokenVerifier = await getTokenVerifier();
|
|
17
|
+
return await tokenVerifier.verify(accessToken);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.error('OptiID token validation failed:', error);
|
|
20
|
+
return false;
|
|
27
21
|
}
|
|
22
|
+
}
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Extract and validate basic OptiID authentication data from request
|
|
26
|
+
*
|
|
27
|
+
* @param request - The incoming request
|
|
28
|
+
* @returns object with authData and accessToken, or null if invalid
|
|
29
|
+
*/
|
|
30
|
+
function extractAuthData(request: any): { authData: OptiIdAuthData; accessToken: string } | null {
|
|
31
|
+
const authData = request?.bodyJSON?.auth as OptiIdAuthData;
|
|
32
|
+
const accessToken = authData?.credentials?.access_token;
|
|
33
|
+
if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
|
|
34
|
+
logger.error('OptiID token is required but not provided');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { authData, accessToken };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate organization ID matches the app context
|
|
43
|
+
*
|
|
44
|
+
* @param customerId - The customer ID from the auth data
|
|
45
|
+
* @returns true if the organization ID is valid
|
|
46
|
+
*/
|
|
47
|
+
function validateOrganizationId(customerId: string | undefined): boolean {
|
|
48
|
+
if (!customerId) {
|
|
49
|
+
logger.error('Organisation ID is required but not provided');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
const appOrganisationId = getAppContext()?.account?.organizationId;
|
|
54
|
+
if (customerId !== appOrganisationId) {
|
|
55
|
+
logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
|
|
56
|
+
return false;
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
* @param customerId - The customer ID from the auth data
|
|
50
|
-
* @returns true if the organization ID is valid
|
|
51
|
-
*/
|
|
52
|
-
public static validateOrganizationId(customerId: string | undefined): boolean {
|
|
53
|
-
if (!customerId) {
|
|
54
|
-
logger.error('Organisation ID is required but not provided');
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Check if a request should skip authentication (discovery/ready endpoints)
|
|
64
|
+
*
|
|
65
|
+
* @param request - The incoming request
|
|
66
|
+
* @returns true if auth should be skipped
|
|
67
|
+
*/
|
|
68
|
+
function shouldSkipAuth(request: any): boolean {
|
|
69
|
+
return request.path === '/discovery' || request.path === '/ready';
|
|
70
|
+
}
|
|
63
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Core authentication flow - extracts auth data and validates token
|
|
74
|
+
*
|
|
75
|
+
* @param request - The incoming request
|
|
76
|
+
* @param validateOrg - Whether to validate organization ID
|
|
77
|
+
* @returns true if authentication succeeds
|
|
78
|
+
*/
|
|
79
|
+
async function authenticateRequest(request: any, validateOrg: boolean = false): Promise<boolean> {
|
|
80
|
+
if (shouldSkipAuth(request)) {
|
|
64
81
|
return true;
|
|
65
82
|
}
|
|
83
|
+
|
|
84
|
+
const authInfo = extractAuthData(request);
|
|
85
|
+
if (!authInfo) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { authData, accessToken } = authInfo;
|
|
90
|
+
|
|
91
|
+
// Validate organization ID if required
|
|
92
|
+
if (validateOrg && !validateOrganizationId(authData.credentials?.customer_id)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return await validateAccessToken(accessToken);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Authorize a request for regular functions (with organization validation)
|
|
101
|
+
*
|
|
102
|
+
* @param request - The incoming request
|
|
103
|
+
* @returns true if authentication and authorization succeed
|
|
104
|
+
*/
|
|
105
|
+
export async function authorizeRegularRequest(request: any): Promise<boolean> {
|
|
106
|
+
return await authenticateRequest(request, true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Authorize a request for global functions (without organization validation)
|
|
111
|
+
*
|
|
112
|
+
* @param request - The incoming request
|
|
113
|
+
* @returns true if authentication succeeds
|
|
114
|
+
*/
|
|
115
|
+
export async function authorizeGlobalRequest(request: any): Promise<boolean> {
|
|
116
|
+
return await authenticateRequest(request, false);
|
|
66
117
|
}
|
|
@@ -36,7 +36,8 @@ describe('Decorators', () => {
|
|
|
36
36
|
expect.any(Function),
|
|
37
37
|
[],
|
|
38
38
|
'/test-tool',
|
|
39
|
-
[]
|
|
39
|
+
[],
|
|
40
|
+
false
|
|
40
41
|
);
|
|
41
42
|
|
|
42
43
|
// Ensure TestClass is considered "used" by TypeScript
|
|
@@ -78,7 +79,8 @@ describe('Decorators', () => {
|
|
|
78
79
|
})
|
|
79
80
|
]),
|
|
80
81
|
'/tool-with-params',
|
|
81
|
-
[]
|
|
82
|
+
[],
|
|
83
|
+
false
|
|
82
84
|
);
|
|
83
85
|
|
|
84
86
|
// Ensure TestClass is considered "used" by TypeScript
|
|
@@ -119,7 +121,8 @@ describe('Decorators', () => {
|
|
|
119
121
|
scopeBundle: 'calendar',
|
|
120
122
|
required: true
|
|
121
123
|
})
|
|
122
|
-
])
|
|
124
|
+
]),
|
|
125
|
+
false
|
|
123
126
|
);
|
|
124
127
|
|
|
125
128
|
// Ensure TestClass is considered "used" by TypeScript
|
|
@@ -200,7 +203,8 @@ describe('Decorators', () => {
|
|
|
200
203
|
scopeBundle: 'drive',
|
|
201
204
|
required: false
|
|
202
205
|
})
|
|
203
|
-
])
|
|
206
|
+
]),
|
|
207
|
+
false
|
|
204
208
|
);
|
|
205
209
|
|
|
206
210
|
// Ensure TestClass is considered "used" by TypeScript
|
|
@@ -228,7 +232,8 @@ describe('Decorators', () => {
|
|
|
228
232
|
expect.any(Function),
|
|
229
233
|
[],
|
|
230
234
|
'/empty-params',
|
|
231
|
-
[]
|
|
235
|
+
[],
|
|
236
|
+
false
|
|
232
237
|
);
|
|
233
238
|
|
|
234
239
|
expect(TestClass).toBeDefined();
|
|
@@ -256,7 +261,8 @@ describe('Decorators', () => {
|
|
|
256
261
|
expect.any(Function),
|
|
257
262
|
[],
|
|
258
263
|
'/no-auth',
|
|
259
|
-
[]
|
|
264
|
+
[],
|
|
265
|
+
false
|
|
260
266
|
);
|
|
261
267
|
|
|
262
268
|
expect(TestClass).toBeDefined();
|
|
@@ -323,7 +329,8 @@ describe('Decorators', () => {
|
|
|
323
329
|
expect.objectContaining({ name: 'dictParam', type: ParameterType.Dictionary })
|
|
324
330
|
]),
|
|
325
331
|
'/multi-type',
|
|
326
|
-
[]
|
|
332
|
+
[],
|
|
333
|
+
false
|
|
327
334
|
);
|
|
328
335
|
});
|
|
329
336
|
});
|
|
@@ -442,7 +449,8 @@ describe('Decorators', () => {
|
|
|
442
449
|
})
|
|
443
450
|
]),
|
|
444
451
|
'/mixed-tool',
|
|
445
|
-
[]
|
|
452
|
+
[],
|
|
453
|
+
false
|
|
446
454
|
);
|
|
447
455
|
|
|
448
456
|
expect(toolsService.registerInteraction).toHaveBeenCalledWith(
|
|
@@ -489,7 +497,8 @@ describe('Decorators', () => {
|
|
|
489
497
|
scopeBundle: 'calendar',
|
|
490
498
|
required: true // Should default to true
|
|
491
499
|
})
|
|
492
|
-
])
|
|
500
|
+
]),
|
|
501
|
+
false
|
|
493
502
|
);
|
|
494
503
|
});
|
|
495
504
|
|
|
@@ -515,7 +524,8 @@ describe('Decorators', () => {
|
|
|
515
524
|
expect.any(Function),
|
|
516
525
|
[], // Should be empty array when parameters is undefined
|
|
517
526
|
'/undefined-arrays',
|
|
518
|
-
[] // Should be empty array when authRequirements is undefined
|
|
527
|
+
[], // Should be empty array when authRequirements is undefined
|
|
528
|
+
false
|
|
519
529
|
);
|
|
520
530
|
});
|
|
521
531
|
});
|
|
@@ -645,5 +655,37 @@ describe('Decorators', () => {
|
|
|
645
655
|
expect(TestClass).toBeDefined();
|
|
646
656
|
});
|
|
647
657
|
});
|
|
658
|
+
|
|
659
|
+
describe('global tool registration', () => {
|
|
660
|
+
it('should register a global tool with isGlobal: true', () => {
|
|
661
|
+
const config = {
|
|
662
|
+
name: 'globalTool',
|
|
663
|
+
description: 'A global tool',
|
|
664
|
+
parameters: [],
|
|
665
|
+
endpoint: '/global-tool',
|
|
666
|
+
authRequirements: [],
|
|
667
|
+
isGlobal: true
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
class GlobalTestClass {
|
|
671
|
+
@tool(config)
|
|
672
|
+
public async testTool(): Promise<{ result: string }> {
|
|
673
|
+
return { result: 'global-tool-result' };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
expect(toolsService.registerTool).toHaveBeenCalledWith(
|
|
678
|
+
'globalTool',
|
|
679
|
+
'A global tool',
|
|
680
|
+
expect.any(Function),
|
|
681
|
+
[],
|
|
682
|
+
'/global-tool',
|
|
683
|
+
[],
|
|
684
|
+
true
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
expect(GlobalTestClass).toBeDefined();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
648
690
|
});
|
|
649
691
|
|
|
@@ -10,6 +10,7 @@ export interface ToolConfig {
|
|
|
10
10
|
parameters: ParameterConfig[];
|
|
11
11
|
authRequirements?: AuthRequirementConfig[];
|
|
12
12
|
endpoint: string;
|
|
13
|
+
isGlobal?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -76,7 +77,8 @@ export function tool(config: ToolConfig) {
|
|
|
76
77
|
boundHandler,
|
|
77
78
|
parameters,
|
|
78
79
|
config.endpoint,
|
|
79
|
-
authRequirements
|
|
80
|
+
authRequirements,
|
|
81
|
+
config.isGlobal || false
|
|
80
82
|
);
|
|
81
83
|
};
|
|
82
84
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GlobalFunction, Response, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { authorizeGlobalRequest } from '../auth/AuthUtils';
|
|
3
3
|
import { toolsService } from '../service/Service';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -41,21 +41,9 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
41
41
|
/**
|
|
42
42
|
* Authenticate the incoming request by validating only the OptiID token
|
|
43
43
|
*
|
|
44
|
-
* @param request - The incoming request
|
|
45
44
|
* @returns true if authentication succeeds
|
|
46
45
|
*/
|
|
47
46
|
private async authorizeRequest(): Promise<boolean> {
|
|
48
|
-
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const authInfo = AuthUtils.extractAuthData(this.request);
|
|
53
|
-
if (!authInfo) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const { accessToken } = authInfo;
|
|
58
|
-
|
|
59
|
-
return await AuthUtils.validateAccessToken(accessToken);
|
|
47
|
+
return await authorizeGlobalRequest(this.request);
|
|
60
48
|
}
|
|
61
49
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
|
-
import {
|
|
2
|
+
import { authorizeRegularRequest } from '../auth/AuthUtils';
|
|
3
3
|
import { toolsService } from '../service/Service';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -40,25 +40,9 @@ export abstract class ToolFunction extends Function {
|
|
|
40
40
|
/**
|
|
41
41
|
* Authenticate the incoming request by validating the OptiID token and organization ID
|
|
42
42
|
*
|
|
43
|
-
* @param request - The incoming request
|
|
44
43
|
* @returns true if authentication succeeds
|
|
45
44
|
*/
|
|
46
45
|
private async authorizeRequest(): Promise<boolean> {
|
|
47
|
-
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const authInfo = AuthUtils.extractAuthData(this.request);
|
|
52
|
-
if (!authInfo) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { authData, accessToken } = authInfo;
|
|
57
|
-
|
|
58
|
-
if (!AuthUtils.validateOrganizationId(authData.credentials?.customer_id)) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return await AuthUtils.validateAccessToken(accessToken);
|
|
46
|
+
return await authorizeRegularRequest(this.request);
|
|
63
47
|
}
|
|
64
48
|
}
|
package/src/service/Service.ts
CHANGED
|
@@ -160,8 +160,10 @@ export class ToolsService {
|
|
|
160
160
|
this.interactions.set(endpoint, func);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
public async processRequest(
|
|
164
|
-
|
|
163
|
+
public async processRequest(
|
|
164
|
+
req: App.Request,
|
|
165
|
+
functionContext: ToolFunction | GlobalToolFunction
|
|
166
|
+
): Promise<App.Response> {
|
|
165
167
|
if (req.path === '/discovery') {
|
|
166
168
|
return this.handleDiscovery(functionContext);
|
|
167
169
|
} else {
|