@optimizely-opal/opal-tool-ocp-sdk 1.0.0 → 1.1.0-beta.2
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 +51 -13
- package/dist/auth/AuthUtils.d.ts +2 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +5 -5
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/decorator/Decorator.test.js +4 -4
- package/dist/decorator/Decorator.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +4 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +27 -21
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +114 -193
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +4 -1
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +20 -21
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +73 -263
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/service/Service.d.ts +20 -19
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +47 -72
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +229 -133
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +18 -7
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +1 -29
- package/dist/types/Models.js.map +1 -1
- package/dist/utils/ErrorFormatter.d.ts +9 -0
- package/dist/utils/ErrorFormatter.d.ts.map +1 -0
- package/dist/utils/ErrorFormatter.js +25 -0
- package/dist/utils/ErrorFormatter.js.map +1 -0
- package/package.json +1 -1
- package/src/auth/AuthUtils.ts +10 -10
- package/src/decorator/Decorator.test.ts +4 -4
- package/src/function/GlobalToolFunction.test.ts +113 -213
- package/src/function/GlobalToolFunction.ts +31 -31
- package/src/function/ToolFunction.test.ts +78 -285
- package/src/function/ToolFunction.ts +24 -30
- package/src/service/Service.test.ts +238 -174
- package/src/service/Service.ts +68 -92
- package/src/types/Models.ts +24 -15
- package/src/utils/ErrorFormatter.ts +31 -0
|
@@ -2,11 +2,14 @@ import { GlobalToolFunction } from './GlobalToolFunction';
|
|
|
2
2
|
import { toolsService } from '../service/Service';
|
|
3
3
|
import { Response, getAppContext } from '@zaiusinc/app-sdk';
|
|
4
4
|
import { getTokenVerifier } from '../auth/TokenVerifier';
|
|
5
|
+
import { ToolError } from '../types/ToolError';
|
|
6
|
+
import { authenticateGlobalRequest } from '../auth/AuthUtils';
|
|
5
7
|
|
|
6
8
|
// Mock the dependencies
|
|
7
9
|
jest.mock('../service/Service', () => ({
|
|
8
10
|
toolsService: {
|
|
9
11
|
processRequest: jest.fn(),
|
|
12
|
+
requiresOptiIdAuth: jest.fn(),
|
|
10
13
|
},
|
|
11
14
|
}));
|
|
12
15
|
|
|
@@ -14,6 +17,12 @@ jest.mock('../auth/TokenVerifier', () => ({
|
|
|
14
17
|
getTokenVerifier: jest.fn(),
|
|
15
18
|
}));
|
|
16
19
|
|
|
20
|
+
jest.mock('../auth/AuthUtils', () => ({
|
|
21
|
+
authenticateGlobalRequest: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
authenticateInternalRequest: jest.fn().mockResolvedValue(undefined),
|
|
23
|
+
extractAuthData: jest.fn().mockReturnValue(null),
|
|
24
|
+
}));
|
|
25
|
+
|
|
17
26
|
jest.mock('@zaiusinc/app-sdk', () => ({
|
|
18
27
|
GlobalFunction: class {
|
|
19
28
|
protected request: any;
|
|
@@ -180,6 +189,7 @@ describe('GlobalToolFunction', () => {
|
|
|
180
189
|
// Assert
|
|
181
190
|
expect(result).toBe(mockResponse);
|
|
182
191
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled(); // Should not verify token for discovery
|
|
192
|
+
// processRequest is called without authValidator (auth handled internally)
|
|
183
193
|
expect(mockProcessRequest).toHaveBeenCalledWith(discoveryRequest, globalToolFunction);
|
|
184
194
|
});
|
|
185
195
|
});
|
|
@@ -292,278 +302,168 @@ describe('GlobalToolFunction', () => {
|
|
|
292
302
|
});
|
|
293
303
|
|
|
294
304
|
describe('perform', () => {
|
|
295
|
-
it('should
|
|
296
|
-
// Setup mock
|
|
297
|
-
mockTokenVerifier.verify.mockResolvedValue(true);
|
|
305
|
+
it('should call processRequest and return its result', async () => {
|
|
306
|
+
// Setup mock to return a response
|
|
298
307
|
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
299
308
|
|
|
300
309
|
const result = await globalToolFunction.perform();
|
|
301
310
|
|
|
302
311
|
expect(result).toBe(mockResponse);
|
|
303
|
-
|
|
304
|
-
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
305
|
-
// Note: getAppContext should NOT be called for global functions
|
|
306
|
-
expect(mockGetAppContext).not.toHaveBeenCalled();
|
|
312
|
+
// processRequest is called with request and context (no authValidator)
|
|
307
313
|
expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, globalToolFunction);
|
|
308
314
|
});
|
|
315
|
+
});
|
|
309
316
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
317
|
+
describe('inheritance', () => {
|
|
318
|
+
it('should be an instance of GlobalFunction', () => {
|
|
319
|
+
// Assert
|
|
320
|
+
expect(globalToolFunction).toBeInstanceOf(GlobalToolFunction);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should have access to the request property', () => {
|
|
324
|
+
// Assert
|
|
325
|
+
expect(globalToolFunction.getRequest()).toBe(mockRequest);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('system paths routing', () => {
|
|
330
|
+
beforeEach(() => {
|
|
331
|
+
jest.clearAllMocks();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should forward /overrides to processRequest directly', async () => {
|
|
335
|
+
const overridesRequest = {
|
|
336
|
+
path: '/overrides',
|
|
337
|
+
method: 'DELETE',
|
|
338
|
+
bodyJSON: {},
|
|
339
|
+
headers: {
|
|
340
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
341
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
342
|
+
return null;
|
|
343
|
+
})
|
|
323
344
|
}
|
|
324
345
|
};
|
|
325
346
|
|
|
326
|
-
const globalToolFunctionWithDifferentOrgId = new TestGlobalToolFunction(requestWithDifferentOrgId);
|
|
327
|
-
mockTokenVerifier.verify.mockResolvedValue(true);
|
|
328
347
|
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
329
|
-
|
|
330
|
-
const result = await
|
|
348
|
+
const globalFunc = new TestGlobalToolFunction(overridesRequest);
|
|
349
|
+
const result = await globalFunc.perform();
|
|
331
350
|
|
|
332
351
|
expect(result).toBe(mockResponse);
|
|
333
|
-
|
|
334
|
-
expect(
|
|
335
|
-
// Note: Should NOT validate organization ID for global functions
|
|
336
|
-
expect(mockGetAppContext).not.toHaveBeenCalled();
|
|
337
|
-
expect(mockProcessRequest).toHaveBeenCalledWith(requestWithDifferentOrgId, globalToolFunctionWithDifferentOrgId);
|
|
352
|
+
// /overrides is forwarded directly to processRequest (auth handled there)
|
|
353
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, globalFunc);
|
|
338
354
|
});
|
|
339
355
|
|
|
340
|
-
it('should
|
|
341
|
-
|
|
342
|
-
const requestWithoutCustomerId = {
|
|
356
|
+
it('should forward tool endpoints to processRequest', async () => {
|
|
357
|
+
const toolRequest = {
|
|
343
358
|
...mockRequest,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
customer_id: undefined
|
|
351
|
-
}
|
|
352
|
-
}
|
|
359
|
+
path: '/some-tool',
|
|
360
|
+
headers: {
|
|
361
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
362
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
363
|
+
return null;
|
|
364
|
+
})
|
|
353
365
|
}
|
|
354
366
|
};
|
|
355
367
|
|
|
356
|
-
|
|
357
|
-
|
|
368
|
+
// Tool does not require OptiID auth
|
|
369
|
+
(toolsService.requiresOptiIdAuth as jest.Mock).mockReturnValue(false);
|
|
358
370
|
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
359
371
|
|
|
360
|
-
const
|
|
372
|
+
const globalFunc = new TestGlobalToolFunction(toolRequest);
|
|
373
|
+
const result = await globalFunc.perform();
|
|
361
374
|
|
|
362
375
|
expect(result).toBe(mockResponse);
|
|
363
|
-
expect(
|
|
364
|
-
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
365
|
-
expect(mockGetAppContext).not.toHaveBeenCalled();
|
|
366
|
-
expect(mockProcessRequest).toHaveBeenCalledWith(requestWithoutCustomerId, globalToolFunctionWithoutCustomerId);
|
|
376
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(toolRequest, globalFunc);
|
|
367
377
|
});
|
|
378
|
+
});
|
|
368
379
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const result = await globalToolFunction.perform();
|
|
374
|
-
|
|
375
|
-
expect(result.status).toBe(403);
|
|
376
|
-
expect(result.bodyJSON).toEqual({
|
|
377
|
-
title: 'Forbidden',
|
|
378
|
-
status: 403,
|
|
379
|
-
detail: 'Invalid OptiID access token',
|
|
380
|
-
instance: '/test'
|
|
381
|
-
});
|
|
382
|
-
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
383
|
-
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
384
|
-
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
380
|
+
describe('error formatting', () => {
|
|
381
|
+
beforeEach(() => {
|
|
382
|
+
jest.clearAllMocks();
|
|
385
383
|
});
|
|
386
384
|
|
|
387
|
-
it('should
|
|
388
|
-
|
|
389
|
-
const requestWithoutToken = {
|
|
385
|
+
it('should format ToolError as RFC 9457 response when processRequest throws ToolError', async () => {
|
|
386
|
+
const toolRequest = {
|
|
390
387
|
...mockRequest,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
access_token: undefined
|
|
398
|
-
}
|
|
399
|
-
}
|
|
388
|
+
path: '/some-tool',
|
|
389
|
+
headers: {
|
|
390
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
391
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
392
|
+
return null;
|
|
393
|
+
})
|
|
400
394
|
}
|
|
401
395
|
};
|
|
402
396
|
|
|
403
|
-
const
|
|
397
|
+
const toolError = new ToolError('Resource not found', 404, 'The requested resource does not exist');
|
|
398
|
+
mockProcessRequest.mockRejectedValue(toolError);
|
|
399
|
+
(toolsService.requiresOptiIdAuth as jest.Mock).mockReturnValue(false);
|
|
404
400
|
|
|
405
|
-
const
|
|
401
|
+
const globalFunc = new TestGlobalToolFunction(toolRequest);
|
|
402
|
+
const result = await globalFunc.perform();
|
|
406
403
|
|
|
407
|
-
expect(result.status).toBe(
|
|
404
|
+
expect(result.status).toBe(404);
|
|
408
405
|
expect(result.bodyJSON).toEqual({
|
|
409
|
-
title: '
|
|
410
|
-
status:
|
|
411
|
-
detail: '
|
|
412
|
-
instance: '/
|
|
406
|
+
title: 'Resource not found',
|
|
407
|
+
status: 404,
|
|
408
|
+
detail: 'The requested resource does not exist',
|
|
409
|
+
instance: '/some-tool'
|
|
413
410
|
});
|
|
414
|
-
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
415
|
-
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
416
411
|
});
|
|
417
412
|
|
|
418
|
-
it('should
|
|
419
|
-
|
|
420
|
-
const requestWithDifferentProvider = {
|
|
413
|
+
it('should format generic Error as 500 RFC 9457 response when processRequest throws', async () => {
|
|
414
|
+
const toolRequest = {
|
|
421
415
|
...mockRequest,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
416
|
+
path: '/some-tool',
|
|
417
|
+
headers: {
|
|
418
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
419
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
420
|
+
return null;
|
|
421
|
+
})
|
|
428
422
|
}
|
|
429
423
|
};
|
|
430
424
|
|
|
431
|
-
|
|
425
|
+
mockProcessRequest.mockRejectedValue(new Error('Database connection failed'));
|
|
426
|
+
(toolsService.requiresOptiIdAuth as jest.Mock).mockReturnValue(false);
|
|
432
427
|
|
|
433
|
-
const
|
|
428
|
+
const globalFunc = new TestGlobalToolFunction(toolRequest);
|
|
429
|
+
const result = await globalFunc.perform();
|
|
434
430
|
|
|
435
|
-
expect(result.status).toBe(
|
|
431
|
+
expect(result.status).toBe(500);
|
|
436
432
|
expect(result.bodyJSON).toEqual({
|
|
437
|
-
title: '
|
|
438
|
-
status:
|
|
439
|
-
detail: '
|
|
440
|
-
instance: '/
|
|
433
|
+
title: 'Internal Server Error',
|
|
434
|
+
status: 500,
|
|
435
|
+
detail: 'Database connection failed',
|
|
436
|
+
instance: '/some-tool'
|
|
441
437
|
});
|
|
442
|
-
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
443
|
-
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
444
438
|
});
|
|
445
439
|
|
|
446
|
-
it('should
|
|
447
|
-
|
|
448
|
-
const requestWithoutAuth = {
|
|
440
|
+
it('should format ToolError from authorization failure as RFC 9457 response', async () => {
|
|
441
|
+
const toolRequest = {
|
|
449
442
|
...mockRequest,
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
443
|
+
path: '/some-tool',
|
|
444
|
+
headers: {
|
|
445
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
446
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
447
|
+
return null;
|
|
448
|
+
})
|
|
453
449
|
}
|
|
454
450
|
};
|
|
455
451
|
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
expect(result.status).toBe(403);
|
|
461
|
-
expect(result.bodyJSON).toEqual({
|
|
462
|
-
title: 'Forbidden',
|
|
463
|
-
status: 403,
|
|
464
|
-
detail: 'Authentication data is required',
|
|
465
|
-
instance: '/test'
|
|
466
|
-
});
|
|
467
|
-
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
468
|
-
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it('should return 403 response when token verifier initialization fails', async () => {
|
|
472
|
-
// Setup mock to fail during token verifier initialization
|
|
473
|
-
mockGetTokenVerifier.mockRejectedValue(new Error('Failed to initialize token verifier'));
|
|
474
|
-
|
|
475
|
-
const result = await globalToolFunction.perform();
|
|
476
|
-
|
|
477
|
-
expect(result.status).toBe(403);
|
|
478
|
-
expect(result.bodyJSON).toEqual({
|
|
479
|
-
title: 'Forbidden',
|
|
480
|
-
status: 403,
|
|
481
|
-
detail: 'Token verification failed',
|
|
482
|
-
instance: '/test'
|
|
483
|
-
});
|
|
484
|
-
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
485
|
-
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it('should return 403 response when token validation throws an error', async () => {
|
|
489
|
-
// Setup mock token verifier to throw an error
|
|
490
|
-
mockTokenVerifier.verify.mockRejectedValue(new Error('Token validation failed'));
|
|
452
|
+
const authError = new ToolError('Unauthorized', 403, 'Invalid access token');
|
|
453
|
+
(toolsService.requiresOptiIdAuth as jest.Mock).mockReturnValue(true);
|
|
454
|
+
jest.mocked(authenticateGlobalRequest).mockRejectedValue(authError);
|
|
491
455
|
|
|
492
|
-
const
|
|
456
|
+
const globalFunc = new TestGlobalToolFunction(toolRequest);
|
|
457
|
+
const result = await globalFunc.perform();
|
|
493
458
|
|
|
494
459
|
expect(result.status).toBe(403);
|
|
495
460
|
expect(result.bodyJSON).toEqual({
|
|
496
|
-
title: '
|
|
461
|
+
title: 'Unauthorized',
|
|
497
462
|
status: 403,
|
|
498
|
-
detail: '
|
|
499
|
-
instance: '/
|
|
463
|
+
detail: 'Invalid access token',
|
|
464
|
+
instance: '/some-tool'
|
|
500
465
|
});
|
|
501
|
-
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
502
|
-
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
503
466
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
504
467
|
});
|
|
505
468
|
});
|
|
506
|
-
|
|
507
|
-
describe('inheritance', () => {
|
|
508
|
-
it('should be an instance of GlobalFunction', () => {
|
|
509
|
-
// Assert
|
|
510
|
-
expect(globalToolFunction).toBeInstanceOf(GlobalToolFunction);
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it('should have access to the request property', () => {
|
|
514
|
-
// Assert
|
|
515
|
-
expect(globalToolFunction.getRequest()).toBe(mockRequest);
|
|
516
|
-
});
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
describe('authentication differences from ToolFunction', () => {
|
|
520
|
-
it('should NOT validate organization ID (unlike ToolFunction)', async () => {
|
|
521
|
-
// This test demonstrates the key difference between GlobalToolFunction and ToolFunction
|
|
522
|
-
// GlobalToolFunction should work with any customer_id, while ToolFunction requires matching org ID
|
|
523
|
-
|
|
524
|
-
const requestWithRandomOrgId = {
|
|
525
|
-
...mockRequest,
|
|
526
|
-
bodyJSON: {
|
|
527
|
-
...mockRequest.bodyJSON,
|
|
528
|
-
auth: {
|
|
529
|
-
...mockRequest.bodyJSON.auth,
|
|
530
|
-
credentials: {
|
|
531
|
-
...mockRequest.bodyJSON.auth.credentials,
|
|
532
|
-
customer_id: 'random-org-999' // This would fail in ToolFunction but should work here
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
const globalToolFunctionWithRandomOrgId = new TestGlobalToolFunction(requestWithRandomOrgId);
|
|
539
|
-
mockTokenVerifier.verify.mockResolvedValue(true);
|
|
540
|
-
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
541
|
-
|
|
542
|
-
const result = await globalToolFunctionWithRandomOrgId.perform();
|
|
543
|
-
|
|
544
|
-
// Should succeed even with different org ID
|
|
545
|
-
expect(result).toBe(mockResponse);
|
|
546
|
-
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
547
|
-
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
548
|
-
// Crucially: getAppContext should NOT be called (no org validation)
|
|
549
|
-
expect(mockGetAppContext).not.toHaveBeenCalled();
|
|
550
|
-
expect(mockProcessRequest).toHaveBeenCalledWith(requestWithRandomOrgId, globalToolFunctionWithRandomOrgId);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it('should only require valid OptiID token (no organization constraints)', async () => {
|
|
554
|
-
// Test that only token validation is performed, no org validation
|
|
555
|
-
mockTokenVerifier.verify.mockResolvedValue(true);
|
|
556
|
-
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
557
|
-
|
|
558
|
-
const result = await globalToolFunction.perform();
|
|
559
|
-
|
|
560
|
-
expect(result).toBe(mockResponse);
|
|
561
|
-
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
562
|
-
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
563
|
-
|
|
564
|
-
// Key assertion: getAppContext should never be called for global functions
|
|
565
|
-
expect(mockGetAppContext).not.toHaveBeenCalled();
|
|
566
|
-
expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, globalToolFunction);
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
469
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { GlobalFunction, Response,
|
|
2
|
-
import { authenticateGlobalRequest, extractAuthData } from '../auth/AuthUtils';
|
|
1
|
+
import { GlobalFunction, Response, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
|
+
import { authenticateGlobalRequest, authenticateInternalRequest, extractAuthData } from '../auth/AuthUtils';
|
|
3
3
|
import { toolsService } from '../service/Service';
|
|
4
4
|
import { ToolLogger } from '../logging/ToolLogger';
|
|
5
|
-
import { ToolError } from '../types/ToolError';
|
|
6
5
|
import { ReadyResponse } from '../types/Models';
|
|
6
|
+
import { formatErrorResponse } from '../utils/ErrorFormatter';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Abstract base class for global tool-based function execution
|
|
@@ -30,8 +30,8 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
30
30
|
const startTime = Date.now();
|
|
31
31
|
|
|
32
32
|
// Extract customer_id from auth data for global context attribution
|
|
33
|
-
const
|
|
34
|
-
const customerId =
|
|
33
|
+
const optiIdCredentials = extractAuthData(this.request);
|
|
34
|
+
const customerId = optiIdCredentials?.customer_id;
|
|
35
35
|
|
|
36
36
|
amendLogContext({
|
|
37
37
|
opalThreadId: this.request.headers.get('x-opal-thread-id') || '',
|
|
@@ -46,29 +46,6 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
private async handleRequest(): Promise<Response> {
|
|
49
|
-
try {
|
|
50
|
-
await this.authorizeRequest();
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (error instanceof ToolError) {
|
|
53
|
-
return new Response(
|
|
54
|
-
error.status,
|
|
55
|
-
error.toProblemDetails(this.request.path),
|
|
56
|
-
new Headers([['content-type', 'application/problem+json']])
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
// Fallback for unexpected errors
|
|
60
|
-
return new Response(
|
|
61
|
-
500,
|
|
62
|
-
{
|
|
63
|
-
title: 'Internal Server Error',
|
|
64
|
-
status: 500,
|
|
65
|
-
detail: 'An unexpected error occurred during authentication',
|
|
66
|
-
instance: this.request.path
|
|
67
|
-
},
|
|
68
|
-
new Headers([['content-type', 'application/problem+json']])
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
49
|
if (this.request.path === '/ready') {
|
|
73
50
|
const readyResult = await this.ready();
|
|
74
51
|
const readyResponse = typeof readyResult === 'boolean'
|
|
@@ -77,15 +54,38 @@ export abstract class GlobalToolFunction extends GlobalFunction {
|
|
|
77
54
|
return new Response(200, readyResponse);
|
|
78
55
|
}
|
|
79
56
|
|
|
80
|
-
|
|
57
|
+
try {
|
|
58
|
+
await this.authorizeRequest();
|
|
59
|
+
|
|
60
|
+
return await toolsService.processRequest(this.request, this);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return formatErrorResponse(error, this.request.path);
|
|
63
|
+
}
|
|
81
64
|
}
|
|
82
65
|
|
|
83
66
|
/**
|
|
84
|
-
* Authenticate the incoming request
|
|
67
|
+
* Authenticate the incoming request based on the endpoint
|
|
68
|
+
* - /discovery: No auth required
|
|
69
|
+
* - /overrides: Internal auth (header-based token)
|
|
70
|
+
* - Tools/interactions: Global auth if OptiID is required
|
|
85
71
|
*
|
|
86
72
|
* @throws {ToolError} If authentication fails
|
|
87
73
|
*/
|
|
88
74
|
private async authorizeRequest(): Promise<void> {
|
|
89
|
-
|
|
75
|
+
// Discovery endpoint doesn't require auth
|
|
76
|
+
if (this.request.path === '/discovery') {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Use internal authentication for overrides endpoint (header-based token)
|
|
81
|
+
if (this.request.path === '/overrides') {
|
|
82
|
+
await authenticateInternalRequest(this.request);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Tool/interaction endpoints - authenticate only if OptiID is required
|
|
87
|
+
if (toolsService.requiresOptiIdAuth(this.request.path)) {
|
|
88
|
+
await authenticateGlobalRequest(this.request);
|
|
89
|
+
}
|
|
90
90
|
}
|
|
91
91
|
}
|