@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.1 → 1.0.0-beta.10
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 +169 -3
- package/dist/auth/AuthUtils.d.ts +12 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +80 -25
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +161 -117
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +5 -3
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +32 -8
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +73 -12
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +11 -4
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +45 -9
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +278 -11
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +42 -0
- package/dist/logging/ToolLogger.d.ts.map +1 -0
- package/dist/logging/ToolLogger.js +255 -0
- package/dist/logging/ToolLogger.js.map +1 -0
- package/dist/logging/ToolLogger.test.d.ts +2 -0
- package/dist/logging/ToolLogger.test.d.ts.map +1 -0
- package/dist/logging/ToolLogger.test.js +864 -0
- package/dist/logging/ToolLogger.test.js.map +1 -0
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +228 -39
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +558 -22
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +7 -1
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +5 -1
- package/dist/types/Models.js.map +1 -1
- package/dist/types/ToolError.d.ts +72 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +107 -0
- package/dist/types/ToolError.js.map +1 -0
- package/dist/types/ToolError.test.d.ts +2 -0
- package/dist/types/ToolError.test.d.ts.map +1 -0
- package/dist/types/ToolError.test.js +185 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +31 -0
- package/dist/validation/ParameterValidator.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.js +129 -0
- package/dist/validation/ParameterValidator.js.map +1 -0
- package/dist/validation/ParameterValidator.test.d.ts +2 -0
- package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.test.js +323 -0
- package/dist/validation/ParameterValidator.test.js.map +1 -0
- package/package.json +3 -3
- package/src/auth/AuthUtils.test.ts +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +78 -14
- package/src/function/GlobalToolFunction.ts +46 -11
- package/src/function/ToolFunction.test.ts +298 -13
- package/src/function/ToolFunction.ts +61 -13
- package/src/index.ts +2 -1
- package/src/logging/ToolLogger.test.ts +1020 -0
- package/src/logging/ToolLogger.ts +292 -0
- package/src/service/Service.test.ts +712 -28
- package/src/service/Service.ts +288 -38
- package/src/types/Models.ts +8 -1
- package/src/types/ToolError.test.ts +222 -0
- package/src/types/ToolError.ts +125 -0
- package/src/validation/ParameterValidator.test.ts +371 -0
- package/src/validation/ParameterValidator.ts +150 -0
|
@@ -22,12 +22,22 @@ jest.mock('@zaiusinc/app-sdk', () => ({
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
Request: jest.fn().mockImplementation(() => ({})),
|
|
25
|
-
Response: jest.fn().mockImplementation((status, data) => ({
|
|
25
|
+
Response: jest.fn().mockImplementation((status, data, headers) => ({
|
|
26
26
|
status,
|
|
27
27
|
data,
|
|
28
28
|
bodyJSON: data,
|
|
29
|
-
bodyAsU8Array: new Uint8Array()
|
|
29
|
+
bodyAsU8Array: new Uint8Array(),
|
|
30
|
+
headers
|
|
30
31
|
})),
|
|
32
|
+
Headers: jest.fn().mockImplementation((entries) => {
|
|
33
|
+
const headers = new Map();
|
|
34
|
+
if (entries) {
|
|
35
|
+
for (const [key, value] of entries) {
|
|
36
|
+
headers.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return headers;
|
|
40
|
+
}),
|
|
31
41
|
amendLogContext: jest.fn(),
|
|
32
42
|
getAppContext: jest.fn(),
|
|
33
43
|
logger: {
|
|
@@ -36,20 +46,23 @@ jest.mock('@zaiusinc/app-sdk', () => ({
|
|
|
36
46
|
warn: jest.fn(),
|
|
37
47
|
debug: jest.fn(),
|
|
38
48
|
},
|
|
49
|
+
LogVisibility: {
|
|
50
|
+
Zaius: 'zaius'
|
|
51
|
+
},
|
|
39
52
|
}));
|
|
40
53
|
|
|
41
54
|
// Create a concrete implementation for testing
|
|
42
55
|
class TestToolFunction extends ToolFunction {
|
|
43
|
-
private mockReady: jest.MockedFunction<() => Promise<boolean>>;
|
|
56
|
+
private mockReady: jest.MockedFunction<() => Promise<{ ready: boolean; reason?: string }>>;
|
|
44
57
|
|
|
45
58
|
public constructor(request?: any) {
|
|
46
59
|
super(request || {});
|
|
47
60
|
(this as any).request = request;
|
|
48
|
-
this.mockReady = jest.fn().mockResolvedValue(true);
|
|
61
|
+
this.mockReady = jest.fn().mockResolvedValue({ ready: true });
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
// Override the ready method with mock implementation for testing
|
|
52
|
-
protected ready(): Promise<boolean> {
|
|
65
|
+
protected ready(): Promise<{ ready: boolean; reason?: string }> {
|
|
53
66
|
return this.mockReady();
|
|
54
67
|
}
|
|
55
68
|
|
|
@@ -161,7 +174,7 @@ describe('ToolFunction', () => {
|
|
|
161
174
|
const readyRequest = createReadyRequestWithAuth();
|
|
162
175
|
|
|
163
176
|
toolFunction = new TestToolFunction(readyRequest);
|
|
164
|
-
toolFunction.getMockReady().mockResolvedValue(true);
|
|
177
|
+
toolFunction.getMockReady().mockResolvedValue({ ready: true });
|
|
165
178
|
|
|
166
179
|
// Act
|
|
167
180
|
const result = await toolFunction.perform();
|
|
@@ -177,7 +190,7 @@ describe('ToolFunction', () => {
|
|
|
177
190
|
const readyRequest = createReadyRequestWithAuth();
|
|
178
191
|
|
|
179
192
|
toolFunction = new TestToolFunction(readyRequest);
|
|
180
|
-
toolFunction.getMockReady().mockResolvedValue(false);
|
|
193
|
+
toolFunction.getMockReady().mockResolvedValue({ ready: false });
|
|
181
194
|
|
|
182
195
|
// Act
|
|
183
196
|
const result = await toolFunction.perform();
|
|
@@ -188,6 +201,22 @@ describe('ToolFunction', () => {
|
|
|
188
201
|
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
189
202
|
});
|
|
190
203
|
|
|
204
|
+
it('should return ready: false with reason when ready method returns false with reason', async () => {
|
|
205
|
+
// Arrange
|
|
206
|
+
const readyRequest = createReadyRequestWithAuth();
|
|
207
|
+
|
|
208
|
+
toolFunction = new TestToolFunction(readyRequest);
|
|
209
|
+
toolFunction.getMockReady().mockResolvedValue({ ready: false, reason: 'Database connection failed' });
|
|
210
|
+
|
|
211
|
+
// Act
|
|
212
|
+
const result = await toolFunction.perform();
|
|
213
|
+
|
|
214
|
+
// Assert
|
|
215
|
+
expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
|
|
216
|
+
expect(result).toEqual(new Response(200, { ready: false, reason: 'Database connection failed' }));
|
|
217
|
+
expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
|
|
218
|
+
});
|
|
219
|
+
|
|
191
220
|
it('should handle ready method throwing an error', async () => {
|
|
192
221
|
// Arrange
|
|
193
222
|
const readyRequest = createReadyRequestWithAuth();
|
|
@@ -248,7 +277,13 @@ describe('ToolFunction', () => {
|
|
|
248
277
|
|
|
249
278
|
const result = await toolFunction.perform();
|
|
250
279
|
|
|
251
|
-
expect(result).
|
|
280
|
+
expect(result.status).toBe(403);
|
|
281
|
+
expect(result.bodyJSON).toEqual({
|
|
282
|
+
title: 'Forbidden',
|
|
283
|
+
status: 403,
|
|
284
|
+
detail: 'Invalid OptiID access token',
|
|
285
|
+
instance: '/test'
|
|
286
|
+
});
|
|
252
287
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
253
288
|
expect(mockTokenVerifier.verify).toHaveBeenCalledWith('valid-access-token');
|
|
254
289
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
@@ -274,7 +309,13 @@ describe('ToolFunction', () => {
|
|
|
274
309
|
|
|
275
310
|
const result = await toolFunctionWithDifferentOrgId.perform();
|
|
276
311
|
|
|
277
|
-
expect(result).
|
|
312
|
+
expect(result.status).toBe(403);
|
|
313
|
+
expect(result.bodyJSON).toEqual({
|
|
314
|
+
title: 'Forbidden',
|
|
315
|
+
status: 403,
|
|
316
|
+
detail: 'Organization ID does not match',
|
|
317
|
+
instance: '/test'
|
|
318
|
+
});
|
|
278
319
|
expect(mockGetAppContext).toHaveBeenCalled();
|
|
279
320
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
280
321
|
});
|
|
@@ -299,7 +340,13 @@ describe('ToolFunction', () => {
|
|
|
299
340
|
|
|
300
341
|
const result = await toolFunctionWithoutToken.perform();
|
|
301
342
|
|
|
302
|
-
expect(result).
|
|
343
|
+
expect(result.status).toBe(403);
|
|
344
|
+
expect(result.bodyJSON).toEqual({
|
|
345
|
+
title: 'Forbidden',
|
|
346
|
+
status: 403,
|
|
347
|
+
detail: 'OptiID access token is required',
|
|
348
|
+
instance: '/test'
|
|
349
|
+
});
|
|
303
350
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
304
351
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
305
352
|
});
|
|
@@ -324,7 +371,13 @@ describe('ToolFunction', () => {
|
|
|
324
371
|
|
|
325
372
|
const result = await toolFunctionWithoutCustomerId.perform();
|
|
326
373
|
|
|
327
|
-
expect(result).
|
|
374
|
+
expect(result.status).toBe(403);
|
|
375
|
+
expect(result.bodyJSON).toEqual({
|
|
376
|
+
title: 'Forbidden',
|
|
377
|
+
status: 403,
|
|
378
|
+
detail: 'Organization ID is required',
|
|
379
|
+
instance: '/test'
|
|
380
|
+
});
|
|
328
381
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
329
382
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
330
383
|
});
|
|
@@ -343,7 +396,13 @@ describe('ToolFunction', () => {
|
|
|
343
396
|
|
|
344
397
|
const result = await toolFunctionWithoutAuth.perform();
|
|
345
398
|
|
|
346
|
-
expect(result).
|
|
399
|
+
expect(result.status).toBe(403);
|
|
400
|
+
expect(result.bodyJSON).toEqual({
|
|
401
|
+
title: 'Forbidden',
|
|
402
|
+
status: 403,
|
|
403
|
+
detail: 'Authentication data is required',
|
|
404
|
+
instance: '/test'
|
|
405
|
+
});
|
|
347
406
|
expect(mockGetTokenVerifier).not.toHaveBeenCalled();
|
|
348
407
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
349
408
|
});
|
|
@@ -354,7 +413,13 @@ describe('ToolFunction', () => {
|
|
|
354
413
|
|
|
355
414
|
const result = await toolFunction.perform();
|
|
356
415
|
|
|
357
|
-
expect(result).
|
|
416
|
+
expect(result.status).toBe(403);
|
|
417
|
+
expect(result.bodyJSON).toEqual({
|
|
418
|
+
title: 'Forbidden',
|
|
419
|
+
status: 403,
|
|
420
|
+
detail: 'Token verification failed',
|
|
421
|
+
instance: '/test'
|
|
422
|
+
});
|
|
358
423
|
expect(mockGetTokenVerifier).toHaveBeenCalled();
|
|
359
424
|
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
360
425
|
});
|
|
@@ -371,4 +436,224 @@ describe('ToolFunction', () => {
|
|
|
371
436
|
expect(toolFunction.getRequest()).toBe(mockRequest);
|
|
372
437
|
});
|
|
373
438
|
});
|
|
439
|
+
|
|
440
|
+
describe('internal request authentication', () => {
|
|
441
|
+
beforeEach(() => {
|
|
442
|
+
// Reset mocks before each test
|
|
443
|
+
jest.clearAllMocks();
|
|
444
|
+
setupAuthMocks();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should use internal authentication for /overrides endpoint', async () => {
|
|
448
|
+
const overridesRequest = {
|
|
449
|
+
path: '/overrides',
|
|
450
|
+
method: 'DELETE',
|
|
451
|
+
bodyJSON: {},
|
|
452
|
+
headers: {
|
|
453
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
454
|
+
if (name === 'Authorization' || name === 'authorization') return 'internal-token';
|
|
455
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
456
|
+
return null;
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
462
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
463
|
+
const result = await toolFunctionOverrides.perform();
|
|
464
|
+
|
|
465
|
+
expect(result).toBe(mockResponse);
|
|
466
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should throw ToolError when internal authentication fails for /overrides endpoint', async () => {
|
|
470
|
+
const overridesRequest = {
|
|
471
|
+
path: '/overrides',
|
|
472
|
+
method: 'DELETE',
|
|
473
|
+
bodyJSON: {},
|
|
474
|
+
headers: {
|
|
475
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
476
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
477
|
+
return null; // No Authorization header
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
483
|
+
const result = await toolFunctionOverrides.perform();
|
|
484
|
+
|
|
485
|
+
expect(result.status).toBe(401);
|
|
486
|
+
expect(result.bodyJSON).toEqual({
|
|
487
|
+
title: 'Unauthorized',
|
|
488
|
+
status: 401,
|
|
489
|
+
detail: 'Internal request authentication failed',
|
|
490
|
+
instance: '/overrides'
|
|
491
|
+
});
|
|
492
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should use regular authentication for non-overrides endpoints', async () => {
|
|
496
|
+
const regularRequest = {
|
|
497
|
+
...mockRequest,
|
|
498
|
+
path: '/regular-tool',
|
|
499
|
+
headers: {
|
|
500
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
501
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
502
|
+
return null;
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
508
|
+
const toolFunctionRegular = new TestToolFunction(regularRequest);
|
|
509
|
+
const result = await toolFunctionRegular.perform();
|
|
510
|
+
|
|
511
|
+
expect(result).toBe(mockResponse);
|
|
512
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(regularRequest, toolFunctionRegular);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should handle internal authentication with valid Authorization header', async () => {
|
|
516
|
+
const overridesRequest = {
|
|
517
|
+
path: '/overrides',
|
|
518
|
+
method: 'PATCH',
|
|
519
|
+
bodyJSON: {
|
|
520
|
+
functions: [
|
|
521
|
+
{
|
|
522
|
+
name: 'test_tool',
|
|
523
|
+
description: 'Updated description'
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
},
|
|
527
|
+
headers: {
|
|
528
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
529
|
+
if (name === 'Authorization') return 'valid-internal-token';
|
|
530
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
531
|
+
return null;
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
537
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
538
|
+
const result = await toolFunctionOverrides.perform();
|
|
539
|
+
|
|
540
|
+
expect(result).toBe(mockResponse);
|
|
541
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should handle internal authentication with lowercase authorization header', async () => {
|
|
545
|
+
const overridesRequest = {
|
|
546
|
+
path: '/overrides',
|
|
547
|
+
method: 'PATCH',
|
|
548
|
+
bodyJSON: {
|
|
549
|
+
functions: [
|
|
550
|
+
{
|
|
551
|
+
name: 'test_tool',
|
|
552
|
+
description: 'Updated description'
|
|
553
|
+
}
|
|
554
|
+
]
|
|
555
|
+
},
|
|
556
|
+
headers: {
|
|
557
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
558
|
+
if (name === 'authorization') return 'valid-internal-token';
|
|
559
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
560
|
+
return null;
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
mockProcessRequest.mockResolvedValue(mockResponse);
|
|
566
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
567
|
+
const result = await toolFunctionOverrides.perform();
|
|
568
|
+
|
|
569
|
+
expect(result).toBe(mockResponse);
|
|
570
|
+
expect(mockProcessRequest).toHaveBeenCalledWith(overridesRequest, toolFunctionOverrides);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should fail internal authentication when token verification fails', async () => {
|
|
574
|
+
// Mock token verifier to return false for invalid token
|
|
575
|
+
mockTokenVerifier.verify.mockResolvedValue(false);
|
|
576
|
+
|
|
577
|
+
const overridesRequest = {
|
|
578
|
+
path: '/overrides',
|
|
579
|
+
method: 'DELETE',
|
|
580
|
+
bodyJSON: {},
|
|
581
|
+
headers: {
|
|
582
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
583
|
+
if (name === 'Authorization') return 'invalid-token';
|
|
584
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
585
|
+
return null;
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
591
|
+
const result = await toolFunctionOverrides.perform();
|
|
592
|
+
|
|
593
|
+
expect(result.status).toBe(401);
|
|
594
|
+
expect(result.bodyJSON).toEqual({
|
|
595
|
+
title: 'Unauthorized',
|
|
596
|
+
status: 401,
|
|
597
|
+
detail: 'Internal request authentication failed',
|
|
598
|
+
instance: '/overrides'
|
|
599
|
+
});
|
|
600
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should fail internal authentication when token verifier throws error', async () => {
|
|
604
|
+
// Mock token verifier to throw an error
|
|
605
|
+
mockTokenVerifier.verify.mockRejectedValue(new Error('Token service unavailable'));
|
|
606
|
+
|
|
607
|
+
const overridesRequest = {
|
|
608
|
+
path: '/overrides',
|
|
609
|
+
method: 'DELETE',
|
|
610
|
+
bodyJSON: {},
|
|
611
|
+
headers: {
|
|
612
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
613
|
+
if (name === 'Authorization') return 'some-token';
|
|
614
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
615
|
+
return null;
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
621
|
+
const result = await toolFunctionOverrides.perform();
|
|
622
|
+
|
|
623
|
+
expect(result.status).toBe(401);
|
|
624
|
+
expect(result.bodyJSON).toEqual({
|
|
625
|
+
title: 'Unauthorized',
|
|
626
|
+
status: 401,
|
|
627
|
+
detail: 'Internal request authentication failed',
|
|
628
|
+
instance: '/overrides'
|
|
629
|
+
});
|
|
630
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should fail internal authentication when headers object is missing', async () => {
|
|
634
|
+
const overridesRequest = {
|
|
635
|
+
path: '/overrides',
|
|
636
|
+
method: 'DELETE',
|
|
637
|
+
bodyJSON: {},
|
|
638
|
+
headers: {
|
|
639
|
+
get: jest.fn().mockImplementation((name: string) => {
|
|
640
|
+
if (name === 'x-opal-thread-id') return 'test-thread-id';
|
|
641
|
+
return null;
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const toolFunctionOverrides = new TestToolFunction(overridesRequest);
|
|
647
|
+
const result = await toolFunctionOverrides.perform();
|
|
648
|
+
|
|
649
|
+
expect(result.status).toBe(401);
|
|
650
|
+
expect(result.bodyJSON).toEqual({
|
|
651
|
+
title: 'Unauthorized',
|
|
652
|
+
status: 401,
|
|
653
|
+
detail: 'Internal request authentication failed',
|
|
654
|
+
instance: '/overrides'
|
|
655
|
+
});
|
|
656
|
+
expect(mockProcessRequest).not.toHaveBeenCalled();
|
|
657
|
+
});
|
|
658
|
+
});
|
|
374
659
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
|
-
import { authenticateRegularRequest } from '../auth/AuthUtils';
|
|
1
|
+
import { Function, Response, Headers, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
|
+
import { authenticateRegularRequest, authenticateInternalRequest } from '../auth/AuthUtils';
|
|
3
3
|
import { toolsService } from '../service/Service';
|
|
4
|
+
import { ToolLogger } from '../logging/ToolLogger';
|
|
5
|
+
import { ToolError } from '../types/ToolError';
|
|
6
|
+
import { ReadyResponse } from '../types/Models';
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Abstract base class for tool-based function execution
|
|
@@ -12,27 +15,66 @@ export abstract class ToolFunction extends Function {
|
|
|
12
15
|
* Override this method to implement any required credentials and/or other configuration
|
|
13
16
|
* exist and are valid. Reasonable caching should be utilized to prevent excessive requests to external resources.
|
|
14
17
|
* @async
|
|
15
|
-
* @returns
|
|
18
|
+
* @returns ReadyResponse containing ready status and optional reason when not ready
|
|
16
19
|
*/
|
|
17
|
-
protected ready(): Promise<boolean> {
|
|
18
|
-
return Promise.resolve(true);
|
|
20
|
+
protected ready(): Promise<ReadyResponse | boolean> {
|
|
21
|
+
return Promise.resolve({ ready: true });
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
* Process the incoming request
|
|
25
|
+
* Process the incoming request with logging
|
|
23
26
|
*
|
|
24
27
|
* @returns Response as the HTTP response
|
|
25
28
|
*/
|
|
26
29
|
public async perform(): Promise<Response> {
|
|
30
|
+
const startTime = Date.now();
|
|
27
31
|
amendLogContext({ opalThreadId: this.request.headers.get('x-opal-thread-id') || '' });
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
|
|
33
|
+
ToolLogger.logRequest(this.request);
|
|
34
|
+
|
|
35
|
+
const response = await this.handleRequest();
|
|
36
|
+
|
|
37
|
+
ToolLogger.logResponse(this.request, response, Date.now() - startTime);
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handle the core request processing logic
|
|
43
|
+
*
|
|
44
|
+
* @returns Response as the HTTP response
|
|
45
|
+
*/
|
|
46
|
+
private async handleRequest(): Promise<Response> {
|
|
47
|
+
try {
|
|
48
|
+
await this.authorizeRequest();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof ToolError) {
|
|
51
|
+
return new Response(
|
|
52
|
+
error.status,
|
|
53
|
+
error.toProblemDetails(this.request.path),
|
|
54
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
// Fallback for unexpected errors
|
|
58
|
+
return new Response(
|
|
59
|
+
500,
|
|
60
|
+
{
|
|
61
|
+
title: 'Internal Server Error',
|
|
62
|
+
status: 500,
|
|
63
|
+
detail: 'An unexpected error occurred during authentication',
|
|
64
|
+
instance: this.request.path
|
|
65
|
+
},
|
|
66
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
67
|
+
);
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
if (this.request.path === '/ready') {
|
|
33
|
-
const
|
|
34
|
-
|
|
71
|
+
const readyResult = await this.ready();
|
|
72
|
+
const readyResponse = typeof readyResult === 'boolean'
|
|
73
|
+
? { ready: readyResult }
|
|
74
|
+
: readyResult;
|
|
75
|
+
return new Response(200, readyResponse);
|
|
35
76
|
}
|
|
77
|
+
|
|
36
78
|
// Pass 'this' as context so decorated methods can use the existing instance
|
|
37
79
|
return toolsService.processRequest(this.request, this);
|
|
38
80
|
}
|
|
@@ -40,9 +82,15 @@ export abstract class ToolFunction extends Function {
|
|
|
40
82
|
/**
|
|
41
83
|
* Authenticate the incoming request by validating the OptiID token and organization ID
|
|
42
84
|
*
|
|
43
|
-
* @
|
|
85
|
+
* @throws {ToolError} If authentication fails
|
|
44
86
|
*/
|
|
45
|
-
private async authorizeRequest(): Promise<
|
|
46
|
-
|
|
87
|
+
private async authorizeRequest(): Promise<void> {
|
|
88
|
+
// Use internal authentication for overrides endpoint (header-based token)
|
|
89
|
+
if (this.request.path === '/overrides') {
|
|
90
|
+
await authenticateInternalRequest(this.request);
|
|
91
|
+
} else {
|
|
92
|
+
// Use regular authentication for other endpoints (body-based token with org validation)
|
|
93
|
+
await authenticateRegularRequest(this.request);
|
|
94
|
+
}
|
|
47
95
|
}
|
|
48
96
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './function/ToolFunction';
|
|
2
2
|
export * from './function/GlobalToolFunction';
|
|
3
3
|
export * from './types/Models';
|
|
4
|
+
export * from './types/ToolError';
|
|
4
5
|
export * from './decorator/Decorator';
|
|
5
6
|
export * from './auth/TokenVerifier';
|
|
6
|
-
export { Tool, Interaction, InteractionResult } from './service/Service';
|
|
7
|
+
export { Tool, Interaction, InteractionResult, NestedInteractions } from './service/Service';
|