@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
package/src/service/Service.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/* eslint-disable max-classes-per-file */
|
|
2
|
-
import { AuthRequirement, IslandResponse, Parameter } from '../types/Models';
|
|
2
|
+
import { AuthRequirement, IslandResponse, OAuthAuthData, OptiIdAuthData, 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
|
|
|
@@ -53,14 +53,14 @@ export class InteractionResult {
|
|
|
53
53
|
/**
|
|
54
54
|
* Interaction definition for an Opal interaction
|
|
55
55
|
*/
|
|
56
|
-
export class Interaction
|
|
56
|
+
export class Interaction {
|
|
57
57
|
public constructor(
|
|
58
58
|
public name: string,
|
|
59
59
|
public endpoint: string,
|
|
60
60
|
public handler: (
|
|
61
61
|
functionContext: ToolFunction | GlobalToolFunction,
|
|
62
62
|
data: unknown,
|
|
63
|
-
authData
|
|
63
|
+
authData: OptiIdAuthData | OAuthAuthData
|
|
64
64
|
) => Promise<InteractionResult>
|
|
65
65
|
) {}
|
|
66
66
|
}
|
|
@@ -68,7 +68,7 @@ export class Interaction<TAuthData> {
|
|
|
68
68
|
/**
|
|
69
69
|
* Tool definition for an Opal tool
|
|
70
70
|
*/
|
|
71
|
-
export class Tool
|
|
71
|
+
export class Tool {
|
|
72
72
|
/**
|
|
73
73
|
* HTTP method for the endpoint (default: POST)
|
|
74
74
|
*/
|
|
@@ -91,7 +91,7 @@ export class Tool<TAuthData> {
|
|
|
91
91
|
public handler: (
|
|
92
92
|
functionContext: ToolFunction | GlobalToolFunction,
|
|
93
93
|
params: unknown,
|
|
94
|
-
authData
|
|
94
|
+
authData: OptiIdAuthData | OAuthAuthData
|
|
95
95
|
) => Promise<unknown>,
|
|
96
96
|
public authRequirements: AuthRequirement[] = [DEFAULT_OPTIID_AUTH]
|
|
97
97
|
) {}
|
|
@@ -114,8 +114,8 @@ export class Tool<TAuthData> {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
export class ToolsService {
|
|
117
|
-
private functions: Map<string, Tool
|
|
118
|
-
private interactions: Map<string, Interaction
|
|
117
|
+
private functions: Map<string, Tool> = new Map();
|
|
118
|
+
private interactions: Map<string, Interaction> = new Map();
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
121
|
* Generate KV store key for tool overrides
|
|
@@ -144,7 +144,7 @@ export class ToolsService {
|
|
|
144
144
|
* @param overrides Override data
|
|
145
145
|
* @returns Tools with overrides applied
|
|
146
146
|
*/
|
|
147
|
-
private applyOverrides(tools:
|
|
147
|
+
private applyOverrides(tools: Tool[], overrides: StoredOverrides): Tool[] {
|
|
148
148
|
return tools.map((tool) => {
|
|
149
149
|
const override = overrides[tool.name];
|
|
150
150
|
if (!override) {
|
|
@@ -207,7 +207,7 @@ export class ToolsService {
|
|
|
207
207
|
* @param functionName Function name
|
|
208
208
|
* @returns Tool definitions with overrides applied
|
|
209
209
|
*/
|
|
210
|
-
public async getToolsWithOverrides(appVersionId: string, functionName: string): Promise<
|
|
210
|
+
public async getToolsWithOverrides(appVersionId: string, functionName: string): Promise<Tool[]> {
|
|
211
211
|
const tools = Array.from(this.functions.values());
|
|
212
212
|
const overrides = await this.getOverrides(appVersionId, functionName);
|
|
213
213
|
|
|
@@ -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,29 +257,28 @@ 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
|
-
public registerTool
|
|
262
|
+
public registerTool(
|
|
278
263
|
name: string,
|
|
279
264
|
description: string,
|
|
280
265
|
handler: (
|
|
281
266
|
functionContext: ToolFunction | GlobalToolFunction,
|
|
282
267
|
params: unknown,
|
|
283
|
-
authData
|
|
268
|
+
authData: OptiIdAuthData | OAuthAuthData
|
|
284
269
|
) => Promise<unknown>,
|
|
285
270
|
parameters: Parameter[],
|
|
286
271
|
endpoint: string,
|
|
287
272
|
authRequirements?: AuthRequirement[]
|
|
288
273
|
): void {
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
const func = new Tool<TAuthData>(
|
|
274
|
+
const resolvedAuthRequirements = this.withDefaultAuthRequirements(authRequirements);
|
|
275
|
+
const func = new Tool(
|
|
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
|
}
|
|
@@ -305,16 +289,16 @@ export class ToolsService {
|
|
|
305
289
|
* @param handler Function implementing the tool
|
|
306
290
|
* @param endpoint API endpoint for the tool
|
|
307
291
|
*/
|
|
308
|
-
public registerInteraction
|
|
292
|
+
public registerInteraction(
|
|
309
293
|
name: string,
|
|
310
294
|
handler: (
|
|
311
295
|
functionContext: ToolFunction | GlobalToolFunction,
|
|
312
296
|
data: unknown,
|
|
313
|
-
authData
|
|
297
|
+
authData: OptiIdAuthData | OAuthAuthData
|
|
314
298
|
) => Promise<InteractionResult>,
|
|
315
299
|
endpoint: string
|
|
316
300
|
): void {
|
|
317
|
-
const func = new Interaction
|
|
301
|
+
const func = new Interaction(name, endpoint, handler);
|
|
318
302
|
this.interactions.set(endpoint, func);
|
|
319
303
|
}
|
|
320
304
|
|
|
@@ -327,61 +311,53 @@ 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
|
-
}
|
|
323
|
+
const params = req.bodyJSON?.parameters ?? req.bodyJSON;
|
|
345
324
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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);
|
|
329
|
+
}
|
|
351
330
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return this.formatErrorResponse(error, func.endpoint);
|
|
331
|
+
// Extract auth data from body JSON
|
|
332
|
+
const authData = req.bodyJSON?.auth;
|
|
333
|
+
|
|
334
|
+
// Validate auth data is present if tool has auth requirements
|
|
335
|
+
if (func.authRequirements.length > 0 && !authData) {
|
|
336
|
+
throw new ToolError('Authentication data is required', 403);
|
|
359
337
|
}
|
|
338
|
+
|
|
339
|
+
const result = await func.handler(functionContext, params, authData);
|
|
340
|
+
return new App.Response(200, result);
|
|
360
341
|
}
|
|
361
342
|
|
|
362
343
|
// Handle interactions
|
|
363
344
|
const interaction = this.interactions.get(req.path);
|
|
364
345
|
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
|
-
}
|
|
346
|
+
const params = req.bodyJSON?.data ?? req.bodyJSON;
|
|
372
347
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const result = await interaction.handler(functionContext, params, authData);
|
|
377
|
-
return new App.Response(200, result);
|
|
378
|
-
} catch (error: any) {
|
|
379
|
-
logger.error(`Error in function ${interaction.name}:`, error);
|
|
380
|
-
return this.formatErrorResponse(error, interaction.endpoint);
|
|
348
|
+
// Extract auth data from body JSON
|
|
349
|
+
const authData = req.bodyJSON?.auth;
|
|
381
350
|
|
|
351
|
+
// Interactions always require auth
|
|
352
|
+
if (!authData) {
|
|
353
|
+
throw new ToolError('Authentication data is required', 403);
|
|
382
354
|
}
|
|
355
|
+
|
|
356
|
+
const result = await interaction.handler(functionContext, params, authData);
|
|
357
|
+
return new App.Response(200, result);
|
|
383
358
|
}
|
|
384
|
-
|
|
359
|
+
|
|
360
|
+
throw new ToolError('Function not found', 404);
|
|
385
361
|
}
|
|
386
362
|
|
|
387
363
|
/**
|
package/src/types/Models.ts
CHANGED
|
@@ -43,29 +43,38 @@ export class Parameter {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Credentials structure
|
|
46
|
+
* Credentials structure ofr OptiID provider
|
|
47
47
|
*/
|
|
48
|
-
export
|
|
48
|
+
export interface OptiIdAuthDataCredentials {
|
|
49
|
+
customer_id: string;
|
|
50
|
+
instance_id: string;
|
|
51
|
+
access_token: string;
|
|
52
|
+
product_sku: string;
|
|
53
|
+
}
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Credentials structure for OAuth providers
|
|
57
|
+
*/
|
|
58
|
+
export interface OAuthProviderAuthDataCredentials {
|
|
59
|
+
access_token: string;
|
|
60
|
+
token_type: string;
|
|
61
|
+
expires_at: string;
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
export interface OptiIdAuthData {
|
|
65
|
+
provider: 'OptiID';
|
|
66
|
+
credentials: OptiIdAuthDataCredentials;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface OAuthAuthData {
|
|
70
|
+
provider: string;
|
|
71
|
+
credentials: OAuthProviderAuthDataCredentials;
|
|
72
|
+
}
|
|
58
73
|
|
|
59
74
|
/**
|
|
60
75
|
* Auth data structure
|
|
61
76
|
*/
|
|
62
|
-
export
|
|
63
|
-
|
|
64
|
-
public constructor(
|
|
65
|
-
public provider: string,
|
|
66
|
-
public credentials: OptiIdAuthDataCredentials
|
|
67
|
-
) {}
|
|
68
|
-
}
|
|
77
|
+
export type AuthData = OptiIdAuthData | OAuthAuthData;
|
|
69
78
|
|
|
70
79
|
/**
|
|
71
80
|
* Authentication requirements for an Opal tool
|
|
@@ -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
|
+
}
|