skedyul 1.2.19 → 1.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli/index.js +512 -731
  2. package/dist/config/app-config.d.ts +26 -2
  3. package/dist/config/index.d.ts +1 -2
  4. package/dist/config/types/env.d.ts +8 -2
  5. package/dist/config/types/form.d.ts +10 -6
  6. package/dist/config/types/index.d.ts +0 -1
  7. package/dist/config/types/page.d.ts +2 -2
  8. package/dist/config/types/webhook.d.ts +2 -1
  9. package/dist/dedicated/server.js +503 -766
  10. package/dist/esm/index.mjs +503 -720
  11. package/dist/index.d.ts +2 -3
  12. package/dist/index.js +503 -722
  13. package/dist/server/config-serializer.d.ts +12 -0
  14. package/dist/server/dedicated.d.ts +3 -2
  15. package/dist/server/handlers/index.d.ts +12 -0
  16. package/dist/server/handlers/install-handler.d.ts +9 -0
  17. package/dist/server/handlers/oauth-callback-handler.d.ts +9 -0
  18. package/dist/server/handlers/provision-handler.d.ts +9 -0
  19. package/dist/server/handlers/types.d.ts +101 -0
  20. package/dist/server/handlers/uninstall-handler.d.ts +9 -0
  21. package/dist/server/handlers/webhook-handler.d.ts +28 -0
  22. package/dist/server/index.d.ts +15 -6
  23. package/dist/server/serverless.d.ts +3 -2
  24. package/dist/server/startup-logger.d.ts +3 -2
  25. package/dist/server/tool-handler.d.ts +1 -1
  26. package/dist/server/utils/http.d.ts +3 -2
  27. package/dist/server.d.ts +1 -1
  28. package/dist/server.js +503 -766
  29. package/dist/serverless/server.mjs +503 -744
  30. package/dist/types/handlers.d.ts +6 -24
  31. package/dist/types/index.d.ts +4 -4
  32. package/dist/types/server.d.ts +1 -34
  33. package/dist/types/shared.d.ts +5 -0
  34. package/dist/types/tool.d.ts +5 -7
  35. package/dist/types/webhook.d.ts +14 -6
  36. package/package.json +1 -1
  37. package/dist/config/resolve.d.ts +0 -27
  38. package/dist/config/types/compute.d.ts +0 -9
@@ -2078,8 +2078,8 @@ function createRequestState(maxRequests, ttlExtendSeconds, runtimeLabel, toolNam
2078
2078
  };
2079
2079
  }
2080
2080
  function createCallToolHandler(registry, state, onMaxRequests) {
2081
- return async function callTool(nameRaw, argsRaw) {
2082
- const toolName = String(nameRaw);
2081
+ return async function callTool(toolNameInput, toolArgsInput) {
2082
+ const toolName = String(toolNameInput);
2083
2083
  const tool = registry[toolName];
2084
2084
  if (!tool) {
2085
2085
  throw new Error(`Tool "${toolName}" not found in registry`);
@@ -2088,7 +2088,7 @@ function createCallToolHandler(registry, state, onMaxRequests) {
2088
2088
  throw new Error(`Tool "${toolName}" handler is not a function`);
2089
2089
  }
2090
2090
  const fn = tool.handler;
2091
- const args = argsRaw ?? {};
2091
+ const args = toolArgsInput ?? {};
2092
2092
  const estimateMode = args.estimate === true;
2093
2093
  if (!estimateMode) {
2094
2094
  state.incrementRequestCount();
@@ -2343,51 +2343,6 @@ async function handleCoreMethod(method, params) {
2343
2343
  };
2344
2344
  }
2345
2345
 
2346
- // src/server/handler-helpers.ts
2347
- function parseHandlerEnvelope(parsedBody) {
2348
- if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody) || !("env" in parsedBody) || !("request" in parsedBody)) {
2349
- return null;
2350
- }
2351
- const envelope = parsedBody;
2352
- if (typeof envelope.env !== "object" || envelope.env === null || Array.isArray(envelope.env)) {
2353
- return null;
2354
- }
2355
- if (typeof envelope.request !== "object" || envelope.request === null || Array.isArray(envelope.request)) {
2356
- return null;
2357
- }
2358
- return {
2359
- env: envelope.env,
2360
- request: envelope.request,
2361
- context: envelope.context
2362
- };
2363
- }
2364
- function buildRequestFromRaw(raw) {
2365
- let parsedBody = raw.body;
2366
- const contentType = raw.headers["content-type"] ?? "";
2367
- if (contentType.includes("application/json")) {
2368
- try {
2369
- parsedBody = raw.body ? JSON.parse(raw.body) : {};
2370
- } catch {
2371
- parsedBody = raw.body;
2372
- }
2373
- }
2374
- return {
2375
- method: raw.method,
2376
- url: raw.url,
2377
- path: raw.path,
2378
- headers: raw.headers,
2379
- query: raw.query,
2380
- body: parsedBody,
2381
- rawBody: raw.body ? Buffer.from(raw.body, "utf-8") : void 0
2382
- };
2383
- }
2384
- function buildRequestScopedConfig(env) {
2385
- return {
2386
- baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2387
- apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2388
- };
2389
- }
2390
-
2391
2346
  // src/server/startup-logger.ts
2392
2347
  function padEnd(str, length) {
2393
2348
  if (str.length >= length) {
@@ -2395,10 +2350,11 @@ function padEnd(str, length) {
2395
2350
  }
2396
2351
  return str + " ".repeat(length - str.length);
2397
2352
  }
2398
- function printStartupLog(config, tools, webhookRegistry, port) {
2353
+ function printStartupLog(config, tools, port) {
2399
2354
  if (process.env.NODE_ENV === "test") {
2400
2355
  return;
2401
2356
  }
2357
+ const webhookRegistry = config.webhooks;
2402
2358
  const webhookCount = webhookRegistry ? Object.keys(webhookRegistry).length : 0;
2403
2359
  const webhookNames = webhookRegistry ? Object.keys(webhookRegistry) : [];
2404
2360
  const maxRequests = config.maxRequests ?? parseNumberEnv(process.env.MCP_MAX_REQUESTS) ?? null;
@@ -2411,9 +2367,9 @@ function printStartupLog(config, tools, webhookRegistry, port) {
2411
2367
  console.log(`\u2551 \u{1F680} Skedyul MCP Server Starting \u2551`);
2412
2368
  console.log(`\u2560${divider}\u2563`);
2413
2369
  console.log(`\u2551 \u2551`);
2414
- console.log(`\u2551 \u{1F4E6} Server: ${padEnd(config.metadata.name, 49)}\u2551`);
2415
- console.log(`\u2551 \u{1F3F7}\uFE0F Version: ${padEnd(config.metadata.version, 49)}\u2551`);
2416
- console.log(`\u2551 \u26A1 Compute: ${padEnd(config.computeLayer, 49)}\u2551`);
2370
+ console.log(`\u2551 \u{1F4E6} Server: ${padEnd(config.name, 49)}\u2551`);
2371
+ console.log(`\u2551 \u{1F3F7}\uFE0F Version: ${padEnd(config.version ?? "N/A", 49)}\u2551`);
2372
+ console.log(`\u2551 \u26A1 Compute: ${padEnd(config.computeLayer ?? "serverless", 49)}\u2551`);
2417
2373
  if (port) {
2418
2374
  console.log(`\u2551 \u{1F310} Port: ${padEnd(String(port), 49)}\u2551`);
2419
2375
  }
@@ -2455,67 +2411,439 @@ function printStartupLog(config, tools, webhookRegistry, port) {
2455
2411
  console.log("");
2456
2412
  }
2457
2413
 
2458
- // src/config/resolve.ts
2459
- async function resolveDynamicImport(value) {
2460
- if (value === void 0 || value === null) {
2461
- return void 0;
2414
+ // src/server/config-serializer.ts
2415
+ function serializeConfig(config) {
2416
+ const registry = config.tools;
2417
+ const webhookRegistry = config.webhooks;
2418
+ return {
2419
+ name: config.name,
2420
+ version: config.version,
2421
+ description: config.description,
2422
+ computeLayer: config.computeLayer,
2423
+ tools: registry ? Object.entries(registry).map(([key, tool]) => ({
2424
+ name: tool.name || key,
2425
+ description: tool.description,
2426
+ timeout: tool.timeout,
2427
+ retries: tool.retries
2428
+ })) : [],
2429
+ webhooks: webhookRegistry ? Object.values(webhookRegistry).map((w) => ({
2430
+ name: w.name,
2431
+ description: w.description,
2432
+ methods: w.methods ?? ["POST"],
2433
+ type: w.type ?? "WEBHOOK"
2434
+ })) : [],
2435
+ provision: isProvisionConfig(config.provision) ? config.provision : void 0,
2436
+ agents: config.agents
2437
+ };
2438
+ }
2439
+ function isProvisionConfig(value) {
2440
+ return value !== void 0 && value !== null && !(value instanceof Promise);
2441
+ }
2442
+
2443
+ // src/server/handlers/install-handler.ts
2444
+ async function handleInstall(body, hooks) {
2445
+ if (!hooks?.install) {
2446
+ return {
2447
+ status: 404,
2448
+ body: { error: "Install handler not configured" }
2449
+ };
2450
+ }
2451
+ if (!body.context?.appInstallationId || !body.context?.workplace) {
2452
+ return {
2453
+ status: 400,
2454
+ body: {
2455
+ error: {
2456
+ code: -32602,
2457
+ message: "Missing context (appInstallationId and workplace required)"
2458
+ }
2459
+ }
2460
+ };
2461
+ }
2462
+ const installContext = {
2463
+ env: body.env ?? {},
2464
+ workplace: body.context.workplace,
2465
+ appInstallationId: body.context.appInstallationId,
2466
+ app: body.context.app,
2467
+ invocation: body.invocation,
2468
+ log: createContextLogger()
2469
+ };
2470
+ const requestConfig = {
2471
+ baseUrl: body.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2472
+ apiToken: body.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2473
+ };
2474
+ try {
2475
+ const installHook = hooks.install;
2476
+ const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
2477
+ const result = await runWithLogContext({ invocation: body.invocation }, async () => {
2478
+ return await runWithConfig(requestConfig, async () => {
2479
+ return await installHandler(installContext);
2480
+ });
2481
+ });
2482
+ return {
2483
+ status: 200,
2484
+ body: { env: result.env ?? {}, redirect: result.redirect }
2485
+ };
2486
+ } catch (err) {
2487
+ if (err instanceof InstallError) {
2488
+ return {
2489
+ status: 400,
2490
+ body: {
2491
+ error: {
2492
+ code: err.code,
2493
+ message: err.message,
2494
+ field: err.field
2495
+ }
2496
+ }
2497
+ };
2498
+ }
2499
+ return {
2500
+ status: 500,
2501
+ body: {
2502
+ error: {
2503
+ code: -32603,
2504
+ message: err instanceof Error ? err.message : String(err ?? "")
2505
+ }
2506
+ }
2507
+ };
2508
+ }
2509
+ }
2510
+
2511
+ // src/server/handlers/uninstall-handler.ts
2512
+ async function handleUninstall(body, hooks) {
2513
+ if (!hooks?.uninstall) {
2514
+ return {
2515
+ status: 404,
2516
+ body: { error: "Uninstall handler not configured" }
2517
+ };
2518
+ }
2519
+ if (!body.context?.appInstallationId || !body.context?.workplace || !body.context?.app) {
2520
+ return {
2521
+ status: 400,
2522
+ body: {
2523
+ error: {
2524
+ code: -32602,
2525
+ message: "Missing context (appInstallationId, workplace and app required)"
2526
+ }
2527
+ }
2528
+ };
2529
+ }
2530
+ const uninstallContext = {
2531
+ env: body.env ?? {},
2532
+ workplace: body.context.workplace,
2533
+ appInstallationId: body.context.appInstallationId,
2534
+ app: body.context.app,
2535
+ invocation: body.invocation,
2536
+ log: createContextLogger()
2537
+ };
2538
+ const requestConfig = {
2539
+ baseUrl: body.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2540
+ apiToken: body.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2541
+ };
2542
+ try {
2543
+ const uninstallHook = hooks.uninstall;
2544
+ const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
2545
+ const result = await runWithLogContext({ invocation: body.invocation }, async () => {
2546
+ return await runWithConfig(requestConfig, async () => {
2547
+ return await uninstallHandlerFn(uninstallContext);
2548
+ });
2549
+ });
2550
+ return {
2551
+ status: 200,
2552
+ body: { cleanedWebhookIds: result.cleanedWebhookIds ?? [] }
2553
+ };
2554
+ } catch (err) {
2555
+ return {
2556
+ status: 500,
2557
+ body: {
2558
+ error: {
2559
+ code: -32603,
2560
+ message: err instanceof Error ? err.message : String(err ?? "")
2561
+ }
2562
+ }
2563
+ };
2564
+ }
2565
+ }
2566
+
2567
+ // src/server/handlers/provision-handler.ts
2568
+ async function handleProvision(body, hooks) {
2569
+ if (!hooks?.provision) {
2570
+ return {
2571
+ status: 404,
2572
+ body: { error: "Provision handler not configured" }
2573
+ };
2574
+ }
2575
+ if (!body.context?.app) {
2576
+ return {
2577
+ status: 400,
2578
+ body: {
2579
+ error: {
2580
+ code: -32602,
2581
+ message: "Missing context (app required)"
2582
+ }
2583
+ }
2584
+ };
2462
2585
  }
2463
- if (value instanceof Promise) {
2464
- const resolved = await value;
2465
- if (resolved && typeof resolved === "object" && "default" in resolved) {
2466
- return resolved.default;
2586
+ const mergedEnv = {};
2587
+ for (const [key, value] of Object.entries(process.env)) {
2588
+ if (value !== void 0) {
2589
+ mergedEnv[key] = value;
2467
2590
  }
2468
- return resolved;
2469
2591
  }
2470
- return value;
2592
+ Object.assign(mergedEnv, body.env ?? {});
2593
+ const provisionContext = {
2594
+ env: mergedEnv,
2595
+ app: body.context.app,
2596
+ invocation: body.invocation,
2597
+ log: createContextLogger()
2598
+ };
2599
+ const requestConfig = {
2600
+ baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
2601
+ apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
2602
+ };
2603
+ try {
2604
+ const provisionHook = hooks.provision;
2605
+ const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
2606
+ const result = await runWithLogContext({ invocation: body.invocation }, async () => {
2607
+ return await runWithConfig(requestConfig, async () => {
2608
+ return await provisionHandler(provisionContext);
2609
+ });
2610
+ });
2611
+ return {
2612
+ status: 200,
2613
+ body: result
2614
+ };
2615
+ } catch (err) {
2616
+ return {
2617
+ status: 500,
2618
+ body: {
2619
+ error: {
2620
+ code: -32603,
2621
+ message: err instanceof Error ? err.message : String(err ?? "")
2622
+ }
2623
+ }
2624
+ };
2625
+ }
2471
2626
  }
2472
- function serializeTools(registry) {
2473
- return Object.entries(registry).map(([key, tool]) => ({
2474
- name: tool.name || key,
2475
- displayName: tool.label,
2476
- description: tool.description,
2477
- timeout: tool.timeout,
2478
- retries: tool.retries
2479
- }));
2627
+
2628
+ // src/server/handler-helpers.ts
2629
+ function parseHandlerEnvelope(parsedBody) {
2630
+ if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody) || !("env" in parsedBody) || !("request" in parsedBody)) {
2631
+ return null;
2632
+ }
2633
+ const envelope = parsedBody;
2634
+ if (typeof envelope.env !== "object" || envelope.env === null || Array.isArray(envelope.env)) {
2635
+ return null;
2636
+ }
2637
+ if (typeof envelope.request !== "object" || envelope.request === null || Array.isArray(envelope.request)) {
2638
+ return null;
2639
+ }
2640
+ return {
2641
+ env: envelope.env,
2642
+ request: envelope.request,
2643
+ context: envelope.context
2644
+ };
2480
2645
  }
2481
- function serializeWebhooks(registry) {
2482
- return Object.values(registry).map((webhook2) => ({
2483
- name: webhook2.name,
2484
- description: webhook2.description,
2485
- methods: webhook2.methods ?? ["POST"],
2486
- type: webhook2.type ?? "WEBHOOK"
2487
- }));
2646
+ function buildRequestFromRaw(raw) {
2647
+ let parsedBody = raw.body;
2648
+ const contentType = raw.headers["content-type"] ?? "";
2649
+ if (contentType.includes("application/json")) {
2650
+ try {
2651
+ parsedBody = raw.body ? JSON.parse(raw.body) : {};
2652
+ } catch {
2653
+ parsedBody = raw.body;
2654
+ }
2655
+ }
2656
+ return {
2657
+ method: raw.method,
2658
+ url: raw.url,
2659
+ path: raw.path,
2660
+ headers: raw.headers,
2661
+ query: raw.query,
2662
+ body: parsedBody,
2663
+ rawBody: raw.body ? Buffer.from(raw.body, "utf-8") : void 0
2664
+ };
2488
2665
  }
2489
- async function resolveConfig(config, registry, webhookRegistry) {
2490
- const provision = await resolveDynamicImport(
2491
- config.provision
2492
- );
2493
- const install = await resolveDynamicImport(
2494
- config.install
2495
- );
2496
- const tools = serializeTools(registry);
2497
- const webhooks = webhookRegistry ? serializeWebhooks(webhookRegistry) : [];
2666
+ function buildRequestScopedConfig(env) {
2498
2667
  return {
2499
- name: config.name,
2500
- version: config.version,
2501
- description: config.description,
2502
- computeLayer: config.computeLayer,
2503
- tools,
2504
- webhooks,
2505
- provision,
2506
- agents: config.agents
2668
+ baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2669
+ apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2670
+ };
2671
+ }
2672
+
2673
+ // src/server/handlers/oauth-callback-handler.ts
2674
+ async function handleOAuthCallback(parsedBody, hooks) {
2675
+ if (!hooks?.oauth_callback) {
2676
+ return {
2677
+ status: 404,
2678
+ body: { error: "OAuth callback handler not configured" }
2679
+ };
2680
+ }
2681
+ const envelope = parseHandlerEnvelope(parsedBody);
2682
+ if (!envelope) {
2683
+ console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
2684
+ return {
2685
+ status: 400,
2686
+ body: {
2687
+ error: {
2688
+ code: -32602,
2689
+ message: "Missing envelope format: expected { env, request }"
2690
+ }
2691
+ }
2692
+ };
2693
+ }
2694
+ const invocation = parsedBody.invocation;
2695
+ const oauthRequest = buildRequestFromRaw(envelope.request);
2696
+ const requestConfig = buildRequestScopedConfig(envelope.env);
2697
+ const oauthCallbackContext = {
2698
+ request: oauthRequest,
2699
+ invocation,
2700
+ log: createContextLogger()
2701
+ };
2702
+ try {
2703
+ const oauthCallbackHook = hooks.oauth_callback;
2704
+ const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
2705
+ const result = await runWithLogContext({ invocation }, async () => {
2706
+ return await runWithConfig(requestConfig, async () => {
2707
+ return await oauthCallbackHandler(oauthCallbackContext);
2708
+ });
2709
+ });
2710
+ return {
2711
+ status: 200,
2712
+ body: {
2713
+ appInstallationId: result.appInstallationId,
2714
+ env: result.env ?? {}
2715
+ }
2716
+ };
2717
+ } catch (err) {
2718
+ const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
2719
+ return {
2720
+ status: 500,
2721
+ body: {
2722
+ error: {
2723
+ code: -32603,
2724
+ message: errorMessage
2725
+ }
2726
+ }
2727
+ };
2728
+ }
2729
+ }
2730
+
2731
+ // src/server/handlers/webhook-handler.ts
2732
+ function parseWebhookRequest(parsedBody, method, url, path2, headers, query, rawBody, appIdHeader, appVersionIdHeader) {
2733
+ const isEnvelope = typeof parsedBody === "object" && parsedBody !== null && "env" in parsedBody && "request" in parsedBody && "context" in parsedBody;
2734
+ if (isEnvelope) {
2735
+ const envelope = parsedBody;
2736
+ const requestEnv = envelope.env ?? {};
2737
+ const invocation = envelope.invocation;
2738
+ let originalParsedBody = envelope.request.body;
2739
+ const originalContentType = envelope.request.headers["content-type"] ?? "";
2740
+ if (originalContentType.includes("application/json")) {
2741
+ try {
2742
+ originalParsedBody = envelope.request.body ? JSON.parse(envelope.request.body) : {};
2743
+ } catch {
2744
+ }
2745
+ }
2746
+ const webhookRequest2 = {
2747
+ method: envelope.request.method,
2748
+ url: envelope.request.url,
2749
+ path: envelope.request.path,
2750
+ headers: envelope.request.headers,
2751
+ query: envelope.request.query,
2752
+ body: originalParsedBody,
2753
+ rawBody: envelope.request.body ? Buffer.from(envelope.request.body, "utf-8") : void 0
2754
+ };
2755
+ const envVars = { ...process.env, ...requestEnv };
2756
+ const app = envelope.context.app;
2757
+ let webhookContext2;
2758
+ if (envelope.context.appInstallationId && envelope.context.workplace) {
2759
+ webhookContext2 = {
2760
+ env: envVars,
2761
+ app,
2762
+ appInstallationId: envelope.context.appInstallationId,
2763
+ workplace: envelope.context.workplace,
2764
+ registration: envelope.context.registration ?? {},
2765
+ invocation,
2766
+ log: createContextLogger()
2767
+ };
2768
+ } else {
2769
+ webhookContext2 = {
2770
+ env: envVars,
2771
+ app,
2772
+ invocation,
2773
+ log: createContextLogger()
2774
+ };
2775
+ }
2776
+ return { webhookRequest: webhookRequest2, webhookContext: webhookContext2, requestEnv, invocation };
2777
+ }
2778
+ if (!appIdHeader || !appVersionIdHeader) {
2779
+ return {
2780
+ error: "Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)"
2781
+ };
2782
+ }
2783
+ const webhookRequest = {
2784
+ method,
2785
+ url,
2786
+ path: path2,
2787
+ headers,
2788
+ query,
2789
+ body: parsedBody,
2790
+ rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
2791
+ };
2792
+ const webhookContext = {
2793
+ env: process.env,
2794
+ app: { id: appIdHeader, versionId: appVersionIdHeader },
2795
+ log: createContextLogger()
2507
2796
  };
2797
+ return { webhookRequest, webhookContext, requestEnv: {} };
2508
2798
  }
2509
- function createMinimalConfig(name, version) {
2799
+ async function executeWebhookHandler(handle, webhookRegistry, data) {
2800
+ const webhookDef = webhookRegistry[handle];
2801
+ if (!webhookDef) {
2802
+ return {
2803
+ status: 404,
2804
+ body: { error: `Webhook handler '${handle}' not found` }
2805
+ };
2806
+ }
2807
+ const originalEnv = { ...process.env };
2808
+ Object.assign(process.env, data.requestEnv);
2809
+ const requestConfig = {
2810
+ baseUrl: data.requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2811
+ apiToken: data.requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2812
+ };
2813
+ let webhookResponse;
2814
+ try {
2815
+ webhookResponse = await runWithLogContext({ invocation: data.invocation }, async () => {
2816
+ return await runWithConfig(requestConfig, async () => {
2817
+ return await webhookDef.handler(data.webhookRequest, data.webhookContext);
2818
+ });
2819
+ });
2820
+ } catch (err) {
2821
+ console.error(`Webhook handler '${handle}' error:`, err);
2822
+ return {
2823
+ status: 500,
2824
+ body: { error: "Webhook handler error" }
2825
+ };
2826
+ } finally {
2827
+ process.env = originalEnv;
2828
+ }
2510
2829
  return {
2511
- name,
2512
- version
2830
+ status: webhookResponse.status ?? 200,
2831
+ body: webhookResponse.body,
2832
+ headers: webhookResponse.headers
2513
2833
  };
2514
2834
  }
2835
+ function isMethodAllowed(webhookRegistry, handle, method) {
2836
+ const webhookDef = webhookRegistry[handle];
2837
+ if (!webhookDef) return false;
2838
+ const allowedMethods = webhookDef.methods ?? ["POST"];
2839
+ return allowedMethods.includes(method);
2840
+ }
2515
2841
 
2516
2842
  // src/server/dedicated.ts
2517
- function createDedicatedServerInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
2843
+ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer) {
2518
2844
  const port = getListeningPort(config);
2845
+ const registry = config.tools;
2846
+ const webhookRegistry = config.webhooks;
2519
2847
  const httpServer = http.createServer(
2520
2848
  async (req, res) => {
2521
2849
  function sendCoreResult(result) {
@@ -2532,30 +2860,16 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2532
2860
  return;
2533
2861
  }
2534
2862
  if (pathname === "/config" && req.method === "GET") {
2535
- let appConfig = config.appConfig;
2536
- if (!appConfig && config.appConfigLoader) {
2537
- const loaded = await config.appConfigLoader();
2538
- appConfig = loaded.default;
2539
- }
2540
- if (!appConfig) {
2541
- appConfig = createMinimalConfig(
2542
- config.metadata.name,
2543
- config.metadata.version
2544
- );
2545
- }
2546
- const serializedConfig = await resolveConfig(appConfig, registry, webhookRegistry);
2547
- sendJSON(res, 200, serializedConfig);
2863
+ sendJSON(res, 200, serializeConfig(config));
2548
2864
  return;
2549
2865
  }
2550
2866
  if (pathname.startsWith("/webhooks/") && webhookRegistry) {
2551
2867
  const handle = pathname.slice("/webhooks/".length);
2552
- const webhookDef = webhookRegistry[handle];
2553
- if (!webhookDef) {
2868
+ if (!webhookRegistry[handle]) {
2554
2869
  sendJSON(res, 404, { error: `Webhook handler '${handle}' not found` });
2555
2870
  return;
2556
2871
  }
2557
- const allowedMethods = webhookDef.methods ?? ["POST"];
2558
- if (!allowedMethods.includes(req.method)) {
2872
+ if (!isMethodAllowed(webhookRegistry, handle, req.method ?? "POST")) {
2559
2873
  sendJSON(res, 405, { error: `Method ${req.method} not allowed` });
2560
2874
  return;
2561
2875
  }
@@ -2577,87 +2891,34 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2577
2891
  } else {
2578
2892
  parsedBody = rawBody;
2579
2893
  }
2580
- const envelope = parseHandlerEnvelope(parsedBody);
2581
- let webhookRequest;
2582
- let webhookContext;
2583
- let requestEnv = {};
2584
- let invocation;
2585
- if (envelope && "context" in envelope && envelope.context) {
2586
- const context = envelope.context;
2587
- requestEnv = envelope.env;
2588
- invocation = parsedBody.invocation;
2589
- webhookRequest = buildRequestFromRaw(envelope.request);
2590
- const envVars = { ...process.env, ...envelope.env };
2591
- const app = context.app;
2592
- if (context.appInstallationId && context.workplace) {
2593
- webhookContext = {
2594
- env: envVars,
2595
- app,
2596
- appInstallationId: context.appInstallationId,
2597
- workplace: context.workplace,
2598
- registration: context.registration ?? {},
2599
- invocation,
2600
- log: createContextLogger()
2601
- };
2602
- } else {
2603
- webhookContext = {
2604
- env: envVars,
2605
- app,
2606
- invocation,
2607
- log: createContextLogger()
2608
- };
2609
- }
2610
- } else {
2611
- const appId = req.headers["x-skedyul-app-id"];
2612
- const appVersionId = req.headers["x-skedyul-app-version-id"];
2613
- if (!appId || !appVersionId) {
2614
- throw new Error("Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)");
2615
- }
2616
- webhookRequest = {
2617
- method: req.method ?? "POST",
2618
- url: url.toString(),
2619
- path: pathname,
2620
- headers: req.headers,
2621
- query: Object.fromEntries(url.searchParams.entries()),
2622
- body: parsedBody,
2623
- rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
2624
- };
2625
- webhookContext = {
2626
- env: process.env,
2627
- app: { id: appId, versionId: appVersionId },
2628
- log: createContextLogger()
2629
- };
2630
- }
2631
- const originalEnv = { ...process.env };
2632
- Object.assign(process.env, requestEnv);
2633
- const requestConfig = buildRequestScopedConfig(requestEnv);
2634
- let webhookResponse;
2635
- try {
2636
- webhookResponse = await runWithLogContext({ invocation }, async () => {
2637
- return await runWithConfig(requestConfig, async () => {
2638
- return await webhookDef.handler(webhookRequest, webhookContext);
2639
- });
2640
- });
2641
- } catch (err) {
2642
- console.error(`Webhook handler '${handle}' error:`, err);
2643
- sendJSON(res, 500, { error: "Webhook handler error" });
2894
+ const parseResult = parseWebhookRequest(
2895
+ parsedBody,
2896
+ req.method ?? "POST",
2897
+ url.toString(),
2898
+ pathname,
2899
+ req.headers,
2900
+ Object.fromEntries(url.searchParams.entries()),
2901
+ rawBody,
2902
+ req.headers["x-skedyul-app-id"],
2903
+ req.headers["x-skedyul-app-version-id"]
2904
+ );
2905
+ if ("error" in parseResult) {
2906
+ sendJSON(res, 400, { error: parseResult.error });
2644
2907
  return;
2645
- } finally {
2646
- process.env = originalEnv;
2647
2908
  }
2648
- const status = webhookResponse.status ?? 200;
2909
+ const result = await executeWebhookHandler(handle, webhookRegistry, parseResult);
2649
2910
  const responseHeaders = {
2650
- ...webhookResponse.headers
2911
+ ...result.headers
2651
2912
  };
2652
2913
  if (!responseHeaders["Content-Type"] && !responseHeaders["content-type"]) {
2653
2914
  responseHeaders["Content-Type"] = "application/json";
2654
2915
  }
2655
- res.writeHead(status, responseHeaders);
2656
- if (webhookResponse.body !== void 0) {
2657
- if (typeof webhookResponse.body === "string") {
2658
- res.end(webhookResponse.body);
2916
+ res.writeHead(result.status, responseHeaders);
2917
+ if (result.body !== void 0) {
2918
+ if (typeof result.body === "string") {
2919
+ res.end(result.body);
2659
2920
  } else {
2660
- res.end(JSON.stringify(webhookResponse.body));
2921
+ res.end(JSON.stringify(result.body));
2661
2922
  }
2662
2923
  } else {
2663
2924
  res.end();
@@ -2696,10 +2957,6 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2696
2957
  return;
2697
2958
  }
2698
2959
  if (pathname === "/oauth_callback" && req.method === "POST") {
2699
- if (!config.hooks?.oauth_callback) {
2700
- sendJSON(res, 404, { error: "OAuth callback handler not configured" });
2701
- return;
2702
- }
2703
2960
  let parsedBody;
2704
2961
  try {
2705
2962
  parsedBody = await parseJSONBody(req);
@@ -2710,50 +2967,11 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2710
2967
  });
2711
2968
  return;
2712
2969
  }
2713
- const envelope = parseHandlerEnvelope(parsedBody);
2714
- if (!envelope) {
2715
- console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
2716
- sendJSON(res, 400, {
2717
- error: { code: -32602, message: "Missing envelope format: expected { env, request }" }
2718
- });
2719
- return;
2720
- }
2721
- const invocation = parsedBody.invocation;
2722
- const oauthRequest = buildRequestFromRaw(envelope.request);
2723
- const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
2724
- const oauthCallbackContext = {
2725
- request: oauthRequest,
2726
- invocation,
2727
- log: createContextLogger()
2728
- };
2729
- try {
2730
- const oauthCallbackHook = config.hooks.oauth_callback;
2731
- const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
2732
- const result = await runWithLogContext({ invocation }, async () => {
2733
- return await runWithConfig(oauthCallbackRequestConfig, async () => {
2734
- return await oauthCallbackHandler(oauthCallbackContext);
2735
- });
2736
- });
2737
- sendJSON(res, 200, {
2738
- appInstallationId: result.appInstallationId,
2739
- env: result.env ?? {}
2740
- });
2741
- } catch (err) {
2742
- const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
2743
- sendJSON(res, 500, {
2744
- error: {
2745
- code: -32603,
2746
- message: errorMessage
2747
- }
2748
- });
2749
- }
2970
+ const result = await handleOAuthCallback(parsedBody, config.hooks);
2971
+ sendJSON(res, result.status, result.body);
2750
2972
  return;
2751
2973
  }
2752
2974
  if (pathname === "/install" && req.method === "POST") {
2753
- if (!config.hooks?.install) {
2754
- sendJSON(res, 404, { error: "Install handler not configured" });
2755
- return;
2756
- }
2757
2975
  let installBody;
2758
2976
  try {
2759
2977
  installBody = await parseJSONBody(req);
@@ -2763,61 +2981,11 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2763
2981
  });
2764
2982
  return;
2765
2983
  }
2766
- if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
2767
- sendJSON(res, 400, {
2768
- error: { code: -32602, message: "Missing context (appInstallationId and workplace required)" }
2769
- });
2770
- return;
2771
- }
2772
- const installContext = {
2773
- env: installBody.env ?? {},
2774
- workplace: installBody.context.workplace,
2775
- appInstallationId: installBody.context.appInstallationId,
2776
- app: installBody.context.app,
2777
- invocation: installBody.invocation,
2778
- log: createContextLogger()
2779
- };
2780
- const installRequestConfig = {
2781
- baseUrl: installBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2782
- apiToken: installBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2783
- };
2784
- try {
2785
- const installHook = config.hooks.install;
2786
- const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
2787
- const result = await runWithLogContext({ invocation: installBody.invocation }, async () => {
2788
- return await runWithConfig(installRequestConfig, async () => {
2789
- return await installHandler(installContext);
2790
- });
2791
- });
2792
- sendJSON(res, 200, {
2793
- env: result.env ?? {},
2794
- redirect: result.redirect
2795
- });
2796
- } catch (err) {
2797
- if (err instanceof InstallError) {
2798
- sendJSON(res, 400, {
2799
- error: {
2800
- code: err.code,
2801
- message: err.message,
2802
- field: err.field
2803
- }
2804
- });
2805
- } else {
2806
- sendJSON(res, 500, {
2807
- error: {
2808
- code: -32603,
2809
- message: err instanceof Error ? err.message : String(err ?? "")
2810
- }
2811
- });
2812
- }
2813
- }
2984
+ const result = await handleInstall(installBody, config.hooks);
2985
+ sendJSON(res, result.status, result.body);
2814
2986
  return;
2815
2987
  }
2816
2988
  if (pathname === "/uninstall" && req.method === "POST") {
2817
- if (!config.hooks?.uninstall) {
2818
- sendJSON(res, 404, { error: "Uninstall handler not configured" });
2819
- return;
2820
- }
2821
2989
  let uninstallBody;
2822
2990
  try {
2823
2991
  uninstallBody = await parseJSONBody(req);
@@ -2827,53 +2995,11 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2827
2995
  });
2828
2996
  return;
2829
2997
  }
2830
- if (!uninstallBody.context?.appInstallationId || !uninstallBody.context?.workplace || !uninstallBody.context?.app) {
2831
- sendJSON(res, 400, {
2832
- error: {
2833
- code: -32602,
2834
- message: "Missing context (appInstallationId, workplace and app required)"
2835
- }
2836
- });
2837
- return;
2838
- }
2839
- const uninstallContext = {
2840
- env: uninstallBody.env ?? {},
2841
- workplace: uninstallBody.context.workplace,
2842
- appInstallationId: uninstallBody.context.appInstallationId,
2843
- app: uninstallBody.context.app,
2844
- invocation: uninstallBody.invocation,
2845
- log: createContextLogger()
2846
- };
2847
- const uninstallRequestConfig = {
2848
- baseUrl: uninstallBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2849
- apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2850
- };
2851
- try {
2852
- const uninstallHook = config.hooks.uninstall;
2853
- const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
2854
- const result = await runWithLogContext({ invocation: uninstallBody.invocation }, async () => {
2855
- return await runWithConfig(uninstallRequestConfig, async () => {
2856
- return await uninstallHandlerFn(uninstallContext);
2857
- });
2858
- });
2859
- sendJSON(res, 200, {
2860
- cleanedWebhookIds: result.cleanedWebhookIds ?? []
2861
- });
2862
- } catch (err) {
2863
- sendJSON(res, 500, {
2864
- error: {
2865
- code: -32603,
2866
- message: err instanceof Error ? err.message : String(err ?? "")
2867
- }
2868
- });
2869
- }
2998
+ const result = await handleUninstall(uninstallBody, config.hooks);
2999
+ sendJSON(res, result.status, result.body);
2870
3000
  return;
2871
3001
  }
2872
3002
  if (pathname === "/provision" && req.method === "POST") {
2873
- if (!config.hooks?.provision) {
2874
- sendJSON(res, 404, { error: "Provision handler not configured" });
2875
- return;
2876
- }
2877
3003
  let provisionBody;
2878
3004
  try {
2879
3005
  provisionBody = await parseJSONBody(req);
@@ -2883,46 +3009,8 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
2883
3009
  });
2884
3010
  return;
2885
3011
  }
2886
- if (!provisionBody.context?.app) {
2887
- sendJSON(res, 400, {
2888
- error: { code: -32602, message: "Missing context (app required)" }
2889
- });
2890
- return;
2891
- }
2892
- const mergedEnv = {};
2893
- for (const [key, value] of Object.entries(process.env)) {
2894
- if (value !== void 0) {
2895
- mergedEnv[key] = value;
2896
- }
2897
- }
2898
- Object.assign(mergedEnv, provisionBody.env ?? {});
2899
- const provisionContext = {
2900
- env: mergedEnv,
2901
- app: provisionBody.context.app,
2902
- invocation: provisionBody.invocation,
2903
- log: createContextLogger()
2904
- };
2905
- const provisionRequestConfig = {
2906
- baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
2907
- apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
2908
- };
2909
- try {
2910
- const provisionHook = config.hooks.provision;
2911
- const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
2912
- const result = await runWithLogContext({ invocation: provisionBody.invocation }, async () => {
2913
- return await runWithConfig(provisionRequestConfig, async () => {
2914
- return await provisionHandler(provisionContext);
2915
- });
2916
- });
2917
- sendJSON(res, 200, result);
2918
- } catch (err) {
2919
- sendJSON(res, 500, {
2920
- error: {
2921
- code: -32603,
2922
- message: err instanceof Error ? err.message : String(err ?? "")
2923
- }
2924
- });
2925
- }
3012
+ const result = await handleProvision(provisionBody, config.hooks);
3013
+ sendJSON(res, result.status, result.body);
2926
3014
  return;
2927
3015
  }
2928
3016
  if (pathname === "/core" && req.method === "POST") {
@@ -3073,7 +3161,7 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
3073
3161
  const finalPort = listenPort ?? port;
3074
3162
  return new Promise((resolve2, reject) => {
3075
3163
  httpServer.listen(finalPort, () => {
3076
- printStartupLog(config, tools, webhookRegistry, finalPort);
3164
+ printStartupLog(config, tools, finalPort);
3077
3165
  resolve2();
3078
3166
  });
3079
3167
  httpServer.once("error", reject);
@@ -3084,13 +3172,15 @@ function createDedicatedServerInstance(config, tools, callTool, state, mcpServer
3084
3172
  }
3085
3173
 
3086
3174
  // src/server/serverless.ts
3087
- function createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
3175
+ function createServerlessInstance(config, tools, callTool, state, mcpServer) {
3088
3176
  const headers = getDefaultHeaders(config.cors);
3177
+ const registry = config.tools;
3178
+ const webhookRegistry = config.webhooks;
3089
3179
  let hasLoggedStartup = false;
3090
3180
  return {
3091
3181
  async handler(event) {
3092
3182
  if (!hasLoggedStartup) {
3093
- printStartupLog(config, tools, webhookRegistry);
3183
+ printStartupLog(config, tools);
3094
3184
  hasLoggedStartup = true;
3095
3185
  }
3096
3186
  try {
@@ -3101,12 +3191,10 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
3101
3191
  }
3102
3192
  if (path2.startsWith("/webhooks/") && webhookRegistry) {
3103
3193
  const handle = path2.slice("/webhooks/".length);
3104
- const webhookDef = webhookRegistry[handle];
3105
- if (!webhookDef) {
3194
+ if (!webhookRegistry[handle]) {
3106
3195
  return createResponse(404, { error: `Webhook handler '${handle}' not found` }, headers);
3107
3196
  }
3108
- const allowedMethods = webhookDef.methods ?? ["POST"];
3109
- if (!allowedMethods.includes(method)) {
3197
+ if (!isMethodAllowed(webhookRegistry, handle, method)) {
3110
3198
  return createResponse(405, { error: `Method ${method} not allowed` }, headers);
3111
3199
  }
3112
3200
  const rawBody = event.body ?? "";
@@ -3121,107 +3209,34 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
3121
3209
  } else {
3122
3210
  parsedBody = rawBody;
3123
3211
  }
3124
- const isEnvelope = typeof parsedBody === "object" && parsedBody !== null && "env" in parsedBody && "request" in parsedBody && "context" in parsedBody;
3125
- let webhookRequest;
3126
- let webhookContext;
3127
- let requestEnv = {};
3128
- let invocation;
3129
- if (isEnvelope) {
3130
- const envelope = parsedBody;
3131
- requestEnv = envelope.env ?? {};
3132
- invocation = envelope.invocation;
3133
- let originalParsedBody = envelope.request.body;
3134
- const originalContentType = envelope.request.headers["content-type"] ?? "";
3135
- if (originalContentType.includes("application/json")) {
3136
- try {
3137
- originalParsedBody = envelope.request.body ? JSON.parse(envelope.request.body) : {};
3138
- } catch {
3139
- }
3140
- }
3141
- webhookRequest = {
3142
- method: envelope.request.method,
3143
- url: envelope.request.url,
3144
- path: envelope.request.path,
3145
- headers: envelope.request.headers,
3146
- query: envelope.request.query,
3147
- body: originalParsedBody,
3148
- rawBody: envelope.request.body ? Buffer.from(envelope.request.body, "utf-8") : void 0
3149
- };
3150
- const envVars = { ...process.env, ...requestEnv };
3151
- const app = envelope.context.app;
3152
- if (envelope.context.appInstallationId && envelope.context.workplace) {
3153
- webhookContext = {
3154
- env: envVars,
3155
- app,
3156
- appInstallationId: envelope.context.appInstallationId,
3157
- workplace: envelope.context.workplace,
3158
- registration: envelope.context.registration ?? {},
3159
- invocation,
3160
- log: createContextLogger()
3161
- };
3162
- } else {
3163
- webhookContext = {
3164
- env: envVars,
3165
- app,
3166
- invocation,
3167
- log: createContextLogger()
3168
- };
3169
- }
3170
- } else {
3171
- const appId = event.headers?.["x-skedyul-app-id"] ?? event.headers?.["X-Skedyul-App-Id"];
3172
- const appVersionId = event.headers?.["x-skedyul-app-version-id"] ?? event.headers?.["X-Skedyul-App-Version-Id"];
3173
- if (!appId || !appVersionId) {
3174
- throw new Error("Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)");
3175
- }
3176
- const forwardedProto = event.headers?.["x-forwarded-proto"] ?? event.headers?.["X-Forwarded-Proto"];
3177
- const protocol = forwardedProto ?? "https";
3178
- const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
3179
- const queryString = event.queryStringParameters ? "?" + new URLSearchParams(event.queryStringParameters).toString() : "";
3180
- const webhookUrl = `${protocol}://${host}${path2}${queryString}`;
3181
- webhookRequest = {
3182
- method,
3183
- url: webhookUrl,
3184
- path: path2,
3185
- headers: event.headers,
3186
- query: event.queryStringParameters ?? {},
3187
- body: parsedBody,
3188
- rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
3189
- };
3190
- webhookContext = {
3191
- env: process.env,
3192
- app: { id: appId, versionId: appVersionId },
3193
- log: createContextLogger()
3194
- };
3195
- }
3196
- const originalEnv = { ...process.env };
3197
- Object.assign(process.env, requestEnv);
3198
- const requestConfig = {
3199
- baseUrl: requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
3200
- apiToken: requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
3201
- };
3202
- let webhookResponse;
3203
- try {
3204
- webhookResponse = await runWithLogContext({ invocation }, async () => {
3205
- return await runWithConfig(requestConfig, async () => {
3206
- return await webhookDef.handler(webhookRequest, webhookContext);
3207
- });
3208
- });
3209
- } catch (err) {
3210
- console.error(`Webhook handler '${handle}' error:`, err);
3211
- return createResponse(500, { error: "Webhook handler error" }, headers);
3212
- } finally {
3213
- process.env = originalEnv;
3212
+ const forwardedProto = event.headers?.["x-forwarded-proto"] ?? event.headers?.["X-Forwarded-Proto"];
3213
+ const protocol = forwardedProto ?? "https";
3214
+ const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
3215
+ const queryString = event.queryStringParameters ? "?" + new URLSearchParams(event.queryStringParameters).toString() : "";
3216
+ const webhookUrl = `${protocol}://${host}${path2}${queryString}`;
3217
+ const parseResult = parseWebhookRequest(
3218
+ parsedBody,
3219
+ method,
3220
+ webhookUrl,
3221
+ path2,
3222
+ event.headers,
3223
+ event.queryStringParameters ?? {},
3224
+ rawBody,
3225
+ event.headers?.["x-skedyul-app-id"] ?? event.headers?.["X-Skedyul-App-Id"],
3226
+ event.headers?.["x-skedyul-app-version-id"] ?? event.headers?.["X-Skedyul-App-Version-Id"]
3227
+ );
3228
+ if ("error" in parseResult) {
3229
+ return createResponse(400, { error: parseResult.error }, headers);
3214
3230
  }
3231
+ const result = await executeWebhookHandler(handle, webhookRegistry, parseResult);
3215
3232
  const responseHeaders = {
3216
3233
  ...headers,
3217
- ...webhookResponse.headers
3234
+ ...result.headers
3218
3235
  };
3219
- const status = webhookResponse.status ?? 200;
3220
- const body = webhookResponse.body;
3221
3236
  return {
3222
- statusCode: status,
3237
+ statusCode: result.status,
3223
3238
  headers: responseHeaders,
3224
- body: body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : ""
3239
+ body: result.body !== void 0 ? typeof result.body === "string" ? result.body : JSON.stringify(result.body) : ""
3225
3240
  };
3226
3241
  }
3227
3242
  if (path2 === "/core" && method === "POST") {
@@ -3357,9 +3372,6 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
3357
3372
  }
3358
3373
  }
3359
3374
  if (path2 === "/install" && method === "POST") {
3360
- if (!config.hooks?.install) {
3361
- return createResponse(404, { error: "Install handler not configured" }, headers);
3362
- }
3363
3375
  let installBody;
3364
3376
  try {
3365
3377
  installBody = event.body ? JSON.parse(event.body) : {};
@@ -3370,68 +3382,10 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
3370
3382
  headers
3371
3383
  );
3372
3384
  }
3373
- if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
3374
- return createResponse(
3375
- 400,
3376
- { error: { code: -32602, message: "Missing context (appInstallationId and workplace required)" } },
3377
- headers
3378
- );
3379
- }
3380
- const installContext = {
3381
- env: installBody.env ?? {},
3382
- workplace: installBody.context.workplace,
3383
- appInstallationId: installBody.context.appInstallationId,
3384
- app: installBody.context.app,
3385
- invocation: installBody.invocation,
3386
- log: createContextLogger()
3387
- };
3388
- const installRequestConfig = {
3389
- baseUrl: installBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
3390
- apiToken: installBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
3391
- };
3392
- try {
3393
- const installHook = config.hooks.install;
3394
- const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
3395
- const result = await runWithLogContext({ invocation: installBody.invocation }, async () => {
3396
- return await runWithConfig(installRequestConfig, async () => {
3397
- return await installHandler(installContext);
3398
- });
3399
- });
3400
- return createResponse(
3401
- 200,
3402
- { env: result.env ?? {}, redirect: result.redirect },
3403
- headers
3404
- );
3405
- } catch (err) {
3406
- if (err instanceof InstallError) {
3407
- return createResponse(
3408
- 400,
3409
- {
3410
- error: {
3411
- code: err.code,
3412
- message: err.message,
3413
- field: err.field
3414
- }
3415
- },
3416
- headers
3417
- );
3418
- }
3419
- return createResponse(
3420
- 500,
3421
- {
3422
- error: {
3423
- code: -32603,
3424
- message: err instanceof Error ? err.message : String(err ?? "")
3425
- }
3426
- },
3427
- headers
3428
- );
3429
- }
3385
+ const result = await handleInstall(installBody, config.hooks);
3386
+ return createResponse(result.status, result.body, headers);
3430
3387
  }
3431
3388
  if (path2 === "/uninstall" && method === "POST") {
3432
- if (!config.hooks?.uninstall) {
3433
- return createResponse(404, { error: "Uninstall handler not configured" }, headers);
3434
- }
3435
3389
  let uninstallBody;
3436
3390
  try {
3437
3391
  uninstallBody = event.body ? JSON.parse(event.body) : {};
@@ -3442,137 +3396,24 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
3442
3396
  headers
3443
3397
  );
3444
3398
  }
3445
- if (!uninstallBody.context?.appInstallationId || !uninstallBody.context?.workplace || !uninstallBody.context?.app) {
3446
- return createResponse(
3447
- 400,
3448
- {
3449
- error: {
3450
- code: -32602,
3451
- message: "Missing context (appInstallationId, workplace and app required)"
3452
- }
3453
- },
3454
- headers
3455
- );
3456
- }
3457
- const uninstallContext = {
3458
- env: uninstallBody.env ?? {},
3459
- workplace: uninstallBody.context.workplace,
3460
- appInstallationId: uninstallBody.context.appInstallationId,
3461
- app: uninstallBody.context.app,
3462
- invocation: uninstallBody.invocation,
3463
- log: createContextLogger()
3464
- };
3465
- const uninstallRequestConfig = {
3466
- baseUrl: uninstallBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
3467
- apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
3468
- };
3469
- try {
3470
- const uninstallHook = config.hooks.uninstall;
3471
- const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
3472
- const result = await runWithLogContext({ invocation: uninstallBody.invocation }, async () => {
3473
- return await runWithConfig(uninstallRequestConfig, async () => {
3474
- return await uninstallHandlerFn(uninstallContext);
3475
- });
3476
- });
3477
- return createResponse(
3478
- 200,
3479
- { cleanedWebhookIds: result.cleanedWebhookIds ?? [] },
3480
- headers
3481
- );
3482
- } catch (err) {
3483
- return createResponse(
3484
- 500,
3485
- {
3486
- error: {
3487
- code: -32603,
3488
- message: err instanceof Error ? err.message : String(err ?? "")
3489
- }
3490
- },
3491
- headers
3492
- );
3493
- }
3399
+ const result = await handleUninstall(uninstallBody, config.hooks);
3400
+ return createResponse(result.status, result.body, headers);
3494
3401
  }
3495
3402
  if (path2 === "/provision" && method === "POST") {
3496
- console.log("[serverless] /provision endpoint called");
3497
- if (!config.hooks?.provision) {
3498
- console.log("[serverless] No provision handler configured");
3499
- return createResponse(404, { error: "Provision handler not configured" }, headers);
3500
- }
3501
3403
  let provisionBody;
3502
3404
  try {
3503
3405
  provisionBody = event.body ? JSON.parse(event.body) : {};
3504
- console.log("[serverless] Provision body parsed:", {
3505
- hasEnv: !!provisionBody.env,
3506
- hasContext: !!provisionBody.context,
3507
- appId: provisionBody.context?.app?.id,
3508
- versionId: provisionBody.context?.app?.versionId
3509
- });
3510
3406
  } catch {
3511
- console.log("[serverless] Failed to parse provision body");
3512
3407
  return createResponse(
3513
3408
  400,
3514
3409
  { error: { code: -32700, message: "Parse error" } },
3515
3410
  headers
3516
3411
  );
3517
3412
  }
3518
- if (!provisionBody.context?.app) {
3519
- console.log("[serverless] Missing app context in provision body");
3520
- return createResponse(
3521
- 400,
3522
- { error: { code: -32602, message: "Missing context (app required)" } },
3523
- headers
3524
- );
3525
- }
3526
- const mergedEnv = {};
3527
- for (const [key, value] of Object.entries(process.env)) {
3528
- if (value !== void 0) {
3529
- mergedEnv[key] = value;
3530
- }
3531
- }
3532
- Object.assign(mergedEnv, provisionBody.env ?? {});
3533
- const provisionContext = {
3534
- env: mergedEnv,
3535
- app: provisionBody.context.app,
3536
- invocation: provisionBody.invocation,
3537
- log: createContextLogger()
3538
- };
3539
- const provisionRequestConfig = {
3540
- baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
3541
- apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
3542
- };
3543
- console.log("[serverless] Calling provision handler...");
3544
- try {
3545
- const provisionHook = config.hooks.provision;
3546
- const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
3547
- const result = await runWithLogContext({ invocation: provisionBody.invocation }, async () => {
3548
- return await runWithConfig(provisionRequestConfig, async () => {
3549
- return await provisionHandler(provisionContext);
3550
- });
3551
- });
3552
- console.log("[serverless] Provision handler completed successfully");
3553
- return createResponse(200, result, headers);
3554
- } catch (err) {
3555
- console.error("[serverless] Provision handler failed:", err instanceof Error ? err.message : String(err));
3556
- return createResponse(
3557
- 500,
3558
- {
3559
- error: {
3560
- code: -32603,
3561
- message: err instanceof Error ? err.message : String(err ?? "")
3562
- }
3563
- },
3564
- headers
3565
- );
3566
- }
3413
+ const result = await handleProvision(provisionBody, config.hooks);
3414
+ return createResponse(result.status, result.body, headers);
3567
3415
  }
3568
3416
  if (path2 === "/oauth_callback" && method === "POST") {
3569
- if (!config.hooks?.oauth_callback) {
3570
- return createResponse(
3571
- 404,
3572
- { error: "OAuth callback handler not configured" },
3573
- headers
3574
- );
3575
- }
3576
3417
  let parsedBody;
3577
3418
  try {
3578
3419
  parsedBody = event.body ? JSON.parse(event.body) : {};
@@ -3584,70 +3425,14 @@ function createServerlessInstance(config, tools, callTool, state, mcpServer, reg
3584
3425
  headers
3585
3426
  );
3586
3427
  }
3587
- const envelope = parseHandlerEnvelope(parsedBody);
3588
- if (!envelope) {
3589
- console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
3590
- return createResponse(
3591
- 400,
3592
- { error: { code: -32602, message: "Missing envelope format: expected { env, request }" } },
3593
- headers
3594
- );
3595
- }
3596
- const invocation = parsedBody.invocation;
3597
- const oauthRequest = buildRequestFromRaw(envelope.request);
3598
- const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
3599
- const oauthCallbackContext = {
3600
- request: oauthRequest,
3601
- invocation,
3602
- log: createContextLogger()
3603
- };
3604
- try {
3605
- const oauthCallbackHook = config.hooks.oauth_callback;
3606
- const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
3607
- const result = await runWithLogContext({ invocation }, async () => {
3608
- return await runWithConfig(oauthCallbackRequestConfig, async () => {
3609
- return await oauthCallbackHandler(oauthCallbackContext);
3610
- });
3611
- });
3612
- return createResponse(
3613
- 200,
3614
- {
3615
- appInstallationId: result.appInstallationId,
3616
- env: result.env ?? {}
3617
- },
3618
- headers
3619
- );
3620
- } catch (err) {
3621
- const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
3622
- return createResponse(
3623
- 500,
3624
- {
3625
- error: {
3626
- code: -32603,
3627
- message: errorMessage
3628
- }
3629
- },
3630
- headers
3631
- );
3632
- }
3428
+ const result = await handleOAuthCallback(parsedBody, config.hooks);
3429
+ return createResponse(result.status, result.body, headers);
3633
3430
  }
3634
3431
  if (path2 === "/health" && method === "GET") {
3635
3432
  return createResponse(200, state.getHealthStatus(), headers);
3636
3433
  }
3637
3434
  if (path2 === "/config" && method === "GET") {
3638
- let appConfig = config.appConfig;
3639
- if (!appConfig && config.appConfigLoader) {
3640
- const loaded = await config.appConfigLoader();
3641
- appConfig = loaded.default;
3642
- }
3643
- if (!appConfig) {
3644
- appConfig = createMinimalConfig(
3645
- config.metadata.name,
3646
- config.metadata.version
3647
- );
3648
- }
3649
- const serializedConfig = await resolveConfig(appConfig, registry, webhookRegistry);
3650
- return createResponse(200, serializedConfig, headers);
3435
+ return createResponse(200, serializeConfig(config), headers);
3651
3436
  }
3652
3437
  if (path2 === "/mcp" && method === "POST") {
3653
3438
  let body;
@@ -3865,9 +3650,11 @@ console.log("[skedyul-node/server] All imports complete");
3865
3650
  console.log("[skedyul-node/server] Installing context logger...");
3866
3651
  installContextLogger();
3867
3652
  console.log("[skedyul-node/server] Context logger installed");
3868
- function createSkedyulServer(config, registry, webhookRegistry) {
3653
+ function createSkedyulServer(config) {
3869
3654
  console.log("[createSkedyulServer] Step 1: mergeRuntimeEnv()");
3870
3655
  mergeRuntimeEnv();
3656
+ const registry = config.tools;
3657
+ const webhookRegistry = config.webhooks;
3871
3658
  console.log("[createSkedyulServer] Step 2: coreApi setup");
3872
3659
  if (config.coreApi?.service) {
3873
3660
  coreApiService.register(config.coreApi.service);
@@ -3879,7 +3666,7 @@ function createSkedyulServer(config, registry, webhookRegistry) {
3879
3666
  const tools = buildToolMetadata(registry);
3880
3667
  console.log("[createSkedyulServer] Step 3 done, tools:", tools.length);
3881
3668
  const toolNames = Object.values(registry).map((tool) => tool.name);
3882
- const runtimeLabel = config.computeLayer;
3669
+ const runtimeLabel = config.computeLayer ?? "serverless";
3883
3670
  const maxRequests = config.maxRequests ?? parseNumberEnv(process.env.MCP_MAX_REQUESTS) ?? null;
3884
3671
  const ttlExtendSeconds = config.ttlExtendSeconds ?? parseNumberEnv(process.env.MCP_TTL_EXTEND) ?? 3600;
3885
3672
  console.log("[createSkedyulServer] Step 4: createRequestState()");
@@ -3892,8 +3679,8 @@ function createSkedyulServer(config, registry, webhookRegistry) {
3892
3679
  console.log("[createSkedyulServer] Step 4 done");
3893
3680
  console.log("[createSkedyulServer] Step 5: new McpServer()");
3894
3681
  const mcpServer = new McpServer({
3895
- name: config.metadata.name,
3896
- version: config.metadata.version
3682
+ name: config.name,
3683
+ version: config.version ?? "0.0.0"
3897
3684
  });
3898
3685
  console.log("[createSkedyulServer] Step 5 done");
3899
3686
  const dedicatedShutdown = () => {
@@ -4021,13 +3808,11 @@ function createSkedyulServer(config, registry, webhookRegistry) {
4021
3808
  tools,
4022
3809
  callTool,
4023
3810
  state,
4024
- mcpServer,
4025
- registry,
4026
- webhookRegistry
3811
+ mcpServer
4027
3812
  );
4028
3813
  }
4029
3814
  console.log("[createSkedyulServer] Creating serverless instance");
4030
- const serverlessInstance = createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry);
3815
+ const serverlessInstance = createServerlessInstance(config, tools, callTool, state, mcpServer);
4031
3816
  console.log("[createSkedyulServer] Serverless instance created successfully");
4032
3817
  return serverlessInstance;
4033
3818
  }
@@ -4348,7 +4133,6 @@ export {
4348
4133
  communicationChannel,
4349
4134
  configure,
4350
4135
  createContextLogger,
4351
- createMinimalConfig,
4352
4136
  createServerHookContext,
4353
4137
  createToolCallContext,
4354
4138
  createWebhookContext,
@@ -4375,7 +4159,6 @@ export {
4375
4159
  isWorkflowDependency,
4376
4160
  loadConfig,
4377
4161
  report,
4378
- resolveConfig,
4379
4162
  resource,
4380
4163
  runWithConfig,
4381
4164
  safeParseConfig,