@optimizely-opal/opal-tool-ocp-sdk 0.0.0-OCP-1487.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.
Files changed (72) hide show
  1. package/README.md +631 -0
  2. package/dist/auth/AuthUtils.d.ts +31 -0
  3. package/dist/auth/AuthUtils.d.ts.map +1 -0
  4. package/dist/auth/AuthUtils.js +64 -0
  5. package/dist/auth/AuthUtils.js.map +1 -0
  6. package/dist/auth/AuthUtils.test.d.ts +2 -0
  7. package/dist/auth/AuthUtils.test.d.ts.map +1 -0
  8. package/dist/auth/AuthUtils.test.js +469 -0
  9. package/dist/auth/AuthUtils.test.js.map +1 -0
  10. package/dist/auth/TokenVerifier.d.ts +31 -0
  11. package/dist/auth/TokenVerifier.d.ts.map +1 -0
  12. package/dist/auth/TokenVerifier.js +127 -0
  13. package/dist/auth/TokenVerifier.js.map +1 -0
  14. package/dist/auth/TokenVerifier.test.d.ts +2 -0
  15. package/dist/auth/TokenVerifier.test.d.ts.map +1 -0
  16. package/dist/auth/TokenVerifier.test.js +125 -0
  17. package/dist/auth/TokenVerifier.test.js.map +1 -0
  18. package/dist/decorator/Decorator.d.ts +48 -0
  19. package/dist/decorator/Decorator.d.ts.map +1 -0
  20. package/dist/decorator/Decorator.js +53 -0
  21. package/dist/decorator/Decorator.js.map +1 -0
  22. package/dist/decorator/Decorator.test.d.ts +2 -0
  23. package/dist/decorator/Decorator.test.d.ts.map +1 -0
  24. package/dist/decorator/Decorator.test.js +528 -0
  25. package/dist/decorator/Decorator.test.js.map +1 -0
  26. package/dist/function/GlobalToolFunction.d.ts +28 -0
  27. package/dist/function/GlobalToolFunction.d.ts.map +1 -0
  28. package/dist/function/GlobalToolFunction.js +56 -0
  29. package/dist/function/GlobalToolFunction.js.map +1 -0
  30. package/dist/function/GlobalToolFunction.test.d.ts +2 -0
  31. package/dist/function/GlobalToolFunction.test.d.ts.map +1 -0
  32. package/dist/function/GlobalToolFunction.test.js +425 -0
  33. package/dist/function/GlobalToolFunction.test.js.map +1 -0
  34. package/dist/function/ToolFunction.d.ts +28 -0
  35. package/dist/function/ToolFunction.d.ts.map +1 -0
  36. package/dist/function/ToolFunction.js +60 -0
  37. package/dist/function/ToolFunction.js.map +1 -0
  38. package/dist/function/ToolFunction.test.d.ts +2 -0
  39. package/dist/function/ToolFunction.test.d.ts.map +1 -0
  40. package/dist/function/ToolFunction.test.js +314 -0
  41. package/dist/function/ToolFunction.test.js.map +1 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +26 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/service/Service.d.ts +80 -0
  47. package/dist/service/Service.d.ts.map +1 -0
  48. package/dist/service/Service.js +210 -0
  49. package/dist/service/Service.js.map +1 -0
  50. package/dist/service/Service.test.d.ts +2 -0
  51. package/dist/service/Service.test.d.ts.map +1 -0
  52. package/dist/service/Service.test.js +427 -0
  53. package/dist/service/Service.test.js.map +1 -0
  54. package/dist/types/Models.d.ts +126 -0
  55. package/dist/types/Models.d.ts.map +1 -0
  56. package/dist/types/Models.js +181 -0
  57. package/dist/types/Models.js.map +1 -0
  58. package/package.json +64 -0
  59. package/src/auth/AuthUtils.test.ts +586 -0
  60. package/src/auth/AuthUtils.ts +66 -0
  61. package/src/auth/TokenVerifier.test.ts +165 -0
  62. package/src/auth/TokenVerifier.ts +145 -0
  63. package/src/decorator/Decorator.test.ts +649 -0
  64. package/src/decorator/Decorator.ts +111 -0
  65. package/src/function/GlobalToolFunction.test.ts +505 -0
  66. package/src/function/GlobalToolFunction.ts +61 -0
  67. package/src/function/ToolFunction.test.ts +374 -0
  68. package/src/function/ToolFunction.ts +64 -0
  69. package/src/index.ts +5 -0
  70. package/src/service/Service.test.ts +661 -0
  71. package/src/service/Service.ts +213 -0
  72. package/src/types/Models.ts +163 -0
@@ -0,0 +1,586 @@
1
+ import { AuthUtils } from './AuthUtils';
2
+ import { getAppContext, logger } from '@zaiusinc/app-sdk';
3
+ import { getTokenVerifier } from './TokenVerifier';
4
+ import { OptiIdAuthData } from '../types/Models';
5
+
6
+ // Mock the dependencies
7
+ jest.mock('./TokenVerifier', () => ({
8
+ getTokenVerifier: jest.fn(),
9
+ }));
10
+
11
+ jest.mock('@zaiusinc/app-sdk', () => ({
12
+ getAppContext: jest.fn(),
13
+ logger: {
14
+ info: jest.fn(),
15
+ error: jest.fn(),
16
+ warn: jest.fn(),
17
+ debug: jest.fn(),
18
+ },
19
+ }));
20
+
21
+ describe('AuthUtils', () => {
22
+ let mockGetTokenVerifier: jest.MockedFunction<typeof getTokenVerifier>;
23
+ let mockGetAppContext: jest.MockedFunction<typeof getAppContext>;
24
+ let mockTokenVerifier: jest.Mocked<{
25
+ verify: (token: string) => Promise<any>;
26
+ }>;
27
+
28
+ beforeEach(() => {
29
+ jest.clearAllMocks();
30
+
31
+ // Create mock token verifier
32
+ mockTokenVerifier = {
33
+ verify: jest.fn(),
34
+ };
35
+
36
+ // Setup the mocks
37
+ mockGetTokenVerifier = jest.mocked(getTokenVerifier);
38
+ mockGetAppContext = jest.mocked(getAppContext);
39
+
40
+ mockGetTokenVerifier.mockResolvedValue(mockTokenVerifier as any);
41
+ mockGetAppContext.mockReturnValue({
42
+ account: {
43
+ organizationId: 'app-org-123'
44
+ }
45
+ } as any);
46
+ });
47
+
48
+ describe('validateAccessToken', () => {
49
+ it('should return true for valid token', async () => {
50
+ // Arrange
51
+ const validToken = 'valid-access-token';
52
+ mockTokenVerifier.verify.mockResolvedValue(true);
53
+
54
+ // Act
55
+ const result = await AuthUtils.validateAccessToken(validToken);
56
+
57
+ // Assert
58
+ expect(result).toBe(true);
59
+ expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
60
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith(validToken);
61
+ expect(logger.error).not.toHaveBeenCalled();
62
+ });
63
+
64
+ it('should return false for invalid token', async () => {
65
+ // Arrange
66
+ const invalidToken = 'invalid-access-token';
67
+ mockTokenVerifier.verify.mockResolvedValue(false);
68
+
69
+ // Act
70
+ const result = await AuthUtils.validateAccessToken(invalidToken);
71
+
72
+ // Assert
73
+ expect(result).toBe(false);
74
+ expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
75
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith(invalidToken);
76
+ expect(logger.error).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('should return false for undefined token', async () => {
80
+ // Act
81
+ const result = await AuthUtils.validateAccessToken(undefined);
82
+
83
+ // Assert
84
+ expect(result).toBe(false);
85
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
86
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
87
+ expect(logger.error).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it('should return false for null token', async () => {
91
+ // Act
92
+ const result = await AuthUtils.validateAccessToken(null as any);
93
+
94
+ // Assert
95
+ expect(result).toBe(false);
96
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
97
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
98
+ expect(logger.error).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it('should return false for empty string token', async () => {
102
+ // Act
103
+ const result = await AuthUtils.validateAccessToken('');
104
+
105
+ // Assert
106
+ expect(result).toBe(false);
107
+ expect(mockGetTokenVerifier).not.toHaveBeenCalled();
108
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
109
+ expect(logger.error).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('should return false and log error when getTokenVerifier fails', async () => {
113
+ // Arrange
114
+ const validToken = 'valid-access-token';
115
+ const error = new Error('Failed to get token verifier');
116
+ mockGetTokenVerifier.mockRejectedValue(error);
117
+
118
+ // Act
119
+ const result = await AuthUtils.validateAccessToken(validToken);
120
+
121
+ // Assert
122
+ expect(result).toBe(false);
123
+ expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
124
+ expect(mockTokenVerifier.verify).not.toHaveBeenCalled();
125
+ expect(logger.error).toHaveBeenCalledWith('OptiID token validation failed:', error);
126
+ });
127
+
128
+ it('should return false and log error when token verification throws', async () => {
129
+ // Arrange
130
+ const validToken = 'valid-access-token';
131
+ const error = new Error('Token verification failed');
132
+ mockTokenVerifier.verify.mockRejectedValue(error);
133
+
134
+ // Act
135
+ const result = await AuthUtils.validateAccessToken(validToken);
136
+
137
+ // Assert
138
+ expect(result).toBe(false);
139
+ expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
140
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith(validToken);
141
+ expect(logger.error).toHaveBeenCalledWith('OptiID token validation failed:', error);
142
+ });
143
+
144
+ it('should handle whitespace-only token', async () => {
145
+ // Arrange
146
+ mockTokenVerifier.verify.mockResolvedValue(false);
147
+
148
+ // Act
149
+ const result = await AuthUtils.validateAccessToken(' ');
150
+
151
+ // Assert - whitespace-only string should be treated as truthy and passed to verifier
152
+ expect(result).toBe(false);
153
+ expect(mockGetTokenVerifier).toHaveBeenCalledTimes(1);
154
+ expect(mockTokenVerifier.verify).toHaveBeenCalledWith(' ');
155
+ });
156
+ });
157
+
158
+ describe('extractAuthData', () => {
159
+ const createValidRequest = (): any => ({
160
+ bodyJSON: {
161
+ auth: {
162
+ provider: 'OptiID',
163
+ credentials: {
164
+ access_token: 'valid-access-token',
165
+ customer_id: 'org-123',
166
+ instance_id: 'instance-456',
167
+ product_sku: 'OPAL'
168
+ }
169
+ } as OptiIdAuthData
170
+ }
171
+ });
172
+
173
+ it('should extract auth data successfully from valid request', () => {
174
+ // Arrange
175
+ const request = createValidRequest();
176
+
177
+ // Act
178
+ const result = AuthUtils.extractAuthData(request);
179
+
180
+ // Assert
181
+ expect(result).not.toBeNull();
182
+ expect(result?.authData).toBe(request.bodyJSON.auth);
183
+ expect(result?.accessToken).toBe('valid-access-token');
184
+ expect(logger.error).not.toHaveBeenCalled();
185
+ });
186
+
187
+ it('should handle case-insensitive provider name', () => {
188
+ // Arrange
189
+ const request = createValidRequest();
190
+ request.bodyJSON.auth.provider = 'optiid'; // lowercase
191
+
192
+ // Act
193
+ const result = AuthUtils.extractAuthData(request);
194
+
195
+ // Assert
196
+ expect(result).not.toBeNull();
197
+ expect(result?.authData).toBe(request.bodyJSON.auth);
198
+ expect(result?.accessToken).toBe('valid-access-token');
199
+ expect(logger.error).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it('should handle mixed case provider name', () => {
203
+ // Arrange
204
+ const request = createValidRequest();
205
+ request.bodyJSON.auth.provider = 'OpTiId'; // mixed case
206
+
207
+ // Act
208
+ const result = AuthUtils.extractAuthData(request);
209
+
210
+ // Assert
211
+ expect(result).not.toBeNull();
212
+ expect(result?.authData).toBe(request.bodyJSON.auth);
213
+ expect(result?.accessToken).toBe('valid-access-token');
214
+ expect(logger.error).not.toHaveBeenCalled();
215
+ });
216
+
217
+ it('should return null when access token is missing', () => {
218
+ // Arrange
219
+ const request = createValidRequest();
220
+ delete request.bodyJSON.auth.credentials.access_token;
221
+
222
+ // Act
223
+ const result = AuthUtils.extractAuthData(request);
224
+
225
+ // Assert
226
+ expect(result).toBeNull();
227
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
228
+ });
229
+
230
+ it('should return null when access token is undefined', () => {
231
+ // Arrange
232
+ const request = createValidRequest();
233
+ request.bodyJSON.auth.credentials.access_token = undefined;
234
+
235
+ // Act
236
+ const result = AuthUtils.extractAuthData(request);
237
+
238
+ // Assert
239
+ expect(result).toBeNull();
240
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
241
+ });
242
+
243
+ it('should return null when access token is empty string', () => {
244
+ // Arrange
245
+ const request = createValidRequest();
246
+ request.bodyJSON.auth.credentials.access_token = '';
247
+
248
+ // Act
249
+ const result = AuthUtils.extractAuthData(request);
250
+
251
+ // Assert
252
+ expect(result).toBeNull();
253
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
254
+ });
255
+
256
+ it('should return null when provider is not OptiID', () => {
257
+ // Arrange
258
+ const request = createValidRequest();
259
+ request.bodyJSON.auth.provider = 'SomeOtherProvider';
260
+
261
+ // Act
262
+ const result = AuthUtils.extractAuthData(request);
263
+
264
+ // Assert
265
+ expect(result).toBeNull();
266
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
267
+ });
268
+
269
+ it('should return null when provider is missing', () => {
270
+ // Arrange
271
+ const request = createValidRequest();
272
+ delete request.bodyJSON.auth.provider;
273
+
274
+ // Act
275
+ const result = AuthUtils.extractAuthData(request);
276
+
277
+ // Assert
278
+ expect(result).toBeNull();
279
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
280
+ });
281
+
282
+ it('should return null when auth structure is missing', () => {
283
+ // Arrange
284
+ const request = {
285
+ bodyJSON: {
286
+ parameters: { some: 'data' }
287
+ }
288
+ };
289
+
290
+ // Act
291
+ const result = AuthUtils.extractAuthData(request);
292
+
293
+ // Assert
294
+ expect(result).toBeNull();
295
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
296
+ });
297
+
298
+ it('should return null when credentials structure is missing', () => {
299
+ // Arrange
300
+ const request = {
301
+ bodyJSON: {
302
+ auth: {
303
+ provider: 'OptiID'
304
+ }
305
+ }
306
+ };
307
+
308
+ // Act
309
+ const result = AuthUtils.extractAuthData(request);
310
+
311
+ // Assert
312
+ expect(result).toBeNull();
313
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
314
+ });
315
+
316
+ it('should return null when bodyJSON is missing', () => {
317
+ // Arrange
318
+ const request = {};
319
+
320
+ // Act
321
+ const result = AuthUtils.extractAuthData(request);
322
+
323
+ // Assert
324
+ expect(result).toBeNull();
325
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
326
+ });
327
+
328
+ it('should return null when request is null', () => {
329
+ // Act
330
+ const result = AuthUtils.extractAuthData(null);
331
+
332
+ // Assert
333
+ expect(result).toBeNull();
334
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
335
+ });
336
+
337
+ it('should return null when request is undefined', () => {
338
+ // Act
339
+ const result = AuthUtils.extractAuthData(undefined);
340
+
341
+ // Assert
342
+ expect(result).toBeNull();
343
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
344
+ });
345
+ });
346
+
347
+ describe('validateOrganizationId', () => {
348
+ beforeEach(() => {
349
+ mockGetAppContext.mockReturnValue({
350
+ account: {
351
+ organizationId: 'app-org-123'
352
+ }
353
+ } as any);
354
+ });
355
+
356
+ it('should return true when customer ID matches app organization ID', () => {
357
+ // Act
358
+ const result = AuthUtils.validateOrganizationId('app-org-123');
359
+
360
+ // Assert
361
+ expect(result).toBe(true);
362
+ expect(mockGetAppContext).toHaveBeenCalledTimes(1);
363
+ expect(logger.error).not.toHaveBeenCalled();
364
+ });
365
+
366
+ it('should return false when customer ID does not match app organization ID', () => {
367
+ // Act
368
+ const result = AuthUtils.validateOrganizationId('different-org-456');
369
+
370
+ // Assert
371
+ expect(result).toBe(false);
372
+ expect(mockGetAppContext).toHaveBeenCalledTimes(1);
373
+ expect(logger.error).toHaveBeenCalledWith(
374
+ 'Invalid organisation ID: expected app-org-123, received different-org-456'
375
+ );
376
+ });
377
+
378
+ it('should return false when customer ID is undefined', () => {
379
+ // Act
380
+ const result = AuthUtils.validateOrganizationId(undefined);
381
+
382
+ // Assert
383
+ expect(result).toBe(false);
384
+ expect(mockGetAppContext).not.toHaveBeenCalled();
385
+ expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
386
+ });
387
+
388
+ it('should return false when customer ID is null', () => {
389
+ // Act
390
+ const result = AuthUtils.validateOrganizationId(null as any);
391
+
392
+ // Assert
393
+ expect(result).toBe(false);
394
+ expect(mockGetAppContext).not.toHaveBeenCalled();
395
+ expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
396
+ });
397
+
398
+ it('should return false when customer ID is empty string', () => {
399
+ // Act
400
+ const result = AuthUtils.validateOrganizationId('');
401
+
402
+ // Assert
403
+ expect(result).toBe(false);
404
+ expect(mockGetAppContext).not.toHaveBeenCalled();
405
+ expect(logger.error).toHaveBeenCalledWith('Organisation ID is required but not provided');
406
+ });
407
+
408
+ it('should handle case when app context has no account', () => {
409
+ // Arrange
410
+ mockGetAppContext.mockReturnValue({} as any);
411
+
412
+ // Act
413
+ const result = AuthUtils.validateOrganizationId('some-org-123');
414
+
415
+ // Assert
416
+ expect(result).toBe(false);
417
+ expect(mockGetAppContext).toHaveBeenCalledTimes(1);
418
+ expect(logger.error).toHaveBeenCalledWith(
419
+ 'Invalid organisation ID: expected undefined, received some-org-123'
420
+ );
421
+ });
422
+
423
+ it('should handle case when app context account has no organizationId', () => {
424
+ // Arrange
425
+ mockGetAppContext.mockReturnValue({
426
+ account: {}
427
+ } as any);
428
+
429
+ // Act
430
+ const result = AuthUtils.validateOrganizationId('some-org-123');
431
+
432
+ // Assert
433
+ expect(result).toBe(false);
434
+ expect(mockGetAppContext).toHaveBeenCalledTimes(1);
435
+ expect(logger.error).toHaveBeenCalledWith(
436
+ 'Invalid organisation ID: expected undefined, received some-org-123'
437
+ );
438
+ });
439
+
440
+ it('should handle case when app context is null', () => {
441
+ // Arrange
442
+ mockGetAppContext.mockReturnValue(null as any);
443
+
444
+ // Act
445
+ const result = AuthUtils.validateOrganizationId('some-org-123');
446
+
447
+ // Assert
448
+ expect(result).toBe(false);
449
+ expect(mockGetAppContext).toHaveBeenCalledTimes(1);
450
+ expect(logger.error).toHaveBeenCalledWith(
451
+ 'Invalid organisation ID: expected undefined, received some-org-123'
452
+ );
453
+ });
454
+
455
+ it('should be case-sensitive for organization ID matching', () => {
456
+ // Arrange
457
+ mockGetAppContext.mockReturnValue({
458
+ account: {
459
+ organizationId: 'App-Org-123' // different case
460
+ }
461
+ } as any);
462
+
463
+ // Act
464
+ const result = AuthUtils.validateOrganizationId('app-org-123');
465
+
466
+ // Assert
467
+ expect(result).toBe(false);
468
+ expect(mockGetAppContext).toHaveBeenCalledTimes(1);
469
+ expect(logger.error).toHaveBeenCalledWith(
470
+ 'Invalid organisation ID: expected App-Org-123, received app-org-123'
471
+ );
472
+ });
473
+
474
+ it('should handle whitespace-only customer ID', () => {
475
+ // Act
476
+ const result = AuthUtils.validateOrganizationId(' ');
477
+
478
+ // Assert
479
+ expect(result).toBe(false);
480
+ expect(mockGetAppContext).toHaveBeenCalledTimes(1);
481
+ expect(logger.error).toHaveBeenCalledWith(
482
+ 'Invalid organisation ID: expected app-org-123, received '
483
+ );
484
+ });
485
+ });
486
+
487
+ describe('integration scenarios', () => {
488
+ it('should handle complete authentication flow for valid request', async () => {
489
+ // Arrange
490
+ const request = {
491
+ bodyJSON: {
492
+ auth: {
493
+ provider: 'OptiID',
494
+ credentials: {
495
+ access_token: 'valid-access-token',
496
+ customer_id: 'app-org-123',
497
+ instance_id: 'instance-456',
498
+ product_sku: 'OPAL'
499
+ }
500
+ } as OptiIdAuthData
501
+ }
502
+ };
503
+
504
+ mockGetAppContext.mockReturnValue({
505
+ account: {
506
+ organizationId: 'app-org-123'
507
+ }
508
+ } as any);
509
+
510
+ mockTokenVerifier.verify.mockResolvedValue(true);
511
+
512
+ // Act
513
+ const authInfo = AuthUtils.extractAuthData(request);
514
+ const isValidOrg = authInfo
515
+ ? AuthUtils.validateOrganizationId(authInfo.authData.credentials?.customer_id)
516
+ : false;
517
+ const isValidToken = authInfo ? await AuthUtils.validateAccessToken(authInfo.accessToken) : false;
518
+
519
+ // Assert
520
+ expect(authInfo).not.toBeNull();
521
+ expect(isValidOrg).toBe(true);
522
+ expect(isValidToken).toBe(true);
523
+ expect(logger.error).not.toHaveBeenCalled();
524
+ });
525
+
526
+ it('should handle complete authentication flow for invalid provider', async () => {
527
+ // Arrange
528
+ const request = {
529
+ bodyJSON: {
530
+ auth: {
531
+ provider: 'SomeOtherProvider',
532
+ credentials: {
533
+ access_token: 'valid-access-token',
534
+ customer_id: 'app-org-123',
535
+ instance_id: 'instance-456',
536
+ product_sku: 'OPAL'
537
+ }
538
+ } as OptiIdAuthData
539
+ }
540
+ };
541
+
542
+ // Act
543
+ const authInfo = AuthUtils.extractAuthData(request);
544
+
545
+ // Assert
546
+ expect(authInfo).toBeNull();
547
+ expect(logger.error).toHaveBeenCalledWith('OptiID token is required but not provided');
548
+ });
549
+
550
+ it('should handle complete authentication flow for organization mismatch', async () => {
551
+ // Arrange
552
+ const request = {
553
+ bodyJSON: {
554
+ auth: {
555
+ provider: 'OptiID',
556
+ credentials: {
557
+ access_token: 'valid-access-token',
558
+ customer_id: 'different-org-456',
559
+ instance_id: 'instance-456',
560
+ product_sku: 'OPAL'
561
+ }
562
+ } as OptiIdAuthData
563
+ }
564
+ };
565
+
566
+ mockGetAppContext.mockReturnValue({
567
+ account: {
568
+ organizationId: 'app-org-123'
569
+ }
570
+ } as any);
571
+
572
+ // Act
573
+ const authInfo = AuthUtils.extractAuthData(request);
574
+ const isValidOrg = authInfo
575
+ ? AuthUtils.validateOrganizationId(authInfo.authData.credentials?.customer_id)
576
+ : false;
577
+
578
+ // Assert
579
+ expect(authInfo).not.toBeNull();
580
+ expect(isValidOrg).toBe(false);
581
+ expect(logger.error).toHaveBeenCalledWith(
582
+ 'Invalid organisation ID: expected app-org-123, received different-org-456'
583
+ );
584
+ });
585
+ });
586
+ });
@@ -0,0 +1,66 @@
1
+ import { getAppContext, logger } from '@zaiusinc/app-sdk';
2
+ import { getTokenVerifier } from './TokenVerifier';
3
+ import { OptiIdAuthData } from '../types/Models';
4
+
5
+ /**
6
+ * Common authentication utilities for all function types
7
+ */
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);
25
+ return false;
26
+ }
27
+ }
28
+
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
+ }
42
+
43
+ return { authData, accessToken };
44
+ }
45
+
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
+ }
57
+
58
+ const appOrganisationId = getAppContext()?.account?.organizationId;
59
+ if (customerId !== appOrganisationId) {
60
+ logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
61
+ return false;
62
+ }
63
+
64
+ return true;
65
+ }
66
+ }