@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.
@@ -3,64 +3,115 @@ import { getTokenVerifier } from './TokenVerifier';
3
3
  import { OptiIdAuthData } from '../types/Models';
4
4
 
5
5
  /**
6
- * Common authentication utilities for all function types
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
- export class AuthUtils {
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
- * Extract and validate basic OptiID authentication data from request
31
- *
32
- * @param request - The incoming request
33
- * @returns object with authData and accessToken, or null if invalid
34
- */
35
- public static extractAuthData(request: any): { authData: OptiIdAuthData; accessToken: string } | null {
36
- const authData = request?.bodyJSON?.auth as OptiIdAuthData;
37
- const accessToken = authData?.credentials?.access_token;
38
- if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
39
- logger.error('OptiID token is required but not provided');
40
- return null;
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
- return { authData, accessToken };
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
- * Validate organization ID matches the app context
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
- const appOrganisationId = getAppContext()?.account?.organizationId;
59
- if (customerId !== appOrganisationId) {
60
- logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
61
- return false;
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 { AuthUtils } from '../auth/AuthUtils';
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
- if (this.request.path === '/discovery' || this.request.path === '/ready') {
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 { AuthUtils } from '../auth/AuthUtils';
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
- if (this.request.path === '/discovery' || this.request.path === '/ready') {
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
  }
@@ -160,8 +160,10 @@ export class ToolsService {
160
160
  this.interactions.set(endpoint, func);
161
161
  }
162
162
 
163
- public async processRequest(req: App.Request,
164
- functionContext: ToolFunction | GlobalToolFunction): Promise<App.Response> {
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 {