skedyul 0.3.0 → 0.3.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/dist/.build-stamp +1 -1
- package/dist/config/app-config.d.ts +73 -0
- package/dist/config/app-config.js +12 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +33 -0
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +119 -0
- package/dist/config/types/agent.d.ts +29 -0
- package/dist/config/types/agent.js +5 -0
- package/dist/config/types/channel.d.ts +46 -0
- package/dist/config/types/channel.js +2 -0
- package/dist/config/types/compute.d.ts +1 -0
- package/dist/config/types/compute.js +5 -0
- package/dist/config/types/env.d.ts +16 -0
- package/dist/config/types/env.js +5 -0
- package/dist/config/types/index.d.ts +9 -0
- package/dist/config/types/index.js +26 -0
- package/dist/config/types/model.d.ts +62 -0
- package/dist/config/types/model.js +2 -0
- package/dist/config/types/page.d.ts +436 -0
- package/dist/config/types/page.js +5 -0
- package/dist/config/types/resource.d.ts +30 -0
- package/dist/config/types/resource.js +5 -0
- package/dist/config/types/webhook.d.ts +35 -0
- package/dist/config/types/webhook.js +5 -0
- package/dist/config/types/workflow.d.ts +24 -0
- package/dist/config/types/workflow.js +2 -0
- package/dist/config/utils.d.ts +16 -0
- package/dist/config/utils.js +37 -0
- package/dist/config.d.ts +5 -767
- package/dist/config.js +11 -151
- package/dist/schemas.d.ts +43 -43
- package/dist/server/core-api-handler.d.ts +8 -0
- package/dist/server/core-api-handler.js +148 -0
- package/dist/server/dedicated.d.ts +7 -0
- package/dist/server/dedicated.js +610 -0
- package/dist/server/handler-helpers.d.ts +24 -0
- package/dist/server/handler-helpers.js +75 -0
- package/dist/server/index.d.ts +19 -0
- package/dist/server/index.js +196 -0
- package/dist/server/serverless.d.ts +7 -0
- package/dist/server/serverless.js +629 -0
- package/dist/server/startup-logger.d.ts +9 -0
- package/dist/server/startup-logger.js +113 -0
- package/dist/server/tool-handler.d.ts +14 -0
- package/dist/server/tool-handler.js +189 -0
- package/dist/server/types.d.ts +22 -0
- package/dist/server/types.js +2 -0
- package/dist/server/utils/env.d.ts +12 -0
- package/dist/server/utils/env.js +38 -0
- package/dist/server/utils/http.d.ts +30 -0
- package/dist/server/utils/http.js +81 -0
- package/dist/server/utils/index.d.ts +3 -0
- package/dist/server/utils/index.js +24 -0
- package/dist/server/utils/schema.d.ts +22 -0
- package/dist/server/utils/schema.js +102 -0
- package/dist/server.d.ts +7 -11
- package/dist/server.js +39 -2026
- package/dist/types/aws.d.ts +15 -0
- package/dist/types/aws.js +5 -0
- package/dist/types/handlers.d.ts +122 -0
- package/dist/types/handlers.js +2 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.js +16 -0
- package/dist/types/server.d.ts +43 -0
- package/dist/types/server.js +2 -0
- package/dist/types/shared.d.ts +16 -0
- package/dist/types/shared.js +5 -0
- package/dist/types/tool-context.d.ts +64 -0
- package/dist/types/tool-context.js +12 -0
- package/dist/types/tool.d.ts +96 -0
- package/dist/types/tool.js +19 -0
- package/dist/types/webhook.d.ts +116 -0
- package/dist/types/webhook.js +7 -0
- package/dist/types.d.ts +4 -461
- package/dist/types.js +21 -31
- package/package.json +2 -2
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createServerlessInstance = createServerlessInstance;
|
|
4
|
+
const service_1 = require("../core/service");
|
|
5
|
+
const client_1 = require("../core/client");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const core_api_handler_1 = require("./core-api-handler");
|
|
8
|
+
const handler_helpers_1 = require("./handler-helpers");
|
|
9
|
+
const startup_logger_1 = require("./startup-logger");
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
/**
|
|
12
|
+
* Creates a serverless (Lambda-style) server instance
|
|
13
|
+
*/
|
|
14
|
+
function createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
|
|
15
|
+
const headers = (0, utils_1.getDefaultHeaders)(config.cors);
|
|
16
|
+
// Print startup log once on cold start
|
|
17
|
+
let hasLoggedStartup = false;
|
|
18
|
+
return {
|
|
19
|
+
async handler(event) {
|
|
20
|
+
// Log startup info on first invocation (cold start)
|
|
21
|
+
if (!hasLoggedStartup) {
|
|
22
|
+
(0, startup_logger_1.printStartupLog)(config, tools, webhookRegistry);
|
|
23
|
+
hasLoggedStartup = true;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const path = event.path;
|
|
27
|
+
const method = event.httpMethod;
|
|
28
|
+
if (method === 'OPTIONS') {
|
|
29
|
+
return (0, utils_1.createResponse)(200, { message: 'OK' }, headers);
|
|
30
|
+
}
|
|
31
|
+
// Handle webhook requests: /webhooks/{handle}
|
|
32
|
+
if (path.startsWith('/webhooks/') && webhookRegistry) {
|
|
33
|
+
const handle = path.slice('/webhooks/'.length);
|
|
34
|
+
const webhookDef = webhookRegistry[handle];
|
|
35
|
+
if (!webhookDef) {
|
|
36
|
+
return (0, utils_1.createResponse)(404, { error: `Webhook handler '${handle}' not found` }, headers);
|
|
37
|
+
}
|
|
38
|
+
// Check if HTTP method is allowed
|
|
39
|
+
const allowedMethods = webhookDef.methods ?? ['POST'];
|
|
40
|
+
if (!allowedMethods.includes(method)) {
|
|
41
|
+
return (0, utils_1.createResponse)(405, { error: `Method ${method} not allowed` }, headers);
|
|
42
|
+
}
|
|
43
|
+
// Get raw body
|
|
44
|
+
const rawBody = event.body ?? '';
|
|
45
|
+
// Parse body based on content type
|
|
46
|
+
let parsedBody;
|
|
47
|
+
const contentType = event.headers?.['content-type'] ?? event.headers?.['Content-Type'] ?? '';
|
|
48
|
+
if (contentType.includes('application/json')) {
|
|
49
|
+
try {
|
|
50
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
parsedBody = rawBody;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
parsedBody = rawBody;
|
|
58
|
+
}
|
|
59
|
+
// Check if this is an envelope format from the platform
|
|
60
|
+
// Envelope format: { env: {...}, request: {...}, context: {...} }
|
|
61
|
+
const isEnvelope = (typeof parsedBody === 'object' &&
|
|
62
|
+
parsedBody !== null &&
|
|
63
|
+
'env' in parsedBody &&
|
|
64
|
+
'request' in parsedBody &&
|
|
65
|
+
'context' in parsedBody);
|
|
66
|
+
let webhookRequest;
|
|
67
|
+
let webhookContext;
|
|
68
|
+
let requestEnv = {};
|
|
69
|
+
if (isEnvelope) {
|
|
70
|
+
// Platform envelope format - extract env, request, and context
|
|
71
|
+
const envelope = parsedBody;
|
|
72
|
+
requestEnv = envelope.env ?? {};
|
|
73
|
+
// Parse the original request body
|
|
74
|
+
let originalParsedBody = envelope.request.body;
|
|
75
|
+
const originalContentType = envelope.request.headers['content-type'] ?? '';
|
|
76
|
+
if (originalContentType.includes('application/json')) {
|
|
77
|
+
try {
|
|
78
|
+
originalParsedBody = envelope.request.body ? JSON.parse(envelope.request.body) : {};
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Keep as string if JSON parsing fails
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
webhookRequest = {
|
|
85
|
+
method: envelope.request.method,
|
|
86
|
+
url: envelope.request.url,
|
|
87
|
+
path: envelope.request.path,
|
|
88
|
+
headers: envelope.request.headers,
|
|
89
|
+
query: envelope.request.query,
|
|
90
|
+
body: originalParsedBody,
|
|
91
|
+
rawBody: envelope.request.body ? Buffer.from(envelope.request.body, 'utf-8') : undefined,
|
|
92
|
+
};
|
|
93
|
+
const envVars = { ...process.env, ...requestEnv };
|
|
94
|
+
const app = envelope.context.app;
|
|
95
|
+
// Build webhook context based on whether we have installation context
|
|
96
|
+
if (envelope.context.appInstallationId && envelope.context.workplace) {
|
|
97
|
+
// Runtime webhook context
|
|
98
|
+
webhookContext = {
|
|
99
|
+
env: envVars,
|
|
100
|
+
app,
|
|
101
|
+
appInstallationId: envelope.context.appInstallationId,
|
|
102
|
+
workplace: envelope.context.workplace,
|
|
103
|
+
registration: envelope.context.registration ?? {},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Provision webhook context
|
|
108
|
+
webhookContext = {
|
|
109
|
+
env: envVars,
|
|
110
|
+
app,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Direct request format (legacy or direct calls) - requires app info from headers or fail
|
|
116
|
+
const appId = event.headers?.['x-skedyul-app-id'] ?? event.headers?.['X-Skedyul-App-Id'];
|
|
117
|
+
const appVersionId = event.headers?.['x-skedyul-app-version-id'] ?? event.headers?.['X-Skedyul-App-Version-Id'];
|
|
118
|
+
if (!appId || !appVersionId) {
|
|
119
|
+
throw new Error('Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)');
|
|
120
|
+
}
|
|
121
|
+
const forwardedProto = event.headers?.['x-forwarded-proto'] ??
|
|
122
|
+
event.headers?.['X-Forwarded-Proto'];
|
|
123
|
+
const protocol = forwardedProto ?? 'https';
|
|
124
|
+
const host = event.headers?.host ?? event.headers?.Host ?? 'localhost';
|
|
125
|
+
const queryString = event.queryStringParameters
|
|
126
|
+
? '?' + new URLSearchParams(event.queryStringParameters).toString()
|
|
127
|
+
: '';
|
|
128
|
+
const webhookUrl = `${protocol}://${host}${path}${queryString}`;
|
|
129
|
+
webhookRequest = {
|
|
130
|
+
method,
|
|
131
|
+
url: webhookUrl,
|
|
132
|
+
path,
|
|
133
|
+
headers: event.headers,
|
|
134
|
+
query: event.queryStringParameters ?? {},
|
|
135
|
+
body: parsedBody,
|
|
136
|
+
rawBody: rawBody ? Buffer.from(rawBody, 'utf-8') : undefined,
|
|
137
|
+
};
|
|
138
|
+
// Direct calls are provision-level (no installation context)
|
|
139
|
+
webhookContext = {
|
|
140
|
+
env: process.env,
|
|
141
|
+
app: { id: appId, versionId: appVersionId },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Temporarily inject env into process.env for skedyul client to use
|
|
145
|
+
// (same pattern as tool handler)
|
|
146
|
+
const originalEnv = { ...process.env };
|
|
147
|
+
Object.assign(process.env, requestEnv);
|
|
148
|
+
// Build request-scoped config for the skedyul client
|
|
149
|
+
// This uses AsyncLocalStorage to override the global config (same pattern as tools)
|
|
150
|
+
const requestConfig = {
|
|
151
|
+
baseUrl: requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? '',
|
|
152
|
+
apiToken: requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? '',
|
|
153
|
+
};
|
|
154
|
+
// Invoke the handler with request-scoped config
|
|
155
|
+
let webhookResponse;
|
|
156
|
+
try {
|
|
157
|
+
webhookResponse = await (0, client_1.runWithConfig)(requestConfig, async () => {
|
|
158
|
+
return await webhookDef.handler(webhookRequest, webhookContext);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.error(`Webhook handler '${handle}' error:`, err);
|
|
163
|
+
return (0, utils_1.createResponse)(500, { error: 'Webhook handler error' }, headers);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
// Restore original env
|
|
167
|
+
process.env = originalEnv;
|
|
168
|
+
}
|
|
169
|
+
// Build response headers
|
|
170
|
+
const responseHeaders = {
|
|
171
|
+
...headers,
|
|
172
|
+
...webhookResponse.headers,
|
|
173
|
+
};
|
|
174
|
+
const status = webhookResponse.status ?? 200;
|
|
175
|
+
const body = webhookResponse.body;
|
|
176
|
+
return {
|
|
177
|
+
statusCode: status,
|
|
178
|
+
headers: responseHeaders,
|
|
179
|
+
body: body !== undefined
|
|
180
|
+
? (typeof body === 'string' ? body : JSON.stringify(body))
|
|
181
|
+
: '',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (path === '/core' && method === 'POST') {
|
|
185
|
+
let coreBody;
|
|
186
|
+
try {
|
|
187
|
+
coreBody = event.body ? JSON.parse(event.body) : {};
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return (0, utils_1.createResponse)(400, {
|
|
191
|
+
error: {
|
|
192
|
+
code: -32700,
|
|
193
|
+
message: 'Parse error',
|
|
194
|
+
},
|
|
195
|
+
}, headers);
|
|
196
|
+
}
|
|
197
|
+
if (!coreBody?.method) {
|
|
198
|
+
return (0, utils_1.createResponse)(400, {
|
|
199
|
+
error: {
|
|
200
|
+
code: -32602,
|
|
201
|
+
message: 'Missing method',
|
|
202
|
+
},
|
|
203
|
+
}, headers);
|
|
204
|
+
}
|
|
205
|
+
const coreMethod = coreBody.method;
|
|
206
|
+
const result = await (0, core_api_handler_1.handleCoreMethod)(coreMethod, coreBody.params);
|
|
207
|
+
return (0, utils_1.createResponse)(result.status, result.payload, headers);
|
|
208
|
+
}
|
|
209
|
+
if (path === '/core/webhook' && method === 'POST') {
|
|
210
|
+
const rawWebhookBody = event.body ?? '';
|
|
211
|
+
let webhookBody;
|
|
212
|
+
try {
|
|
213
|
+
webhookBody = rawWebhookBody ? JSON.parse(rawWebhookBody) : {};
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return (0, utils_1.createResponse)(400, { status: 'parse-error' }, headers);
|
|
217
|
+
}
|
|
218
|
+
const forwardedProto = event.headers?.['x-forwarded-proto'] ??
|
|
219
|
+
event.headers?.['X-Forwarded-Proto'];
|
|
220
|
+
const protocol = forwardedProto ?? 'https';
|
|
221
|
+
const host = event.headers?.host ?? event.headers?.Host ?? 'localhost';
|
|
222
|
+
const webhookUrl = `${protocol}://${host}${event.path}`;
|
|
223
|
+
const coreWebhookRequest = {
|
|
224
|
+
method,
|
|
225
|
+
headers: (event.headers ?? {}),
|
|
226
|
+
body: webhookBody,
|
|
227
|
+
query: event.queryStringParameters ?? {},
|
|
228
|
+
url: webhookUrl,
|
|
229
|
+
path: event.path,
|
|
230
|
+
rawBody: rawWebhookBody
|
|
231
|
+
? Buffer.from(rawWebhookBody, 'utf-8')
|
|
232
|
+
: undefined,
|
|
233
|
+
};
|
|
234
|
+
const webhookResponse = await service_1.coreApiService.dispatchWebhook(coreWebhookRequest);
|
|
235
|
+
return (0, utils_1.createResponse)(webhookResponse.status, webhookResponse.body ?? {}, headers);
|
|
236
|
+
}
|
|
237
|
+
if (path === '/estimate' && method === 'POST') {
|
|
238
|
+
let estimateBody;
|
|
239
|
+
try {
|
|
240
|
+
estimateBody = event.body ? JSON.parse(event.body) : {};
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return (0, utils_1.createResponse)(400, {
|
|
244
|
+
error: {
|
|
245
|
+
code: -32700,
|
|
246
|
+
message: 'Parse error',
|
|
247
|
+
},
|
|
248
|
+
}, headers);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const toolName = estimateBody.name;
|
|
252
|
+
const toolArgs = estimateBody.inputs ?? {};
|
|
253
|
+
// Find tool by name
|
|
254
|
+
let toolKey = null;
|
|
255
|
+
let tool = null;
|
|
256
|
+
for (const [key, t] of Object.entries(registry)) {
|
|
257
|
+
if (t.name === toolName || key === toolName) {
|
|
258
|
+
toolKey = key;
|
|
259
|
+
tool = t;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!tool || !toolKey) {
|
|
264
|
+
return (0, utils_1.createResponse)(400, {
|
|
265
|
+
error: {
|
|
266
|
+
code: -32602,
|
|
267
|
+
message: `Tool "${toolName}" not found`,
|
|
268
|
+
},
|
|
269
|
+
}, headers);
|
|
270
|
+
}
|
|
271
|
+
const inputSchema = (0, utils_1.getZodSchema)(tool.inputSchema);
|
|
272
|
+
// Validate arguments against Zod schema
|
|
273
|
+
const validatedArgs = inputSchema ? inputSchema.parse(toolArgs) : toolArgs;
|
|
274
|
+
const estimateResponse = await callTool(toolKey, {
|
|
275
|
+
inputs: validatedArgs,
|
|
276
|
+
estimate: true,
|
|
277
|
+
});
|
|
278
|
+
return (0, utils_1.createResponse)(200, {
|
|
279
|
+
billing: estimateResponse.billing ?? { credits: 0 },
|
|
280
|
+
}, headers);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
return (0, utils_1.createResponse)(500, {
|
|
284
|
+
error: {
|
|
285
|
+
code: -32603,
|
|
286
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
287
|
+
},
|
|
288
|
+
}, headers);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Handle /install endpoint for install handlers
|
|
292
|
+
if (path === '/install' && method === 'POST') {
|
|
293
|
+
if (!config.hooks?.install) {
|
|
294
|
+
return (0, utils_1.createResponse)(404, { error: 'Install handler not configured' }, headers);
|
|
295
|
+
}
|
|
296
|
+
let installBody;
|
|
297
|
+
try {
|
|
298
|
+
installBody = event.body ? JSON.parse(event.body) : {};
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return (0, utils_1.createResponse)(400, { error: { code: -32700, message: 'Parse error' } }, headers);
|
|
302
|
+
}
|
|
303
|
+
if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
|
|
304
|
+
return (0, utils_1.createResponse)(400, { error: { code: -32602, message: 'Missing context (appInstallationId and workplace required)' } }, headers);
|
|
305
|
+
}
|
|
306
|
+
const installContext = {
|
|
307
|
+
env: installBody.env ?? {},
|
|
308
|
+
workplace: installBody.context.workplace,
|
|
309
|
+
appInstallationId: installBody.context.appInstallationId,
|
|
310
|
+
app: installBody.context.app,
|
|
311
|
+
};
|
|
312
|
+
// Build request-scoped config for SDK access
|
|
313
|
+
// Use env from request body (contains generated token from workflow)
|
|
314
|
+
const installRequestConfig = {
|
|
315
|
+
baseUrl: installBody.env?.SKEDYUL_API_URL ??
|
|
316
|
+
process.env.SKEDYUL_API_URL ??
|
|
317
|
+
'',
|
|
318
|
+
apiToken: installBody.env?.SKEDYUL_API_TOKEN ??
|
|
319
|
+
process.env.SKEDYUL_API_TOKEN ??
|
|
320
|
+
'',
|
|
321
|
+
};
|
|
322
|
+
try {
|
|
323
|
+
const installHook = config.hooks.install;
|
|
324
|
+
const installHandler = typeof installHook === 'function'
|
|
325
|
+
? installHook
|
|
326
|
+
: installHook.handler;
|
|
327
|
+
const result = await (0, client_1.runWithConfig)(installRequestConfig, async () => {
|
|
328
|
+
return await installHandler(installContext);
|
|
329
|
+
});
|
|
330
|
+
return (0, utils_1.createResponse)(200, { env: result.env ?? {}, redirect: result.redirect }, headers);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
// Check for typed install errors
|
|
334
|
+
if (err instanceof errors_1.InstallError) {
|
|
335
|
+
return (0, utils_1.createResponse)(400, {
|
|
336
|
+
error: {
|
|
337
|
+
code: err.code,
|
|
338
|
+
message: err.message,
|
|
339
|
+
field: err.field,
|
|
340
|
+
},
|
|
341
|
+
}, headers);
|
|
342
|
+
}
|
|
343
|
+
return (0, utils_1.createResponse)(500, {
|
|
344
|
+
error: {
|
|
345
|
+
code: -32603,
|
|
346
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
347
|
+
},
|
|
348
|
+
}, headers);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Handle /uninstall endpoint for uninstall handlers
|
|
352
|
+
if (path === '/uninstall' && method === 'POST') {
|
|
353
|
+
if (!config.hooks?.uninstall) {
|
|
354
|
+
return (0, utils_1.createResponse)(404, { error: 'Uninstall handler not configured' }, headers);
|
|
355
|
+
}
|
|
356
|
+
let uninstallBody;
|
|
357
|
+
try {
|
|
358
|
+
uninstallBody = event.body ? JSON.parse(event.body) : {};
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return (0, utils_1.createResponse)(400, { error: { code: -32700, message: 'Parse error' } }, headers);
|
|
362
|
+
}
|
|
363
|
+
if (!uninstallBody.context?.appInstallationId ||
|
|
364
|
+
!uninstallBody.context?.workplace ||
|
|
365
|
+
!uninstallBody.context?.app) {
|
|
366
|
+
return (0, utils_1.createResponse)(400, {
|
|
367
|
+
error: {
|
|
368
|
+
code: -32602,
|
|
369
|
+
message: 'Missing context (appInstallationId, workplace and app required)',
|
|
370
|
+
},
|
|
371
|
+
}, headers);
|
|
372
|
+
}
|
|
373
|
+
const uninstallContext = {
|
|
374
|
+
env: uninstallBody.env ?? {},
|
|
375
|
+
workplace: uninstallBody.context.workplace,
|
|
376
|
+
appInstallationId: uninstallBody.context.appInstallationId,
|
|
377
|
+
app: uninstallBody.context.app,
|
|
378
|
+
};
|
|
379
|
+
const uninstallRequestConfig = {
|
|
380
|
+
baseUrl: uninstallBody.env?.SKEDYUL_API_URL ??
|
|
381
|
+
process.env.SKEDYUL_API_URL ??
|
|
382
|
+
'',
|
|
383
|
+
apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ??
|
|
384
|
+
process.env.SKEDYUL_API_TOKEN ??
|
|
385
|
+
'',
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
const uninstallHook = config.hooks.uninstall;
|
|
389
|
+
const uninstallHandlerFn = typeof uninstallHook === 'function' ? uninstallHook : uninstallHook.handler;
|
|
390
|
+
const result = await (0, client_1.runWithConfig)(uninstallRequestConfig, async () => {
|
|
391
|
+
return await uninstallHandlerFn(uninstallContext);
|
|
392
|
+
});
|
|
393
|
+
return (0, utils_1.createResponse)(200, { cleanedWebhookIds: result.cleanedWebhookIds ?? [] }, headers);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
return (0, utils_1.createResponse)(500, {
|
|
397
|
+
error: {
|
|
398
|
+
code: -32603,
|
|
399
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
400
|
+
},
|
|
401
|
+
}, headers);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Handle /oauth_callback endpoint for OAuth callbacks (called by platform route)
|
|
405
|
+
if (path === '/oauth_callback' && method === 'POST') {
|
|
406
|
+
if (!config.hooks?.oauth_callback) {
|
|
407
|
+
return (0, utils_1.createResponse)(404, { error: 'OAuth callback handler not configured' }, headers);
|
|
408
|
+
}
|
|
409
|
+
let parsedBody;
|
|
410
|
+
try {
|
|
411
|
+
parsedBody = event.body ? JSON.parse(event.body) : {};
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
console.error('[OAuth Callback] Failed to parse JSON body:', err);
|
|
415
|
+
return (0, utils_1.createResponse)(400, { error: { code: -32700, message: 'Parse error' } }, headers);
|
|
416
|
+
}
|
|
417
|
+
// Parse envelope using shared helper
|
|
418
|
+
const envelope = (0, handler_helpers_1.parseHandlerEnvelope)(parsedBody);
|
|
419
|
+
if (!envelope) {
|
|
420
|
+
console.error('[OAuth Callback] Failed to parse envelope. Body:', JSON.stringify(parsedBody, null, 2));
|
|
421
|
+
return (0, utils_1.createResponse)(400, { error: { code: -32602, message: 'Missing envelope format: expected { env, request }' } }, headers);
|
|
422
|
+
}
|
|
423
|
+
// Convert raw request to rich request using shared helper
|
|
424
|
+
const oauthRequest = (0, handler_helpers_1.buildRequestFromRaw)(envelope.request);
|
|
425
|
+
// Build request-scoped config using shared helper
|
|
426
|
+
const oauthCallbackRequestConfig = (0, handler_helpers_1.buildRequestScopedConfig)(envelope.env);
|
|
427
|
+
const oauthCallbackContext = {
|
|
428
|
+
request: oauthRequest,
|
|
429
|
+
};
|
|
430
|
+
try {
|
|
431
|
+
const oauthCallbackHook = config.hooks.oauth_callback;
|
|
432
|
+
const oauthCallbackHandler = typeof oauthCallbackHook === 'function'
|
|
433
|
+
? oauthCallbackHook
|
|
434
|
+
: oauthCallbackHook.handler;
|
|
435
|
+
const result = await (0, client_1.runWithConfig)(oauthCallbackRequestConfig, async () => {
|
|
436
|
+
return await oauthCallbackHandler(oauthCallbackContext);
|
|
437
|
+
});
|
|
438
|
+
return (0, utils_1.createResponse)(200, {
|
|
439
|
+
appInstallationId: result.appInstallationId,
|
|
440
|
+
env: result.env ?? {},
|
|
441
|
+
}, headers);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
const errorMessage = err instanceof Error ? err.message : String(err ?? 'Unknown error');
|
|
445
|
+
return (0, utils_1.createResponse)(500, {
|
|
446
|
+
error: {
|
|
447
|
+
code: -32603,
|
|
448
|
+
message: errorMessage,
|
|
449
|
+
},
|
|
450
|
+
}, headers);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (path === '/health' && method === 'GET') {
|
|
454
|
+
return (0, utils_1.createResponse)(200, state.getHealthStatus(), headers);
|
|
455
|
+
}
|
|
456
|
+
if (path === '/mcp' && method === 'POST') {
|
|
457
|
+
let body;
|
|
458
|
+
try {
|
|
459
|
+
body = event.body ? JSON.parse(event.body) : {};
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return (0, utils_1.createResponse)(400, {
|
|
463
|
+
jsonrpc: '2.0',
|
|
464
|
+
id: null,
|
|
465
|
+
error: {
|
|
466
|
+
code: -32700,
|
|
467
|
+
message: 'Parse error',
|
|
468
|
+
},
|
|
469
|
+
}, headers);
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const { jsonrpc, id, method: rpcMethod, params } = body;
|
|
473
|
+
if (jsonrpc !== '2.0') {
|
|
474
|
+
return (0, utils_1.createResponse)(400, {
|
|
475
|
+
jsonrpc: '2.0',
|
|
476
|
+
id,
|
|
477
|
+
error: {
|
|
478
|
+
code: -32600,
|
|
479
|
+
message: 'Invalid Request',
|
|
480
|
+
},
|
|
481
|
+
}, headers);
|
|
482
|
+
}
|
|
483
|
+
let result;
|
|
484
|
+
if (rpcMethod === 'tools/list') {
|
|
485
|
+
result = { tools };
|
|
486
|
+
}
|
|
487
|
+
else if (rpcMethod === 'tools/call') {
|
|
488
|
+
const toolName = params?.name;
|
|
489
|
+
// Support both formats:
|
|
490
|
+
// 1. Skedyul format: { inputs: {...}, context: {...}, env: {...} }
|
|
491
|
+
// 2. Standard MCP format: { ...directArgs }
|
|
492
|
+
const rawArgs = (params?.arguments ?? {});
|
|
493
|
+
const hasSkedyulFormat = 'inputs' in rawArgs || 'env' in rawArgs || 'context' in rawArgs;
|
|
494
|
+
const toolInputs = hasSkedyulFormat ? (rawArgs.inputs ?? {}) : rawArgs;
|
|
495
|
+
const toolContext = hasSkedyulFormat ? rawArgs.context : undefined;
|
|
496
|
+
const toolEnv = hasSkedyulFormat ? rawArgs.env : undefined;
|
|
497
|
+
// Find tool by name (check both registry key and tool.name)
|
|
498
|
+
let toolKey = null;
|
|
499
|
+
let tool = null;
|
|
500
|
+
for (const [key, t] of Object.entries(registry)) {
|
|
501
|
+
if (t.name === toolName || key === toolName) {
|
|
502
|
+
toolKey = key;
|
|
503
|
+
tool = t;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (!tool || !toolKey) {
|
|
508
|
+
return (0, utils_1.createResponse)(200, {
|
|
509
|
+
jsonrpc: '2.0',
|
|
510
|
+
id,
|
|
511
|
+
error: {
|
|
512
|
+
code: -32602,
|
|
513
|
+
message: `Tool "${toolName}" not found`,
|
|
514
|
+
},
|
|
515
|
+
}, headers);
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const inputSchema = (0, utils_1.getZodSchema)(tool.inputSchema);
|
|
519
|
+
const outputSchema = (0, utils_1.getZodSchema)(tool.outputSchema);
|
|
520
|
+
const hasOutputSchema = Boolean(outputSchema);
|
|
521
|
+
const validatedInputs = inputSchema
|
|
522
|
+
? inputSchema.parse(toolInputs)
|
|
523
|
+
: toolInputs;
|
|
524
|
+
const toolResult = await callTool(toolKey, {
|
|
525
|
+
inputs: validatedInputs,
|
|
526
|
+
context: toolContext,
|
|
527
|
+
env: toolEnv,
|
|
528
|
+
});
|
|
529
|
+
// Transform internal format to MCP protocol format
|
|
530
|
+
// Note: effect is embedded in structuredContent as __effect
|
|
531
|
+
// for consistency with dedicated mode (MCP SDK strips custom fields)
|
|
532
|
+
if (toolResult.error) {
|
|
533
|
+
const errorOutput = { error: toolResult.error };
|
|
534
|
+
result = {
|
|
535
|
+
content: [{ type: 'text', text: JSON.stringify(errorOutput) }],
|
|
536
|
+
structuredContent: errorOutput,
|
|
537
|
+
isError: true,
|
|
538
|
+
billing: toolResult.billing,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
const outputData = toolResult.output;
|
|
543
|
+
const structuredContent = outputData
|
|
544
|
+
? { ...outputData, __effect: toolResult.effect }
|
|
545
|
+
: toolResult.effect
|
|
546
|
+
? { __effect: toolResult.effect }
|
|
547
|
+
: undefined;
|
|
548
|
+
result = {
|
|
549
|
+
content: [{ type: 'text', text: JSON.stringify(toolResult.output) }],
|
|
550
|
+
structuredContent,
|
|
551
|
+
billing: toolResult.billing,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
catch (validationError) {
|
|
556
|
+
return (0, utils_1.createResponse)(200, {
|
|
557
|
+
jsonrpc: '2.0',
|
|
558
|
+
id,
|
|
559
|
+
error: {
|
|
560
|
+
code: -32602,
|
|
561
|
+
message: validationError instanceof Error
|
|
562
|
+
? validationError.message
|
|
563
|
+
: 'Invalid arguments',
|
|
564
|
+
},
|
|
565
|
+
}, headers);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
else if (rpcMethod === 'webhooks/list') {
|
|
569
|
+
// Return registered webhooks with their metadata
|
|
570
|
+
const webhooks = webhookRegistry
|
|
571
|
+
? Object.values(webhookRegistry).map((w) => ({
|
|
572
|
+
name: w.name,
|
|
573
|
+
description: w.description,
|
|
574
|
+
methods: w.methods ?? ['POST'],
|
|
575
|
+
type: w.type ?? 'WEBHOOK',
|
|
576
|
+
}))
|
|
577
|
+
: [];
|
|
578
|
+
result = { webhooks };
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
return (0, utils_1.createResponse)(200, {
|
|
582
|
+
jsonrpc: '2.0',
|
|
583
|
+
id,
|
|
584
|
+
error: {
|
|
585
|
+
code: -32601,
|
|
586
|
+
message: `Method not found: ${rpcMethod}`,
|
|
587
|
+
},
|
|
588
|
+
}, headers);
|
|
589
|
+
}
|
|
590
|
+
return (0, utils_1.createResponse)(200, {
|
|
591
|
+
jsonrpc: '2.0',
|
|
592
|
+
id,
|
|
593
|
+
result,
|
|
594
|
+
}, headers);
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
return (0, utils_1.createResponse)(500, {
|
|
598
|
+
jsonrpc: '2.0',
|
|
599
|
+
id: body?.id ?? null,
|
|
600
|
+
error: {
|
|
601
|
+
code: -32603,
|
|
602
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
603
|
+
},
|
|
604
|
+
}, headers);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return (0, utils_1.createResponse)(404, {
|
|
608
|
+
jsonrpc: '2.0',
|
|
609
|
+
id: null,
|
|
610
|
+
error: {
|
|
611
|
+
code: -32601,
|
|
612
|
+
message: 'Not Found',
|
|
613
|
+
},
|
|
614
|
+
}, headers);
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
return (0, utils_1.createResponse)(500, {
|
|
618
|
+
jsonrpc: '2.0',
|
|
619
|
+
id: null,
|
|
620
|
+
error: {
|
|
621
|
+
code: -32603,
|
|
622
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
623
|
+
},
|
|
624
|
+
}, headers);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
getHealthStatus: () => state.getHealthStatus(),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SkedyulServerConfig, ToolMetadata, WebhookRegistry } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Pad a string to the right with spaces
|
|
4
|
+
*/
|
|
5
|
+
export declare function padEnd(str: string, length: number): string;
|
|
6
|
+
/**
|
|
7
|
+
* Prints a styled startup log showing server configuration
|
|
8
|
+
*/
|
|
9
|
+
export declare function printStartupLog(config: SkedyulServerConfig, tools: ToolMetadata[], webhookRegistry?: WebhookRegistry, port?: number): void;
|