@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.
Files changed (44) hide show
  1. package/README.md +51 -13
  2. package/dist/auth/AuthUtils.d.ts +2 -5
  3. package/dist/auth/AuthUtils.d.ts.map +1 -1
  4. package/dist/auth/AuthUtils.js +5 -5
  5. package/dist/auth/AuthUtils.js.map +1 -1
  6. package/dist/decorator/Decorator.test.js +4 -4
  7. package/dist/decorator/Decorator.test.js.map +1 -1
  8. package/dist/function/GlobalToolFunction.d.ts +4 -1
  9. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  10. package/dist/function/GlobalToolFunction.js +27 -21
  11. package/dist/function/GlobalToolFunction.js.map +1 -1
  12. package/dist/function/GlobalToolFunction.test.js +114 -193
  13. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  14. package/dist/function/ToolFunction.d.ts +4 -1
  15. package/dist/function/ToolFunction.d.ts.map +1 -1
  16. package/dist/function/ToolFunction.js +20 -21
  17. package/dist/function/ToolFunction.js.map +1 -1
  18. package/dist/function/ToolFunction.test.js +73 -263
  19. package/dist/function/ToolFunction.test.js.map +1 -1
  20. package/dist/service/Service.d.ts +20 -19
  21. package/dist/service/Service.d.ts.map +1 -1
  22. package/dist/service/Service.js +47 -72
  23. package/dist/service/Service.js.map +1 -1
  24. package/dist/service/Service.test.js +229 -133
  25. package/dist/service/Service.test.js.map +1 -1
  26. package/dist/types/Models.d.ts +18 -7
  27. package/dist/types/Models.d.ts.map +1 -1
  28. package/dist/types/Models.js +1 -29
  29. package/dist/types/Models.js.map +1 -1
  30. package/dist/utils/ErrorFormatter.d.ts +9 -0
  31. package/dist/utils/ErrorFormatter.d.ts.map +1 -0
  32. package/dist/utils/ErrorFormatter.js +25 -0
  33. package/dist/utils/ErrorFormatter.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/auth/AuthUtils.ts +10 -10
  36. package/src/decorator/Decorator.test.ts +4 -4
  37. package/src/function/GlobalToolFunction.test.ts +113 -213
  38. package/src/function/GlobalToolFunction.ts +31 -31
  39. package/src/function/ToolFunction.test.ts +78 -285
  40. package/src/function/ToolFunction.ts +24 -30
  41. package/src/service/Service.test.ts +238 -174
  42. package/src/service/Service.ts +68 -92
  43. package/src/types/Models.ts +24 -15
  44. package/src/utils/ErrorFormatter.ts +31 -0
@@ -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, Headers, getAppContext } from '@zaiusinc/app-sdk';
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 that will be enforced for all tools
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<TAuthData> {
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?: TAuthData
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<TAuthData> {
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?: TAuthData
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<any>> = new Map();
118
- private interactions: Map<string, Interaction<any>> = new Map();
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: Array<Tool<any>>, overrides: StoredOverrides): Array<Tool<any>> {
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<Array<Tool<any>>> {
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
- * Enforce OptiID authentication for tools by ensuring OptiID auth requirement is present
222
+ * Apply default auth requirements if none are specified
223
223
  * @param authRequirements Original authentication requirements
224
- * @returns Enforced authentication requirements with OptiID
224
+ * @returns Auth requirements with default OptiID if none specified, otherwise unchanged
225
225
  */
226
- private enforceOptiIdAuth(authRequirements?: AuthRequirement[]): AuthRequirement[] {
227
- const hasOptiIdProvider = authRequirements
228
- && authRequirements.some((auth) => auth.provider.toLowerCase() === 'optiid');
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
- return [...(authRequirements || []), DEFAULT_OPTIID_AUTH];
232
+ // Respect developer's choice - return as-is
233
+ return authRequirements;
235
234
  }
236
235
 
237
236
  /**
238
- * Format an error as RFC 9457 Problem Details response
239
- * @param error The error to format
240
- * @param instance URI reference identifying the specific occurrence
241
- * @returns RFC 9457 compliant Response
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
- private formatErrorResponse(error: any, instance: string): App.Response {
244
- let status = 500;
245
- let problemDetails: Record<string, unknown>;
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
- return new App.Response(
262
- status,
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<TAuthData>(
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?: TAuthData
268
+ authData: OptiIdAuthData | OAuthAuthData
284
269
  ) => Promise<unknown>,
285
270
  parameters: Parameter[],
286
271
  endpoint: string,
287
272
  authRequirements?: AuthRequirement[]
288
273
  ): void {
289
- // Enforce OptiID authentication for all tools
290
- const enforcedAuthRequirements = this.enforceOptiIdAuth(authRequirements);
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
- enforcedAuthRequirements
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<TAuthData>(
292
+ public registerInteraction(
309
293
  name: string,
310
294
  handler: (
311
295
  functionContext: ToolFunction | GlobalToolFunction,
312
296
  data: unknown,
313
- authData?: TAuthData
297
+ authData: OptiIdAuthData | OAuthAuthData
314
298
  ) => Promise<InteractionResult>,
315
299
  endpoint: string
316
300
  ): void {
317
- const func = new Interaction<TAuthData>(name, endpoint, handler);
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
- try {
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
- // 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
- }
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
- // Extract auth data from body JSON
353
- const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
354
- const result = await func.handler(functionContext, params, authData);
355
- return new App.Response(200, result);
356
- } catch (error: any) {
357
- logger.error(`Error in function ${func.name}:`, error);
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
- try {
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
- // Extract auth data from body JSON
374
- const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
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
- return new App.Response(404, 'Function not found');
359
+
360
+ throw new ToolError('Function not found', 404);
385
361
  }
386
362
 
387
363
  /**
@@ -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 class OptiIdAuthDataCredentials {
48
+ export interface OptiIdAuthDataCredentials {
49
+ customer_id: string;
50
+ instance_id: string;
51
+ access_token: string;
52
+ product_sku: string;
53
+ }
49
54
 
50
- public constructor(
51
- public customer_id: string,
52
- public instance_id: string,
53
- public access_token: string,
54
- public product_sku: string
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 class OptiIdAuthData {
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
+ }