@vida-global/apps-tools 1.0.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.
@@ -0,0 +1,99 @@
1
+ const {BaseProvider, BaseProviderConfigurator } = require('../../../provider');
2
+ const {logger} = require('@vida-global/core');
3
+
4
+ class MCPAuthConfigurator extends BaseProviderConfigurator {
5
+ async initialize() {
6
+ logger.info('MCP Auth Configurator initialized');
7
+ }
8
+ }
9
+
10
+ class MCPAuthProvider extends BaseProvider {
11
+ type = 'auth'
12
+ name = 'mcp'
13
+ configurator = MCPAuthConfigurator
14
+
15
+ async initialize() {
16
+ logger.info('MCP Auth Provider initialized');
17
+ this._client = null;
18
+ }
19
+
20
+ async uninstall() {
21
+ logger.info('MCP Auth Provider Uninstalled');
22
+ await this.userContext.deleteProviderConfig(
23
+ this.type, this.name, this.appId, this.appVersion
24
+ )
25
+ this._client = null;
26
+ }
27
+
28
+ async update(config) {
29
+ logger.info('MCP Auth Provider Updated')
30
+ await this.userContext.setProviderConfig(
31
+ this.type, this.name, config,
32
+ this.appId, this.appVersion
33
+ )
34
+ this.config = config;
35
+ this._client = null;
36
+ }
37
+
38
+ async getManifest() {
39
+ return this.config
40
+ }
41
+
42
+ async isConfigured() {
43
+ if (this.config && this.config.parameters) {
44
+ const { properties, required } = this.config.parameters;
45
+ if (required && Array.isArray(required)) {
46
+ for (const field of required) {
47
+ const property = properties[field];
48
+ if (!property || !property.value ||
49
+ (property.type === 'string' && property.value.trim() === '')) {
50
+ return false;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+
58
+ async getClient() {
59
+ // Lazy import to avoid hard dependency unless used
60
+ let MCPClient;
61
+ try {
62
+ MCPClient = await import('mcp-client').then(m => m.MCPClient || m.default);
63
+ } catch (e) {
64
+ console.error(e)
65
+ throw new Error('mcp-client dependency is not installed. Please install mcp-client to use MCP app.');
66
+ }
67
+
68
+ const mcpUrl = this.config?.parameters?.properties?.mcpUrl?.value;
69
+ const authHeader = this.config?.parameters?.properties?.authHeader?.value;
70
+ if (!mcpUrl) {
71
+ throw new Error('MCP URL is not configured in provider auth:mcp');
72
+ }
73
+
74
+ if (this._client && this._client.__connected) {
75
+ return this._client;
76
+ }
77
+
78
+ const client = new MCPClient({ name: 'VADER-MCP', version: '1.0.0' });
79
+ const useSSE = this.config?.parameters?.properties?.useSSE?.value;
80
+ let connectOptions = {};
81
+ if (useSSE) {
82
+ connectOptions = { url: mcpUrl, type: 'sse' };
83
+ } else {
84
+ connectOptions = { url: mcpUrl };
85
+ }
86
+ if (authHeader) {
87
+ connectOptions.headers = { Authorization: authHeader };
88
+ }
89
+ console.log(`Connecting to MCP using ${useSSE ? 'SSE' : 'HTTP'}.`)
90
+ await client.connect(connectOptions);
91
+ console.log('Connected to MCP')
92
+ // mark connected
93
+ client.__connected = true;
94
+ this._client = client;
95
+ return client;
96
+ }
97
+ }
98
+
99
+ module.exports = MCPAuthProvider;
@@ -0,0 +1,257 @@
1
+ const {logger} = require('@vida-global/core');
2
+ const {BaseProvider, BaseProviderConfigurator} = require('../../../provider');
3
+ const axios = require('axios');
4
+ const { URL } = require('url');
5
+ const _ = require('lodash');
6
+
7
+
8
+ const oAuthConfig = {
9
+ scopes: [
10
+ 'Calendars.ReadWrite',
11
+ 'Calendars.Read',
12
+ 'User.Read'
13
+ ],
14
+ initiateOAuthUrl: '/vader/oauth/outlook/initiate',
15
+ callbackPath: '/vader/oauth/outlook/callback',
16
+ authorizationEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
17
+ tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
18
+ }
19
+
20
+
21
+ class OutlookOAuthConfigurator extends BaseProviderConfigurator {
22
+ async storeToken(user, tokenResponse) {
23
+ let tokenInfo = {
24
+ accessToken: tokenResponse.access_token,
25
+ refreshToken: tokenResponse.refresh_token,
26
+ expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
27
+ };
28
+
29
+ await this.redis.set(this.TOKEN_KEY(user), JSON.stringify(tokenInfo));
30
+ }
31
+
32
+ getAuthorizationUrl(redirectUri, state) {
33
+ const params = new URLSearchParams({
34
+ client_id: this.config.clientId,
35
+ response_type: 'code',
36
+ redirect_uri: redirectUri,
37
+ scope: oAuthConfig.scopes.join(' '),
38
+ response_mode: 'query',
39
+ state: state
40
+ });
41
+
42
+ return `${oAuthConfig.authorizationEndpoint}?${params.toString()}`;
43
+ }
44
+
45
+ buildRoutes(app) {
46
+ app.get(oAuthConfig.initiateOAuthUrl, async (req, res) => {
47
+ const {redirectUri, userId} = req.query;
48
+
49
+ if (!redirectUri || !userId) {
50
+ return res.status(400).send('Missing required parameters: redirectUri and userId');
51
+ }
52
+
53
+ // State data
54
+ const stateData = {
55
+ redirectUri,
56
+ userId,
57
+ // timestamp: Date.now()
58
+ };
59
+
60
+ // Configure redirect URI and generate authorization URL
61
+ const authUrl = this.getAuthorizationUrl(redirectUri, Buffer.from(JSON.stringify(stateData)).toString('base64'));
62
+
63
+ res.redirect(authUrl);
64
+ })
65
+
66
+ app.get(oAuthConfig.callbackPath, async (req, res) => {
67
+ let redirectUri = null;
68
+ try {
69
+ redirectUri = await this.handleOAuthCallback(req);
70
+ res.redirect(redirectUri);
71
+ } catch (error) {
72
+ console.error('OAuth callback error:', error);
73
+ res.status(500).send('Authentication failed. Please try again.');
74
+ }
75
+ });
76
+ }
77
+
78
+ async handleOAuthCallback(req) {
79
+ if (req.query.error) {
80
+ throw new Error(`OAuth error: ${req.query.error}`);
81
+ }
82
+
83
+ const {code, state} = req.query;
84
+ if (!code) {
85
+ throw new Error('No authorization code received');
86
+ }
87
+ if (!state) {
88
+ throw new Error('No state parameter received');
89
+ }
90
+
91
+ const sessionData = JSON.parse(Buffer.from(state, 'base64').toString());
92
+ let {redirectUri, userId} = sessionData;
93
+ if (!redirectUri || !userId) {
94
+ throw new Error('Invalid state parameter');
95
+ }
96
+ redirectUri = decodeURIComponent(redirectUri);
97
+ let user = {id: userId};
98
+ let success = true
99
+ try {
100
+ const response = await axios.post(
101
+ oAuthConfig.tokenEndpoint, {
102
+ client_id: this.config.clientId,
103
+ client_secret: this.config.clientSecret,
104
+ code: code,
105
+ redirect_uri: redirectUri,
106
+ grant_type: 'authorization_code'
107
+ }, {
108
+ headers: {
109
+ 'Content-Type': 'application/x-www-form-urlencoded'
110
+ }
111
+ });
112
+ await this.storeToken(user, response.data);
113
+ } catch (error) {
114
+ console.error('Error exchanging code for token:', error);
115
+ success = false;
116
+ }
117
+ const finalUrl = new URL(redirectUri);
118
+ finalUrl.searchParams.append(
119
+ 'success', success ? 'true' : 'false'
120
+ );
121
+ finalUrl.searchParams.append(
122
+ 'userId', user.id
123
+ )
124
+ return finalUrl.toString();
125
+ }
126
+ }
127
+
128
+
129
+ class OutlookAuthProvider extends BaseProvider {
130
+ type = 'auth';
131
+ name = 'oauth2';
132
+ configurator = OutlookOAuthConfigurator;
133
+
134
+ TOKEN_KEY (user) {
135
+ return `vader:oauth:outlook:${user.id}:token`;
136
+ }
137
+
138
+ OAUTH_SESSION_KEY (user) {
139
+ return `vader:oauth:outlook:${user.id}:session`;
140
+ }
141
+
142
+ async initialize() {
143
+ if (!this.isServer && this.user) {
144
+ // Load token from Redis if available
145
+ const tokenData = await this.redis.get(this.TOKEN_KEY(this.user));
146
+ if (tokenData) {
147
+ this.tokenInfo = JSON.parse(tokenData);
148
+ }
149
+ if (this.isTokenExpired()) {
150
+ await this.refreshToken();
151
+ }
152
+ }
153
+ this.config = {
154
+ ...this.config,
155
+ ...oAuthConfig
156
+ }
157
+ logger.info('Outlook Oauth Provider Initialized')
158
+ }
159
+
160
+ async uninstall () {
161
+ await this.redis.del(this.TOKEN_KEY(this.user));
162
+ await this.redis.del(this.OAUTH_SESSION_KEY(this.user));
163
+ }
164
+
165
+ async isConfigured() {
166
+ if (!this.config.clientId || !this.config.clientSecret) {
167
+ return false;
168
+ }
169
+
170
+ if (!this.tokenInfo) {
171
+ return false;
172
+ }
173
+
174
+ // Check if the token is expired and try to refresh
175
+ if (this.isTokenExpired() && !(await this.refreshToken())) {
176
+ logger.warn('Outlook Oauth Provider: Token expired and failed to refresh');
177
+ return false;
178
+ }
179
+
180
+ return true;
181
+ }
182
+
183
+ async getManifest() {
184
+ if (this.isServer) {
185
+ return {
186
+ ...oAuthConfig
187
+ }
188
+ } else {
189
+ return {
190
+ ...oAuthConfig,
191
+ parameters: {
192
+ type: "object",
193
+ properties: {
194
+ accessToken: {
195
+ type: "string",
196
+ description: "Access Token for outlook API.",
197
+ value: await this.getAccessToken(),
198
+ }
199
+ },
200
+ required: ["accessToken"],
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ isTokenExpired() {
207
+ if (!this.tokenInfo || !this.tokenInfo.expiresAt) {
208
+ return true;
209
+ }
210
+ // Add a 5-minute buffer before expiration
211
+ return Date.now() >= (this.tokenInfo.expiresAt - 300000);
212
+ }
213
+
214
+ async getAccessToken() {
215
+ if (this.isTokenExpired()) {
216
+ await this.refreshToken();
217
+ }
218
+ return this.tokenInfo?.accessToken;
219
+ }
220
+
221
+ async refreshToken() {
222
+ if (!this.tokenInfo?.refreshToken) {
223
+ return false;
224
+ }
225
+
226
+ try {
227
+ const response = await axios.post(oAuthConfig.tokenEndpoint, {
228
+ client_id: this.config.clientId,
229
+ client_secret: this.config.clientSecret,
230
+ refresh_token: this.tokenInfo.refreshToken,
231
+ grant_type: 'refresh_token'
232
+ }, {
233
+ headers: {
234
+ 'Content-Type': 'application/x-www-form-urlencoded'
235
+ }
236
+ });
237
+
238
+ await this.storeToken(response.data);
239
+ return true;
240
+ } catch (error) {
241
+ logger.error('Error refreshing token:', error);
242
+ return false;
243
+ }
244
+ }
245
+
246
+ async storeToken(user, tokenResponse) {
247
+ let tokenInfo = {
248
+ accessToken: tokenResponse.access_token,
249
+ refreshToken: tokenResponse.refresh_token,
250
+ expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
251
+ };
252
+
253
+ await this.redis.set(this.TOKEN_KEY(this.user), JSON.stringify(tokenInfo));
254
+ }
255
+ }
256
+
257
+ module.exports = OutlookAuthProvider;
@@ -0,0 +1,111 @@
1
+ const axios = require('axios');
2
+ const {BaseProvider} = require('../../../provider');
3
+
4
+
5
+ class SquareUpProvider extends BaseProvider {
6
+ type = 'auth';
7
+ name = 'square';
8
+
9
+ async getField (fieldName) {
10
+ return await this.readData(fieldName);
11
+ }
12
+
13
+ async isConfigured() {
14
+ const {refreshToken, accessToken} = await this.getCredentials();
15
+ return !!(refreshToken && accessToken);
16
+ }
17
+
18
+ async getClientConfig() {
19
+ const siteUrl = await this.readConfig('siteUrl');
20
+ const clientId = await this.readConfig('clientId');
21
+ const clientSecret = await this.readConfig('clientSecret');
22
+ const redirectUrl = `${siteUrl}/api/v2/providers/${this.type}/${this.name}/receive`;
23
+ return {redirectUrl, clientId, clientSecret};
24
+ }
25
+
26
+ async getAuthUrl(redirectUrl) {
27
+ const {clientId, redirectUrl: defaultRedirectUrl} = await this.getClientConfig();
28
+ const finalRedirectUrl = redirectUrl || defaultRedirectUrl;
29
+ const state = `${this.user.id}|${this.agent.id}|${redirectUrl ? `|${redirectUrl}` : ''}`;
30
+ const scope = (this.appParams.scope || ['APPOINTMENTS_READ', 'CUSTOMERS_READ']).join(' ');
31
+
32
+ // Generate the SquareUp authorization URL
33
+ return `https://connect.squareup.com/oauth2/authorize?client_id=${clientId}&response_type=code&scope=${scope}&redirect_uri=${encodeURIComponent(finalRedirectUrl)}&state=${state}`;
34
+ }
35
+
36
+ async exchangeCodeForToken(code) {
37
+ const {clientId, clientSecret, redirectUrl} = await this.getClientConfig();
38
+
39
+ try {
40
+ const response = await axios.post('https://connect.squareup.com/oauth2/token', {
41
+ client_id: clientId,
42
+ client_secret: clientSecret,
43
+ code: code,
44
+ grant_type: 'authorization_code',
45
+ redirect_uri: redirectUrl,
46
+ });
47
+
48
+ return response.data;
49
+ } catch (error) {
50
+ console.error('SQUAREUP PROVIDER ERROR: Failed to exchange code for token:', error);
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ async setCredentials(code) {
56
+ const tokenData = await this.exchangeCodeForToken(code);
57
+ await this.writeData('squareUpOauthAccessToken', tokenData.access_token);
58
+ await this.writeData('squareUpOauthRefreshToken', tokenData.refresh_token);
59
+ await this.writeData('squareUpOauthExpires', Date.now() + tokenData.expires_in * 1000); // in milliseconds
60
+ return 'success';
61
+ }
62
+
63
+ async getCredentials() {
64
+ const refreshToken = await this.readData('squareUpOauthRefreshToken');
65
+ const accessToken = await this.readData('squareUpOauthAccessToken');
66
+ return {refreshToken, accessToken};
67
+ }
68
+
69
+ async refreshAccessToken() {
70
+ const {clientId, clientSecret} = await this.getClientConfig();
71
+ const refreshToken = await this.readData('squareUpOauthRefreshToken');
72
+
73
+ try {
74
+ const response = await axios.post('https://connect.squareup.com/oauth2/token', {
75
+ client_id: clientId,
76
+ client_secret: clientSecret,
77
+ refresh_token: refreshToken,
78
+ grant_type: 'refresh_token',
79
+ });
80
+
81
+ const tokenData = response.data;
82
+ await this.writeData('squareUpOauthAccessToken', tokenData.access_token);
83
+ await this.writeData('squareUpOauthExpires', Date.now() + tokenData.expires_in * 1000); // in milliseconds
84
+ } catch (error) {
85
+ console.error('SQUAREUP PROVIDER ERROR: Failed to refresh access token:', error);
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ async revokeCredentials() {
91
+ const accessToken = await this.readData('squareUpOauthAccessToken');
92
+
93
+ try {
94
+ if (accessToken) {
95
+ await axios.post('https://connect.squareup.com/oauth2/revoke', {
96
+ client_id: await this.readConfig('clientId'),
97
+ access_token: accessToken,
98
+ });
99
+ await this.deleteData('squareUpOauthAccessToken');
100
+ await this.deleteData('squareUpOauthRefreshToken');
101
+ await this.deleteData('squareUpOauthExpires');
102
+ }
103
+ return 'success';
104
+ } catch (error) {
105
+ console.error('SQUAREUP PROVIDER ERROR: Failed to revoke credentials:', error);
106
+ return 'squareup-error';
107
+ }
108
+ }
109
+ }
110
+
111
+ module.exports = SquareUpProvider;
@@ -0,0 +1,51 @@
1
+ const { logger } = require('@vida-global/core');
2
+
3
+
4
+ class MockProvider {
5
+ constructor(providerType, providerName, {user, agent, caller, appParams}, mockData) {
6
+ this.providerType = providerType
7
+ this.providerName = providerName
8
+ this.user = user
9
+ this.agent = agent
10
+ this.caller = caller
11
+ this.appParams = appParams
12
+ this.mockData = mockData
13
+ }
14
+
15
+ static getObjectProxy(instance)
16
+ {
17
+ return new Proxy(instance, {
18
+ get(target, prop, receiver) {
19
+ if (prop in target) {
20
+ if (typeof target[prop] === 'function') {
21
+ return target[prop].bind(target)
22
+ } else {
23
+ return target[prop]
24
+ }
25
+ } else {
26
+ const method = target.mockData.methods?.find((m) => m.method === prop)
27
+ if (method) {
28
+ return method.value.bind(target)
29
+ }
30
+ return Reflect.get(target, prop, receiver);
31
+ }
32
+ }
33
+ });
34
+ }
35
+
36
+ async isConfigured() {
37
+ return true
38
+ }
39
+
40
+ async getField(name) {
41
+ const field = this.mockData.fields?.find((f) => f.field === name)
42
+ if (!field) {
43
+ // logger.error(`Could not find field ${name} on provider type ${this.providerType} with name ${this.providerName}`)
44
+ throw Error(`MockData: Could not find field ${name} on provider type ${this.providerType} with name ${this.providerName}`)
45
+ }
46
+ return field.value
47
+ }
48
+ }
49
+
50
+ module.exports = MockProvider;
51
+
@@ -0,0 +1,112 @@
1
+ const { VidaServerController } = require('@vida-global/core');
2
+
3
+
4
+ class AppController extends VidaServerController {
5
+
6
+ /***********************************************************************************************
7
+ * FUNCTIONS
8
+ ***********************************************************************************************/
9
+ async postInvokeFunction() {
10
+ try {
11
+ await this.invokeFunction();
12
+ } catch (err) {
13
+ this.logger.error({message: err.message, stack: err.stack}, 'Unhandled Exception')
14
+ this.statusCode = err.status || 500;
15
+ await this.render({
16
+ error: {message: err.message, status: err.status || 500}
17
+ })
18
+ }
19
+ }
20
+
21
+
22
+ async invokeFunction() {
23
+ await this.#invokeAppAction('Function');
24
+ }
25
+
26
+
27
+ /***********************************************************************************************
28
+ * HOOKS
29
+ ***********************************************************************************************/
30
+ async postInvokeHook() {
31
+ try {
32
+ await this.invokeHook();
33
+ } catch (error) {
34
+ logger.info(error)
35
+ return res.status(500).json({error})
36
+ }
37
+ }
38
+
39
+
40
+ async invokeHook() {
41
+ await this.#invokeAppAction('Hook');
42
+ }
43
+
44
+
45
+ /***********************************************************************************************
46
+ * CALL HELPER
47
+ ***********************************************************************************************/
48
+ async postInvokeAppManager() {
49
+ try {
50
+ await this.invokeAppManager();
51
+ } catch (error) {
52
+ logger.info(error)
53
+ return res.status(500).json({error})
54
+ }
55
+ }
56
+
57
+
58
+ async invokeAppManager() {
59
+ const { functionArgs,
60
+ functionName,
61
+ userContext } = this.params
62
+ const context = this.appManager.context(userContext);
63
+ let resp = await context[functionName](...functionArgs)
64
+ if (resp.serialize && typeof resp.serialize === 'function') {
65
+ resp = await resp.serialize()
66
+ }
67
+ await this.render(resp);
68
+ }
69
+
70
+
71
+ /***********************************************************************************************
72
+ * CALL HELPER
73
+ ***********************************************************************************************/
74
+ async getOpenApi() {
75
+ const specs = await this.appManager.generateOpenApiSpec();
76
+ await this.render(specs);
77
+ }
78
+
79
+
80
+ /***********************************************************************************************
81
+ * MISC
82
+ ***********************************************************************************************/
83
+ static get routes() {
84
+ return {
85
+ postInvokeAppManager: '/invoke/apps/manager/:functionName',
86
+ postInvokeFunction: '/invoke/app/function/:appId/:appVersion/:functionName',
87
+ postInvokeHook: '/invoke/app/hook/:appId/:appVersion/:hookName',
88
+ getOpenApi: '/openapi.json'
89
+ }
90
+ }
91
+
92
+
93
+ async #invokeAppAction(actionType) {
94
+ const { appId,
95
+ appVersion,
96
+ userContext,
97
+ appManifest } = this.params;
98
+ const actionName = this.params[`${actionType.toLowerCase()}Name`];
99
+ const actionArgs = this.params[`${actionType.toLowerCase()}Args`];
100
+ const context = this.appManager.context(userContext);
101
+ const actionHandler = `handle${actionType}Call`;
102
+ const resp = await context[actionHandler](appId,
103
+ appVersion,
104
+ appManifest,
105
+ actionName,
106
+ actionArgs);
107
+ await this.render(resp);
108
+ }
109
+ }
110
+
111
+
112
+ module.exports = { AppController };
@@ -0,0 +1,73 @@
1
+ const { AppServerAppManager } = require('../apps/appManager/appServerAppManager');
2
+ const swaggerUi = require("swagger-ui-express");
3
+ const { VidaServer, logger } = require('@vida-global/core');
4
+
5
+
6
+ class AppServer extends VidaServer{
7
+ #appManager;
8
+ #apps;
9
+
10
+ constructor({ apps, useMocks=true }) {
11
+ super(...arguments);
12
+ this.#appManager = new AppServerAppManager({ apps, useMocks });
13
+ }
14
+
15
+
16
+ buildController(controllerCls, request, response) {
17
+ const controller = super.buildController(...arguments);
18
+ controller.appManager = this.#appManager;
19
+ return controller;
20
+ }
21
+
22
+
23
+ setupMiddleware() {
24
+ super.setupMiddleware();
25
+ this.initializeSwaggerDocs();
26
+ this.use(this.contentLengthMiddleware);
27
+ }
28
+
29
+
30
+ initializeSwaggerDocs() {
31
+ const swaggerSpecUrl = "/openapi.json"
32
+ const options = {
33
+ swaggerOptions: {
34
+ url: swaggerSpecUrl
35
+ }
36
+ }
37
+ this.use("/api-docs", swaggerUi.serve, swaggerUi.setup(null, options))
38
+ this.logger.info(`API Docs available at: http://${this.host}:${this.port}/api-docs`)
39
+ }
40
+
41
+
42
+ contentLengthMiddleware(err, req, res, next) {
43
+ if (err.status === 413) {
44
+ logger.error({
45
+ error: err,
46
+ route: req.originalUrl,
47
+ method: req.method,
48
+ contentLength: req.headers['content-length'],
49
+ payload: req.body
50
+ }, 'Request entity too large');
51
+
52
+ return res.status(413).json({
53
+ status: 'error',
54
+ message: 'Request entity too large',
55
+ details: {
56
+ maxSize: '50mb',
57
+ receivedSize: req.headers['content-length']
58
+ }
59
+ });
60
+ }
61
+ next();
62
+ }
63
+
64
+
65
+ get controllerDirectories() {
66
+ return [__dirname];
67
+ }
68
+ }
69
+
70
+
71
+ module.exports = {
72
+ AppServer
73
+ }