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,610 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createDedicatedServerInstance = createDedicatedServerInstance;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
9
|
+
const service_1 = require("../core/service");
|
|
10
|
+
const client_1 = require("../core/client");
|
|
11
|
+
const errors_1 = require("../errors");
|
|
12
|
+
const core_api_handler_1 = require("./core-api-handler");
|
|
13
|
+
const handler_helpers_1 = require("./handler-helpers");
|
|
14
|
+
const startup_logger_1 = require("./startup-logger");
|
|
15
|
+
const utils_1 = require("./utils");
|
|
16
|
+
/**
|
|
17
|
+
* Creates a dedicated (long-running HTTP) server instance
|
|
18
|
+
*/
|
|
19
|
+
function createDedicatedServerInstance(config, tools, callTool, state, mcpServer, webhookRegistry) {
|
|
20
|
+
const port = (0, utils_1.getListeningPort)(config);
|
|
21
|
+
const httpServer = http_1.default.createServer(async (req, res) => {
|
|
22
|
+
function sendCoreResult(result) {
|
|
23
|
+
(0, utils_1.sendJSON)(res, result.status, result.payload);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
27
|
+
const pathname = url.pathname;
|
|
28
|
+
if (pathname === '/health' && req.method === 'GET') {
|
|
29
|
+
(0, utils_1.sendJSON)(res, 200, state.getHealthStatus());
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Handle webhook requests: /webhooks/{handle}
|
|
33
|
+
if (pathname.startsWith('/webhooks/') && webhookRegistry) {
|
|
34
|
+
const handle = pathname.slice('/webhooks/'.length);
|
|
35
|
+
const webhookDef = webhookRegistry[handle];
|
|
36
|
+
if (!webhookDef) {
|
|
37
|
+
(0, utils_1.sendJSON)(res, 404, { error: `Webhook handler '${handle}' not found` });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Check if HTTP method is allowed
|
|
41
|
+
const allowedMethods = webhookDef.methods ?? ['POST'];
|
|
42
|
+
if (!allowedMethods.includes(req.method)) {
|
|
43
|
+
(0, utils_1.sendJSON)(res, 405, { error: `Method ${req.method} not allowed` });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Read raw request body
|
|
47
|
+
let rawBody;
|
|
48
|
+
try {
|
|
49
|
+
rawBody = await (0, utils_1.readRawRequestBody)(req);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
(0, utils_1.sendJSON)(res, 400, { error: 'Failed to read request body' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Parse body based on content type
|
|
56
|
+
let parsedBody;
|
|
57
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
58
|
+
if (contentType.includes('application/json')) {
|
|
59
|
+
try {
|
|
60
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
parsedBody = rawBody;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
parsedBody = rawBody;
|
|
68
|
+
}
|
|
69
|
+
// Check if this is an envelope format from the platform
|
|
70
|
+
// Envelope format: { env: {...}, request: {...}, context: {...} }
|
|
71
|
+
const envelope = (0, handler_helpers_1.parseHandlerEnvelope)(parsedBody);
|
|
72
|
+
let webhookRequest;
|
|
73
|
+
let webhookContext;
|
|
74
|
+
let requestEnv = {};
|
|
75
|
+
if (envelope && 'context' in envelope && envelope.context) {
|
|
76
|
+
// Platform envelope format - use shared helpers
|
|
77
|
+
const context = envelope.context;
|
|
78
|
+
requestEnv = envelope.env;
|
|
79
|
+
// Convert raw request to rich request using shared helper
|
|
80
|
+
webhookRequest = (0, handler_helpers_1.buildRequestFromRaw)(envelope.request);
|
|
81
|
+
const envVars = { ...process.env, ...envelope.env };
|
|
82
|
+
const app = context.app;
|
|
83
|
+
// Build webhook context based on whether we have installation context
|
|
84
|
+
if (context.appInstallationId && context.workplace) {
|
|
85
|
+
// Runtime webhook context
|
|
86
|
+
webhookContext = {
|
|
87
|
+
env: envVars,
|
|
88
|
+
app,
|
|
89
|
+
appInstallationId: context.appInstallationId,
|
|
90
|
+
workplace: context.workplace,
|
|
91
|
+
registration: context.registration ?? {},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Provision webhook context
|
|
96
|
+
webhookContext = {
|
|
97
|
+
env: envVars,
|
|
98
|
+
app,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Direct request format (legacy or direct calls) - requires app info from headers or fail
|
|
104
|
+
const appId = req.headers['x-skedyul-app-id'];
|
|
105
|
+
const appVersionId = req.headers['x-skedyul-app-version-id'];
|
|
106
|
+
if (!appId || !appVersionId) {
|
|
107
|
+
throw new Error('Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)');
|
|
108
|
+
}
|
|
109
|
+
webhookRequest = {
|
|
110
|
+
method: req.method ?? 'POST',
|
|
111
|
+
url: url.toString(),
|
|
112
|
+
path: pathname,
|
|
113
|
+
headers: req.headers,
|
|
114
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
115
|
+
body: parsedBody,
|
|
116
|
+
rawBody: rawBody ? Buffer.from(rawBody, 'utf-8') : undefined,
|
|
117
|
+
};
|
|
118
|
+
// Direct calls are provision-level (no installation context)
|
|
119
|
+
webhookContext = {
|
|
120
|
+
env: process.env,
|
|
121
|
+
app: { id: appId, versionId: appVersionId },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Temporarily inject env into process.env for skedyul client to use
|
|
125
|
+
// (same pattern as tool handler)
|
|
126
|
+
const originalEnv = { ...process.env };
|
|
127
|
+
Object.assign(process.env, requestEnv);
|
|
128
|
+
// Build request-scoped config for the skedyul client
|
|
129
|
+
// This uses AsyncLocalStorage to override the global config (same pattern as tools)
|
|
130
|
+
const requestConfig = (0, handler_helpers_1.buildRequestScopedConfig)(requestEnv);
|
|
131
|
+
// Invoke the handler with request-scoped config
|
|
132
|
+
let webhookResponse;
|
|
133
|
+
try {
|
|
134
|
+
webhookResponse = await (0, client_1.runWithConfig)(requestConfig, async () => {
|
|
135
|
+
return await webhookDef.handler(webhookRequest, webhookContext);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.error(`Webhook handler '${handle}' error:`, err);
|
|
140
|
+
(0, utils_1.sendJSON)(res, 500, { error: 'Webhook handler error' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
// Restore original env
|
|
145
|
+
process.env = originalEnv;
|
|
146
|
+
}
|
|
147
|
+
// Send response
|
|
148
|
+
const status = webhookResponse.status ?? 200;
|
|
149
|
+
const responseHeaders = {
|
|
150
|
+
...webhookResponse.headers,
|
|
151
|
+
};
|
|
152
|
+
// Default to JSON content type if not specified
|
|
153
|
+
if (!responseHeaders['Content-Type'] && !responseHeaders['content-type']) {
|
|
154
|
+
responseHeaders['Content-Type'] = 'application/json';
|
|
155
|
+
}
|
|
156
|
+
res.writeHead(status, responseHeaders);
|
|
157
|
+
if (webhookResponse.body !== undefined) {
|
|
158
|
+
if (typeof webhookResponse.body === 'string') {
|
|
159
|
+
res.end(webhookResponse.body);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
res.end(JSON.stringify(webhookResponse.body));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
res.end();
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (pathname === '/estimate' && req.method === 'POST') {
|
|
171
|
+
let estimateBody;
|
|
172
|
+
try {
|
|
173
|
+
estimateBody = (await (0, utils_1.parseJSONBody)(req));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
177
|
+
error: {
|
|
178
|
+
code: -32700,
|
|
179
|
+
message: 'Parse error',
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const estimateResponse = await callTool(estimateBody.name, {
|
|
186
|
+
inputs: estimateBody.inputs,
|
|
187
|
+
estimate: true,
|
|
188
|
+
});
|
|
189
|
+
(0, utils_1.sendJSON)(res, 200, {
|
|
190
|
+
billing: estimateResponse.billing ?? { credits: 0 },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
(0, utils_1.sendJSON)(res, 500, {
|
|
195
|
+
error: {
|
|
196
|
+
code: -32603,
|
|
197
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Handle /oauth_callback endpoint for OAuth callbacks (called by Temporal workflow)
|
|
204
|
+
if (pathname === '/oauth_callback' && req.method === 'POST') {
|
|
205
|
+
if (!config.hooks?.oauth_callback) {
|
|
206
|
+
(0, utils_1.sendJSON)(res, 404, { error: 'OAuth callback handler not configured' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
let parsedBody;
|
|
210
|
+
try {
|
|
211
|
+
parsedBody = await (0, utils_1.parseJSONBody)(req);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
console.error('[OAuth Callback] Failed to parse JSON body:', err);
|
|
215
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
216
|
+
error: { code: -32700, message: 'Parse error' },
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Parse envelope using shared helper
|
|
221
|
+
const envelope = (0, handler_helpers_1.parseHandlerEnvelope)(parsedBody);
|
|
222
|
+
if (!envelope) {
|
|
223
|
+
console.error('[OAuth Callback] Failed to parse envelope. Body:', JSON.stringify(parsedBody, null, 2));
|
|
224
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
225
|
+
error: { code: -32602, message: 'Missing envelope format: expected { env, request }' },
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Convert raw request to rich request using shared helper
|
|
230
|
+
const oauthRequest = (0, handler_helpers_1.buildRequestFromRaw)(envelope.request);
|
|
231
|
+
// Build request-scoped config using shared helper
|
|
232
|
+
const oauthCallbackRequestConfig = (0, handler_helpers_1.buildRequestScopedConfig)(envelope.env);
|
|
233
|
+
const oauthCallbackContext = {
|
|
234
|
+
request: oauthRequest,
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
const oauthCallbackHook = config.hooks.oauth_callback;
|
|
238
|
+
const oauthCallbackHandler = typeof oauthCallbackHook === 'function'
|
|
239
|
+
? oauthCallbackHook
|
|
240
|
+
: oauthCallbackHook.handler;
|
|
241
|
+
const result = await (0, client_1.runWithConfig)(oauthCallbackRequestConfig, async () => {
|
|
242
|
+
return await oauthCallbackHandler(oauthCallbackContext);
|
|
243
|
+
});
|
|
244
|
+
(0, utils_1.sendJSON)(res, 200, {
|
|
245
|
+
appInstallationId: result.appInstallationId,
|
|
246
|
+
env: result.env ?? {},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const errorMessage = err instanceof Error ? err.message : String(err ?? 'Unknown error');
|
|
251
|
+
(0, utils_1.sendJSON)(res, 500, {
|
|
252
|
+
error: {
|
|
253
|
+
code: -32603,
|
|
254
|
+
message: errorMessage,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Handle /install endpoint for install handlers
|
|
261
|
+
if (pathname === '/install' && req.method === 'POST') {
|
|
262
|
+
if (!config.hooks?.install) {
|
|
263
|
+
(0, utils_1.sendJSON)(res, 404, { error: 'Install handler not configured' });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
let installBody;
|
|
267
|
+
try {
|
|
268
|
+
installBody = (await (0, utils_1.parseJSONBody)(req));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
272
|
+
error: { code: -32700, message: 'Parse error' },
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
|
|
277
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
278
|
+
error: { code: -32602, message: 'Missing context (appInstallationId and workplace required)' },
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const installContext = {
|
|
283
|
+
env: installBody.env ?? {},
|
|
284
|
+
workplace: installBody.context.workplace,
|
|
285
|
+
appInstallationId: installBody.context.appInstallationId,
|
|
286
|
+
app: installBody.context.app,
|
|
287
|
+
};
|
|
288
|
+
// Build request-scoped config for SDK access
|
|
289
|
+
// Use env from request body (contains generated token from workflow)
|
|
290
|
+
const installRequestConfig = {
|
|
291
|
+
baseUrl: installBody.env?.SKEDYUL_API_URL ??
|
|
292
|
+
process.env.SKEDYUL_API_URL ??
|
|
293
|
+
'',
|
|
294
|
+
apiToken: installBody.env?.SKEDYUL_API_TOKEN ??
|
|
295
|
+
process.env.SKEDYUL_API_TOKEN ??
|
|
296
|
+
'',
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
const installHook = config.hooks.install;
|
|
300
|
+
const installHandler = typeof installHook === 'function'
|
|
301
|
+
? installHook
|
|
302
|
+
: installHook.handler;
|
|
303
|
+
const result = await (0, client_1.runWithConfig)(installRequestConfig, async () => {
|
|
304
|
+
return await installHandler(installContext);
|
|
305
|
+
});
|
|
306
|
+
(0, utils_1.sendJSON)(res, 200, {
|
|
307
|
+
env: result.env ?? {},
|
|
308
|
+
redirect: result.redirect,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
// Check for typed install errors
|
|
313
|
+
if (err instanceof errors_1.InstallError) {
|
|
314
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
315
|
+
error: {
|
|
316
|
+
code: err.code,
|
|
317
|
+
message: err.message,
|
|
318
|
+
field: err.field,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
(0, utils_1.sendJSON)(res, 500, {
|
|
324
|
+
error: {
|
|
325
|
+
code: -32603,
|
|
326
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Handle /uninstall endpoint for uninstall handlers
|
|
334
|
+
if (pathname === '/uninstall' && req.method === 'POST') {
|
|
335
|
+
if (!config.hooks?.uninstall) {
|
|
336
|
+
(0, utils_1.sendJSON)(res, 404, { error: 'Uninstall handler not configured' });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
let uninstallBody;
|
|
340
|
+
try {
|
|
341
|
+
uninstallBody = (await (0, utils_1.parseJSONBody)(req));
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
345
|
+
error: { code: -32700, message: 'Parse error' },
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!uninstallBody.context?.appInstallationId ||
|
|
350
|
+
!uninstallBody.context?.workplace ||
|
|
351
|
+
!uninstallBody.context?.app) {
|
|
352
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
353
|
+
error: {
|
|
354
|
+
code: -32602,
|
|
355
|
+
message: 'Missing context (appInstallationId, workplace and app required)',
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const uninstallContext = {
|
|
361
|
+
env: uninstallBody.env ?? {},
|
|
362
|
+
workplace: uninstallBody.context.workplace,
|
|
363
|
+
appInstallationId: uninstallBody.context.appInstallationId,
|
|
364
|
+
app: uninstallBody.context.app,
|
|
365
|
+
};
|
|
366
|
+
const uninstallRequestConfig = {
|
|
367
|
+
baseUrl: uninstallBody.env?.SKEDYUL_API_URL ??
|
|
368
|
+
process.env.SKEDYUL_API_URL ??
|
|
369
|
+
'',
|
|
370
|
+
apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ??
|
|
371
|
+
process.env.SKEDYUL_API_TOKEN ??
|
|
372
|
+
'',
|
|
373
|
+
};
|
|
374
|
+
try {
|
|
375
|
+
const uninstallHook = config.hooks.uninstall;
|
|
376
|
+
const uninstallHandlerFn = typeof uninstallHook === 'function' ? uninstallHook : uninstallHook.handler;
|
|
377
|
+
const result = await (0, client_1.runWithConfig)(uninstallRequestConfig, async () => {
|
|
378
|
+
return await uninstallHandlerFn(uninstallContext);
|
|
379
|
+
});
|
|
380
|
+
(0, utils_1.sendJSON)(res, 200, {
|
|
381
|
+
cleanedWebhookIds: result.cleanedWebhookIds ?? [],
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
(0, utils_1.sendJSON)(res, 500, {
|
|
386
|
+
error: {
|
|
387
|
+
code: -32603,
|
|
388
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Handle /provision endpoint for provision handlers
|
|
395
|
+
if (pathname === '/provision' && req.method === 'POST') {
|
|
396
|
+
if (!config.hooks?.provision) {
|
|
397
|
+
(0, utils_1.sendJSON)(res, 404, { error: 'Provision handler not configured' });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
let provisionBody;
|
|
401
|
+
try {
|
|
402
|
+
provisionBody = (await (0, utils_1.parseJSONBody)(req));
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
406
|
+
error: { code: -32700, message: 'Parse error' },
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!provisionBody.context?.app) {
|
|
411
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
412
|
+
error: { code: -32602, message: 'Missing context (app required)' },
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// SECURITY: Merge process.env (baked-in secrets) with request env (API token).
|
|
417
|
+
// This ensures secrets like MAILGUN_API_KEY come from the container,
|
|
418
|
+
// while runtime values like SKEDYUL_API_TOKEN come from the request.
|
|
419
|
+
const mergedEnv = {};
|
|
420
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
421
|
+
if (value !== undefined) {
|
|
422
|
+
mergedEnv[key] = value;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Request env overrides process.env (e.g., for SKEDYUL_API_TOKEN)
|
|
426
|
+
Object.assign(mergedEnv, provisionBody.env ?? {});
|
|
427
|
+
const provisionContext = {
|
|
428
|
+
env: mergedEnv,
|
|
429
|
+
app: provisionBody.context.app,
|
|
430
|
+
};
|
|
431
|
+
// Build request-scoped config for SDK access
|
|
432
|
+
// Use merged env for consistency
|
|
433
|
+
const provisionRequestConfig = {
|
|
434
|
+
baseUrl: mergedEnv.SKEDYUL_API_URL ?? '',
|
|
435
|
+
apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? '',
|
|
436
|
+
};
|
|
437
|
+
try {
|
|
438
|
+
const provisionHook = config.hooks.provision;
|
|
439
|
+
const provisionHandler = typeof provisionHook === 'function'
|
|
440
|
+
? provisionHook
|
|
441
|
+
: provisionHook.handler;
|
|
442
|
+
const result = await (0, client_1.runWithConfig)(provisionRequestConfig, async () => {
|
|
443
|
+
return await provisionHandler(provisionContext);
|
|
444
|
+
});
|
|
445
|
+
(0, utils_1.sendJSON)(res, 200, result);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
(0, utils_1.sendJSON)(res, 500, {
|
|
449
|
+
error: {
|
|
450
|
+
code: -32603,
|
|
451
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (pathname === '/core' && req.method === 'POST') {
|
|
458
|
+
let coreBody;
|
|
459
|
+
try {
|
|
460
|
+
coreBody = (await (0, utils_1.parseJSONBody)(req));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
464
|
+
error: {
|
|
465
|
+
code: -32700,
|
|
466
|
+
message: 'Parse error',
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!coreBody?.method) {
|
|
472
|
+
(0, utils_1.sendJSON)(res, 400, {
|
|
473
|
+
error: {
|
|
474
|
+
code: -32602,
|
|
475
|
+
message: 'Missing method',
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const method = coreBody.method;
|
|
481
|
+
const result = await (0, core_api_handler_1.handleCoreMethod)(method, coreBody.params);
|
|
482
|
+
sendCoreResult(result);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (pathname === '/core/webhook' && req.method === 'POST') {
|
|
486
|
+
let rawWebhookBody;
|
|
487
|
+
try {
|
|
488
|
+
rawWebhookBody = await (0, utils_1.readRawRequestBody)(req);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
(0, utils_1.sendJSON)(res, 400, { status: 'parse-error' });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
let webhookBody;
|
|
495
|
+
try {
|
|
496
|
+
webhookBody = rawWebhookBody ? JSON.parse(rawWebhookBody) : {};
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
(0, utils_1.sendJSON)(res, 400, { status: 'parse-error' });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const normalizedHeaders = Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [
|
|
503
|
+
key,
|
|
504
|
+
typeof value === 'string' ? value : value?.[0] ?? '',
|
|
505
|
+
]));
|
|
506
|
+
const coreWebhookRequest = {
|
|
507
|
+
method: req.method ?? 'POST',
|
|
508
|
+
headers: normalizedHeaders,
|
|
509
|
+
body: webhookBody,
|
|
510
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
511
|
+
url: url.toString(),
|
|
512
|
+
path: url.pathname,
|
|
513
|
+
rawBody: rawWebhookBody
|
|
514
|
+
? Buffer.from(rawWebhookBody, 'utf-8')
|
|
515
|
+
: undefined,
|
|
516
|
+
};
|
|
517
|
+
const webhookResponse = await service_1.coreApiService.dispatchWebhook(coreWebhookRequest);
|
|
518
|
+
res.writeHead(webhookResponse.status, {
|
|
519
|
+
'Content-Type': 'application/json',
|
|
520
|
+
});
|
|
521
|
+
res.end(JSON.stringify(webhookResponse.body ?? {}));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (pathname === '/mcp' && req.method === 'POST') {
|
|
525
|
+
try {
|
|
526
|
+
const body = await (0, utils_1.parseJSONBody)(req);
|
|
527
|
+
// Handle tools/list directly to include custom metadata (timeout, displayName, outputSchema)
|
|
528
|
+
// The MCP SDK only returns standard fields, so we intercept and return the full metadata
|
|
529
|
+
if (body?.method === 'tools/list') {
|
|
530
|
+
(0, utils_1.sendJSON)(res, 200, {
|
|
531
|
+
jsonrpc: '2.0',
|
|
532
|
+
id: body.id ?? null,
|
|
533
|
+
result: { tools },
|
|
534
|
+
});
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
// Handle webhooks/list before passing to MCP SDK transport
|
|
538
|
+
if (body?.method === 'webhooks/list') {
|
|
539
|
+
const webhooks = webhookRegistry
|
|
540
|
+
? Object.values(webhookRegistry).map((w) => ({
|
|
541
|
+
name: w.name,
|
|
542
|
+
description: w.description,
|
|
543
|
+
methods: w.methods ?? ['POST'],
|
|
544
|
+
type: w.type ?? 'WEBHOOK',
|
|
545
|
+
}))
|
|
546
|
+
: [];
|
|
547
|
+
(0, utils_1.sendJSON)(res, 200, {
|
|
548
|
+
jsonrpc: '2.0',
|
|
549
|
+
id: body.id ?? null,
|
|
550
|
+
result: { webhooks },
|
|
551
|
+
});
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Pass to MCP SDK transport for standard MCP methods (tools/call, etc.)
|
|
555
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
556
|
+
sessionIdGenerator: undefined,
|
|
557
|
+
enableJsonResponse: true,
|
|
558
|
+
});
|
|
559
|
+
res.on('close', () => {
|
|
560
|
+
transport.close();
|
|
561
|
+
});
|
|
562
|
+
await mcpServer.connect(transport);
|
|
563
|
+
await transport.handleRequest(req, res, body);
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
(0, utils_1.sendJSON)(res, 500, {
|
|
567
|
+
jsonrpc: '2.0',
|
|
568
|
+
id: null,
|
|
569
|
+
error: {
|
|
570
|
+
code: -32603,
|
|
571
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
(0, utils_1.sendJSON)(res, 404, {
|
|
578
|
+
jsonrpc: '2.0',
|
|
579
|
+
id: null,
|
|
580
|
+
error: {
|
|
581
|
+
code: -32601,
|
|
582
|
+
message: 'Not Found',
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
(0, utils_1.sendJSON)(res, 500, {
|
|
588
|
+
jsonrpc: '2.0',
|
|
589
|
+
id: null,
|
|
590
|
+
error: {
|
|
591
|
+
code: -32603,
|
|
592
|
+
message: err instanceof Error ? err.message : String(err ?? ''),
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
return {
|
|
598
|
+
async listen(listenPort) {
|
|
599
|
+
const finalPort = listenPort ?? port;
|
|
600
|
+
return new Promise((resolve, reject) => {
|
|
601
|
+
httpServer.listen(finalPort, () => {
|
|
602
|
+
(0, startup_logger_1.printStartupLog)(config, tools, webhookRegistry, finalPort);
|
|
603
|
+
resolve();
|
|
604
|
+
});
|
|
605
|
+
httpServer.once('error', reject);
|
|
606
|
+
});
|
|
607
|
+
},
|
|
608
|
+
getHealthStatus: () => state.getHealthStatus(),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HandlerRawRequest, WebhookRequest } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Parses a handler envelope from the request body.
|
|
4
|
+
* Detects envelope format: { env: {...}, request: {...}, context?: {...} }
|
|
5
|
+
* Returns the extracted env and request, or null if not an envelope.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseHandlerEnvelope(parsedBody: unknown): {
|
|
8
|
+
env: Record<string, string>;
|
|
9
|
+
request: HandlerRawRequest;
|
|
10
|
+
context?: unknown;
|
|
11
|
+
} | null;
|
|
12
|
+
/**
|
|
13
|
+
* Converts a raw HandlerRawRequest (wire format) to a rich WebhookRequest.
|
|
14
|
+
* Parses JSON body if content-type is application/json, creates Buffer rawBody.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildRequestFromRaw(raw: HandlerRawRequest): WebhookRequest;
|
|
17
|
+
/**
|
|
18
|
+
* Builds request-scoped config by merging env from envelope with process.env fallbacks.
|
|
19
|
+
* Used for SKEDYUL_API_TOKEN and SKEDYUL_API_URL overrides.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildRequestScopedConfig(env: Record<string, string>): {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
apiToken: string;
|
|
24
|
+
};
|