@optimizely-opal/opal-tool-ocp-sdk 0.0.0-dev.5 → 0.0.0-devmg.12

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.
Files changed (40) hide show
  1. package/README.md +106 -45
  2. package/dist/auth/TokenVerifier.d.ts +31 -0
  3. package/dist/auth/TokenVerifier.d.ts.map +1 -0
  4. package/dist/auth/TokenVerifier.js +128 -0
  5. package/dist/auth/TokenVerifier.js.map +1 -0
  6. package/dist/auth/TokenVerifier.test.d.ts +2 -0
  7. package/dist/auth/TokenVerifier.test.d.ts.map +1 -0
  8. package/dist/auth/TokenVerifier.test.js +116 -0
  9. package/dist/auth/TokenVerifier.test.js.map +1 -0
  10. package/dist/decorator/Decorator.test.js.map +1 -1
  11. package/dist/function/ToolFunction.d.ts +11 -7
  12. package/dist/function/ToolFunction.d.ts.map +1 -1
  13. package/dist/function/ToolFunction.js +53 -10
  14. package/dist/function/ToolFunction.js.map +1 -1
  15. package/dist/function/ToolFunction.test.js +225 -122
  16. package/dist/function/ToolFunction.test.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/service/Service.d.ts +14 -13
  22. package/dist/service/Service.d.ts.map +1 -1
  23. package/dist/service/Service.js +22 -16
  24. package/dist/service/Service.js.map +1 -1
  25. package/dist/service/Service.test.js +53 -36
  26. package/dist/service/Service.test.js.map +1 -1
  27. package/dist/types/Models.d.ts +5 -5
  28. package/dist/types/Models.d.ts.map +1 -1
  29. package/dist/types/Models.js +9 -9
  30. package/dist/types/Models.js.map +1 -1
  31. package/package.json +10 -3
  32. package/src/auth/TokenVerifier.test.ts +154 -0
  33. package/src/auth/TokenVerifier.ts +146 -0
  34. package/src/decorator/Decorator.test.ts +4 -4
  35. package/src/function/ToolFunction.test.ts +251 -128
  36. package/src/function/ToolFunction.ts +60 -11
  37. package/src/index.ts +1 -0
  38. package/src/service/Service.test.ts +55 -37
  39. package/src/service/Service.ts +29 -22
  40. package/src/types/Models.ts +4 -4
@@ -22,6 +22,7 @@ jest.mock('@zaiusinc/app-sdk', () => ({
22
22
  describe('ToolsService', () => {
23
23
  let mockTool: Tool<unknown>;
24
24
  let mockInteraction: Interaction<unknown>;
25
+ let mockToolFunction: ToolFunction;
25
26
 
26
27
  beforeEach(() => {
27
28
  // Clear registered functions and interactions before each test
@@ -31,6 +32,14 @@ describe('ToolsService', () => {
31
32
  // Reset all mocks
32
33
  jest.clearAllMocks();
33
34
 
35
+ // Create mock ToolFunction
36
+ mockToolFunction = {
37
+ ready: jest.fn().mockResolvedValue(true),
38
+ perform: jest.fn(),
39
+ validateBearerToken: jest.fn().mockReturnValue(true),
40
+ request: {} as any
41
+ } as any;
42
+
34
43
  // Create mock tool handler
35
44
  const mockToolHandler = jest.fn().mockResolvedValue({ result: 'success' });
36
45
 
@@ -94,7 +103,7 @@ describe('ToolsService', () => {
94
103
  );
95
104
 
96
105
  const discoveryRequest = createMockRequest({ path: '/discovery' });
97
- const response = await toolsService.processRequest(discoveryRequest);
106
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
98
107
 
99
108
  expect(response.status).toBe(200);
100
109
  expect(response).toHaveProperty('bodyJSON');
@@ -113,13 +122,14 @@ describe('ToolsService', () => {
113
122
  description: mockTool.description,
114
123
  parameters: mockTool.parameters.map((p: Parameter) => p.toJSON()),
115
124
  endpoint: mockTool.endpoint,
116
- http_method: 'POST'
125
+ http_method: 'POST',
126
+ auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
117
127
  });
118
128
  });
119
129
 
120
130
  it('should return empty functions array when no tools are registered', async () => {
121
131
  const discoveryRequest = createMockRequest({ path: '/discovery' });
122
- const response = await toolsService.processRequest(discoveryRequest);
132
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
123
133
 
124
134
  expect(response.status).toBe(200);
125
135
  expect(response.bodyAsU8Array).toBeDefined();
@@ -153,7 +163,7 @@ describe('ToolsService', () => {
153
163
  );
154
164
 
155
165
  const discoveryRequest = createMockRequest({ path: '/discovery' });
156
- const response = await toolsService.processRequest(discoveryRequest);
166
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
157
167
 
158
168
  expect(response.status).toBe(200);
159
169
 
@@ -172,7 +182,8 @@ describe('ToolsService', () => {
172
182
  description: mockTool.description,
173
183
  parameters: mockTool.parameters.map((p: Parameter) => p.toJSON()),
174
184
  endpoint: mockTool.endpoint,
175
- http_method: 'POST'
185
+ http_method: 'POST',
186
+ auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
176
187
  });
177
188
 
178
189
  expect(secondFunction).toEqual({
@@ -181,7 +192,10 @@ describe('ToolsService', () => {
181
192
  parameters: [],
182
193
  endpoint: '/second-tool',
183
194
  http_method: 'POST',
184
- auth_requirements: authRequirements.map((auth) => auth.toJSON())
195
+ auth_requirements: [
196
+ { provider: 'oauth2', scope_bundle: 'calendar', required: true },
197
+ { provider: 'OptiID', scope_bundle: 'default', required: true }
198
+ ]
185
199
  });
186
200
  });
187
201
  });
@@ -199,11 +213,11 @@ describe('ToolsService', () => {
199
213
 
200
214
  it('should execute tool successfully with parameters', async () => {
201
215
  const mockRequest = createMockRequest();
202
- const response = await toolsService.processRequest(mockRequest);
216
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
203
217
 
204
218
  expect(response.status).toBe(200);
205
219
  expect(mockTool.handler).toHaveBeenCalledWith(
206
- undefined, // functionContext
220
+ mockToolFunction, // functionContext
207
221
  { param1: 'test-value' },
208
222
  undefined
209
223
  );
@@ -213,8 +227,12 @@ describe('ToolsService', () => {
213
227
  // Create a mock ToolFunction instance
214
228
  const mockToolFunctionInstance = {
215
229
  someProperty: 'test-value',
216
- someMethod: jest.fn()
217
- };
230
+ someMethod: jest.fn(),
231
+ ready: jest.fn().mockResolvedValue(true),
232
+ perform: jest.fn(),
233
+ validateBearerToken: jest.fn().mockReturnValue(true),
234
+ request: {} as any
235
+ } as any;
218
236
 
219
237
  const mockRequest = createMockRequest();
220
238
  const response = await toolsService.processRequest(mockRequest, mockToolFunctionInstance);
@@ -307,11 +325,11 @@ describe('ToolsService', () => {
307
325
  })
308
326
  });
309
327
 
310
- const response = await toolsService.processRequest(requestWithAuth);
328
+ const response = await toolsService.processRequest(requestWithAuth, mockToolFunction);
311
329
 
312
330
  expect(response.status).toBe(200);
313
331
  expect(mockTool.handler).toHaveBeenCalledWith(
314
- undefined, // functionContext
332
+ mockToolFunction, // functionContext
315
333
  { param1: 'test-value' },
316
334
  authData
317
335
  );
@@ -323,11 +341,11 @@ describe('ToolsService', () => {
323
341
  body: JSON.stringify({ param1: 'test-value' })
324
342
  });
325
343
 
326
- const response = await toolsService.processRequest(requestWithoutWrapper);
344
+ const response = await toolsService.processRequest(requestWithoutWrapper, mockToolFunction);
327
345
 
328
346
  expect(response.status).toBe(200);
329
347
  expect(mockTool.handler).toHaveBeenCalledWith(
330
- undefined, // functionContext
348
+ mockToolFunction, // functionContext
331
349
  { param1: 'test-value' },
332
350
  undefined
333
351
  );
@@ -338,7 +356,7 @@ describe('ToolsService', () => {
338
356
  jest.mocked(mockTool.handler).mockRejectedValueOnce(new Error(errorMessage));
339
357
 
340
358
  const mockRequest = createMockRequest();
341
- const response = await toolsService.processRequest(mockRequest);
359
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
342
360
 
343
361
  expect(response.status).toBe(500);
344
362
  expect(logger.error).toHaveBeenCalledWith(
@@ -351,7 +369,7 @@ describe('ToolsService', () => {
351
369
  jest.mocked(mockTool.handler).mockRejectedValueOnce({});
352
370
 
353
371
  const mockRequest = createMockRequest();
354
- const response = await toolsService.processRequest(mockRequest);
372
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
355
373
 
356
374
  expect(response.status).toBe(500);
357
375
  });
@@ -373,10 +391,10 @@ describe('ToolsService', () => {
373
391
  body: JSON.stringify({ data: { param1: 'test-value' } })
374
392
  });
375
393
 
376
- const response = await toolsService.processRequest(interactionRequest);
394
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
377
395
 
378
396
  expect(response.status).toBe(200);
379
- expect(mockInteraction.handler).toHaveBeenCalledWith(undefined, { param1: 'test-value' }, undefined);
397
+ expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
380
398
  });
381
399
 
382
400
  it('should handle interaction request body without data wrapper', async () => {
@@ -386,10 +404,10 @@ describe('ToolsService', () => {
386
404
  body: JSON.stringify({ param1: 'test-value' })
387
405
  });
388
406
 
389
- const response = await toolsService.processRequest(interactionRequest);
407
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
390
408
 
391
409
  expect(response.status).toBe(200);
392
- expect(mockInteraction.handler).toHaveBeenCalledWith(undefined, { param1: 'test-value' }, undefined);
410
+ expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
393
411
  });
394
412
 
395
413
  it('should execute interaction with OptiID auth data when provided', async () => {
@@ -410,11 +428,11 @@ describe('ToolsService', () => {
410
428
  })
411
429
  });
412
430
 
413
- const response = await toolsService.processRequest(interactionRequest);
431
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
414
432
 
415
433
  expect(response.status).toBe(200);
416
434
  expect(mockInteraction.handler).toHaveBeenCalledWith(
417
- undefined, // functionContext
435
+ mockToolFunction, // functionContext
418
436
  { param1: 'test-value' },
419
437
  authData
420
438
  );
@@ -438,11 +456,11 @@ describe('ToolsService', () => {
438
456
  })
439
457
  });
440
458
 
441
- const response = await toolsService.processRequest(interactionRequest);
459
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
442
460
 
443
461
  expect(response.status).toBe(200);
444
462
  expect(mockInteraction.handler).toHaveBeenCalledWith(
445
- undefined, // functionContext
463
+ mockToolFunction, // functionContext
446
464
  {
447
465
  param1: 'test-value',
448
466
  auth: authData
@@ -460,7 +478,7 @@ describe('ToolsService', () => {
460
478
  bodyJSON: { data: { param1: 'test-value' } }
461
479
  });
462
480
 
463
- const response = await toolsService.processRequest(interactionRequest);
481
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
464
482
 
465
483
  expect(response.status).toBe(500);
466
484
  expect(logger.error).toHaveBeenCalledWith(
@@ -473,7 +491,7 @@ describe('ToolsService', () => {
473
491
  describe('error cases', () => {
474
492
  it('should return 404 when no matching tool or interaction is found', async () => {
475
493
  const unknownRequest = createMockRequest({ path: '/unknown-endpoint' });
476
- const response = await toolsService.processRequest(unknownRequest);
494
+ const response = await toolsService.processRequest(unknownRequest, mockToolFunction);
477
495
 
478
496
  expect(response.status).toBe(404);
479
497
  });
@@ -496,7 +514,7 @@ describe('ToolsService', () => {
496
514
  path: '/optid-auth-tool'
497
515
  });
498
516
 
499
- const response = await toolsService.processRequest(authRequest);
517
+ const response = await toolsService.processRequest(authRequest, mockToolFunction);
500
518
 
501
519
  expect(response.status).toBe(200);
502
520
  });
@@ -517,10 +535,10 @@ describe('ToolsService', () => {
517
535
  body: null
518
536
  });
519
537
 
520
- const response = await toolsService.processRequest(requestWithNullBody);
538
+ const response = await toolsService.processRequest(requestWithNullBody, mockToolFunction);
521
539
 
522
540
  expect(response.status).toBe(200);
523
- expect(mockTool.handler).toHaveBeenCalledWith(undefined, null, undefined);
541
+ expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
524
542
  });
525
543
 
526
544
  it('should handle request with undefined bodyJSON', async () => {
@@ -537,10 +555,10 @@ describe('ToolsService', () => {
537
555
  body: undefined
538
556
  });
539
557
 
540
- const response = await toolsService.processRequest(requestWithUndefinedBody);
558
+ const response = await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
541
559
 
542
560
  expect(response.status).toBe(200);
543
- expect(mockTool.handler).toHaveBeenCalledWith(undefined, undefined, undefined);
561
+ expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
544
562
  });
545
563
 
546
564
  it('should extract auth data from bodyJSON when body exists', async () => {
@@ -568,11 +586,11 @@ describe('ToolsService', () => {
568
586
  })
569
587
  });
570
588
 
571
- const response = await toolsService.processRequest(requestWithAuth);
589
+ const response = await toolsService.processRequest(requestWithAuth, mockToolFunction);
572
590
 
573
591
  expect(response.status).toBe(200);
574
592
  expect(mockTool.handler).toHaveBeenCalledWith(
575
- undefined, // functionContext
593
+ mockToolFunction, // functionContext
576
594
  { param1: 'test-value' },
577
595
  authData
578
596
  );
@@ -597,11 +615,11 @@ describe('ToolsService', () => {
597
615
  })
598
616
  });
599
617
 
600
- const response = await toolsService.processRequest(requestWithoutAuth);
618
+ const response = await toolsService.processRequest(requestWithoutAuth, mockToolFunction);
601
619
 
602
620
  expect(response.status).toBe(200);
603
621
  expect(mockTool.handler).toHaveBeenCalledWith(
604
- undefined, // functionContext
622
+ mockToolFunction, // functionContext
605
623
  { param1: 'test-value' },
606
624
  undefined
607
625
  );
@@ -629,11 +647,11 @@ describe('ToolsService', () => {
629
647
  body: ''
630
648
  });
631
649
 
632
- const response = await toolsService.processRequest(requestWithAuthButNoBody);
650
+ const response = await toolsService.processRequest(requestWithAuthButNoBody, mockToolFunction);
633
651
 
634
652
  expect(response.status).toBe(200);
635
653
  expect(mockTool.handler).toHaveBeenCalledWith(
636
- undefined, // functionContext
654
+ mockToolFunction, // functionContext
637
655
  { param1: 'test-value' },
638
656
  authData
639
657
  );
@@ -2,6 +2,12 @@
2
2
  import { AuthRequirement, Parameter } from '../types/Models';
3
3
  import * as App from '@zaiusinc/app-sdk';
4
4
  import { logger } from '@zaiusinc/app-sdk';
5
+ import { ToolFunction } from '../function/ToolFunction';
6
+
7
+ /**
8
+ * Default OptiID authentication requirement that will be enforced for all tools
9
+ */
10
+ const DEFAULT_OPTIID_AUTH = new AuthRequirement('OptiID', 'default', true);
5
11
 
6
12
 
7
13
 
@@ -22,7 +28,7 @@ export class Interaction<TAuthData> {
22
28
  public constructor(
23
29
  public name: string,
24
30
  public endpoint: string,
25
- public handler: (functionContext: any, data: unknown, authData?: TAuthData) => Promise<InteractionResult>
31
+ public handler: (functionContext: ToolFunction, data: unknown, authData?: TAuthData) => Promise<InteractionResult>
26
32
  ) {}
27
33
  }
28
34
 
@@ -42,15 +48,15 @@ export class Tool<TAuthData> {
42
48
  * @param parameters Function parameters
43
49
  * @param endpoint API endpoint
44
50
  * @param handler Function implementing the tool
45
- * @param authRequirements Authentication requirements (optional)
51
+ * @param authRequirements Authentication requirements (mandatory - OptiID enforced)
46
52
  */
47
53
  public constructor(
48
54
  public name: string,
49
55
  public description: string,
50
56
  public parameters: Parameter[],
51
57
  public endpoint: string,
52
- public handler: (functionContext: any, params: unknown, authData?: TAuthData) => Promise<unknown>,
53
- public authRequirements?: AuthRequirement[]
58
+ public handler: (functionContext: ToolFunction, params: unknown, authData?: TAuthData) => Promise<unknown>,
59
+ public authRequirements: AuthRequirement[] = [DEFAULT_OPTIID_AUTH]
54
60
  ) {}
55
61
 
56
62
  /**
@@ -62,13 +68,10 @@ export class Tool<TAuthData> {
62
68
  description: this.description,
63
69
  parameters: this.parameters.map((p) => p.toJSON()),
64
70
  endpoint: this.endpoint,
65
- http_method: this.httpMethod
71
+ http_method: this.httpMethod,
72
+ auth_requirements: this.authRequirements.map((auth) => auth.toJSON())
66
73
  };
67
74
 
68
- if (this.authRequirements && this.authRequirements.length > 0) {
69
- result.auth_requirements = this.authRequirements.map((auth) => auth.toJSON());
70
- }
71
-
72
75
  return result;
73
76
  }
74
77
  }
@@ -78,18 +81,19 @@ export class ToolsService {
78
81
  private interactions: Map<string, Interaction<any>> = new Map();
79
82
 
80
83
  /**
81
- * Extract Bearer token from Authorization header
82
- * @param headers Request headers (Map-like object or Headers object with get method)
83
- * @returns Bearer token string or undefined
84
+ * Enforce OptiID authentication for tools by ensuring OptiID auth requirement is present
85
+ * @param authRequirements Original authentication requirements
86
+ * @returns Enforced authentication requirements with OptiID
84
87
  */
85
- public extractBearerToken(headers: App.Headers): string | undefined {
86
- let bearerToken: string | undefined;
88
+ private enforceOptiIdAuth(authRequirements?: AuthRequirement[]): AuthRequirement[] {
89
+ const hasOptiIdProvider = authRequirements
90
+ && authRequirements.some((auth) => auth.provider.toLowerCase() === 'optiid');
87
91
 
88
- const authHeader = headers ? headers.get('authorization') : undefined;
89
- if (authHeader && authHeader.startsWith('Bearer ')) {
90
- bearerToken = authHeader.substring(7).trim();
92
+ if (hasOptiIdProvider) {
93
+ return authRequirements;
91
94
  }
92
- return bearerToken;
95
+
96
+ return [...(authRequirements || []), DEFAULT_OPTIID_AUTH];
93
97
  }
94
98
 
95
99
  /**
@@ -104,12 +108,14 @@ export class ToolsService {
104
108
  public registerTool<TAuthData>(
105
109
  name: string,
106
110
  description: string,
107
- handler: (functionContext: any, params: unknown, authData?: TAuthData) => Promise<unknown>,
111
+ handler: (functionContext: ToolFunction, params: unknown, authData?: TAuthData) => Promise<unknown>,
108
112
  parameters: Parameter[],
109
113
  endpoint: string,
110
114
  authRequirements?: AuthRequirement[]
111
115
  ): void {
112
- const func = new Tool<TAuthData>(name, description, parameters, endpoint, handler, authRequirements);
116
+ // Enforce OptiID authentication for all tools
117
+ const enforcedAuthRequirements = this.enforceOptiIdAuth(authRequirements);
118
+ const func = new Tool<TAuthData>(name, description, parameters, endpoint, handler, enforcedAuthRequirements);
113
119
  this.functions.set(endpoint, func);
114
120
  }
115
121
 
@@ -121,14 +127,15 @@ export class ToolsService {
121
127
  */
122
128
  public registerInteraction<TAuthData>(
123
129
  name: string,
124
- handler: (functionContext: any, data: unknown, authData?: TAuthData) => Promise<InteractionResult>,
130
+ handler: (functionContext: ToolFunction, data: unknown, authData?: TAuthData) => Promise<InteractionResult>,
125
131
  endpoint: string
126
132
  ): void {
127
133
  const func = new Interaction<TAuthData>(name, endpoint, handler);
128
134
  this.interactions.set(endpoint, func);
129
135
  }
130
136
 
131
- public async processRequest(req: App.Request, functionContext?: any): Promise<App.Response> {
137
+ public async processRequest(req: App.Request,
138
+ functionContext: ToolFunction): Promise<App.Response> {
132
139
  if (req.path === '/discovery') {
133
140
  return new App.Response(200, { functions: Array.from(this.functions.values()).map((f) => f.toJSON()) });
134
141
  } else {
@@ -48,10 +48,10 @@ export class Parameter {
48
48
  export class OptiIdAuthDataCredentials {
49
49
 
50
50
  public constructor(
51
- public customerId: string,
52
- public instanceId: string,
53
- public accessToken: string,
54
- public productSku: string
51
+ public customer_id: string,
52
+ public instance_id: string,
53
+ public access_token: string,
54
+ public product_sku: string
55
55
  ) {}
56
56
  }
57
57