@optimizely-opal/opal-tools-sdk 0.1.5-dev → 0.1.8-dev
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/.prettierignore +5 -0
- package/.prettierrc +1 -0
- package/README.md +20 -10
- package/dist/auth.d.ts +5 -5
- package/dist/auth.js +2 -2
- package/dist/decorators.d.ts +10 -8
- package/dist/decorators.js +5 -44
- package/dist/index.d.ts +10 -5
- package/dist/index.js +11 -5
- package/dist/models.d.ts +137 -116
- package/dist/models.js +104 -77
- package/dist/proteus.d.ts +1451 -0
- package/dist/proteus.js +86 -0
- package/dist/registerResource.d.ts +60 -0
- package/dist/registerResource.js +59 -0
- package/dist/registerTool.d.ts +79 -0
- package/dist/registerTool.js +106 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +1 -1
- package/dist/service.d.ts +20 -6
- package/dist/service.js +106 -19
- package/eslint.config.js +20 -0
- package/package.json +20 -10
- package/scripts/generate-proteus.ts +135 -0
- package/scripts/lint.sh +7 -0
- package/src/auth.ts +21 -16
- package/src/decorators.ts +32 -67
- package/src/index.ts +10 -5
- package/src/models.ts +133 -103
- package/src/proteus.ts +2129 -0
- package/src/registerResource.ts +82 -0
- package/src/registerTool.ts +171 -0
- package/src/registry.ts +2 -2
- package/src/service.ts +161 -31
- package/tests/integration.test.ts +497 -0
- package/tests/proteus.test.ts +122 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +3 -3
- package/vitest.config.ts +7 -0
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optimizely-opal/opal-tools-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8-dev",
|
|
4
4
|
"description": "SDK for creating Opal-compatible tools services",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
8
|
+
"build": "tsc -p tsconfig.build.json",
|
|
9
|
+
"generate:proteus": "ts-node scripts/generate-proteus.ts",
|
|
10
|
+
"lint": "bash scripts/lint.sh",
|
|
11
|
+
"prepublishOnly": "npm run build",
|
|
12
|
+
"test": "vitest run"
|
|
11
13
|
},
|
|
12
14
|
"keywords": [
|
|
13
15
|
"opal",
|
|
@@ -22,16 +24,24 @@
|
|
|
22
24
|
"reflect-metadata": "^0.1.13"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
|
-
"@types/
|
|
26
|
-
"
|
|
27
|
-
"
|
|
27
|
+
"@types/node": "^20.4.5",
|
|
28
|
+
"@types/supertest": "^6.0.2",
|
|
29
|
+
"eslint": "^9.39.2",
|
|
30
|
+
"eslint-plugin-perfectionist": "^5.4.0",
|
|
31
|
+
"json-schema-to-typescript": "^13.1.1",
|
|
32
|
+
"prettier": "^3.8.1",
|
|
33
|
+
"supertest": "^7.0.0",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"typescript-eslint": "^8.54.0",
|
|
36
|
+
"vitest": "^1.2.0"
|
|
28
37
|
},
|
|
29
38
|
"peerDependencies": {
|
|
30
|
-
"typescript": "^5.1.6",
|
|
31
|
-
"@types/node": "^20.4.5",
|
|
32
39
|
"@types/express": "^4.17.17",
|
|
40
|
+
"@types/node": "^20.4.5",
|
|
33
41
|
"axios": "^1.6.0",
|
|
34
|
-
"express": "^4.18.2"
|
|
42
|
+
"express": "^4.18.2",
|
|
43
|
+
"typescript": "^5.1.6",
|
|
44
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
35
45
|
},
|
|
36
46
|
"repository": {
|
|
37
47
|
"type": "git",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
/**
|
|
3
|
+
* Generate Adaptive Proteus Document types from JSON schema.
|
|
4
|
+
*
|
|
5
|
+
* This script uses json-schema-to-typescript to generate TypeScript interfaces
|
|
6
|
+
* from the proteus-document-spec.json schema. The generated types will replace the
|
|
7
|
+
* manual Proteus builders once we're ready to switch over.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npm run generate:proteus
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import { compile } from "json-schema-to-typescript";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate builder functions and ProteusResponse class.
|
|
19
|
+
*
|
|
20
|
+
* The generated TypeScript interfaces are great for validation, but we want to
|
|
21
|
+
* keep the builder API (UI.Document(), etc.) for ease of use.
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
function generateBuilderCode(schema: any): string {
|
|
25
|
+
// Extract all Proteus components from definitions
|
|
26
|
+
const proteusComponents: Array<{ fullName: string; shortName: string }> = [];
|
|
27
|
+
const skipTypes = new Set([
|
|
28
|
+
"ProteusAtomicCondition",
|
|
29
|
+
"ProteusCondition",
|
|
30
|
+
"ProteusElement",
|
|
31
|
+
"ProteusEventHandler",
|
|
32
|
+
"ProteusNode",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
if (schema.definitions) {
|
|
36
|
+
for (const componentName of Object.keys(schema.definitions)) {
|
|
37
|
+
if (
|
|
38
|
+
componentName.startsWith("Proteus") &&
|
|
39
|
+
!skipTypes.has(componentName)
|
|
40
|
+
) {
|
|
41
|
+
// Extract the short name (e.g., "Document" from "ProteusDocument")
|
|
42
|
+
const shortName = componentName.substring(7); // Remove "Proteus" prefix
|
|
43
|
+
proteusComponents.push({ fullName: componentName, shortName });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Sort components for consistent output
|
|
49
|
+
proteusComponents.sort((a, b) => a.shortName.localeCompare(b.shortName));
|
|
50
|
+
|
|
51
|
+
// Generate factory functions that return properly typed objects
|
|
52
|
+
const builderMethods = proteusComponents
|
|
53
|
+
.map(
|
|
54
|
+
({ fullName, shortName }) =>
|
|
55
|
+
` ${shortName}: (props: Omit<${fullName}, '$type'>): ${fullName} => ({ $type: '${shortName}' as const, ...props }),`,
|
|
56
|
+
)
|
|
57
|
+
.join("\n");
|
|
58
|
+
|
|
59
|
+
return `
|
|
60
|
+
/**
|
|
61
|
+
* Builder namespace for Adaptive UI Document components.
|
|
62
|
+
*
|
|
63
|
+
* Usage:
|
|
64
|
+
* UI.Document({ children: [...] })
|
|
65
|
+
* UI.Heading({ children: "Title", level: "2" })
|
|
66
|
+
* UI.Input({ name: "field_name", placeholder: "Enter..." })
|
|
67
|
+
*/
|
|
68
|
+
export const UI = {
|
|
69
|
+
MIME_TYPE: "application/vnd.opal.proteus+json" as const,
|
|
70
|
+
${builderMethods}
|
|
71
|
+
};
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
// Paths
|
|
77
|
+
const scriptDir = __dirname;
|
|
78
|
+
const sdkRoot = path.join(scriptDir, "..");
|
|
79
|
+
const schemaFile = path.join(sdkRoot, "..", "proteus-document-spec.json");
|
|
80
|
+
const outputFile = path.join(sdkRoot, "src", "proteus.ts");
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(schemaFile)) {
|
|
83
|
+
console.error(`Error: Schema file not found at ${schemaFile}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Read the schema
|
|
89
|
+
const schema = JSON.parse(fs.readFileSync(schemaFile, "utf-8"));
|
|
90
|
+
|
|
91
|
+
// Create a new schema that references all definitions to force their generation
|
|
92
|
+
const schemaWithExports = {
|
|
93
|
+
...schema,
|
|
94
|
+
definitions: schema.definitions,
|
|
95
|
+
// Export each definition by creating a oneOf at the root
|
|
96
|
+
oneOf: Object.keys(schema.definitions || {})
|
|
97
|
+
.filter((key) => key.startsWith("Proteus"))
|
|
98
|
+
.map((key) => ({ $ref: `#/definitions/${key}` })),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Generate TypeScript types
|
|
102
|
+
const ts = await compile(schemaWithExports, "ProteusDocumentSchema", {
|
|
103
|
+
bannerComment: `/**
|
|
104
|
+
* Generated by json-schema-to-typescript
|
|
105
|
+
* DO NOT MODIFY - This file is auto-generated from proteus-document-spec.json
|
|
106
|
+
* Run 'npm run generate:proteus' to regenerate
|
|
107
|
+
*/`,
|
|
108
|
+
declareExternallyReferenced: true,
|
|
109
|
+
strictIndexSignatures: true,
|
|
110
|
+
style: {
|
|
111
|
+
semi: true,
|
|
112
|
+
singleQuote: true,
|
|
113
|
+
},
|
|
114
|
+
unknownAny: true,
|
|
115
|
+
unreachableDefinitions: false,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Post-process to add builder functions
|
|
119
|
+
const finalContent = ts + "\n" + generateBuilderCode(schema);
|
|
120
|
+
|
|
121
|
+
// Write the output file
|
|
122
|
+
fs.writeFileSync(outputFile, finalContent, "utf-8");
|
|
123
|
+
|
|
124
|
+
console.log(`Generated: ${outputFile}`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("Error: Generation failed");
|
|
127
|
+
console.error(error);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main().catch((error) => {
|
|
133
|
+
console.error(error);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
package/scripts/lint.sh
ADDED
package/src/auth.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
interface AuthOptions {
|
|
3
|
+
provider: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
scopeBundle: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Middleware to handle authentication requirements
|
|
3
10
|
* @param req Express request
|
|
@@ -7,46 +14,44 @@
|
|
|
7
14
|
export function authMiddleware(req: any, res: any, next: any) {
|
|
8
15
|
const authHeader = req.headers.authorization;
|
|
9
16
|
if (!authHeader && req.authRequired) {
|
|
10
|
-
return res.status(401).json({ error:
|
|
17
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
11
18
|
}
|
|
12
|
-
|
|
19
|
+
|
|
13
20
|
// The Tools Management Service will provide the appropriate token
|
|
14
21
|
// in the Authorization header
|
|
15
22
|
next();
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
interface AuthOptions {
|
|
19
|
-
provider: string;
|
|
20
|
-
scopeBundle: string;
|
|
21
|
-
required?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
25
|
/**
|
|
25
26
|
* Decorator to indicate that a tool requires authentication
|
|
26
27
|
* @param options Authentication options
|
|
27
28
|
*/
|
|
28
29
|
export function requiresAuth(options: AuthOptions) {
|
|
29
|
-
return function(
|
|
30
|
+
return function (
|
|
31
|
+
target: any,
|
|
32
|
+
propertyKey?: string,
|
|
33
|
+
descriptor?: PropertyDescriptor,
|
|
34
|
+
) {
|
|
30
35
|
const isMethod = propertyKey && descriptor;
|
|
31
|
-
|
|
36
|
+
|
|
32
37
|
if (isMethod && descriptor) {
|
|
33
38
|
const originalMethod = descriptor.value;
|
|
34
|
-
|
|
35
|
-
descriptor.value = function(...args: any[]) {
|
|
39
|
+
|
|
40
|
+
descriptor.value = function (...args: any[]) {
|
|
36
41
|
// Store auth requirements in function metadata
|
|
37
42
|
const fn = originalMethod as any;
|
|
38
43
|
fn.__authRequirements__ = {
|
|
39
44
|
provider: options.provider,
|
|
45
|
+
required: options.required ?? true,
|
|
40
46
|
scopeBundle: options.scopeBundle,
|
|
41
|
-
required: options.required ?? true
|
|
42
47
|
};
|
|
43
|
-
|
|
48
|
+
|
|
44
49
|
return originalMethod.apply(this, args);
|
|
45
50
|
};
|
|
46
|
-
|
|
51
|
+
|
|
47
52
|
return descriptor;
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
|
|
50
55
|
return target;
|
|
51
56
|
};
|
|
52
57
|
}
|
package/src/decorators.ts
CHANGED
|
@@ -1,70 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
|
|
4
|
+
import { AuthRequirement, Parameter, ParameterType } from "./models";
|
|
5
|
+
import { registry } from "./registry";
|
|
4
6
|
|
|
5
7
|
interface ParameterDefinition {
|
|
6
|
-
name: string;
|
|
7
|
-
type: ParameterType;
|
|
8
8
|
description: string;
|
|
9
|
+
name: string;
|
|
9
10
|
required: boolean;
|
|
11
|
+
type: ParameterType;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
interface ToolOptions {
|
|
13
|
-
name: string;
|
|
14
|
-
description: string;
|
|
15
|
-
parameters?: ParameterDefinition[];
|
|
16
15
|
authRequirements?: {
|
|
17
16
|
provider: string;
|
|
18
|
-
scopeBundle: string;
|
|
19
17
|
required?: boolean;
|
|
18
|
+
scopeBundle: string;
|
|
20
19
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* @param type TypeScript type
|
|
26
|
-
*/
|
|
27
|
-
function mapTypeToParameterType(type: any): ParameterType {
|
|
28
|
-
if (type === String || type.name === 'String') {
|
|
29
|
-
return ParameterType.String;
|
|
30
|
-
} else if (type === Number || type.name === 'Number') {
|
|
31
|
-
return ParameterType.Number;
|
|
32
|
-
} else if (type === Boolean || type.name === 'Boolean') {
|
|
33
|
-
return ParameterType.Boolean;
|
|
34
|
-
} else if (type === Array || type.name === 'Array') {
|
|
35
|
-
return ParameterType.List;
|
|
36
|
-
} else if (type === Object || type.name === 'Object') {
|
|
37
|
-
return ParameterType.Dictionary;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Default to string
|
|
41
|
-
return ParameterType.String;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Extract parameters from a TypeScript interface
|
|
46
|
-
* @param paramType Parameter type object
|
|
47
|
-
*/
|
|
48
|
-
function extractParameters(paramType: any): Parameter[] {
|
|
49
|
-
const parameters: Parameter[] = [];
|
|
50
|
-
|
|
51
|
-
// This is very basic and doesn't handle complex types
|
|
52
|
-
// For production use, this would need to be more sophisticated
|
|
53
|
-
for (const key in paramType) {
|
|
54
|
-
if (paramType.hasOwnProperty(key)) {
|
|
55
|
-
const type = typeof paramType[key] === 'undefined' ? String : paramType[key].constructor;
|
|
56
|
-
const required = true; // In a real implementation, we'd detect optional parameters
|
|
57
|
-
|
|
58
|
-
parameters.push(new Parameter(
|
|
59
|
-
key,
|
|
60
|
-
mapTypeToParameterType(type),
|
|
61
|
-
'', // Description - in a real impl we'd use TypeDoc or similar
|
|
62
|
-
required
|
|
63
|
-
));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return parameters;
|
|
20
|
+
description: string;
|
|
21
|
+
name: string;
|
|
22
|
+
parameters?: ParameterDefinition[];
|
|
23
|
+
uiResource?: string;
|
|
68
24
|
}
|
|
69
25
|
|
|
70
26
|
/**
|
|
@@ -75,6 +31,7 @@ function extractParameters(paramType: any): Parameter[] {
|
|
|
75
31
|
* - authRequirements: (Optional) Authentication requirements
|
|
76
32
|
* Format: { provider: "oauth_provider", scopeBundle: "permissions_scope", required: true }
|
|
77
33
|
* Example: { provider: "google", scopeBundle: "calendar", required: true }
|
|
34
|
+
* - uiResource: (Optional) URI of associated UI resource for dynamic rendering (e.g., "ui://my-app/create-form")
|
|
78
35
|
*
|
|
79
36
|
* Note: If your tool requires authentication, define your handler function with two parameters:
|
|
80
37
|
* ```
|
|
@@ -84,24 +41,30 @@ function extractParameters(paramType: any): Parameter[] {
|
|
|
84
41
|
* ```
|
|
85
42
|
*/
|
|
86
43
|
export function tool(options: ToolOptions) {
|
|
87
|
-
return function(
|
|
44
|
+
return function (
|
|
45
|
+
target: any,
|
|
46
|
+
propertyKey?: string,
|
|
47
|
+
descriptor?: PropertyDescriptor,
|
|
48
|
+
) {
|
|
88
49
|
const isMethod = propertyKey && descriptor;
|
|
89
50
|
const handler = isMethod ? descriptor.value : target;
|
|
90
51
|
|
|
91
52
|
// Generate endpoint from name - ensure hyphens instead of underscores
|
|
92
|
-
const endpoint = `/tools/${options.name.replace(/_/g,
|
|
53
|
+
const endpoint = `/tools/${options.name.replace(/_/g, "-")}`;
|
|
93
54
|
|
|
94
55
|
// Convert parameter definitions to Parameter objects
|
|
95
56
|
const parameters: Parameter[] = [];
|
|
96
57
|
if (options.parameters && options.parameters.length > 0) {
|
|
97
58
|
// Use the explicitly provided parameter definitions
|
|
98
59
|
for (const paramDef of options.parameters) {
|
|
99
|
-
parameters.push(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
60
|
+
parameters.push(
|
|
61
|
+
new Parameter(
|
|
62
|
+
paramDef.name,
|
|
63
|
+
paramDef.type,
|
|
64
|
+
paramDef.description,
|
|
65
|
+
paramDef.required,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
105
68
|
}
|
|
106
69
|
}
|
|
107
70
|
|
|
@@ -112,8 +75,8 @@ export function tool(options: ToolOptions) {
|
|
|
112
75
|
new AuthRequirement(
|
|
113
76
|
options.authRequirements.provider,
|
|
114
77
|
options.authRequirements.scopeBundle,
|
|
115
|
-
options.authRequirements.required ?? true
|
|
116
|
-
)
|
|
78
|
+
options.authRequirements.required ?? true,
|
|
79
|
+
),
|
|
117
80
|
];
|
|
118
81
|
}
|
|
119
82
|
|
|
@@ -125,7 +88,9 @@ export function tool(options: ToolOptions) {
|
|
|
125
88
|
handler,
|
|
126
89
|
parameters,
|
|
127
90
|
endpoint,
|
|
128
|
-
authRequirements
|
|
91
|
+
authRequirements,
|
|
92
|
+
false,
|
|
93
|
+
options.uiResource,
|
|
129
94
|
);
|
|
130
95
|
}
|
|
131
96
|
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "reflect-metadata";
|
|
2
2
|
|
|
3
|
-
export {
|
|
4
|
-
export { tool } from
|
|
5
|
-
export
|
|
6
|
-
export
|
|
3
|
+
export { requiresAuth } from "./auth";
|
|
4
|
+
export { tool } from "./decorators";
|
|
5
|
+
export * from "./models";
|
|
6
|
+
export { UI } from "./proteus";
|
|
7
|
+
export type * from "./proteus";
|
|
8
|
+
export { registerResource } from "./registerResource";
|
|
9
|
+
export { registerTool } from "./registerTool";
|
|
10
|
+
export type { RequestHandlerExtra } from "./registerTool";
|
|
11
|
+
export { ToolsService } from "./service";
|