@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,244 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import {
|
|
3
|
+
listConnections,
|
|
4
|
+
getConnection,
|
|
5
|
+
createConnection,
|
|
6
|
+
updateConnection,
|
|
7
|
+
deleteConnection,
|
|
8
|
+
} from "../linear-connections.js";
|
|
9
|
+
import { linearCache } from "../linear-cache.js";
|
|
10
|
+
|
|
11
|
+
/** Mask an API key, showing only the last 4 characters. */
|
|
12
|
+
function maskApiKey(key: string): string {
|
|
13
|
+
if (key.length <= 4) return "****";
|
|
14
|
+
return "****" + key.slice(-4);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Verify a Linear API key by calling the viewer query. Returns workspace info. */
|
|
18
|
+
async function verifyLinearApiKey(apiKey: string): Promise<{
|
|
19
|
+
ok: boolean;
|
|
20
|
+
workspaceName?: string;
|
|
21
|
+
workspaceId?: string;
|
|
22
|
+
viewerName?: string;
|
|
23
|
+
viewerEmail?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}> {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch("https://api.linear.app/graphql", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
Authorization: apiKey,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
query: `
|
|
35
|
+
query CompanionVerifyConnection {
|
|
36
|
+
viewer { id name email }
|
|
37
|
+
organization { id name }
|
|
38
|
+
}
|
|
39
|
+
`,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const json = await response.json().catch(() => ({})) as {
|
|
44
|
+
data?: {
|
|
45
|
+
viewer?: { id?: string; name?: string | null; email?: string | null } | null;
|
|
46
|
+
organization?: { id?: string; name?: string | null } | null;
|
|
47
|
+
};
|
|
48
|
+
errors?: Array<{ message?: string }>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (!response.ok || (json.errors && json.errors.length > 0)) {
|
|
52
|
+
const msg = json.errors?.[0]?.message || response.statusText || "Verification failed";
|
|
53
|
+
return { ok: false, error: msg };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
workspaceName: json.data?.organization?.name || "",
|
|
59
|
+
workspaceId: json.data?.organization?.id || "",
|
|
60
|
+
viewerName: json.data?.viewer?.name || "",
|
|
61
|
+
viewerEmail: json.data?.viewer?.email || "",
|
|
62
|
+
};
|
|
63
|
+
} catch (e: unknown) {
|
|
64
|
+
return { ok: false, error: e instanceof Error ? e.message : "Verification failed" };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerLinearConnectionRoutes(api: Hono): void {
|
|
69
|
+
// ─── List all connections (API keys masked) ────────────────────────
|
|
70
|
+
|
|
71
|
+
api.get("/linear/connections", (c) => {
|
|
72
|
+
const conns = listConnections().map((conn) => ({
|
|
73
|
+
id: conn.id,
|
|
74
|
+
name: conn.name,
|
|
75
|
+
apiKeyLast4: maskApiKey(conn.apiKey),
|
|
76
|
+
workspaceName: conn.workspaceName,
|
|
77
|
+
workspaceId: conn.workspaceId,
|
|
78
|
+
viewerName: conn.viewerName,
|
|
79
|
+
viewerEmail: conn.viewerEmail,
|
|
80
|
+
connected: conn.connected,
|
|
81
|
+
autoTransition: conn.autoTransition,
|
|
82
|
+
autoTransitionStateId: conn.autoTransitionStateId,
|
|
83
|
+
autoTransitionStateName: conn.autoTransitionStateName,
|
|
84
|
+
archiveTransition: conn.archiveTransition,
|
|
85
|
+
archiveTransitionStateId: conn.archiveTransitionStateId,
|
|
86
|
+
archiveTransitionStateName: conn.archiveTransitionStateName,
|
|
87
|
+
}));
|
|
88
|
+
return c.json({ connections: conns });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ─── Create a new connection (verifies API key) ────────────────────
|
|
92
|
+
|
|
93
|
+
api.post("/linear/connections", async (c) => {
|
|
94
|
+
const body = await c.req.json().catch(() => ({})) as Record<string, unknown>;
|
|
95
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
96
|
+
const apiKey = typeof body.apiKey === "string" ? body.apiKey.trim() : "";
|
|
97
|
+
|
|
98
|
+
if (!name) return c.json({ error: "name is required" }, 400);
|
|
99
|
+
if (!apiKey) return c.json({ error: "apiKey is required" }, 400);
|
|
100
|
+
|
|
101
|
+
// Verify the API key against Linear
|
|
102
|
+
const verification = await verifyLinearApiKey(apiKey);
|
|
103
|
+
|
|
104
|
+
// Only persist the connection if verification succeeds
|
|
105
|
+
if (!verification.ok) {
|
|
106
|
+
return c.json({
|
|
107
|
+
connection: null,
|
|
108
|
+
verified: false,
|
|
109
|
+
error: verification.error,
|
|
110
|
+
}, 422);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const conn = createConnection({ name, apiKey });
|
|
114
|
+
|
|
115
|
+
// Update with verification results
|
|
116
|
+
updateConnection(conn.id, {
|
|
117
|
+
connected: true,
|
|
118
|
+
workspaceName: verification.workspaceName || "",
|
|
119
|
+
workspaceId: verification.workspaceId || "",
|
|
120
|
+
viewerName: verification.viewerName || "",
|
|
121
|
+
viewerEmail: verification.viewerEmail || "",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const updated = getConnection(conn.id)!;
|
|
125
|
+
return c.json({
|
|
126
|
+
connection: {
|
|
127
|
+
id: updated.id,
|
|
128
|
+
name: updated.name,
|
|
129
|
+
apiKeyLast4: maskApiKey(updated.apiKey),
|
|
130
|
+
workspaceName: updated.workspaceName,
|
|
131
|
+
workspaceId: updated.workspaceId,
|
|
132
|
+
viewerName: updated.viewerName,
|
|
133
|
+
viewerEmail: updated.viewerEmail,
|
|
134
|
+
connected: updated.connected,
|
|
135
|
+
autoTransition: updated.autoTransition,
|
|
136
|
+
autoTransitionStateId: updated.autoTransitionStateId,
|
|
137
|
+
autoTransitionStateName: updated.autoTransitionStateName,
|
|
138
|
+
archiveTransition: updated.archiveTransition,
|
|
139
|
+
archiveTransitionStateId: updated.archiveTransitionStateId,
|
|
140
|
+
archiveTransitionStateName: updated.archiveTransitionStateName,
|
|
141
|
+
},
|
|
142
|
+
verified: true,
|
|
143
|
+
}, 201);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── Update a connection ───────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
api.put("/linear/connections/:id", async (c) => {
|
|
149
|
+
const id = c.req.param("id");
|
|
150
|
+
const body = await c.req.json().catch(() => ({})) as Record<string, unknown>;
|
|
151
|
+
|
|
152
|
+
const existing = getConnection(id);
|
|
153
|
+
if (!existing) return c.json({ error: "Connection not found" }, 404);
|
|
154
|
+
|
|
155
|
+
const patch: Record<string, unknown> = {};
|
|
156
|
+
if (typeof body.name === "string") patch.name = body.name;
|
|
157
|
+
if (typeof body.apiKey === "string" && body.apiKey.trim()) {
|
|
158
|
+
patch.apiKey = body.apiKey.trim();
|
|
159
|
+
// If the API key changed, mark as needing re-verification
|
|
160
|
+
patch.connected = false;
|
|
161
|
+
}
|
|
162
|
+
if (typeof body.autoTransition === "boolean") patch.autoTransition = body.autoTransition;
|
|
163
|
+
if (typeof body.autoTransitionStateId === "string") patch.autoTransitionStateId = body.autoTransitionStateId;
|
|
164
|
+
if (typeof body.autoTransitionStateName === "string") patch.autoTransitionStateName = body.autoTransitionStateName;
|
|
165
|
+
if (typeof body.archiveTransition === "boolean") patch.archiveTransition = body.archiveTransition;
|
|
166
|
+
if (typeof body.archiveTransitionStateId === "string") patch.archiveTransitionStateId = body.archiveTransitionStateId;
|
|
167
|
+
if (typeof body.archiveTransitionStateName === "string") patch.archiveTransitionStateName = body.archiveTransitionStateName;
|
|
168
|
+
|
|
169
|
+
const updated = updateConnection(id, patch as Partial<Omit<typeof existing, "id" | "createdAt">>);
|
|
170
|
+
if (!updated) return c.json({ error: "Update failed" }, 500);
|
|
171
|
+
|
|
172
|
+
// Invalidate caches for this connection
|
|
173
|
+
linearCache.invalidate(`${id}:`);
|
|
174
|
+
|
|
175
|
+
return c.json({
|
|
176
|
+
connection: {
|
|
177
|
+
id: updated.id,
|
|
178
|
+
name: updated.name,
|
|
179
|
+
apiKeyLast4: maskApiKey(updated.apiKey),
|
|
180
|
+
workspaceName: updated.workspaceName,
|
|
181
|
+
workspaceId: updated.workspaceId,
|
|
182
|
+
viewerName: updated.viewerName,
|
|
183
|
+
viewerEmail: updated.viewerEmail,
|
|
184
|
+
connected: updated.connected,
|
|
185
|
+
autoTransition: updated.autoTransition,
|
|
186
|
+
autoTransitionStateId: updated.autoTransitionStateId,
|
|
187
|
+
autoTransitionStateName: updated.autoTransitionStateName,
|
|
188
|
+
archiveTransition: updated.archiveTransition,
|
|
189
|
+
archiveTransitionStateId: updated.archiveTransitionStateId,
|
|
190
|
+
archiveTransitionStateName: updated.archiveTransitionStateName,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ─── Delete a connection ───────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
api.delete("/linear/connections/:id", (c) => {
|
|
198
|
+
const id = c.req.param("id");
|
|
199
|
+
const deleted = deleteConnection(id);
|
|
200
|
+
if (!deleted) return c.json({ error: "Connection not found" }, 404);
|
|
201
|
+
// Invalidate caches for this connection
|
|
202
|
+
linearCache.invalidate(`${id}:`);
|
|
203
|
+
return c.json({ ok: true });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─── Re-verify a connection ────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
api.post("/linear/connections/:id/verify", async (c) => {
|
|
209
|
+
const id = c.req.param("id");
|
|
210
|
+
const conn = getConnection(id);
|
|
211
|
+
if (!conn) return c.json({ error: "Connection not found" }, 404);
|
|
212
|
+
|
|
213
|
+
const verification = await verifyLinearApiKey(conn.apiKey);
|
|
214
|
+
updateConnection(id, {
|
|
215
|
+
connected: verification.ok,
|
|
216
|
+
workspaceName: verification.ok ? (verification.workspaceName || "") : conn.workspaceName,
|
|
217
|
+
workspaceId: verification.ok ? (verification.workspaceId || "") : conn.workspaceId,
|
|
218
|
+
viewerName: verification.ok ? (verification.viewerName || "") : conn.viewerName,
|
|
219
|
+
viewerEmail: verification.ok ? (verification.viewerEmail || "") : conn.viewerEmail,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const updated = getConnection(id)!;
|
|
223
|
+
return c.json({
|
|
224
|
+
connection: {
|
|
225
|
+
id: updated.id,
|
|
226
|
+
name: updated.name,
|
|
227
|
+
apiKeyLast4: maskApiKey(updated.apiKey),
|
|
228
|
+
workspaceName: updated.workspaceName,
|
|
229
|
+
workspaceId: updated.workspaceId,
|
|
230
|
+
viewerName: updated.viewerName,
|
|
231
|
+
viewerEmail: updated.viewerEmail,
|
|
232
|
+
connected: updated.connected,
|
|
233
|
+
autoTransition: updated.autoTransition,
|
|
234
|
+
autoTransitionStateId: updated.autoTransitionStateId,
|
|
235
|
+
autoTransitionStateName: updated.autoTransitionStateName,
|
|
236
|
+
archiveTransition: updated.archiveTransition,
|
|
237
|
+
archiveTransitionStateId: updated.archiveTransitionStateId,
|
|
238
|
+
archiveTransitionStateName: updated.archiveTransitionStateName,
|
|
239
|
+
},
|
|
240
|
+
verified: verification.ok,
|
|
241
|
+
error: verification.error,
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for linear-oauth-connection-routes.ts — Hono route handlers
|
|
3
|
+
* for Linear OAuth connection CRUD.
|
|
4
|
+
*
|
|
5
|
+
* Validates:
|
|
6
|
+
* - GET /linear/oauth-connections — list connections (secrets masked)
|
|
7
|
+
* - POST /linear/oauth-connections — create with validation
|
|
8
|
+
* - PUT /linear/oauth-connections/:id — update fields
|
|
9
|
+
* - DELETE /linear/oauth-connections/:id — delete with agent guard (409)
|
|
10
|
+
* - GET /linear/oauth-connections/:id/authorize-url — generate OAuth URL
|
|
11
|
+
* - 404 responses for non-existent connections
|
|
12
|
+
*/
|
|
13
|
+
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
14
|
+
|
|
15
|
+
// ─── Mock linear-oauth-connections module ────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const mockConnection = {
|
|
18
|
+
id: "conn-1",
|
|
19
|
+
name: "Test App",
|
|
20
|
+
oauthClientId: "client-123",
|
|
21
|
+
oauthClientSecret: "secret-456",
|
|
22
|
+
webhookSecret: "webhook-789",
|
|
23
|
+
accessToken: "tok-abc",
|
|
24
|
+
refreshToken: "ref-xyz",
|
|
25
|
+
status: "connected" as const,
|
|
26
|
+
createdAt: 1000,
|
|
27
|
+
updatedAt: 2000,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
vi.mock("../linear-oauth-connections.js", () => ({
|
|
31
|
+
listOAuthConnections: vi.fn(() => []),
|
|
32
|
+
getOAuthConnection: vi.fn(() => null),
|
|
33
|
+
createOAuthConnection: vi.fn(),
|
|
34
|
+
updateOAuthConnection: vi.fn(),
|
|
35
|
+
deleteOAuthConnection: vi.fn(() => false),
|
|
36
|
+
sanitizeOAuthConnection: vi.fn((conn: Record<string, unknown>) => ({
|
|
37
|
+
id: conn.id,
|
|
38
|
+
name: conn.name,
|
|
39
|
+
oauthClientId: conn.oauthClientId,
|
|
40
|
+
status: conn.status,
|
|
41
|
+
hasAccessToken: !!conn.accessToken,
|
|
42
|
+
hasClientSecret: !!conn.oauthClientSecret,
|
|
43
|
+
hasWebhookSecret: !!conn.webhookSecret,
|
|
44
|
+
createdAt: conn.createdAt,
|
|
45
|
+
updatedAt: conn.updatedAt,
|
|
46
|
+
})),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// ─── Mock agent-store module ─────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
vi.mock("../agent-store.js", () => ({
|
|
52
|
+
listAgents: vi.fn(() => []),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// ─── Mock settings-manager module ────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
vi.mock("../settings-manager.js", () => ({
|
|
58
|
+
getSettings: vi.fn(() => ({
|
|
59
|
+
publicUrl: "https://companion.example.com",
|
|
60
|
+
})),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// ─── Mock linear-agent module ────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
vi.mock("../linear-agent.js", () => ({
|
|
66
|
+
getOAuthAuthorizeUrl: vi.fn(() => ({
|
|
67
|
+
url: "https://linear.app/oauth/authorize?client_id=test",
|
|
68
|
+
})),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
import { Hono } from "hono";
|
|
72
|
+
import {
|
|
73
|
+
listOAuthConnections,
|
|
74
|
+
getOAuthConnection,
|
|
75
|
+
createOAuthConnection,
|
|
76
|
+
updateOAuthConnection,
|
|
77
|
+
deleteOAuthConnection,
|
|
78
|
+
} from "../linear-oauth-connections.js";
|
|
79
|
+
import * as agentStore from "../agent-store.js";
|
|
80
|
+
import * as linearAgent from "../linear-agent.js";
|
|
81
|
+
import { registerLinearOAuthConnectionRoutes } from "./linear-oauth-connection-routes.js";
|
|
82
|
+
|
|
83
|
+
// ─── Setup ──────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
let app: Hono;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.clearAllMocks();
|
|
89
|
+
app = new Hono();
|
|
90
|
+
registerLinearOAuthConnectionRoutes(app);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function GET(path: string) {
|
|
96
|
+
return app.request(path, { method: "GET" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function POST(path: string, body: Record<string, unknown>) {
|
|
100
|
+
return app.request(path, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function PUT(path: string, body: Record<string, unknown>) {
|
|
108
|
+
return app.request(path, {
|
|
109
|
+
method: "PUT",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function DELETE(path: string) {
|
|
116
|
+
return app.request(path, { method: "DELETE" });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// Tests
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
describe("linear-oauth-connection-routes", () => {
|
|
124
|
+
// ─── GET /linear/oauth-connections ──────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
describe("GET /linear/oauth-connections", () => {
|
|
127
|
+
it("returns empty list when no connections exist", async () => {
|
|
128
|
+
const res = await GET("/linear/oauth-connections");
|
|
129
|
+
expect(res.status).toBe(200);
|
|
130
|
+
const json = await res.json();
|
|
131
|
+
expect(json.connections).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns sanitized connections list", async () => {
|
|
135
|
+
vi.mocked(listOAuthConnections).mockReturnValue([mockConnection]);
|
|
136
|
+
|
|
137
|
+
const res = await GET("/linear/oauth-connections");
|
|
138
|
+
expect(res.status).toBe(200);
|
|
139
|
+
const json = await res.json();
|
|
140
|
+
|
|
141
|
+
expect(json.connections).toHaveLength(1);
|
|
142
|
+
expect(json.connections[0].name).toBe("Test App");
|
|
143
|
+
expect(json.connections[0].oauthClientId).toBe("client-123");
|
|
144
|
+
// Secrets should be masked (boolean flags instead)
|
|
145
|
+
expect(json.connections[0].hasAccessToken).toBe(true);
|
|
146
|
+
expect(json.connections[0].oauthClientSecret).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ─── POST /linear/oauth-connections ─────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
describe("POST /linear/oauth-connections", () => {
|
|
153
|
+
it("creates a connection with valid body", async () => {
|
|
154
|
+
vi.mocked(createOAuthConnection).mockReturnValue({ ...mockConnection, id: "new-conn" });
|
|
155
|
+
|
|
156
|
+
const res = await POST("/linear/oauth-connections", {
|
|
157
|
+
name: "New App",
|
|
158
|
+
oauthClientId: "new-cid",
|
|
159
|
+
oauthClientSecret: "new-csec",
|
|
160
|
+
webhookSecret: "new-wsec",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(res.status).toBe(201);
|
|
164
|
+
const json = await res.json();
|
|
165
|
+
expect(json.connection).toBeDefined();
|
|
166
|
+
expect(createOAuthConnection).toHaveBeenCalledWith({
|
|
167
|
+
name: "New App",
|
|
168
|
+
oauthClientId: "new-cid",
|
|
169
|
+
oauthClientSecret: "new-csec",
|
|
170
|
+
webhookSecret: "new-wsec",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns 400 when name is missing", async () => {
|
|
175
|
+
const res = await POST("/linear/oauth-connections", {
|
|
176
|
+
oauthClientId: "cid",
|
|
177
|
+
oauthClientSecret: "csec",
|
|
178
|
+
webhookSecret: "wsec",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(res.status).toBe(400);
|
|
182
|
+
const json = await res.json();
|
|
183
|
+
expect(json.error).toBe("name is required");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("returns 400 when oauthClientId is missing", async () => {
|
|
187
|
+
const res = await POST("/linear/oauth-connections", {
|
|
188
|
+
name: "App",
|
|
189
|
+
oauthClientSecret: "csec",
|
|
190
|
+
webhookSecret: "wsec",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(res.status).toBe(400);
|
|
194
|
+
const json = await res.json();
|
|
195
|
+
expect(json.error).toBe("oauthClientId is required");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns 400 when oauthClientSecret is missing", async () => {
|
|
199
|
+
const res = await POST("/linear/oauth-connections", {
|
|
200
|
+
name: "App",
|
|
201
|
+
oauthClientId: "cid",
|
|
202
|
+
webhookSecret: "wsec",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(res.status).toBe(400);
|
|
206
|
+
const json = await res.json();
|
|
207
|
+
expect(json.error).toBe("oauthClientSecret is required");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns 400 when webhookSecret is missing", async () => {
|
|
211
|
+
const res = await POST("/linear/oauth-connections", {
|
|
212
|
+
name: "App",
|
|
213
|
+
oauthClientId: "cid",
|
|
214
|
+
oauthClientSecret: "csec",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(res.status).toBe(400);
|
|
218
|
+
const json = await res.json();
|
|
219
|
+
expect(json.error).toBe("webhookSecret is required");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns 409 when oauthClientId already exists", async () => {
|
|
223
|
+
// Simulate existing connection with same client ID
|
|
224
|
+
vi.mocked(listOAuthConnections).mockReturnValue([mockConnection]);
|
|
225
|
+
|
|
226
|
+
const res = await POST("/linear/oauth-connections", {
|
|
227
|
+
name: "Duplicate",
|
|
228
|
+
oauthClientId: "client-123", // same as mockConnection
|
|
229
|
+
oauthClientSecret: "csec",
|
|
230
|
+
webhookSecret: "wsec",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(res.status).toBe(409);
|
|
234
|
+
const json = await res.json();
|
|
235
|
+
expect(json.error).toBe("A connection with this OAuth client ID already exists");
|
|
236
|
+
expect(createOAuthConnection).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("trims whitespace-only fields and returns 400", async () => {
|
|
240
|
+
const res = await POST("/linear/oauth-connections", {
|
|
241
|
+
name: " ",
|
|
242
|
+
oauthClientId: "cid",
|
|
243
|
+
oauthClientSecret: "csec",
|
|
244
|
+
webhookSecret: "wsec",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(res.status).toBe(400);
|
|
248
|
+
const json = await res.json();
|
|
249
|
+
expect(json.error).toBe("name is required");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ─── PUT /linear/oauth-connections/:id ──────────────────────────────────
|
|
254
|
+
|
|
255
|
+
describe("PUT /linear/oauth-connections/:id", () => {
|
|
256
|
+
it("updates a connection", async () => {
|
|
257
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
258
|
+
vi.mocked(updateOAuthConnection).mockReturnValue({
|
|
259
|
+
...mockConnection,
|
|
260
|
+
name: "Updated App",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const res = await PUT("/linear/oauth-connections/conn-1", {
|
|
264
|
+
name: "Updated App",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(res.status).toBe(200);
|
|
268
|
+
const json = await res.json();
|
|
269
|
+
expect(json.connection.name).toBe("Updated App");
|
|
270
|
+
expect(updateOAuthConnection).toHaveBeenCalledWith("conn-1", { name: "Updated App" });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("returns 404 for non-existent connection", async () => {
|
|
274
|
+
vi.mocked(getOAuthConnection).mockReturnValue(null);
|
|
275
|
+
|
|
276
|
+
const res = await PUT("/linear/oauth-connections/nope", { name: "x" });
|
|
277
|
+
expect(res.status).toBe(404);
|
|
278
|
+
const json = await res.json();
|
|
279
|
+
expect(json.error).toBe("OAuth connection not found");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("returns 409 when updating oauthClientId to one that already exists", async () => {
|
|
283
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
284
|
+
// Another connection already uses this clientId
|
|
285
|
+
vi.mocked(listOAuthConnections).mockReturnValue([
|
|
286
|
+
{ ...mockConnection, id: "conn-other", oauthClientId: "existing-cid" },
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const res = await PUT("/linear/oauth-connections/conn-1", { oauthClientId: "existing-cid" });
|
|
290
|
+
expect(res.status).toBe(409);
|
|
291
|
+
const json = await res.json();
|
|
292
|
+
expect(json.error).toContain("already exists");
|
|
293
|
+
// Should NOT call updateOAuthConnection
|
|
294
|
+
expect(updateOAuthConnection).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("allows updating oauthClientId to the same value (no-op duplicate)", async () => {
|
|
298
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
299
|
+
vi.mocked(updateOAuthConnection).mockReturnValue({ ...mockConnection, updatedAt: 9999 });
|
|
300
|
+
|
|
301
|
+
// Updating to the same clientId the connection already has should be allowed
|
|
302
|
+
const res = await PUT("/linear/oauth-connections/conn-1", { oauthClientId: mockConnection.oauthClientId });
|
|
303
|
+
expect(res.status).toBe(200);
|
|
304
|
+
expect(updateOAuthConnection).toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("returns 500 when update fails", async () => {
|
|
308
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
309
|
+
vi.mocked(updateOAuthConnection).mockReturnValue(null);
|
|
310
|
+
|
|
311
|
+
const res = await PUT("/linear/oauth-connections/conn-1", { name: "x" });
|
|
312
|
+
expect(res.status).toBe(500);
|
|
313
|
+
const json = await res.json();
|
|
314
|
+
expect(json.error).toBe("Update failed");
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ─── DELETE /linear/oauth-connections/:id ───────────────────────────────
|
|
319
|
+
|
|
320
|
+
describe("DELETE /linear/oauth-connections/:id", () => {
|
|
321
|
+
it("deletes a connection with no referencing agents", async () => {
|
|
322
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
323
|
+
vi.mocked(agentStore.listAgents).mockReturnValue([]);
|
|
324
|
+
vi.mocked(deleteOAuthConnection).mockReturnValue(true);
|
|
325
|
+
|
|
326
|
+
const res = await DELETE("/linear/oauth-connections/conn-1");
|
|
327
|
+
expect(res.status).toBe(200);
|
|
328
|
+
const json = await res.json();
|
|
329
|
+
expect(json.ok).toBe(true);
|
|
330
|
+
expect(deleteOAuthConnection).toHaveBeenCalledWith("conn-1");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("returns 404 for non-existent connection", async () => {
|
|
334
|
+
vi.mocked(getOAuthConnection).mockReturnValue(null);
|
|
335
|
+
|
|
336
|
+
const res = await DELETE("/linear/oauth-connections/nope");
|
|
337
|
+
expect(res.status).toBe(404);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("returns 409 when agents reference the connection (even disabled ones)", async () => {
|
|
341
|
+
// Disabled agent that references this connection — guard should still catch it
|
|
342
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
343
|
+
vi.mocked(agentStore.listAgents).mockReturnValue([
|
|
344
|
+
{
|
|
345
|
+
id: "agent-1",
|
|
346
|
+
name: "Linear Bot",
|
|
347
|
+
enabled: false,
|
|
348
|
+
triggers: {
|
|
349
|
+
linear: {
|
|
350
|
+
enabled: false,
|
|
351
|
+
oauthConnectionId: "conn-1",
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
] as ReturnType<typeof agentStore.listAgents>);
|
|
356
|
+
|
|
357
|
+
const res = await DELETE("/linear/oauth-connections/conn-1");
|
|
358
|
+
expect(res.status).toBe(409);
|
|
359
|
+
const json = await res.json();
|
|
360
|
+
expect(json.error).toContain("agents are using this OAuth connection");
|
|
361
|
+
expect(json.agents).toHaveLength(1);
|
|
362
|
+
expect(json.agents[0].name).toBe("Linear Bot");
|
|
363
|
+
// deleteOAuthConnection should NOT have been called
|
|
364
|
+
expect(deleteOAuthConnection).not.toHaveBeenCalled();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ─── GET /linear/oauth-connections/:id/authorize-url ───────────────────
|
|
369
|
+
|
|
370
|
+
describe("GET /linear/oauth-connections/:id/authorize-url", () => {
|
|
371
|
+
it("returns authorize URL for valid connection", async () => {
|
|
372
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
373
|
+
|
|
374
|
+
const res = await GET("/linear/oauth-connections/conn-1/authorize-url?returnTo=/%23/integrations/linear-oauth");
|
|
375
|
+
expect(res.status).toBe(200);
|
|
376
|
+
const json = await res.json();
|
|
377
|
+
expect(json.url).toBe("https://linear.app/oauth/authorize?client_id=test");
|
|
378
|
+
|
|
379
|
+
// Verify the correct args were passed to getOAuthAuthorizeUrl
|
|
380
|
+
expect(linearAgent.getOAuthAuthorizeUrl).toHaveBeenCalledWith(
|
|
381
|
+
"client-123",
|
|
382
|
+
"https://companion.example.com/api/linear/oauth/callback",
|
|
383
|
+
expect.objectContaining({
|
|
384
|
+
connectionId: "conn-1",
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("returns 404 for non-existent connection", async () => {
|
|
390
|
+
vi.mocked(getOAuthConnection).mockReturnValue(null);
|
|
391
|
+
|
|
392
|
+
const res = await GET("/linear/oauth-connections/nope/authorize-url");
|
|
393
|
+
expect(res.status).toBe(404);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("returns 500 when authorize URL generation fails", async () => {
|
|
397
|
+
vi.mocked(getOAuthConnection).mockReturnValue(mockConnection);
|
|
398
|
+
vi.mocked(linearAgent.getOAuthAuthorizeUrl).mockReturnValue(null as unknown as ReturnType<typeof linearAgent.getOAuthAuthorizeUrl>);
|
|
399
|
+
|
|
400
|
+
const res = await GET("/linear/oauth-connections/conn-1/authorize-url");
|
|
401
|
+
expect(res.status).toBe(500);
|
|
402
|
+
const json = await res.json();
|
|
403
|
+
expect(json.error).toBe("Failed to generate authorize URL");
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|