patchwork-os 0.2.0-alpha.2 → 0.2.0-alpha.22
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/README.bridge.md +6 -0
- package/README.md +40 -15
- package/deploy/bootstrap-vps.sh +184 -0
- package/dist/approvalHttp.d.ts +11 -2
- package/dist/approvalHttp.js +98 -10
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalQueue.d.ts +12 -1
- package/dist/approvalQueue.js +25 -3
- package/dist/approvalQueue.js.map +1 -1
- package/dist/automation.d.ts +20 -0
- package/dist/automation.js +35 -0
- package/dist/automation.js.map +1 -1
- package/dist/bridge.js +145 -23
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeToken.js +57 -19
- package/dist/bridgeToken.js.map +1 -1
- package/dist/claudeDriver.d.ts +3 -1
- package/dist/claudeDriver.js +48 -0
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +1 -1
- package/dist/claudeOrchestrator.js +14 -8
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/launchd.d.ts +2 -0
- package/dist/commands/launchd.js +94 -0
- package/dist/commands/launchd.js.map +1 -0
- package/dist/commands/recipe.d.ts +256 -0
- package/dist/commands/recipe.js +1313 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/config.d.ts +15 -2
- package/dist/config.js +94 -8
- package/dist/config.js.map +1 -1
- package/dist/connectors/baseConnector.d.ts +117 -0
- package/dist/connectors/baseConnector.js +213 -0
- package/dist/connectors/baseConnector.js.map +1 -0
- package/dist/connectors/confluence.d.ts +111 -0
- package/dist/connectors/confluence.js +406 -0
- package/dist/connectors/confluence.js.map +1 -0
- package/dist/connectors/fixtureLibrary.d.ts +21 -0
- package/dist/connectors/fixtureLibrary.js +70 -0
- package/dist/connectors/fixtureLibrary.js.map +1 -0
- package/dist/connectors/fixtureRecorder.d.ts +1 -0
- package/dist/connectors/fixtureRecorder.js +35 -0
- package/dist/connectors/fixtureRecorder.js.map +1 -0
- package/dist/connectors/github.d.ts +58 -8
- package/dist/connectors/github.js +312 -84
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gmail.d.ts +4 -1
- package/dist/connectors/gmail.js +93 -16
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.d.ts +60 -0
- package/dist/connectors/googleCalendar.js +345 -0
- package/dist/connectors/googleCalendar.js.map +1 -0
- package/dist/connectors/jira.d.ts +98 -0
- package/dist/connectors/jira.js +379 -0
- package/dist/connectors/jira.js.map +1 -0
- package/dist/connectors/linear.d.ts +117 -0
- package/dist/connectors/linear.js +239 -0
- package/dist/connectors/linear.js.map +1 -0
- package/dist/connectors/mcpClient.d.ts +56 -0
- package/dist/connectors/mcpClient.js +189 -0
- package/dist/connectors/mcpClient.js.map +1 -0
- package/dist/connectors/mcpOAuth.d.ts +84 -0
- package/dist/connectors/mcpOAuth.js +389 -0
- package/dist/connectors/mcpOAuth.js.map +1 -0
- package/dist/connectors/mockConnector.d.ts +28 -0
- package/dist/connectors/mockConnector.js +81 -0
- package/dist/connectors/mockConnector.js.map +1 -0
- package/dist/connectors/notion.d.ts +143 -0
- package/dist/connectors/notion.js +424 -0
- package/dist/connectors/notion.js.map +1 -0
- package/dist/connectors/sentry.d.ts +43 -0
- package/dist/connectors/sentry.js +188 -0
- package/dist/connectors/sentry.js.map +1 -0
- package/dist/connectors/slack.d.ts +50 -0
- package/dist/connectors/slack.js +324 -0
- package/dist/connectors/slack.js.map +1 -0
- package/dist/connectors/tokenStorage.d.ts +35 -0
- package/dist/connectors/tokenStorage.js +394 -0
- package/dist/connectors/tokenStorage.js.map +1 -0
- package/dist/connectors/zendesk.d.ts +104 -0
- package/dist/connectors/zendesk.js +424 -0
- package/dist/connectors/zendesk.js.map +1 -0
- package/dist/drivers/claude/api.d.ts +11 -0
- package/dist/drivers/claude/api.js +54 -0
- package/dist/drivers/claude/api.js.map +1 -0
- package/dist/drivers/claude/envSanitizer.d.ts +7 -0
- package/dist/drivers/claude/envSanitizer.js +18 -0
- package/dist/drivers/claude/envSanitizer.js.map +1 -0
- package/dist/drivers/claude/streamParser.d.ts +38 -0
- package/dist/drivers/claude/streamParser.js +34 -0
- package/dist/drivers/claude/streamParser.js.map +1 -0
- package/dist/drivers/claude/subprocess.d.ts +19 -0
- package/dist/drivers/claude/subprocess.js +216 -0
- package/dist/drivers/claude/subprocess.js.map +1 -0
- package/dist/drivers/claude/subprocessSettings.d.ts +9 -0
- package/dist/drivers/claude/subprocessSettings.js +55 -0
- package/dist/drivers/claude/subprocessSettings.js.map +1 -0
- package/dist/drivers/gemini/index.d.ts +18 -0
- package/dist/drivers/gemini/index.js +210 -0
- package/dist/drivers/gemini/index.js.map +1 -0
- package/dist/drivers/grok/index.d.ts +11 -0
- package/dist/drivers/grok/index.js +22 -0
- package/dist/drivers/grok/index.js.map +1 -0
- package/dist/drivers/index.d.ts +23 -0
- package/dist/drivers/index.js +31 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/openai/index.d.ts +24 -0
- package/dist/drivers/openai/index.js +110 -0
- package/dist/drivers/openai/index.js.map +1 -0
- package/dist/drivers/types.d.ts +72 -0
- package/dist/drivers/types.js +30 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/featureFlags.d.ts +73 -0
- package/dist/featureFlags.js +203 -0
- package/dist/featureFlags.js.map +1 -0
- package/dist/fp/automationInterpreter.js +1 -0
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationProgram.d.ts +1 -1
- package/dist/fp/automationProgram.js.map +1 -1
- package/dist/fp/policyParser.js +17 -0
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/index.js +543 -37
- package/dist/index.js.map +1 -1
- package/dist/installGuard.d.ts +25 -0
- package/dist/installGuard.js +48 -0
- package/dist/installGuard.js.map +1 -0
- package/dist/oauth.d.ts +4 -1
- package/dist/oauth.js +50 -14
- package/dist/oauth.js.map +1 -1
- package/dist/patchworkConfig.d.ts +9 -0
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/recipes/chainedRunner.d.ts +104 -0
- package/dist/recipes/chainedRunner.js +359 -0
- package/dist/recipes/chainedRunner.js.map +1 -0
- package/dist/recipes/dependencyGraph.d.ts +39 -0
- package/dist/recipes/dependencyGraph.js +199 -0
- package/dist/recipes/dependencyGraph.js.map +1 -0
- package/dist/recipes/legacyRecipeCompat.d.ts +1 -0
- package/dist/recipes/legacyRecipeCompat.js +97 -0
- package/dist/recipes/legacyRecipeCompat.js.map +1 -0
- package/dist/recipes/nestedRecipeStep.d.ts +58 -0
- package/dist/recipes/nestedRecipeStep.js +95 -0
- package/dist/recipes/nestedRecipeStep.js.map +1 -0
- package/dist/recipes/outputRegistry.d.ts +28 -0
- package/dist/recipes/outputRegistry.js +52 -0
- package/dist/recipes/outputRegistry.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +23 -7
- package/dist/recipes/scheduler.js +135 -41
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schemaGenerator.d.ts +28 -0
- package/dist/recipes/schemaGenerator.js +484 -0
- package/dist/recipes/schemaGenerator.js.map +1 -0
- package/dist/recipes/templateEngine.d.ts +62 -0
- package/dist/recipes/templateEngine.js +182 -0
- package/dist/recipes/templateEngine.js.map +1 -0
- package/dist/recipes/toolRegistry.d.ts +181 -0
- package/dist/recipes/toolRegistry.js +300 -0
- package/dist/recipes/toolRegistry.js.map +1 -0
- package/dist/recipes/tools/calendar.d.ts +6 -0
- package/dist/recipes/tools/calendar.js +61 -0
- package/dist/recipes/tools/calendar.js.map +1 -0
- package/dist/recipes/tools/confluence.d.ts +6 -0
- package/dist/recipes/tools/confluence.js +254 -0
- package/dist/recipes/tools/confluence.js.map +1 -0
- package/dist/recipes/tools/diagnostics.d.ts +6 -0
- package/dist/recipes/tools/diagnostics.js +36 -0
- package/dist/recipes/tools/diagnostics.js.map +1 -0
- package/dist/recipes/tools/file.d.ts +6 -0
- package/dist/recipes/tools/file.js +170 -0
- package/dist/recipes/tools/file.js.map +1 -0
- package/dist/recipes/tools/git.d.ts +6 -0
- package/dist/recipes/tools/git.js +63 -0
- package/dist/recipes/tools/git.js.map +1 -0
- package/dist/recipes/tools/github.d.ts +6 -0
- package/dist/recipes/tools/github.js +91 -0
- package/dist/recipes/tools/github.js.map +1 -0
- package/dist/recipes/tools/gmail.d.ts +6 -0
- package/dist/recipes/tools/gmail.js +210 -0
- package/dist/recipes/tools/gmail.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +18 -0
- package/dist/recipes/tools/index.js +21 -0
- package/dist/recipes/tools/index.js.map +1 -0
- package/dist/recipes/tools/linear.d.ts +6 -0
- package/dist/recipes/tools/linear.js +83 -0
- package/dist/recipes/tools/linear.js.map +1 -0
- package/dist/recipes/tools/notion.d.ts +6 -0
- package/dist/recipes/tools/notion.js +278 -0
- package/dist/recipes/tools/notion.js.map +1 -0
- package/dist/recipes/tools/slack.d.ts +6 -0
- package/dist/recipes/tools/slack.js +72 -0
- package/dist/recipes/tools/slack.js.map +1 -0
- package/dist/recipes/tools/zendesk.d.ts +6 -0
- package/dist/recipes/tools/zendesk.js +245 -0
- package/dist/recipes/tools/zendesk.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +79 -0
- package/dist/recipes/yamlRunner.js +612 -346
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +14 -1
- package/dist/recipesHttp.js +21 -4
- package/dist/recipesHttp.js.map +1 -1
- package/dist/riskTier.js +1 -0
- package/dist/riskTier.js.map +1 -1
- package/dist/runLog.d.ts +23 -0
- package/dist/runLog.js +56 -1
- package/dist/runLog.js.map +1 -1
- package/dist/server.d.ts +19 -1
- package/dist/server.js +682 -31
- package/dist/server.js.map +1 -1
- package/dist/streamableHttp.js +2 -0
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tools/addLinearComment.d.ts +55 -0
- package/dist/tools/addLinearComment.js +72 -0
- package/dist/tools/addLinearComment.js.map +1 -0
- package/dist/tools/bridgeDoctor.js +2 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/createLinearIssue.d.ts +84 -0
- package/dist/tools/createLinearIssue.js +146 -0
- package/dist/tools/createLinearIssue.js.map +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +4 -1
- package/dist/tools/ctxGetTaskContext.js +45 -2
- package/dist/tools/ctxGetTaskContext.js.map +1 -1
- package/dist/tools/fetchCalendarEvents.d.ts +94 -0
- package/dist/tools/fetchCalendarEvents.js +97 -0
- package/dist/tools/fetchCalendarEvents.js.map +1 -0
- package/dist/tools/fetchGithubIssue.d.ts +80 -0
- package/dist/tools/fetchGithubIssue.js +84 -0
- package/dist/tools/fetchGithubIssue.js.map +1 -0
- package/dist/tools/fetchGithubPR.d.ts +89 -0
- package/dist/tools/fetchGithubPR.js +96 -0
- package/dist/tools/fetchGithubPR.js.map +1 -0
- package/dist/tools/fetchLinearIssue.d.ts +112 -0
- package/dist/tools/fetchLinearIssue.js +129 -0
- package/dist/tools/fetchLinearIssue.js.map +1 -0
- package/dist/tools/fetchSentryIssue.d.ts +143 -0
- package/dist/tools/fetchSentryIssue.js +150 -0
- package/dist/tools/fetchSentryIssue.js.map +1 -0
- package/dist/tools/fetchSlackProfile.d.ts +43 -0
- package/dist/tools/fetchSlackProfile.js +46 -0
- package/dist/tools/fetchSlackProfile.js.map +1 -0
- package/dist/tools/getConnectorStatus.d.ts +58 -0
- package/dist/tools/getConnectorStatus.js +56 -0
- package/dist/tools/getConnectorStatus.js.map +1 -0
- package/dist/tools/github/actions.js +4 -2
- package/dist/tools/github/actions.js.map +1 -1
- package/dist/tools/github/composite.d.ts +339 -0
- package/dist/tools/github/composite.js +343 -0
- package/dist/tools/github/composite.js.map +1 -0
- package/dist/tools/github/index.d.ts +2 -1
- package/dist/tools/github/index.js +2 -1
- package/dist/tools/github/index.js.map +1 -1
- package/dist/tools/github/issues.js +8 -4
- package/dist/tools/github/issues.js.map +1 -1
- package/dist/tools/github/pr.d.ts +122 -0
- package/dist/tools/github/pr.js +195 -5
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/index.js +36 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/searchTools.js +1 -1
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/tools/slackListChannels.d.ts +65 -0
- package/dist/tools/slackListChannels.js +70 -0
- package/dist/tools/slackListChannels.js.map +1 -0
- package/dist/tools/slackPostMessage.d.ts +57 -0
- package/dist/tools/slackPostMessage.js +77 -0
- package/dist/tools/slackPostMessage.js.map +1 -0
- package/dist/tools/updateLinearIssue.d.ts +89 -0
- package/dist/tools/updateLinearIssue.js +117 -0
- package/dist/tools/updateLinearIssue.js.map +1 -0
- package/dist/transport.d.ts +7 -1
- package/dist/transport.js +85 -11
- package/dist/transport.js.map +1 -1
- package/package.json +4 -2
- package/scripts/start-all.sh +56 -19
- package/templates/automation-policies/recipe-authoring.json +25 -0
- package/templates/automation-policy.example.json +6 -0
- package/templates/co.patchwork-os.bridge.plist +34 -0
- package/templates/recipes/ctx-loop-test.yaml +75 -0
- package/templates/recipes/lint-on-save.yaml +1 -2
- package/templates/recipes/morning-brief-slack.yaml +57 -0
- package/templates/recipes/morning-brief.yaml +21 -5
- package/templates/recipes/sentry-to-linear.yaml +77 -0
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { WebSocket, WebSocketServer as WsServer } from "ws";
|
|
6
6
|
import { routeApprovalRequest } from "./approvalHttp.js";
|
|
7
7
|
import { getApprovalQueue } from "./approvalQueue.js";
|
|
8
|
+
import { saveBridgeConfigDriver } from "./config.js";
|
|
8
9
|
import { timingSafeStringEqual } from "./crypto.js";
|
|
9
10
|
import { renderDashboardHtml } from "./dashboard.js";
|
|
10
11
|
import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
|
|
@@ -93,14 +94,26 @@ export class Server extends EventEmitter {
|
|
|
93
94
|
saveRecipeFn = null;
|
|
94
95
|
/** Patchwork: set by bridge to query the recipe run audit log. */
|
|
95
96
|
runsFn = null;
|
|
97
|
+
/** Patchwork: set by bridge to fetch a single run by seq for the detail page. */
|
|
98
|
+
runDetailFn = null;
|
|
99
|
+
/** Patchwork: set by bridge to generate a dry-run plan for a recipe by name. */
|
|
100
|
+
runPlanFn = null;
|
|
96
101
|
/** Patchwork: set by bridge to launch a named recipe via the orchestrator. */
|
|
97
102
|
runRecipeFn = null;
|
|
98
103
|
/** Patchwork: admin-controlled managed settings path (highest rule precedence). */
|
|
99
104
|
managedSettingsPath = undefined;
|
|
105
|
+
/** Effective bridge config path to update when dashboard saves driver changes. */
|
|
106
|
+
bridgeConfigPath = undefined;
|
|
100
107
|
/** Patchwork: live approval gate level — mutated by POST /settings, read by bridge per-session setup. */
|
|
101
108
|
approvalGate = "off";
|
|
102
109
|
/** Patchwork: outbound webhook URL for approval notifications (from dashboard.webhookUrl in config). */
|
|
103
110
|
approvalWebhookUrl = undefined;
|
|
111
|
+
/** Patchwork: push relay service URL — when set, per-callId approval tokens are generated. */
|
|
112
|
+
pushServiceUrl = undefined;
|
|
113
|
+
/** Patchwork: bearer token for the push relay service. */
|
|
114
|
+
pushServiceToken = undefined;
|
|
115
|
+
/** Patchwork: public base URL of this bridge, embedded in push payloads as callback base. */
|
|
116
|
+
pushServiceBaseUrl = undefined;
|
|
104
117
|
/** Patchwork: approval decision audit callback wired to activityLog.recordEvent. */
|
|
105
118
|
onApprovalDecision = undefined;
|
|
106
119
|
/** Patchwork: set by bridge to match + fire webhook-triggered recipes. */
|
|
@@ -127,6 +140,7 @@ export class Server extends EventEmitter {
|
|
|
127
140
|
sessionDetailFn = null;
|
|
128
141
|
/** Set by bridge to handle POST /launch-quick-task — invokes launchQuickTask tool in-process. */
|
|
129
142
|
launchQuickTaskFn = null;
|
|
143
|
+
setRecipeEnabledFn = null;
|
|
130
144
|
/**
|
|
131
145
|
* Attach an OAuth 2.0 Authorization Server.
|
|
132
146
|
* When set, the bridge exposes:
|
|
@@ -355,6 +369,144 @@ export class Server extends EventEmitter {
|
|
|
355
369
|
res.end(JSON.stringify({ ok: true, v: PACKAGE_VERSION }));
|
|
356
370
|
return;
|
|
357
371
|
}
|
|
372
|
+
// ── Connector OAuth callbacks (unauthenticated — browser redirect from vendor) ──
|
|
373
|
+
if (parsedUrl.pathname === "/connections/github/callback" &&
|
|
374
|
+
req.method === "GET") {
|
|
375
|
+
void (async () => {
|
|
376
|
+
const { handleGithubCallback } = await import("./connectors/github.js");
|
|
377
|
+
const code = parsedUrl.searchParams.get("code");
|
|
378
|
+
const state = parsedUrl.searchParams.get("state");
|
|
379
|
+
const error = parsedUrl.searchParams.get("error");
|
|
380
|
+
const result = await handleGithubCallback(code, state, error);
|
|
381
|
+
res.writeHead(result.status, {
|
|
382
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
383
|
+
});
|
|
384
|
+
res.end(result.body);
|
|
385
|
+
})();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (parsedUrl.pathname === "/connections/linear/callback" &&
|
|
389
|
+
req.method === "GET") {
|
|
390
|
+
void (async () => {
|
|
391
|
+
const { handleLinearCallback } = await import("./connectors/linear.js");
|
|
392
|
+
const code = parsedUrl.searchParams.get("code");
|
|
393
|
+
const state = parsedUrl.searchParams.get("state");
|
|
394
|
+
const error = parsedUrl.searchParams.get("error");
|
|
395
|
+
const result = await handleLinearCallback(code, state, error);
|
|
396
|
+
res.writeHead(result.status, {
|
|
397
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
398
|
+
});
|
|
399
|
+
res.end(result.body);
|
|
400
|
+
})();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (parsedUrl.pathname === "/connections/sentry/callback" &&
|
|
404
|
+
req.method === "GET") {
|
|
405
|
+
void (async () => {
|
|
406
|
+
const { handleSentryCallback } = await import("./connectors/sentry.js");
|
|
407
|
+
const code = parsedUrl.searchParams.get("code");
|
|
408
|
+
const state = parsedUrl.searchParams.get("state");
|
|
409
|
+
const error = parsedUrl.searchParams.get("error");
|
|
410
|
+
const result = await handleSentryCallback(code, state, error);
|
|
411
|
+
res.writeHead(result.status, {
|
|
412
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
413
|
+
});
|
|
414
|
+
res.end(result.body);
|
|
415
|
+
})();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
|
|
419
|
+
req.method === "GET") {
|
|
420
|
+
void (async () => {
|
|
421
|
+
const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
|
|
422
|
+
const code = parsedUrl.searchParams.get("code");
|
|
423
|
+
const state = parsedUrl.searchParams.get("state");
|
|
424
|
+
const error = parsedUrl.searchParams.get("error");
|
|
425
|
+
const result = await handleCalendarCallback(code, state, error);
|
|
426
|
+
res.writeHead(result.status, {
|
|
427
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
428
|
+
});
|
|
429
|
+
res.end(result.body);
|
|
430
|
+
})();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (parsedUrl.pathname === "/connections/slack/callback" &&
|
|
434
|
+
req.method === "GET") {
|
|
435
|
+
void (async () => {
|
|
436
|
+
const { handleSlackCallback } = await import("./connectors/slack.js");
|
|
437
|
+
const code = parsedUrl.searchParams.get("code");
|
|
438
|
+
const state = parsedUrl.searchParams.get("state");
|
|
439
|
+
const error = parsedUrl.searchParams.get("error");
|
|
440
|
+
const result = await handleSlackCallback(code, state, error);
|
|
441
|
+
res.writeHead(result.status, {
|
|
442
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
443
|
+
});
|
|
444
|
+
res.end(result.body);
|
|
445
|
+
})();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (parsedUrl.pathname === "/connections/gmail/callback" &&
|
|
449
|
+
req.method === "GET") {
|
|
450
|
+
void (async () => {
|
|
451
|
+
const { handleGmailCallback } = await import("./connectors/gmail.js");
|
|
452
|
+
const code = parsedUrl.searchParams.get("code");
|
|
453
|
+
const state = parsedUrl.searchParams.get("state");
|
|
454
|
+
const error = parsedUrl.searchParams.get("error");
|
|
455
|
+
const result = await handleGmailCallback(code, state, error);
|
|
456
|
+
res.writeHead(result.status, {
|
|
457
|
+
"Content-Type": result.contentType ?? "text/html",
|
|
458
|
+
});
|
|
459
|
+
res.end(result.body);
|
|
460
|
+
})();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// ── /schemas/* — unauthenticated registry-derived JSON Schemas ────────
|
|
464
|
+
// Serves recipe.v1.json, dry-run-plan.v1.json, tools/<ns>.json so YAML-LSP
|
|
465
|
+
// editors can resolve `$schema:` headers against a running bridge. No
|
|
466
|
+
// secrets — schemas are generated from the tool registry.
|
|
467
|
+
if (parsedUrl.pathname?.startsWith("/schemas/") && req.method === "GET") {
|
|
468
|
+
try {
|
|
469
|
+
await import("./recipes/tools/index.js");
|
|
470
|
+
const { generateSchemaSet } = await import("./recipes/schemaGenerator.js");
|
|
471
|
+
const schemas = generateSchemaSet();
|
|
472
|
+
const rest = parsedUrl.pathname.slice("/schemas/".length);
|
|
473
|
+
let body;
|
|
474
|
+
if (rest === "recipe.v1.json") {
|
|
475
|
+
body = schemas.recipe;
|
|
476
|
+
}
|
|
477
|
+
else if (rest === "dry-run-plan.v1.json") {
|
|
478
|
+
body = schemas.dryRunPlan;
|
|
479
|
+
}
|
|
480
|
+
else if (rest.startsWith("tools/") && rest.endsWith(".json")) {
|
|
481
|
+
const ns = rest.slice("tools/".length, -".json".length);
|
|
482
|
+
body = schemas.namespaces[ns];
|
|
483
|
+
}
|
|
484
|
+
else if (rest === "" || rest === "index.json") {
|
|
485
|
+
body = {
|
|
486
|
+
recipe: "/schemas/recipe.v1.json",
|
|
487
|
+
dryRunPlan: "/schemas/dry-run-plan.v1.json",
|
|
488
|
+
tools: Object.keys(schemas.namespaces).map((ns) => `/schemas/tools/${ns}.json`),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (body === undefined) {
|
|
492
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
493
|
+
res.end(JSON.stringify({ error: `schema not found: ${rest}` }));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
res.writeHead(200, {
|
|
497
|
+
"Content-Type": "application/schema+json",
|
|
498
|
+
"Cache-Control": "public, max-age=60",
|
|
499
|
+
});
|
|
500
|
+
res.end(JSON.stringify(body, null, 2));
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
504
|
+
res.end(JSON.stringify({
|
|
505
|
+
error: err instanceof Error ? err.message : String(err),
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
358
510
|
// ── Bearer token authentication ───────────────────────────────────────
|
|
359
511
|
// All other HTTP endpoints require a valid Bearer token.
|
|
360
512
|
// Accepts either:
|
|
@@ -372,8 +524,13 @@ export class Server extends EventEmitter {
|
|
|
372
524
|
const oauthResolved = !isStaticToken && this.oauthServer
|
|
373
525
|
? this.oauthServer.resolveBearerToken(bearer)
|
|
374
526
|
: null;
|
|
527
|
+
// Phone-path: approve/reject with x-approval-token bypass bearer check.
|
|
528
|
+
// The token itself is validated inside routeApprovalRequest via queue.validateToken.
|
|
529
|
+
const isPhoneApprovalPath = req.method === "POST" &&
|
|
530
|
+
/^\/(approve|reject)\/[A-Za-z0-9-]+$/.test(parsedUrl.pathname) &&
|
|
531
|
+
!!req.headers["x-approval-token"];
|
|
375
532
|
// oauthResolved is the bridge token if the OAuth token is valid; null otherwise
|
|
376
|
-
if (!isStaticToken && !oauthResolved) {
|
|
533
|
+
if (!isStaticToken && !oauthResolved && !isPhoneApprovalPath) {
|
|
377
534
|
// RFC 6750: only include error= when a token was actually presented but invalid
|
|
378
535
|
const tokenPresented = bearer.length > 0;
|
|
379
536
|
const wwwAuth = this.oauthServer && this.oauthIssuerUrl
|
|
@@ -675,26 +832,124 @@ export class Server extends EventEmitter {
|
|
|
675
832
|
})();
|
|
676
833
|
return;
|
|
677
834
|
}
|
|
678
|
-
if (parsedUrl.pathname === "/connections/gmail
|
|
835
|
+
if (parsedUrl.pathname === "/connections/gmail" &&
|
|
836
|
+
req.method === "DELETE") {
|
|
837
|
+
void (async () => {
|
|
838
|
+
const { handleGmailDisconnect } = await import("./connectors/gmail.js");
|
|
839
|
+
const result = await handleGmailDisconnect();
|
|
840
|
+
res.writeHead(result.status, {
|
|
841
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
842
|
+
});
|
|
843
|
+
res.end(result.body);
|
|
844
|
+
})();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
if (parsedUrl.pathname === "/connections/gmail/test" &&
|
|
848
|
+
req.method === "POST") {
|
|
849
|
+
void (async () => {
|
|
850
|
+
const { handleGmailTest } = await import("./connectors/gmail.js");
|
|
851
|
+
const result = await handleGmailTest();
|
|
852
|
+
res.writeHead(result.status, {
|
|
853
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
854
|
+
});
|
|
855
|
+
res.end(result.body);
|
|
856
|
+
})();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
// ── GitHub MCP connector routes ─────────────────────────────────────
|
|
860
|
+
if (parsedUrl.pathname === "/connections/github/auth" &&
|
|
679
861
|
req.method === "GET") {
|
|
680
862
|
void (async () => {
|
|
681
|
-
const {
|
|
863
|
+
const { handleGithubAuthorize } = await import("./connectors/github.js");
|
|
864
|
+
const result = await handleGithubAuthorize();
|
|
865
|
+
if (result.redirect) {
|
|
866
|
+
res.writeHead(302, { Location: result.redirect });
|
|
867
|
+
res.end();
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
res.writeHead(result.status, {
|
|
871
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
872
|
+
});
|
|
873
|
+
res.end(result.body);
|
|
874
|
+
}
|
|
875
|
+
})();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (parsedUrl.pathname === "/connections/github/test" &&
|
|
879
|
+
req.method === "POST") {
|
|
880
|
+
void (async () => {
|
|
881
|
+
const { handleGithubTest } = await import("./connectors/github.js");
|
|
882
|
+
const result = await handleGithubTest();
|
|
883
|
+
res.writeHead(result.status, {
|
|
884
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
885
|
+
});
|
|
886
|
+
res.end(result.body);
|
|
887
|
+
})();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (parsedUrl.pathname === "/connections/github" &&
|
|
891
|
+
req.method === "DELETE") {
|
|
892
|
+
void (async () => {
|
|
893
|
+
const { handleGithubDisconnect } = await import("./connectors/github.js");
|
|
894
|
+
const result = await handleGithubDisconnect();
|
|
895
|
+
res.writeHead(result.status, {
|
|
896
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
897
|
+
});
|
|
898
|
+
res.end(result.body);
|
|
899
|
+
})();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
// ── Sentry MCP connector routes ─────────────────────────────────────
|
|
903
|
+
if (parsedUrl.pathname === "/connections/sentry/auth" &&
|
|
904
|
+
req.method === "GET") {
|
|
905
|
+
void (async () => {
|
|
906
|
+
const { handleSentryAuthorize } = await import("./connectors/sentry.js");
|
|
907
|
+
const result = await handleSentryAuthorize();
|
|
908
|
+
if (result.redirect) {
|
|
909
|
+
res.writeHead(302, { Location: result.redirect });
|
|
910
|
+
res.end();
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
res.writeHead(result.status, {
|
|
914
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
915
|
+
});
|
|
916
|
+
res.end(result.body);
|
|
917
|
+
}
|
|
918
|
+
})();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (parsedUrl.pathname === "/connections/sentry/callback" &&
|
|
922
|
+
req.method === "GET") {
|
|
923
|
+
void (async () => {
|
|
924
|
+
const { handleSentryCallback } = await import("./connectors/sentry.js");
|
|
682
925
|
const code = parsedUrl.searchParams.get("code");
|
|
683
926
|
const state = parsedUrl.searchParams.get("state");
|
|
684
927
|
const error = parsedUrl.searchParams.get("error");
|
|
685
|
-
const result = await
|
|
928
|
+
const result = await handleSentryCallback(code, state, error);
|
|
686
929
|
res.writeHead(result.status, {
|
|
687
|
-
"Content-Type": result.contentType ?? "
|
|
930
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
688
931
|
});
|
|
689
932
|
res.end(result.body);
|
|
690
933
|
})();
|
|
691
934
|
return;
|
|
692
935
|
}
|
|
693
|
-
if (parsedUrl.pathname === "/connections/
|
|
936
|
+
if (parsedUrl.pathname === "/connections/sentry/test" &&
|
|
937
|
+
req.method === "POST") {
|
|
938
|
+
void (async () => {
|
|
939
|
+
const { handleSentryTest } = await import("./connectors/sentry.js");
|
|
940
|
+
const result = await handleSentryTest();
|
|
941
|
+
res.writeHead(result.status, {
|
|
942
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
943
|
+
});
|
|
944
|
+
res.end(result.body);
|
|
945
|
+
})();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (parsedUrl.pathname === "/connections/sentry" &&
|
|
694
949
|
req.method === "DELETE") {
|
|
695
950
|
void (async () => {
|
|
696
|
-
const {
|
|
697
|
-
const result = await
|
|
951
|
+
const { handleSentryDisconnect } = await import("./connectors/sentry.js");
|
|
952
|
+
const result = await handleSentryDisconnect();
|
|
698
953
|
res.writeHead(result.status, {
|
|
699
954
|
"Content-Type": result.contentType ?? "application/json",
|
|
700
955
|
});
|
|
@@ -702,11 +957,45 @@ export class Server extends EventEmitter {
|
|
|
702
957
|
})();
|
|
703
958
|
return;
|
|
704
959
|
}
|
|
705
|
-
|
|
960
|
+
// ── Linear MCP connector routes ─────────────────────────────────────
|
|
961
|
+
if (parsedUrl.pathname === "/connections/linear/auth" &&
|
|
962
|
+
req.method === "GET") {
|
|
963
|
+
void (async () => {
|
|
964
|
+
const { handleLinearAuthorize } = await import("./connectors/linear.js");
|
|
965
|
+
const result = await handleLinearAuthorize();
|
|
966
|
+
if (result.redirect) {
|
|
967
|
+
res.writeHead(302, { Location: result.redirect });
|
|
968
|
+
res.end();
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
res.writeHead(result.status, {
|
|
972
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
973
|
+
});
|
|
974
|
+
res.end(result.body);
|
|
975
|
+
}
|
|
976
|
+
})();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (parsedUrl.pathname === "/connections/linear/callback" &&
|
|
980
|
+
req.method === "GET") {
|
|
981
|
+
void (async () => {
|
|
982
|
+
const { handleLinearCallback } = await import("./connectors/linear.js");
|
|
983
|
+
const code = parsedUrl.searchParams.get("code");
|
|
984
|
+
const state = parsedUrl.searchParams.get("state");
|
|
985
|
+
const error = parsedUrl.searchParams.get("error");
|
|
986
|
+
const result = await handleLinearCallback(code, state, error);
|
|
987
|
+
res.writeHead(result.status, {
|
|
988
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
989
|
+
});
|
|
990
|
+
res.end(result.body);
|
|
991
|
+
})();
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (parsedUrl.pathname === "/connections/linear/test" &&
|
|
706
995
|
req.method === "POST") {
|
|
707
996
|
void (async () => {
|
|
708
|
-
const {
|
|
709
|
-
const result = await
|
|
997
|
+
const { handleLinearTest } = await import("./connectors/linear.js");
|
|
998
|
+
const result = await handleLinearTest();
|
|
710
999
|
res.writeHead(result.status, {
|
|
711
1000
|
"Content-Type": result.contentType ?? "application/json",
|
|
712
1001
|
});
|
|
@@ -714,28 +1003,233 @@ export class Server extends EventEmitter {
|
|
|
714
1003
|
})();
|
|
715
1004
|
return;
|
|
716
1005
|
}
|
|
717
|
-
if (parsedUrl.pathname === "/connections/
|
|
1006
|
+
if (parsedUrl.pathname === "/connections/linear" &&
|
|
1007
|
+
req.method === "DELETE") {
|
|
1008
|
+
void (async () => {
|
|
1009
|
+
const { handleLinearDisconnect } = await import("./connectors/linear.js");
|
|
1010
|
+
const result = await handleLinearDisconnect();
|
|
1011
|
+
res.writeHead(result.status, {
|
|
1012
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1013
|
+
});
|
|
1014
|
+
res.end(result.body);
|
|
1015
|
+
})();
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
// ── Slack connector routes ──────────────────────────────────────
|
|
1019
|
+
if ((parsedUrl.pathname === "/connections/slack/auth" ||
|
|
1020
|
+
parsedUrl.pathname === "/connections/slack/authorize") &&
|
|
1021
|
+
req.method === "GET") {
|
|
1022
|
+
const { handleSlackAuthorize } = await import("./connectors/slack.js");
|
|
1023
|
+
const result = handleSlackAuthorize();
|
|
1024
|
+
if (result.redirect) {
|
|
1025
|
+
res.writeHead(302, { Location: result.redirect });
|
|
1026
|
+
res.end();
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
res.writeHead(result.status, {
|
|
1030
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1031
|
+
});
|
|
1032
|
+
res.end(result.body);
|
|
1033
|
+
}
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (parsedUrl.pathname === "/connections/slack/test" &&
|
|
718
1037
|
req.method === "POST") {
|
|
719
1038
|
void (async () => {
|
|
720
|
-
const {
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1039
|
+
const { handleSlackTest } = await import("./connectors/slack.js");
|
|
1040
|
+
const result = await handleSlackTest();
|
|
1041
|
+
res.writeHead(result.status, {
|
|
1042
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1043
|
+
});
|
|
1044
|
+
res.end(result.body);
|
|
1045
|
+
})();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (parsedUrl.pathname === "/connections/slack" &&
|
|
1049
|
+
req.method === "DELETE") {
|
|
1050
|
+
const { handleSlackDisconnect } = await import("./connectors/slack.js");
|
|
1051
|
+
const result = handleSlackDisconnect();
|
|
1052
|
+
res.writeHead(result.status, {
|
|
1053
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1054
|
+
});
|
|
1055
|
+
res.end(result.body);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
// ── Notion routes ──────────────────────────────────────────────
|
|
1059
|
+
if (parsedUrl.pathname === "/connections/notion/connect" &&
|
|
1060
|
+
req.method === "POST") {
|
|
1061
|
+
const chunks = [];
|
|
1062
|
+
req.on("data", (c) => chunks.push(c));
|
|
1063
|
+
req.on("end", () => {
|
|
1064
|
+
void (async () => {
|
|
1065
|
+
const { handleNotionConnect } = await import("./connectors/notion.js");
|
|
1066
|
+
const result = await handleNotionConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1067
|
+
res.writeHead(result.status, {
|
|
1068
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1069
|
+
});
|
|
1070
|
+
res.end(result.body);
|
|
1071
|
+
})();
|
|
1072
|
+
});
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (parsedUrl.pathname === "/connections/notion/test" &&
|
|
1076
|
+
req.method === "POST") {
|
|
1077
|
+
void (async () => {
|
|
1078
|
+
const { handleNotionTest } = await import("./connectors/notion.js");
|
|
1079
|
+
const result = await handleNotionTest();
|
|
1080
|
+
res.writeHead(result.status, {
|
|
1081
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1082
|
+
});
|
|
1083
|
+
res.end(result.body);
|
|
1084
|
+
})();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (parsedUrl.pathname === "/connections/notion" &&
|
|
1088
|
+
req.method === "DELETE") {
|
|
1089
|
+
const { handleNotionDisconnect } = await import("./connectors/notion.js");
|
|
1090
|
+
const result = handleNotionDisconnect();
|
|
1091
|
+
res.writeHead(result.status, {
|
|
1092
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1093
|
+
});
|
|
1094
|
+
res.end(result.body);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
// ── Confluence routes ───────────────────────────────────────────
|
|
1098
|
+
if (parsedUrl.pathname === "/connections/confluence/connect" &&
|
|
1099
|
+
req.method === "POST") {
|
|
1100
|
+
const chunks = [];
|
|
1101
|
+
req.on("data", (c) => chunks.push(c));
|
|
1102
|
+
req.on("end", () => {
|
|
1103
|
+
void (async () => {
|
|
1104
|
+
const { handleConfluenceConnect } = await import("./connectors/confluence.js");
|
|
1105
|
+
const result = await handleConfluenceConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1106
|
+
res.writeHead(result.status, {
|
|
1107
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1108
|
+
});
|
|
1109
|
+
res.end(result.body);
|
|
1110
|
+
})();
|
|
1111
|
+
});
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (parsedUrl.pathname === "/connections/confluence/test" &&
|
|
1115
|
+
req.method === "POST") {
|
|
1116
|
+
void (async () => {
|
|
1117
|
+
const { handleConfluenceTest } = await import("./connectors/confluence.js");
|
|
1118
|
+
const result = await handleConfluenceTest();
|
|
1119
|
+
res.writeHead(result.status, {
|
|
1120
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1121
|
+
});
|
|
1122
|
+
res.end(result.body);
|
|
1123
|
+
})();
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (parsedUrl.pathname === "/connections/confluence" &&
|
|
1127
|
+
req.method === "DELETE") {
|
|
1128
|
+
const { handleConfluenceDisconnect } = await import("./connectors/confluence.js");
|
|
1129
|
+
const result = handleConfluenceDisconnect();
|
|
1130
|
+
res.writeHead(result.status, {
|
|
1131
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1132
|
+
});
|
|
1133
|
+
res.end(result.body);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
// ── Zendesk routes ──────────────────────────────────────────────
|
|
1137
|
+
if (parsedUrl.pathname === "/connections/zendesk/connect" &&
|
|
1138
|
+
req.method === "POST") {
|
|
1139
|
+
const chunks = [];
|
|
1140
|
+
req.on("data", (c) => chunks.push(c));
|
|
1141
|
+
req.on("end", () => {
|
|
1142
|
+
void (async () => {
|
|
1143
|
+
const { handleZendeskConnect } = await import("./connectors/zendesk.js");
|
|
1144
|
+
const result = await handleZendeskConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1145
|
+
res.writeHead(result.status, {
|
|
1146
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1147
|
+
});
|
|
1148
|
+
res.end(result.body);
|
|
1149
|
+
})();
|
|
1150
|
+
});
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (parsedUrl.pathname === "/connections/zendesk/test" &&
|
|
1154
|
+
req.method === "POST") {
|
|
1155
|
+
void (async () => {
|
|
1156
|
+
const { handleZendeskTest } = await import("./connectors/zendesk.js");
|
|
1157
|
+
const result = await handleZendeskTest();
|
|
1158
|
+
res.writeHead(result.status, {
|
|
1159
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1160
|
+
});
|
|
1161
|
+
res.end(result.body);
|
|
1162
|
+
})();
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (parsedUrl.pathname === "/connections/zendesk" &&
|
|
1166
|
+
req.method === "DELETE") {
|
|
1167
|
+
const { handleZendeskDisconnect } = await import("./connectors/zendesk.js");
|
|
1168
|
+
const result = handleZendeskDisconnect();
|
|
1169
|
+
res.writeHead(result.status, {
|
|
1170
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1171
|
+
});
|
|
1172
|
+
res.end(result.body);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
// ── Google Calendar routes ──────────────────────────────────────
|
|
1176
|
+
if (parsedUrl.pathname === "/connections/google-calendar/auth" &&
|
|
1177
|
+
req.method === "GET") {
|
|
1178
|
+
void (async () => {
|
|
1179
|
+
const { handleCalendarAuthRedirect } = await import("./connectors/googleCalendar.js");
|
|
1180
|
+
const result = handleCalendarAuthRedirect();
|
|
1181
|
+
if (result.redirect) {
|
|
1182
|
+
res.writeHead(302, { Location: result.redirect });
|
|
1183
|
+
res.end();
|
|
728
1184
|
}
|
|
729
1185
|
else {
|
|
730
|
-
res.writeHead(
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}));
|
|
1186
|
+
res.writeHead(result.status, {
|
|
1187
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1188
|
+
});
|
|
1189
|
+
res.end(result.body);
|
|
735
1190
|
}
|
|
736
1191
|
})();
|
|
737
1192
|
return;
|
|
738
1193
|
}
|
|
1194
|
+
if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
|
|
1195
|
+
req.method === "GET") {
|
|
1196
|
+
void (async () => {
|
|
1197
|
+
const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
|
|
1198
|
+
const code = parsedUrl.searchParams.get("code");
|
|
1199
|
+
const state = parsedUrl.searchParams.get("state");
|
|
1200
|
+
const error = parsedUrl.searchParams.get("error");
|
|
1201
|
+
const result = await handleCalendarCallback(code, state, error);
|
|
1202
|
+
res.writeHead(result.status, {
|
|
1203
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1204
|
+
});
|
|
1205
|
+
res.end(result.body);
|
|
1206
|
+
})();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (parsedUrl.pathname === "/connections/google-calendar/test" &&
|
|
1210
|
+
req.method === "POST") {
|
|
1211
|
+
void (async () => {
|
|
1212
|
+
const { handleCalendarTest } = await import("./connectors/googleCalendar.js");
|
|
1213
|
+
const result = await handleCalendarTest();
|
|
1214
|
+
res.writeHead(result.status, {
|
|
1215
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1216
|
+
});
|
|
1217
|
+
res.end(result.body);
|
|
1218
|
+
})();
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (parsedUrl.pathname === "/connections/google-calendar" &&
|
|
1222
|
+
req.method === "DELETE") {
|
|
1223
|
+
void (async () => {
|
|
1224
|
+
const { handleCalendarDisconnect } = await import("./connectors/googleCalendar.js");
|
|
1225
|
+
const result = await handleCalendarDisconnect();
|
|
1226
|
+
res.writeHead(result.status, {
|
|
1227
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1228
|
+
});
|
|
1229
|
+
res.end(result.body);
|
|
1230
|
+
})();
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
739
1233
|
// ── Inbox routes ────────────────────────────────────────────────────
|
|
740
1234
|
if (parsedUrl.pathname === "/inbox" && req.method === "GET") {
|
|
741
1235
|
void (async () => {
|
|
@@ -831,6 +1325,11 @@ export class Server extends EventEmitter {
|
|
|
831
1325
|
const body = Buffer.concat(chunks).toString("utf-8");
|
|
832
1326
|
const parsed = JSON.parse(body || "{}");
|
|
833
1327
|
const name = parsed.name;
|
|
1328
|
+
const vars = parsed.vars &&
|
|
1329
|
+
typeof parsed.vars === "object" &&
|
|
1330
|
+
!Array.isArray(parsed.vars)
|
|
1331
|
+
? parsed.vars
|
|
1332
|
+
: undefined;
|
|
834
1333
|
if (typeof name !== "string" || !name) {
|
|
835
1334
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
836
1335
|
res.end(JSON.stringify({ ok: false, error: "name required" }));
|
|
@@ -844,7 +1343,7 @@ export class Server extends EventEmitter {
|
|
|
844
1343
|
}));
|
|
845
1344
|
return;
|
|
846
1345
|
}
|
|
847
|
-
const result = await this.runRecipeFn(name);
|
|
1346
|
+
const result = await this.runRecipeFn(name, vars);
|
|
848
1347
|
res.writeHead(result.ok ? 200 : 400, {
|
|
849
1348
|
"Content-Type": "application/json",
|
|
850
1349
|
});
|
|
@@ -886,6 +1385,62 @@ export class Server extends EventEmitter {
|
|
|
886
1385
|
}
|
|
887
1386
|
return;
|
|
888
1387
|
}
|
|
1388
|
+
// GET /runs/:seq — single run detail (includes stepResults if present)
|
|
1389
|
+
const runDetailMatch = req.method === "GET"
|
|
1390
|
+
? /^\/runs\/(\d+)$/.exec(parsedUrl.pathname)
|
|
1391
|
+
: null;
|
|
1392
|
+
if (runDetailMatch?.[1]) {
|
|
1393
|
+
const seq = Number.parseInt(runDetailMatch[1], 10);
|
|
1394
|
+
try {
|
|
1395
|
+
const run = this.runDetailFn?.(seq) ?? null;
|
|
1396
|
+
if (!run) {
|
|
1397
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1398
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1402
|
+
res.end(JSON.stringify({ run }));
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
catch (err) {
|
|
1406
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1407
|
+
res.end(JSON.stringify({
|
|
1408
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1409
|
+
}));
|
|
1410
|
+
}
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
// GET /runs/:seq/plan — dry-run plan for the recipe that produced this run
|
|
1414
|
+
const runPlanMatch = req.method === "GET"
|
|
1415
|
+
? /^\/runs\/(\d+)\/plan$/.exec(parsedUrl.pathname)
|
|
1416
|
+
: null;
|
|
1417
|
+
if (runPlanMatch?.[1]) {
|
|
1418
|
+
const seq = Number.parseInt(runPlanMatch[1], 10);
|
|
1419
|
+
try {
|
|
1420
|
+
const run = this.runDetailFn?.(seq) ?? null;
|
|
1421
|
+
if (!run) {
|
|
1422
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1423
|
+
res.end(JSON.stringify({ error: "run_not_found" }));
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (!this.runPlanFn) {
|
|
1427
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1428
|
+
res.end(JSON.stringify({ error: "plan_unavailable" }));
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const recipeName = run["recipeName"];
|
|
1432
|
+
const plan = await this.runPlanFn(recipeName);
|
|
1433
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1434
|
+
res.end(JSON.stringify({ plan }));
|
|
1435
|
+
}
|
|
1436
|
+
catch (err) {
|
|
1437
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1438
|
+
const status = msg.includes("not found") || msg.includes("ENOENT") ? 404 : 500;
|
|
1439
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1440
|
+
res.end(JSON.stringify({ error: msg }));
|
|
1441
|
+
}
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
889
1444
|
if (req.url === "/recipes" && req.method === "POST") {
|
|
890
1445
|
const chunks = [];
|
|
891
1446
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -919,6 +1474,40 @@ export class Server extends EventEmitter {
|
|
|
919
1474
|
});
|
|
920
1475
|
return;
|
|
921
1476
|
}
|
|
1477
|
+
const recipePatchMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
|
|
1478
|
+
if (recipePatchMatch && req.method === "PATCH") {
|
|
1479
|
+
const name = decodeURIComponent(recipePatchMatch[1]);
|
|
1480
|
+
const chunks = [];
|
|
1481
|
+
req.on("data", (c) => chunks.push(c));
|
|
1482
|
+
req.on("end", () => {
|
|
1483
|
+
try {
|
|
1484
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
1485
|
+
if (typeof body.enabled !== "boolean") {
|
|
1486
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1487
|
+
res.end(JSON.stringify({
|
|
1488
|
+
ok: false,
|
|
1489
|
+
error: "enabled (boolean) required",
|
|
1490
|
+
}));
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (!this.setRecipeEnabledFn) {
|
|
1494
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1495
|
+
res.end(JSON.stringify({ ok: false, error: "Not available" }));
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const result = this.setRecipeEnabledFn(name, body.enabled);
|
|
1499
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
1500
|
+
"Content-Type": "application/json",
|
|
1501
|
+
});
|
|
1502
|
+
res.end(JSON.stringify(result));
|
|
1503
|
+
}
|
|
1504
|
+
catch {
|
|
1505
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1506
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
922
1511
|
if (req.url === "/recipes" && req.method === "GET") {
|
|
923
1512
|
try {
|
|
924
1513
|
const data = this.recipesFn?.() ?? { recipesDir: null, recipes: [] };
|
|
@@ -979,8 +1568,11 @@ export class Server extends EventEmitter {
|
|
|
979
1568
|
req.on("end", () => {
|
|
980
1569
|
try {
|
|
981
1570
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
982
|
-
const
|
|
983
|
-
|
|
1571
|
+
const hasWebhookUpdate = body.webhookUrl !== undefined;
|
|
1572
|
+
const raw = hasWebhookUpdate
|
|
1573
|
+
? (body.webhookUrl?.trim() ?? "")
|
|
1574
|
+
: undefined;
|
|
1575
|
+
if (raw !== undefined && raw !== "" && !/^https:\/\/.+/.test(raw)) {
|
|
984
1576
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
985
1577
|
res.end(JSON.stringify({ error: "webhookUrl must be HTTPS" }));
|
|
986
1578
|
return;
|
|
@@ -1002,16 +1594,69 @@ export class Server extends EventEmitter {
|
|
|
1002
1594
|
port: cfg.dashboard?.port ?? 3000,
|
|
1003
1595
|
requireApproval: cfg.dashboard?.requireApproval ?? ["high"],
|
|
1004
1596
|
pushNotifications: cfg.dashboard?.pushNotifications ?? false,
|
|
1005
|
-
webhookUrl:
|
|
1597
|
+
webhookUrl: hasWebhookUpdate
|
|
1598
|
+
? raw || undefined
|
|
1599
|
+
: cfg.dashboard?.webhookUrl,
|
|
1006
1600
|
};
|
|
1007
1601
|
if (gateRaw !== undefined) {
|
|
1008
1602
|
cfg.approvalGate = gateRaw;
|
|
1009
1603
|
this.approvalGate = gateRaw;
|
|
1010
1604
|
}
|
|
1605
|
+
const driverRaw = body.driver;
|
|
1606
|
+
if (driverRaw !== undefined) {
|
|
1607
|
+
const validDrivers = [
|
|
1608
|
+
"subprocess",
|
|
1609
|
+
"api",
|
|
1610
|
+
"openai",
|
|
1611
|
+
"grok",
|
|
1612
|
+
"gemini",
|
|
1613
|
+
"none",
|
|
1614
|
+
];
|
|
1615
|
+
if (!validDrivers.includes(driverRaw)) {
|
|
1616
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1617
|
+
res.end(JSON.stringify({
|
|
1618
|
+
error: `driver must be one of: ${validDrivers.join(", ")}`,
|
|
1619
|
+
}));
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const driver = driverRaw;
|
|
1623
|
+
cfg.driver = driver;
|
|
1624
|
+
saveBridgeConfigDriver(driver, this.bridgeConfigPath);
|
|
1625
|
+
}
|
|
1626
|
+
if (body.apiKey) {
|
|
1627
|
+
const { provider, key } = body.apiKey;
|
|
1628
|
+
const validProviders = ["anthropic", "openai", "google", "xai"];
|
|
1629
|
+
if (!validProviders.includes(provider) ||
|
|
1630
|
+
typeof key !== "string") {
|
|
1631
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1632
|
+
res.end(JSON.stringify({ error: "Invalid apiKey provider or key" }));
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
cfg.apiKeys = { ...cfg.apiKeys, [provider]: key || undefined };
|
|
1636
|
+
}
|
|
1011
1637
|
savePatchworkConfig(cfg, configPath);
|
|
1012
|
-
|
|
1638
|
+
if (hasWebhookUpdate) {
|
|
1639
|
+
this.approvalWebhookUrl = raw || undefined;
|
|
1640
|
+
}
|
|
1641
|
+
if (body.pushServiceUrl !== undefined) {
|
|
1642
|
+
const pushUrl = body.pushServiceUrl.trim();
|
|
1643
|
+
if (pushUrl && !pushUrl.startsWith("https://")) {
|
|
1644
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1645
|
+
res.end(JSON.stringify({ error: "pushServiceUrl must be HTTPS" }));
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
this.pushServiceUrl = pushUrl || undefined;
|
|
1649
|
+
}
|
|
1650
|
+
if (body.pushServiceToken !== undefined) {
|
|
1651
|
+
this.pushServiceToken = body.pushServiceToken.trim() || undefined;
|
|
1652
|
+
}
|
|
1653
|
+
if (body.pushServiceBaseUrl !== undefined) {
|
|
1654
|
+
this.pushServiceBaseUrl =
|
|
1655
|
+
body.pushServiceBaseUrl.trim() || undefined;
|
|
1656
|
+
}
|
|
1657
|
+
const restartRequired = driverRaw !== undefined || body.apiKey !== undefined;
|
|
1013
1658
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1014
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1659
|
+
res.end(JSON.stringify({ ok: true, restartRequired }));
|
|
1015
1660
|
}
|
|
1016
1661
|
catch (err) {
|
|
1017
1662
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -1100,6 +1745,7 @@ export class Server extends EventEmitter {
|
|
|
1100
1745
|
path: parsedUrl.pathname,
|
|
1101
1746
|
body: parsedBody,
|
|
1102
1747
|
query: parsedUrl.searchParams,
|
|
1748
|
+
approvalToken: req.headers["x-approval-token"],
|
|
1103
1749
|
}, {
|
|
1104
1750
|
queue: getApprovalQueue(),
|
|
1105
1751
|
workspace: process.cwd(),
|
|
@@ -1107,6 +1753,9 @@ export class Server extends EventEmitter {
|
|
|
1107
1753
|
onDecision: this.onApprovalDecision,
|
|
1108
1754
|
webhookUrl: this.approvalWebhookUrl,
|
|
1109
1755
|
approvalGate: this.approvalGate,
|
|
1756
|
+
pushServiceUrl: this.pushServiceUrl,
|
|
1757
|
+
pushServiceToken: this.pushServiceToken,
|
|
1758
|
+
pushServiceBaseUrl: this.pushServiceBaseUrl,
|
|
1110
1759
|
});
|
|
1111
1760
|
res.writeHead(result.status, {
|
|
1112
1761
|
"Content-Type": "application/json",
|
|
@@ -1305,6 +1954,8 @@ export class Server extends EventEmitter {
|
|
|
1305
1954
|
ws.on("error", (err) => {
|
|
1306
1955
|
this.logger.error(`WebSocket client error: ${err.message}`);
|
|
1307
1956
|
});
|
|
1957
|
+
ws.remoteAddr =
|
|
1958
|
+
req.socket.remoteAddress;
|
|
1308
1959
|
this.emit("connection", ws);
|
|
1309
1960
|
});
|
|
1310
1961
|
}
|