@hellcoder/companion 0.96.0
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/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// ─── Linear Agent Interaction SDK Routes ─────────────────────────────────────
|
|
2
|
+
// Webhook endpoint for AgentSessionEvent + OAuth callback for app installation.
|
|
3
|
+
// The webhook route and OAuth callback are registered BEFORE auth middleware —
|
|
4
|
+
// Linear handles its own signature verification via HMAC-SHA256 and the OAuth
|
|
5
|
+
// callback is a redirect from Linear's authorization flow (no auth token in URL).
|
|
6
|
+
|
|
7
|
+
import type { Hono } from "hono";
|
|
8
|
+
import type { LinearAgentBridge } from "../linear-agent-bridge.js";
|
|
9
|
+
import * as linearAgent from "../linear-agent.js";
|
|
10
|
+
import type { AgentSessionEventPayload } from "../linear-agent.js";
|
|
11
|
+
import * as agentStore from "../agent-store.js";
|
|
12
|
+
import * as staging from "../linear-staging.js";
|
|
13
|
+
import { getSettings, updateSettings } from "../settings-manager.js";
|
|
14
|
+
import { findOAuthConnectionByClientId } from "../linear-oauth-connections.js";
|
|
15
|
+
|
|
16
|
+
function sanitizeLogField(value: string | undefined): string {
|
|
17
|
+
return (value || "unknown").replace(/[\r\n\t]/g, "_").slice(0, 128);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register the Linear Agent SDK pre-auth routes (before auth middleware).
|
|
22
|
+
* Includes the webhook (HMAC-SHA256 verified) and the OAuth callback
|
|
23
|
+
* (redirect from Linear — user has no auth token in the URL).
|
|
24
|
+
*/
|
|
25
|
+
export function registerLinearAgentWebhookRoute(
|
|
26
|
+
api: Hono,
|
|
27
|
+
bridge: LinearAgentBridge,
|
|
28
|
+
): void {
|
|
29
|
+
// Webhook endpoint — verified via HMAC-SHA256 signature (per-agent lookup)
|
|
30
|
+
api.post("/linear/agent-webhook", async (c) => {
|
|
31
|
+
const rawBody = await c.req.text();
|
|
32
|
+
const signature = c.req.header("linear-signature") ?? c.req.header("x-linear-signature");
|
|
33
|
+
|
|
34
|
+
// Parse payload first to extract oauthClientId for agent lookup
|
|
35
|
+
let payload: AgentSessionEventPayload;
|
|
36
|
+
try {
|
|
37
|
+
payload = JSON.parse(rawBody) as AgentSessionEventPayload;
|
|
38
|
+
} catch {
|
|
39
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Only handle AgentSessionEvent
|
|
43
|
+
if (payload.type !== "AgentSessionEvent") {
|
|
44
|
+
return c.json({ ok: true, ignored: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const action = sanitizeLogField(payload.action);
|
|
48
|
+
const oauthClientId = sanitizeLogField(payload.oauthClientId);
|
|
49
|
+
const linearSessionId = sanitizeLogField(payload.agentSession?.id);
|
|
50
|
+
|
|
51
|
+
// Look up credentials: first try OAuth connection (new model), then inline (legacy)
|
|
52
|
+
const oauthConn = payload.oauthClientId ? findOAuthConnectionByClientId(payload.oauthClientId) : null;
|
|
53
|
+
let webhookSecret: string;
|
|
54
|
+
let agent: ReturnType<typeof agentStore.listAgents>[number] | undefined;
|
|
55
|
+
|
|
56
|
+
if (oauthConn) {
|
|
57
|
+
// New model: find agent referencing this connection
|
|
58
|
+
webhookSecret = oauthConn.webhookSecret;
|
|
59
|
+
agent = agentStore.listAgents().find(
|
|
60
|
+
(a) => a.enabled && a.triggers?.linear?.enabled
|
|
61
|
+
&& a.triggers.linear.oauthConnectionId === oauthConn.id,
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
// Legacy: find agent by inline oauthClientId
|
|
65
|
+
agent = agentStore.listAgents().find(
|
|
66
|
+
(a) => a.enabled && a.triggers?.linear?.enabled
|
|
67
|
+
&& a.triggers.linear.oauthClientId === payload.oauthClientId,
|
|
68
|
+
);
|
|
69
|
+
webhookSecret = agent?.triggers?.linear?.webhookSecret || "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!agent) {
|
|
73
|
+
console.error(
|
|
74
|
+
`[linear-agent-routes] No agent found for oauthClientId: ${oauthClientId} action=${action} sessionId=${linearSessionId}`,
|
|
75
|
+
);
|
|
76
|
+
return c.json({ error: "No agent configured for this OAuth client" }, 404);
|
|
77
|
+
}
|
|
78
|
+
if (!linearAgent.verifyWebhookSignature(webhookSecret, rawBody, signature ?? null)) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[linear-agent-routes] Invalid webhook signature for oauthClientId: ${oauthClientId} action=${action} sessionId=${linearSessionId} agentId=${agent.id} signature=${signature ? "present" : "missing"}`,
|
|
81
|
+
);
|
|
82
|
+
return c.json({ error: "Invalid signature" }, 401);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(
|
|
86
|
+
`[linear-agent-routes] Accepted AgentSessionEvent action=${action} sessionId=${linearSessionId} oauthClientId=${oauthClientId} agentId=${agent.id}`,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Dispatch asynchronously — must return 200 within 5s
|
|
90
|
+
bridge.handleEvent(payload).catch((err) => {
|
|
91
|
+
console.error("[linear-agent-routes] Error handling event:", err);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return c.json({ ok: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// OAuth callback — redirect from Linear after authorization.
|
|
98
|
+
// Pre-auth because the browser lands here from Linear with no Companion auth token.
|
|
99
|
+
// Protected by the OAuth `state` nonce (CSRF prevention).
|
|
100
|
+
api.get("/linear/oauth/callback", async (c) => {
|
|
101
|
+
const code = c.req.query("code");
|
|
102
|
+
const state = c.req.query("state");
|
|
103
|
+
const error = c.req.query("error");
|
|
104
|
+
|
|
105
|
+
if (error) {
|
|
106
|
+
return c.redirect(`/#/settings/linear?oauth_error=${encodeURIComponent(error)}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!code) {
|
|
110
|
+
return c.redirect("/#/settings/linear?oauth_error=no_code");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validate the state nonce to prevent CSRF
|
|
114
|
+
const stateResult = linearAgent.validateOAuthState(state);
|
|
115
|
+
if (!stateResult.valid) {
|
|
116
|
+
return c.redirect("/#/settings/linear?oauth_error=invalid_state");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Determine redirect target — validate returnTo is a safe relative hash-router path
|
|
120
|
+
// to prevent open redirects (state passes through the browser and could be tampered with)
|
|
121
|
+
const rawReturnTo = stateResult.returnTo;
|
|
122
|
+
const redirectBase = (rawReturnTo && /^\/?#\//.test(rawReturnTo)) ? rawReturnTo : "/#/settings/linear";
|
|
123
|
+
|
|
124
|
+
// Build redirect URI (must match what was sent in the authorize request)
|
|
125
|
+
const settings = getSettings();
|
|
126
|
+
const baseUrl = settings.publicUrl || `http://localhost:${process.env.PORT || 3456}`;
|
|
127
|
+
const redirectUri = `${baseUrl}/api/linear/oauth/callback`;
|
|
128
|
+
|
|
129
|
+
// Determine which credentials to use for token exchange:
|
|
130
|
+
// 1. OAuth connection (new model) — connectionId in state
|
|
131
|
+
// 2. Staging slot (wizard flow) — stagingId in state
|
|
132
|
+
// 3. Global settings (legacy fallback)
|
|
133
|
+
const { getOAuthConnection, updateOAuthConnection } = await import("../linear-oauth-connections.js");
|
|
134
|
+
const oauthConnection = stateResult.connectionId ? getOAuthConnection(stateResult.connectionId) : null;
|
|
135
|
+
const stagingSlot = stateResult.stagingId ? staging.getSlot(stateResult.stagingId) : null;
|
|
136
|
+
|
|
137
|
+
// If a connectionId was in the state but the connection is gone, fail
|
|
138
|
+
if (stateResult.connectionId && !oauthConnection) {
|
|
139
|
+
return c.redirect(`${redirectBase}?oauth_error=${encodeURIComponent("connection_not_found")}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If a stagingId was in the state but the slot is gone (expired/deleted), don't silently
|
|
143
|
+
// fall back to global settings — that would use the wrong OAuth app's credentials
|
|
144
|
+
if (stateResult.stagingId && !stagingSlot) {
|
|
145
|
+
return c.redirect(`${redirectBase}?oauth_error=${encodeURIComponent("staging_slot_expired")}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const clientId = oauthConnection?.oauthClientId || stagingSlot?.clientId || settings.linearOAuthClientId;
|
|
149
|
+
const clientSecret = oauthConnection?.oauthClientSecret || stagingSlot?.clientSecret || settings.linearOAuthClientSecret;
|
|
150
|
+
|
|
151
|
+
const tokens = await linearAgent.exchangeCodeForTokens(
|
|
152
|
+
{ clientId, clientSecret },
|
|
153
|
+
code,
|
|
154
|
+
redirectUri,
|
|
155
|
+
);
|
|
156
|
+
if (!tokens) {
|
|
157
|
+
return c.redirect(`${redirectBase}?oauth_error=token_exchange_failed`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Persist tokens to the appropriate store
|
|
161
|
+
if (stateResult.connectionId && oauthConnection) {
|
|
162
|
+
// New model: store tokens directly in the OAuth connection
|
|
163
|
+
updateOAuthConnection(stateResult.connectionId, {
|
|
164
|
+
accessToken: tokens.accessToken,
|
|
165
|
+
refreshToken: tokens.refreshToken,
|
|
166
|
+
status: "connected",
|
|
167
|
+
});
|
|
168
|
+
} else if (stateResult.stagingId) {
|
|
169
|
+
staging.updateSlotTokens(stateResult.stagingId, tokens);
|
|
170
|
+
} else {
|
|
171
|
+
updateSettings({
|
|
172
|
+
linearOAuthAccessToken: tokens.accessToken,
|
|
173
|
+
linearOAuthRefreshToken: tokens.refreshToken,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log("[linear-agent-routes] OAuth tokens obtained successfully");
|
|
178
|
+
return c.redirect(`${redirectBase}?oauth_success=true`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Register protected Linear Agent SDK routes (after auth middleware).
|
|
184
|
+
* Status + authorize URL + disconnect + staging slot endpoints.
|
|
185
|
+
*/
|
|
186
|
+
export function registerLinearAgentProtectedRoutes(api: Hono): void {
|
|
187
|
+
// ── Staging slot CRUD ──────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
// Create a staging slot for the wizard flow
|
|
190
|
+
api.post("/linear/oauth/staging", async (c) => {
|
|
191
|
+
const body = await c.req.json() as {
|
|
192
|
+
clientId?: string;
|
|
193
|
+
clientSecret?: string;
|
|
194
|
+
webhookSecret?: string;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const clientId = (body.clientId || "").trim();
|
|
198
|
+
const clientSecret = (body.clientSecret || "").trim();
|
|
199
|
+
const webhookSecret = (body.webhookSecret || "").trim();
|
|
200
|
+
|
|
201
|
+
if (!clientId || !clientSecret || !webhookSecret) {
|
|
202
|
+
return c.json({ error: "clientId, clientSecret, and webhookSecret are required" }, 400);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const stagingId = staging.createSlot({ clientId, clientSecret, webhookSecret });
|
|
206
|
+
return c.json({ stagingId });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Check staging slot status
|
|
210
|
+
api.get("/linear/oauth/staging/:id/status", (c) => {
|
|
211
|
+
const id = c.req.param("id");
|
|
212
|
+
const slot = staging.getSlot(id);
|
|
213
|
+
if (!slot) {
|
|
214
|
+
return c.json({ exists: false, hasAccessToken: false, hasClientId: false, hasClientSecret: false });
|
|
215
|
+
}
|
|
216
|
+
return c.json({
|
|
217
|
+
exists: true,
|
|
218
|
+
hasAccessToken: !!slot.accessToken,
|
|
219
|
+
hasClientId: !!slot.clientId,
|
|
220
|
+
hasClientSecret: !!slot.clientSecret,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Delete a staging slot
|
|
225
|
+
api.delete("/linear/oauth/staging/:id", (c) => {
|
|
226
|
+
const id = c.req.param("id");
|
|
227
|
+
staging.deleteSlot(id);
|
|
228
|
+
return c.json({ ok: true });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── OAuth flow endpoints ───────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
// Get OAuth authorize URL for installing the app
|
|
234
|
+
api.get("/linear/oauth/authorize-url", (c) => {
|
|
235
|
+
const settings = getSettings();
|
|
236
|
+
const baseUrl = settings.publicUrl || `http://localhost:${process.env.PORT || 3456}`;
|
|
237
|
+
const redirectUri = `${baseUrl}/api/linear/oauth/callback`;
|
|
238
|
+
const returnTo = c.req.query("returnTo");
|
|
239
|
+
const stagingId = c.req.query("stagingId");
|
|
240
|
+
|
|
241
|
+
// Validate returnTo is a safe relative hash-router path to prevent open redirects
|
|
242
|
+
const safeReturnTo = returnTo && /^\/?#\//.test(returnTo) ? returnTo : undefined;
|
|
243
|
+
|
|
244
|
+
// Use staging slot's clientId if provided, fall back to global settings
|
|
245
|
+
const slot = stagingId ? staging.getSlot(stagingId) : null;
|
|
246
|
+
|
|
247
|
+
// If a stagingId was provided but the slot is expired/missing, fail early
|
|
248
|
+
// rather than generating a URL that will fail at callback time
|
|
249
|
+
if (stagingId && !slot) {
|
|
250
|
+
return c.json({ error: "Staging slot expired or not found" }, 404);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const clientId = slot?.clientId || settings.linearOAuthClientId;
|
|
254
|
+
|
|
255
|
+
const result = linearAgent.getOAuthAuthorizeUrl(clientId, redirectUri, {
|
|
256
|
+
returnTo: safeReturnTo,
|
|
257
|
+
stagingId,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!result) {
|
|
261
|
+
return c.json({ error: "OAuth client ID not configured" }, 400);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return c.json({ url: result.url });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Check OAuth configuration status (global or staging slot)
|
|
268
|
+
api.get("/linear/oauth/status", (c) => {
|
|
269
|
+
const stagingId = c.req.query("stagingId");
|
|
270
|
+
|
|
271
|
+
if (stagingId) {
|
|
272
|
+
const slot = staging.getSlot(stagingId);
|
|
273
|
+
return c.json({
|
|
274
|
+
configured: !!(slot?.clientId && slot?.clientSecret && slot?.accessToken),
|
|
275
|
+
hasClientId: !!slot?.clientId,
|
|
276
|
+
hasClientSecret: !!slot?.clientSecret,
|
|
277
|
+
hasWebhookSecret: !!slot?.webhookSecret,
|
|
278
|
+
hasAccessToken: !!slot?.accessToken,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const settings = getSettings();
|
|
283
|
+
return c.json({
|
|
284
|
+
configured: linearAgent.isLinearOAuthConfigured({
|
|
285
|
+
clientId: settings.linearOAuthClientId,
|
|
286
|
+
clientSecret: settings.linearOAuthClientSecret,
|
|
287
|
+
accessToken: settings.linearOAuthAccessToken,
|
|
288
|
+
}),
|
|
289
|
+
hasClientId: !!settings.linearOAuthClientId.trim(),
|
|
290
|
+
hasClientSecret: !!settings.linearOAuthClientSecret.trim(),
|
|
291
|
+
hasWebhookSecret: !!settings.linearOAuthWebhookSecret.trim(),
|
|
292
|
+
hasAccessToken: !!settings.linearOAuthAccessToken.trim(),
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Disconnect OAuth (clear tokens)
|
|
297
|
+
api.post("/linear/oauth/disconnect", (c) => {
|
|
298
|
+
updateSettings({
|
|
299
|
+
linearOAuthAccessToken: "",
|
|
300
|
+
linearOAuthRefreshToken: "",
|
|
301
|
+
});
|
|
302
|
+
return c.json({ ok: true });
|
|
303
|
+
});
|
|
304
|
+
}
|