@optimizely-opal/opal-tool-ocp-sdk 1.0.0 → 1.1.0-beta.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.
- package/README.md +39 -3
- package/dist/function/GlobalToolFunction.d.ts +4 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +25 -19
- 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 +10 -9
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +42 -74
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +60 -95
- package/dist/service/Service.test.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/function/GlobalToolFunction.test.ts +113 -213
- package/src/function/GlobalToolFunction.ts +29 -29
- package/src/function/ToolFunction.test.ts +78 -285
- package/src/function/ToolFunction.ts +24 -30
- package/src/service/Service.test.ts +61 -113
- package/src/service/Service.ts +45 -79
- package/src/utils/ErrorFormatter.ts +31 -0
|
@@ -71,6 +71,11 @@ jest.mock('@zaiusinc/app-sdk', () => {
|
|
|
71
71
|
};
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
// Mock the authenticateInternalRequest function
|
|
75
|
+
jest.mock('../auth/AuthUtils', () => ({
|
|
76
|
+
authenticateInternalRequest: jest.fn().mockResolvedValue(undefined)
|
|
77
|
+
}));
|
|
78
|
+
|
|
74
79
|
// Get the mocked kvStore for use in tests
|
|
75
80
|
const { storage } = jest.requireMock('@zaiusinc/app-sdk');
|
|
76
81
|
const mockKvStore = storage.kvStore;
|
|
@@ -250,8 +255,7 @@ describe('ToolsService', () => {
|
|
|
250
255
|
endpoint: '/second-tool',
|
|
251
256
|
http_method: 'POST',
|
|
252
257
|
auth_requirements: [
|
|
253
|
-
{ provider: 'oauth2', scope_bundle: 'calendar', required: true }
|
|
254
|
-
{ provider: 'OptiID', scope_bundle: 'default', required: true }
|
|
258
|
+
{ provider: 'oauth2', scope_bundle: 'calendar', required: true }
|
|
255
259
|
]
|
|
256
260
|
});
|
|
257
261
|
});
|
|
@@ -408,95 +412,55 @@ describe('ToolsService', () => {
|
|
|
408
412
|
);
|
|
409
413
|
});
|
|
410
414
|
|
|
411
|
-
it('should
|
|
415
|
+
it('should throw error when tool handler throws a regular error', async () => {
|
|
412
416
|
const errorMessage = 'Tool execution failed';
|
|
413
417
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
414
418
|
|
|
415
419
|
const mockRequest = createMockRequest();
|
|
416
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
417
420
|
|
|
418
|
-
expect(
|
|
419
|
-
|
|
420
|
-
title: 'Internal Server Error',
|
|
421
|
-
status: 500,
|
|
422
|
-
detail: errorMessage,
|
|
423
|
-
instance: mockTool.endpoint
|
|
424
|
-
});
|
|
425
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
426
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
427
|
-
`Error in function ${mockTool.name}:`,
|
|
428
|
-
expect.any(Error)
|
|
429
|
-
);
|
|
421
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
422
|
+
.rejects.toThrow(errorMessage);
|
|
430
423
|
});
|
|
431
424
|
|
|
432
|
-
it('should
|
|
425
|
+
it('should throw when tool handler throws object without message', async () => {
|
|
433
426
|
jest.mocked(mockTool.handler).mockRejectedValueOnce({});
|
|
434
427
|
|
|
435
428
|
const mockRequest = createMockRequest();
|
|
436
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
437
429
|
|
|
438
|
-
expect(
|
|
439
|
-
|
|
440
|
-
title: 'Internal Server Error',
|
|
441
|
-
status: 500,
|
|
442
|
-
detail: 'An unexpected error occurred',
|
|
443
|
-
instance: mockTool.endpoint
|
|
444
|
-
});
|
|
445
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
430
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
431
|
+
.rejects.toEqual({});
|
|
446
432
|
});
|
|
447
433
|
|
|
448
|
-
it('should
|
|
434
|
+
it('should throw ToolError when tool handler throws ToolError', async () => {
|
|
449
435
|
const toolError = new ToolError('Resource not found', 404, 'The requested task does not exist');
|
|
450
436
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
451
437
|
|
|
452
438
|
const mockRequest = createMockRequest();
|
|
453
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
454
439
|
|
|
455
|
-
expect(
|
|
456
|
-
|
|
457
|
-
title: 'Resource not found',
|
|
458
|
-
status: 404,
|
|
459
|
-
detail: 'The requested task does not exist',
|
|
460
|
-
instance: mockTool.endpoint
|
|
461
|
-
});
|
|
462
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
463
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
464
|
-
`Error in function ${mockTool.name}:`,
|
|
465
|
-
expect.any(ToolError)
|
|
466
|
-
);
|
|
440
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
441
|
+
.rejects.toThrow(toolError);
|
|
467
442
|
});
|
|
468
443
|
|
|
469
|
-
it('should
|
|
444
|
+
it('should throw ToolError without detail when detail is not provided', async () => {
|
|
470
445
|
const toolError = new ToolError('Bad request', 400);
|
|
471
446
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
472
447
|
|
|
473
448
|
const mockRequest = createMockRequest();
|
|
474
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
475
449
|
|
|
476
|
-
expect(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
status: 400,
|
|
480
|
-
instance: mockTool.endpoint
|
|
481
|
-
});
|
|
482
|
-
expect(response.bodyJSON).not.toHaveProperty('detail');
|
|
483
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
450
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
451
|
+
.rejects.toThrow(toolError);
|
|
452
|
+
expect(toolError.status).toBe(400);
|
|
484
453
|
});
|
|
485
454
|
|
|
486
|
-
it('should default
|
|
455
|
+
it('should throw ToolError with default 500 status when created without status', async () => {
|
|
487
456
|
const toolError = new ToolError('Database error');
|
|
488
457
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
489
458
|
|
|
490
459
|
const mockRequest = createMockRequest();
|
|
491
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
492
460
|
|
|
493
|
-
expect(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
status: 500,
|
|
497
|
-
instance: mockTool.endpoint
|
|
498
|
-
});
|
|
499
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
461
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
462
|
+
.rejects.toThrow(toolError);
|
|
463
|
+
expect(toolError.status).toBe(500);
|
|
500
464
|
});
|
|
501
465
|
});
|
|
502
466
|
|
|
@@ -594,7 +558,7 @@ describe('ToolsService', () => {
|
|
|
594
558
|
);
|
|
595
559
|
});
|
|
596
560
|
|
|
597
|
-
it('should
|
|
561
|
+
it('should throw error when interaction handler throws a regular error', async () => {
|
|
598
562
|
const errorMessage = 'Interaction execution failed';
|
|
599
563
|
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
600
564
|
|
|
@@ -603,23 +567,11 @@ describe('ToolsService', () => {
|
|
|
603
567
|
bodyJSON: { data: { param1: 'test-value' } }
|
|
604
568
|
});
|
|
605
569
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
expect(response.status).toBe(500);
|
|
609
|
-
expect(response.bodyJSON).toEqual({
|
|
610
|
-
title: 'Internal Server Error',
|
|
611
|
-
status: 500,
|
|
612
|
-
detail: errorMessage,
|
|
613
|
-
instance: mockInteraction.endpoint
|
|
614
|
-
});
|
|
615
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
616
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
617
|
-
`Error in function ${mockInteraction.name}:`,
|
|
618
|
-
expect.any(Error)
|
|
619
|
-
);
|
|
570
|
+
await expect(toolsService.processRequest(interactionRequest, mockToolFunction))
|
|
571
|
+
.rejects.toThrow(errorMessage);
|
|
620
572
|
});
|
|
621
573
|
|
|
622
|
-
it('should
|
|
574
|
+
it('should throw ToolError when interaction handler throws ToolError', async () => {
|
|
623
575
|
const toolError = new ToolError('Webhook validation failed', 400, 'Invalid signature');
|
|
624
576
|
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(toolError);
|
|
625
577
|
|
|
@@ -628,29 +580,26 @@ describe('ToolsService', () => {
|
|
|
628
580
|
bodyJSON: { data: { param1: 'test-value' } }
|
|
629
581
|
});
|
|
630
582
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
expect(response.status).toBe(400);
|
|
634
|
-
expect(response.bodyJSON).toEqual({
|
|
635
|
-
title: 'Webhook validation failed',
|
|
636
|
-
status: 400,
|
|
637
|
-
detail: 'Invalid signature',
|
|
638
|
-
instance: mockInteraction.endpoint
|
|
639
|
-
});
|
|
640
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
641
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
642
|
-
`Error in function ${mockInteraction.name}:`,
|
|
643
|
-
expect.any(ToolError)
|
|
644
|
-
);
|
|
583
|
+
await expect(toolsService.processRequest(interactionRequest, mockToolFunction))
|
|
584
|
+
.rejects.toThrow(toolError);
|
|
645
585
|
});
|
|
646
586
|
});
|
|
647
587
|
|
|
648
588
|
describe('error cases', () => {
|
|
649
|
-
it('should
|
|
589
|
+
it('should throw ToolError with 404 when no matching tool or interaction is found', async () => {
|
|
650
590
|
const unknownRequest = createMockRequest({ path: '/unknown-endpoint' });
|
|
651
|
-
const response = await toolsService.processRequest(unknownRequest, mockToolFunction);
|
|
652
591
|
|
|
653
|
-
expect(
|
|
592
|
+
await expect(toolsService.processRequest(unknownRequest, mockToolFunction))
|
|
593
|
+
.rejects.toThrow(ToolError);
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
await toolsService.processRequest(unknownRequest, mockToolFunction);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
599
|
+
expect((error as ToolError).status).toBe(404);
|
|
600
|
+
// ToolError prepends status to message
|
|
601
|
+
expect((error as ToolError).message).toContain('Function not found');
|
|
602
|
+
}
|
|
654
603
|
});
|
|
655
604
|
|
|
656
605
|
it('should handle tool with OptiID auth requirements', async () => {
|
|
@@ -840,7 +789,7 @@ describe('ToolsService', () => {
|
|
|
840
789
|
jest.clearAllMocks();
|
|
841
790
|
});
|
|
842
791
|
|
|
843
|
-
it('should
|
|
792
|
+
it('should throw ToolError with 400 for invalid parameter types', async () => {
|
|
844
793
|
// Register a tool with specific parameter types
|
|
845
794
|
const toolWithTypedParams = {
|
|
846
795
|
name: 'typed_tool',
|
|
@@ -874,26 +823,25 @@ describe('ToolsService', () => {
|
|
|
874
823
|
}
|
|
875
824
|
});
|
|
876
825
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
826
|
+
await expect(toolsService.processRequest(invalidRequest, mockToolFunction))
|
|
827
|
+
.rejects.toThrow(ToolError);
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
await toolsService.processRequest(invalidRequest, mockToolFunction);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
833
|
+
const toolError = error as ToolError;
|
|
834
|
+
expect(toolError.status).toBe(400);
|
|
835
|
+
// The ToolError has title property, message includes more details
|
|
836
|
+
expect(toolError.toProblemDetails('/typed-tool')).toMatchObject({
|
|
837
|
+
title: 'One or more validation errors occurred.',
|
|
838
|
+
status: 400
|
|
839
|
+
});
|
|
840
|
+
expect(toolError.errors).toHaveLength(3);
|
|
841
|
+
expect(toolError.errors![0]).toHaveProperty('field', 'name');
|
|
842
|
+
expect(toolError.errors![0].message)
|
|
843
|
+
.toBe("Parameter 'name' must be a string, but received number");
|
|
844
|
+
}
|
|
897
845
|
|
|
898
846
|
// Verify the handler was not called
|
|
899
847
|
expect(toolWithTypedParams.handler).not.toHaveBeenCalled();
|
package/src/service/Service.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { AuthRequirement, IslandResponse, Parameter } from '../types/Models';
|
|
3
3
|
import { ToolError } from '../types/ToolError';
|
|
4
4
|
import * as App from '@zaiusinc/app-sdk';
|
|
5
|
-
import { logger,
|
|
5
|
+
import { logger, getAppContext } from '@zaiusinc/app-sdk';
|
|
6
6
|
import { ToolFunction } from '../function/ToolFunction';
|
|
7
7
|
import { GlobalToolFunction } from '../function/GlobalToolFunction';
|
|
8
8
|
import { ParameterValidator } from '../validation/ParameterValidator';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Default OptiID authentication requirement
|
|
11
|
+
* Default OptiID authentication requirement added when tool doesn't specify any auth
|
|
12
12
|
*/
|
|
13
13
|
const DEFAULT_OPTIID_AUTH = new AuthRequirement('OptiID', 'default', true);
|
|
14
14
|
|
|
@@ -219,50 +219,35 @@ export class ToolsService {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
*
|
|
222
|
+
* Apply default auth requirements if none are specified
|
|
223
223
|
* @param authRequirements Original authentication requirements
|
|
224
|
-
* @returns
|
|
224
|
+
* @returns Auth requirements with default OptiID if none specified, otherwise unchanged
|
|
225
225
|
*/
|
|
226
|
-
private
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (hasOptiIdProvider) {
|
|
231
|
-
return authRequirements;
|
|
226
|
+
private withDefaultAuthRequirements(authRequirements?: AuthRequirement[]): AuthRequirement[] {
|
|
227
|
+
// Only add default OptiID if no auth requirements are specified
|
|
228
|
+
if (!authRequirements || authRequirements.length === 0) {
|
|
229
|
+
return [DEFAULT_OPTIID_AUTH];
|
|
232
230
|
}
|
|
233
231
|
|
|
234
|
-
|
|
232
|
+
// Respect developer's choice - return as-is
|
|
233
|
+
return authRequirements;
|
|
235
234
|
}
|
|
236
235
|
|
|
237
236
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* @
|
|
237
|
+
* Check if an endpoint requires OptiID authentication
|
|
238
|
+
* Tools: Check auth requirements for OptiID provider
|
|
239
|
+
* Interactions: Always require OptiID
|
|
240
|
+
* @param endpoint The endpoint path to check
|
|
241
|
+
* @returns true if the endpoint requires OptiID auth
|
|
242
242
|
*/
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (error instanceof ToolError) {
|
|
248
|
-
// Use ToolError's status and format
|
|
249
|
-
status = error.status;
|
|
250
|
-
problemDetails = error.toProblemDetails(instance);
|
|
251
|
-
} else {
|
|
252
|
-
// Convert regular errors to RFC 9457 format with 500 status
|
|
253
|
-
problemDetails = {
|
|
254
|
-
title: 'Internal Server Error',
|
|
255
|
-
status: 500,
|
|
256
|
-
detail: error.message || 'An unexpected error occurred',
|
|
257
|
-
instance
|
|
258
|
-
};
|
|
243
|
+
public requiresOptiIdAuth(endpoint: string): boolean {
|
|
244
|
+
const func = this.functions.get(endpoint);
|
|
245
|
+
if (func) {
|
|
246
|
+
return func.authRequirements.some((auth) => auth.provider.toLowerCase() === 'optiid');
|
|
259
247
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
problemDetails,
|
|
264
|
-
new Headers([['content-type', 'application/problem+json']])
|
|
265
|
-
);
|
|
248
|
+
// Interactions always require OptiID
|
|
249
|
+
const interaction = this.interactions.get(endpoint);
|
|
250
|
+
return !!interaction;
|
|
266
251
|
}
|
|
267
252
|
|
|
268
253
|
/**
|
|
@@ -272,7 +257,7 @@ export class ToolsService {
|
|
|
272
257
|
* @param handler Function implementing the tool
|
|
273
258
|
* @param parameters List of parameters for the tool
|
|
274
259
|
* @param endpoint API endpoint for the tool
|
|
275
|
-
* @param authRequirements Authentication requirements (optional)
|
|
260
|
+
* @param authRequirements Authentication requirements (optional - defaults to OptiID if not specified)
|
|
276
261
|
*/
|
|
277
262
|
public registerTool<TAuthData>(
|
|
278
263
|
name: string,
|
|
@@ -286,15 +271,14 @@ export class ToolsService {
|
|
|
286
271
|
endpoint: string,
|
|
287
272
|
authRequirements?: AuthRequirement[]
|
|
288
273
|
): void {
|
|
289
|
-
|
|
290
|
-
const enforcedAuthRequirements = this.enforceOptiIdAuth(authRequirements);
|
|
274
|
+
const resolvedAuthRequirements = this.withDefaultAuthRequirements(authRequirements);
|
|
291
275
|
const func = new Tool<TAuthData>(
|
|
292
276
|
name,
|
|
293
277
|
description,
|
|
294
278
|
parameters,
|
|
295
279
|
endpoint,
|
|
296
280
|
handler,
|
|
297
|
-
|
|
281
|
+
resolvedAuthRequirements
|
|
298
282
|
);
|
|
299
283
|
this.functions.set(endpoint, func);
|
|
300
284
|
}
|
|
@@ -327,61 +311,43 @@ export class ToolsService {
|
|
|
327
311
|
return await this.handleDiscoveryRequest(functionContext);
|
|
328
312
|
}
|
|
329
313
|
|
|
330
|
-
// Handle overrides endpoint
|
|
314
|
+
// Handle overrides endpoint (auth handled by function layer)
|
|
331
315
|
if (req.path === '/overrides') {
|
|
332
316
|
return await this.handleOverridesRequest(req, functionContext);
|
|
333
317
|
}
|
|
334
318
|
|
|
335
319
|
// Handle regular tool functions
|
|
320
|
+
// Auth is already validated by the function layer
|
|
336
321
|
const func = this.functions.get(req.path);
|
|
337
322
|
if (func) {
|
|
338
|
-
|
|
339
|
-
let params;
|
|
340
|
-
if (req.bodyJSON && req.bodyJSON.parameters) {
|
|
341
|
-
params = req.bodyJSON.parameters;
|
|
342
|
-
} else {
|
|
343
|
-
params = req.bodyJSON;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Validate parameters before calling the handler (only if tool has parameter definitions)
|
|
347
|
-
// ParameterValidator.validate() throws ToolError if validation fails
|
|
348
|
-
if (func.parameters && func.parameters.length > 0) {
|
|
349
|
-
ParameterValidator.validate(params, func.parameters, func.endpoint);
|
|
350
|
-
}
|
|
323
|
+
const params = req.bodyJSON?.parameters ?? req.bodyJSON;
|
|
351
324
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
} catch (error: any) {
|
|
357
|
-
logger.error(`Error in function ${func.name}:`, error);
|
|
358
|
-
return this.formatErrorResponse(error, func.endpoint);
|
|
325
|
+
// Validate parameters before calling the handler (only if tool has parameter definitions)
|
|
326
|
+
// ParameterValidator.validate() throws ToolError if validation fails
|
|
327
|
+
if (func.parameters && func.parameters.length > 0) {
|
|
328
|
+
ParameterValidator.validate(params, func.parameters, func.endpoint);
|
|
359
329
|
}
|
|
330
|
+
|
|
331
|
+
// Extract auth data from body JSON
|
|
332
|
+
const authData = req.bodyJSON?.auth;
|
|
333
|
+
const result = await func.handler(functionContext, params, authData);
|
|
334
|
+
return new App.Response(200, result);
|
|
360
335
|
}
|
|
361
336
|
|
|
362
337
|
// Handle interactions
|
|
338
|
+
// Auth is already validated by the function layer
|
|
363
339
|
const interaction = this.interactions.get(req.path);
|
|
364
340
|
if (interaction) {
|
|
365
|
-
|
|
366
|
-
let params;
|
|
367
|
-
if (req.bodyJSON && req.bodyJSON.data) {
|
|
368
|
-
params = req.bodyJSON.data;
|
|
369
|
-
} else {
|
|
370
|
-
params = req.bodyJSON;
|
|
371
|
-
}
|
|
341
|
+
const params = req.bodyJSON?.data ?? req.bodyJSON;
|
|
372
342
|
|
|
373
|
-
|
|
374
|
-
|
|
343
|
+
// Extract auth data from body JSON
|
|
344
|
+
const authData = req.bodyJSON?.auth;
|
|
375
345
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
} catch (error: any) {
|
|
379
|
-
logger.error(`Error in function ${interaction.name}:`, error);
|
|
380
|
-
return this.formatErrorResponse(error, interaction.endpoint);
|
|
381
|
-
|
|
382
|
-
}
|
|
346
|
+
const result = await interaction.handler(functionContext, params, authData);
|
|
347
|
+
return new App.Response(200, result);
|
|
383
348
|
}
|
|
384
|
-
|
|
349
|
+
|
|
350
|
+
throw new ToolError('Function not found', 404);
|
|
385
351
|
}
|
|
386
352
|
|
|
387
353
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Response, Headers } from '@zaiusinc/app-sdk';
|
|
2
|
+
import { ToolError } from '../types/ToolError';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format an error as RFC 9457 Problem Details response
|
|
6
|
+
* @param error The error to format (ToolError or generic Error)
|
|
7
|
+
* @param instance URI reference identifying the specific occurrence (typically request path)
|
|
8
|
+
* @returns RFC 9457 compliant Response
|
|
9
|
+
*/
|
|
10
|
+
export function formatErrorResponse(error: unknown, instance: string): Response {
|
|
11
|
+
if (error instanceof ToolError) {
|
|
12
|
+
return new Response(
|
|
13
|
+
error.status,
|
|
14
|
+
error.toProblemDetails(instance),
|
|
15
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback for generic errors
|
|
20
|
+
const message = error instanceof Error ? error.message : 'An unexpected error occurred';
|
|
21
|
+
return new Response(
|
|
22
|
+
500,
|
|
23
|
+
{
|
|
24
|
+
title: 'Internal Server Error',
|
|
25
|
+
status: 500,
|
|
26
|
+
detail: message,
|
|
27
|
+
instance
|
|
28
|
+
},
|
|
29
|
+
new Headers([['content-type', 'application/problem+json']])
|
|
30
|
+
);
|
|
31
|
+
}
|