cyrus-edge-worker 0.0.40 â 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EdgeWorker.d.ts +8 -29
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +122 -306
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/SharedApplicationServer.d.ts +21 -63
- package/dist/SharedApplicationServer.d.ts.map +1 -1
- package/dist/SharedApplicationServer.js +93 -764
- package/dist/SharedApplicationServer.js.map +1 -1
- package/package.json +8 -6
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { URL } from "node:url";
|
|
4
|
-
import { forward } from "@ngrok/ngrok";
|
|
5
|
-
import { DEFAULT_PROXY_URL } from "cyrus-core";
|
|
1
|
+
import { CloudflareTunnelClient } from "cyrus-cloudflare-tunnel-client";
|
|
2
|
+
import Fastify from "fastify";
|
|
6
3
|
/**
|
|
7
4
|
* Shared application server that handles both webhooks and OAuth callbacks on a single port
|
|
8
5
|
* Consolidates functionality from SharedWebhookServer and CLI OAuth server
|
|
9
6
|
*/
|
|
10
7
|
export class SharedApplicationServer {
|
|
11
|
-
|
|
8
|
+
app = null;
|
|
12
9
|
webhookHandlers = new Map();
|
|
13
|
-
//
|
|
10
|
+
// Legacy handlers for direct Linear webhook registration (deprecated)
|
|
14
11
|
linearWebhookHandlers = new Map();
|
|
15
12
|
oauthCallbacks = new Map();
|
|
16
|
-
oauthCallbackHandler = null;
|
|
17
|
-
oauthStates = new Map();
|
|
18
13
|
pendingApprovals = new Map();
|
|
19
14
|
port;
|
|
20
15
|
host;
|
|
21
16
|
isListening = false;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
ngrokUrl = null;
|
|
25
|
-
proxyUrl;
|
|
26
|
-
constructor(port = 3456, host = "localhost", ngrokAuthToken, proxyUrl) {
|
|
17
|
+
tunnelClient = null;
|
|
18
|
+
constructor(port = 3456, host = "localhost") {
|
|
27
19
|
this.port = port;
|
|
28
20
|
this.host = host;
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the Fastify app instance (must be called before registering routes)
|
|
24
|
+
*/
|
|
25
|
+
initializeFastify() {
|
|
26
|
+
if (this.app) {
|
|
27
|
+
return; // Already initialized
|
|
28
|
+
}
|
|
29
|
+
this.app = Fastify({
|
|
30
|
+
logger: false,
|
|
31
|
+
});
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Start the shared application server
|
|
@@ -36,30 +37,63 @@ export class SharedApplicationServer {
|
|
|
36
37
|
if (this.isListening) {
|
|
37
38
|
return; // Already listening
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Initialize Fastify if not already done
|
|
41
|
+
this.initializeFastify();
|
|
42
|
+
try {
|
|
43
|
+
await this.app.listen({
|
|
44
|
+
port: this.port,
|
|
45
|
+
host: this.host,
|
|
42
46
|
});
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
this.isListening = true;
|
|
48
|
+
console.log(`đ Shared application server listening on http://${this.host}:${this.port}`);
|
|
49
|
+
// Start Cloudflare tunnel if CLOUDFLARE_TOKEN is set
|
|
50
|
+
if (process.env.CLOUDFLARE_TOKEN) {
|
|
51
|
+
await this.startCloudflareTunnel(process.env.CLOUDFLARE_TOKEN);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
this.isListening = false;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start Cloudflare tunnel and wait for 4 'connected' events
|
|
61
|
+
*/
|
|
62
|
+
async startCloudflareTunnel(cloudflareToken) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
let connectionCount = 0;
|
|
65
|
+
const requiredConnections = 4;
|
|
66
|
+
this.tunnelClient = new CloudflareTunnelClient(cloudflareToken, this.port);
|
|
67
|
+
// Listen for connection events (Cloudflare establishes 4 connections per tunnel)
|
|
68
|
+
this.tunnelClient.on("connected", () => {
|
|
69
|
+
connectionCount++;
|
|
70
|
+
console.log(`đ Cloudflare tunnel connection ${connectionCount}/${requiredConnections} established`);
|
|
71
|
+
if (connectionCount === requiredConnections) {
|
|
72
|
+
console.log("â
Cloudflare tunnel fully connected and ready");
|
|
73
|
+
resolve();
|
|
56
74
|
}
|
|
57
|
-
resolve();
|
|
58
75
|
});
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
// Listen for ready event to get tunnel URL
|
|
77
|
+
this.tunnelClient.on("ready", (tunnelUrl) => {
|
|
78
|
+
console.log(`đ Cloudflare tunnel URL: ${tunnelUrl}`);
|
|
79
|
+
});
|
|
80
|
+
// Listen for error events
|
|
81
|
+
this.tunnelClient.on("error", (error) => {
|
|
82
|
+
console.error("â Cloudflare tunnel error:", error);
|
|
61
83
|
reject(error);
|
|
62
84
|
});
|
|
85
|
+
// Listen for disconnect events
|
|
86
|
+
this.tunnelClient.on("disconnect", (reason) => {
|
|
87
|
+
console.log(`đ Cloudflare tunnel disconnected: ${reason}`);
|
|
88
|
+
});
|
|
89
|
+
// Start the tunnel
|
|
90
|
+
this.tunnelClient.startTunnel().catch(reject);
|
|
91
|
+
// Timeout after 30 seconds
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
if (connectionCount < requiredConnections) {
|
|
94
|
+
reject(new Error(`Timeout waiting for Cloudflare tunnel (${connectionCount}/${requiredConnections} connections)`));
|
|
95
|
+
}
|
|
96
|
+
}, 30000);
|
|
63
97
|
});
|
|
64
98
|
}
|
|
65
99
|
/**
|
|
@@ -72,26 +106,16 @@ export class SharedApplicationServer {
|
|
|
72
106
|
console.log(`đ Rejected pending approval for session ${sessionId} due to shutdown`);
|
|
73
107
|
}
|
|
74
108
|
this.pendingApprovals.clear();
|
|
75
|
-
// Stop
|
|
76
|
-
if (this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.ngrokUrl = null;
|
|
81
|
-
console.log("đ Ngrok tunnel stopped");
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
console.error("đ´ Failed to stop ngrok tunnel:", error);
|
|
85
|
-
}
|
|
109
|
+
// Stop Cloudflare tunnel if running
|
|
110
|
+
if (this.tunnelClient) {
|
|
111
|
+
this.tunnelClient.disconnect();
|
|
112
|
+
this.tunnelClient = null;
|
|
113
|
+
console.log("đ Cloudflare tunnel stopped");
|
|
86
114
|
}
|
|
87
|
-
if (this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.log("đ Shared application server stopped");
|
|
92
|
-
resolve();
|
|
93
|
-
});
|
|
94
|
-
});
|
|
115
|
+
if (this.app && this.isListening) {
|
|
116
|
+
await this.app.close();
|
|
117
|
+
this.isListening = false;
|
|
118
|
+
console.log("đ Shared application server stopped");
|
|
95
119
|
}
|
|
96
120
|
}
|
|
97
121
|
/**
|
|
@@ -101,42 +125,20 @@ export class SharedApplicationServer {
|
|
|
101
125
|
return this.port;
|
|
102
126
|
}
|
|
103
127
|
/**
|
|
104
|
-
* Get the
|
|
128
|
+
* Get the Fastify instance for registering routes
|
|
129
|
+
* Initializes Fastify if not already done
|
|
105
130
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
return process.env.CYRUS_BASE_URL || `http://${this.host}:${this.port}`;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Start ngrok tunnel for the server
|
|
114
|
-
*/
|
|
115
|
-
async startNgrokTunnel() {
|
|
116
|
-
if (!this.ngrokAuthToken) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
console.log("đ Starting ngrok tunnel...");
|
|
121
|
-
this.ngrokListener = await forward({
|
|
122
|
-
addr: this.port,
|
|
123
|
-
authtoken: this.ngrokAuthToken,
|
|
124
|
-
});
|
|
125
|
-
this.ngrokUrl = this.ngrokListener.url();
|
|
126
|
-
console.log(`đ Ngrok tunnel active: ${this.ngrokUrl}`);
|
|
127
|
-
// Override CYRUS_BASE_URL with ngrok URL
|
|
128
|
-
process.env.CYRUS_BASE_URL = this.ngrokUrl || undefined;
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
console.error("đ´ Failed to start ngrok tunnel:", error);
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
131
|
+
getFastifyInstance() {
|
|
132
|
+
this.initializeFastify();
|
|
133
|
+
return this.app;
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
|
-
* Register a webhook handler for a specific token
|
|
136
|
+
* Register a webhook handler for a specific token (LEGACY - deprecated)
|
|
137
137
|
* Supports two signatures:
|
|
138
138
|
* 1. For ndjson-client: (token, secret, handler)
|
|
139
|
-
* 2. For
|
|
139
|
+
* 2. For legacy direct registration: (token, handler) where handler takes (req, res)
|
|
140
|
+
*
|
|
141
|
+
* NOTE: New code should use LinearEventTransport which registers routes directly with Fastify
|
|
140
142
|
*/
|
|
141
143
|
registerWebhookHandler(token, secretOrHandler, handler) {
|
|
142
144
|
if (typeof secretOrHandler === "string" && handler) {
|
|
@@ -145,9 +147,9 @@ export class SharedApplicationServer {
|
|
|
145
147
|
console.log(`đ Registered webhook handler (proxy-style) for token ending in ...${token.slice(-4)}`);
|
|
146
148
|
}
|
|
147
149
|
else if (typeof secretOrHandler === "function") {
|
|
148
|
-
//
|
|
150
|
+
// Legacy direct registration
|
|
149
151
|
this.linearWebhookHandlers.set(token, secretOrHandler);
|
|
150
|
-
console.log(`đ Registered webhook handler (direct-style) for token ending in ...${token.slice(-4)}`);
|
|
152
|
+
console.log(`đ Registered webhook handler (legacy direct-style) for token ending in ...${token.slice(-4)}`);
|
|
151
153
|
}
|
|
152
154
|
else {
|
|
153
155
|
throw new Error("Invalid webhook handler registration parameters");
|
|
@@ -163,13 +165,6 @@ export class SharedApplicationServer {
|
|
|
163
165
|
console.log(`đ Unregistered webhook handler for token ending in ...${token.slice(-4)}`);
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Register an OAuth callback handler
|
|
168
|
-
*/
|
|
169
|
-
registerOAuthCallbackHandler(handler) {
|
|
170
|
-
this.oauthCallbackHandler = handler;
|
|
171
|
-
console.log("đ Registered OAuth callback handler");
|
|
172
|
-
}
|
|
173
168
|
/**
|
|
174
169
|
* Start OAuth flow and return promise that resolves when callback is received
|
|
175
170
|
*/
|
|
@@ -182,7 +177,7 @@ export class SharedApplicationServer {
|
|
|
182
177
|
// Check if we should use direct Linear OAuth (when self-hosting)
|
|
183
178
|
const isExternalHost = process.env.CYRUS_HOST_EXTERNAL?.toLowerCase().trim() === "true";
|
|
184
179
|
const useDirectOAuth = isExternalHost && process.env.LINEAR_CLIENT_ID;
|
|
185
|
-
const callbackBaseUrl = this.
|
|
180
|
+
const callbackBaseUrl = `http://${this.host}:${this.port}`;
|
|
186
181
|
let authUrl;
|
|
187
182
|
if (useDirectOAuth) {
|
|
188
183
|
// Use local OAuth authorize endpoint
|
|
@@ -205,25 +200,10 @@ export class SharedApplicationServer {
|
|
|
205
200
|
});
|
|
206
201
|
}
|
|
207
202
|
/**
|
|
208
|
-
* Get the
|
|
209
|
-
*/
|
|
210
|
-
getPublicUrl() {
|
|
211
|
-
// Use ngrok URL if available
|
|
212
|
-
if (this.ngrokUrl) {
|
|
213
|
-
return this.ngrokUrl;
|
|
214
|
-
}
|
|
215
|
-
// If CYRUS_BASE_URL is set (could be from external proxy), use that
|
|
216
|
-
if (process.env.CYRUS_BASE_URL) {
|
|
217
|
-
return process.env.CYRUS_BASE_URL;
|
|
218
|
-
}
|
|
219
|
-
// Default to local URL
|
|
220
|
-
return `http://${this.host}:${this.port}`;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Get the webhook URL for registration with proxy
|
|
203
|
+
* Get the webhook URL
|
|
224
204
|
*/
|
|
225
205
|
getWebhookUrl() {
|
|
226
|
-
return
|
|
206
|
+
return `http://${this.host}:${this.port}/webhook`;
|
|
227
207
|
}
|
|
228
208
|
/**
|
|
229
209
|
* Get the OAuth callback URL for registration with proxy
|
|
@@ -231,442 +211,6 @@ export class SharedApplicationServer {
|
|
|
231
211
|
getOAuthCallbackUrl() {
|
|
232
212
|
return `http://${this.host}:${this.port}/callback`;
|
|
233
213
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Handle incoming requests (both webhooks and OAuth callbacks)
|
|
236
|
-
*/
|
|
237
|
-
async handleRequest(req, res) {
|
|
238
|
-
try {
|
|
239
|
-
const url = new URL(req.url, `http://${this.host}:${this.port}`);
|
|
240
|
-
if (url.pathname === "/webhook") {
|
|
241
|
-
await this.handleWebhookRequest(req, res);
|
|
242
|
-
}
|
|
243
|
-
else if (url.pathname === "/callback") {
|
|
244
|
-
await this.handleOAuthCallback(req, res, url);
|
|
245
|
-
}
|
|
246
|
-
else if (url.pathname === "/oauth/authorize") {
|
|
247
|
-
await this.handleOAuthAuthorize(req, res, url);
|
|
248
|
-
}
|
|
249
|
-
else if (url.pathname === "/approval") {
|
|
250
|
-
await this.handleApprovalRequest(req, res, url);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
254
|
-
res.end("Not Found");
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
catch (error) {
|
|
258
|
-
console.error("đ Request handling error:", error);
|
|
259
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
260
|
-
res.end("Internal Server Error");
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Handle incoming webhook requests
|
|
265
|
-
*/
|
|
266
|
-
async handleWebhookRequest(req, res) {
|
|
267
|
-
try {
|
|
268
|
-
console.log(`đ Incoming webhook request: ${req.method} ${req.url}`);
|
|
269
|
-
if (req.method !== "POST") {
|
|
270
|
-
console.log(`đ Rejected non-POST request: ${req.method}`);
|
|
271
|
-
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
272
|
-
res.end("Method Not Allowed");
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
// Check if this is a direct Linear webhook (has linear-signature header)
|
|
276
|
-
const linearSignature = req.headers["linear-signature"];
|
|
277
|
-
const isDirectWebhook = !!linearSignature;
|
|
278
|
-
if (isDirectWebhook && this.linearWebhookHandlers.size > 0) {
|
|
279
|
-
// For direct Linear webhooks, pass the raw request to the handler
|
|
280
|
-
// The LinearWebhookClient will handle its own signature verification
|
|
281
|
-
console.log(`đ Direct Linear webhook received, trying ${this.linearWebhookHandlers.size} direct handlers`);
|
|
282
|
-
// Try each direct handler
|
|
283
|
-
for (const [token, handler] of this.linearWebhookHandlers) {
|
|
284
|
-
try {
|
|
285
|
-
// The handler will manage the response
|
|
286
|
-
await handler(req, res);
|
|
287
|
-
console.log(`đ Direct webhook delivered to token ending in ...${token.slice(-4)}`);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
console.error(`đ Error in direct webhook handler for token ...${token.slice(-4)}:`, error);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// No direct handler could process it
|
|
295
|
-
console.error(`đ Direct webhook processing failed for all ${this.linearWebhookHandlers.size} handlers`);
|
|
296
|
-
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
297
|
-
res.end("Unauthorized");
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
// Otherwise, handle as proxy-style webhook
|
|
301
|
-
// Read request body
|
|
302
|
-
let body = "";
|
|
303
|
-
req.on("data", (chunk) => {
|
|
304
|
-
body += chunk.toString();
|
|
305
|
-
});
|
|
306
|
-
req.on("end", () => {
|
|
307
|
-
try {
|
|
308
|
-
// For proxy-style webhooks, we need the signature header
|
|
309
|
-
const signature = req.headers["x-webhook-signature"];
|
|
310
|
-
const timestamp = req.headers["x-webhook-timestamp"];
|
|
311
|
-
console.log(`đ Proxy webhook received with ${body.length} bytes, ${this.webhookHandlers.size} registered handlers`);
|
|
312
|
-
if (!signature) {
|
|
313
|
-
console.log("đ Webhook rejected: Missing signature header");
|
|
314
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
315
|
-
res.end("Missing signature");
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
// Try each registered handler until one verifies the signature
|
|
319
|
-
let handlerAttempts = 0;
|
|
320
|
-
for (const [token, { handler }] of this.webhookHandlers) {
|
|
321
|
-
handlerAttempts++;
|
|
322
|
-
try {
|
|
323
|
-
if (handler(body, signature, timestamp)) {
|
|
324
|
-
// Handler verified signature and processed webhook
|
|
325
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
326
|
-
res.end("OK");
|
|
327
|
-
console.log(`đ Webhook delivered to token ending in ...${token.slice(-4)} (attempt ${handlerAttempts}/${this.webhookHandlers.size})`);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
console.error(`đ Error in webhook handler for token ...${token.slice(-4)}:`, error);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// No handler could verify the signature
|
|
336
|
-
console.error(`đ Webhook signature verification failed for all ${this.webhookHandlers.size} registered handlers`);
|
|
337
|
-
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
338
|
-
res.end("Unauthorized");
|
|
339
|
-
}
|
|
340
|
-
catch (error) {
|
|
341
|
-
console.error("đ Error processing webhook:", error);
|
|
342
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
343
|
-
res.end("Bad Request");
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
req.on("error", (error) => {
|
|
347
|
-
console.error("đ Request error:", error);
|
|
348
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
349
|
-
res.end("Internal Server Error");
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
catch (error) {
|
|
353
|
-
console.error("đ Webhook request error:", error);
|
|
354
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
355
|
-
res.end("Internal Server Error");
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Handle OAuth callback requests
|
|
360
|
-
*/
|
|
361
|
-
async handleOAuthCallback(_req, res, url) {
|
|
362
|
-
try {
|
|
363
|
-
const code = url.searchParams.get("code");
|
|
364
|
-
const state = url.searchParams.get("state");
|
|
365
|
-
// Check if this is a direct Linear callback (has code and state)
|
|
366
|
-
const isExternalHost = process.env.CYRUS_HOST_EXTERNAL?.toLowerCase().trim() === "true";
|
|
367
|
-
const isDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
|
|
368
|
-
// Handle direct callback if both external host and direct webhooks are enabled
|
|
369
|
-
if (code && state && isExternalHost && isDirectWebhooks) {
|
|
370
|
-
await this.handleDirectLinearCallback(_req, res, url);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
// Otherwise handle as proxy callback
|
|
374
|
-
const token = url.searchParams.get("token");
|
|
375
|
-
const workspaceId = url.searchParams.get("workspaceId");
|
|
376
|
-
const workspaceName = url.searchParams.get("workspaceName");
|
|
377
|
-
if (token && workspaceId && workspaceName) {
|
|
378
|
-
// Success! Return the Linear credentials
|
|
379
|
-
const linearCredentials = {
|
|
380
|
-
linearToken: token,
|
|
381
|
-
linearWorkspaceId: workspaceId,
|
|
382
|
-
linearWorkspaceName: workspaceName,
|
|
383
|
-
};
|
|
384
|
-
// Send success response
|
|
385
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
386
|
-
res.end(`
|
|
387
|
-
<!DOCTYPE html>
|
|
388
|
-
<html>
|
|
389
|
-
<head>
|
|
390
|
-
<meta charset="UTF-8">
|
|
391
|
-
<title>Authorization Successful</title>
|
|
392
|
-
</head>
|
|
393
|
-
<body style="font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px;">
|
|
394
|
-
<h1>â
Authorization Successful!</h1>
|
|
395
|
-
<p>You can close this window and return to the terminal.</p>
|
|
396
|
-
<p>Your Linear workspace <strong>${workspaceName}</strong> has been connected.</p>
|
|
397
|
-
<p style="margin-top: 30px;">
|
|
398
|
-
<a href="${this.proxyUrl}/oauth/authorize?callback=${process.env.CYRUS_BASE_URL || `http://${this.host}:${this.port}`}/callback"
|
|
399
|
-
style="padding: 10px 20px; background: #5E6AD2; color: white; text-decoration: none; border-radius: 5px;">
|
|
400
|
-
Connect Another Workspace
|
|
401
|
-
</a>
|
|
402
|
-
</p>
|
|
403
|
-
<script>setTimeout(() => window.close(), 10000)</script>
|
|
404
|
-
</body>
|
|
405
|
-
</html>
|
|
406
|
-
`);
|
|
407
|
-
console.log(`đ OAuth callback received for workspace: ${workspaceName}`);
|
|
408
|
-
// Resolve any waiting promises
|
|
409
|
-
if (this.oauthCallbacks.size > 0) {
|
|
410
|
-
const callback = this.oauthCallbacks.values().next().value;
|
|
411
|
-
if (callback) {
|
|
412
|
-
callback.resolve(linearCredentials);
|
|
413
|
-
this.oauthCallbacks.delete(callback.id);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
// Call the registered OAuth callback handler
|
|
417
|
-
if (this.oauthCallbackHandler) {
|
|
418
|
-
try {
|
|
419
|
-
await this.oauthCallbackHandler(token, workspaceId, workspaceName);
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
console.error("đ Error in OAuth callback handler:", error);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
428
|
-
res.end("<h1>Error: No token received</h1>");
|
|
429
|
-
// Reject any waiting promises
|
|
430
|
-
for (const [id, callback] of this.oauthCallbacks) {
|
|
431
|
-
callback.reject(new Error("No token received"));
|
|
432
|
-
this.oauthCallbacks.delete(id);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
catch (error) {
|
|
437
|
-
console.error("đ OAuth callback error:", error);
|
|
438
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
439
|
-
res.end("Internal Server Error");
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Handle OAuth authorization requests for direct Linear OAuth
|
|
444
|
-
*/
|
|
445
|
-
async handleOAuthAuthorize(_req, res, _url) {
|
|
446
|
-
try {
|
|
447
|
-
// Check if we're in external host mode with direct webhooks
|
|
448
|
-
const isExternalHost = process.env.CYRUS_HOST_EXTERNAL?.toLowerCase().trim() === "true";
|
|
449
|
-
const isDirectWebhooks = process.env.LINEAR_DIRECT_WEBHOOKS?.toLowerCase().trim() === "true";
|
|
450
|
-
// Only handle OAuth locally if both external host AND direct webhooks are enabled
|
|
451
|
-
if (!isExternalHost || !isDirectWebhooks) {
|
|
452
|
-
// Redirect to proxy OAuth endpoint
|
|
453
|
-
const callbackBaseUrl = this.getBaseUrl();
|
|
454
|
-
const proxyAuthUrl = `${this.proxyUrl}/oauth/authorize?callback=${callbackBaseUrl}/callback`;
|
|
455
|
-
res.writeHead(302, { Location: proxyAuthUrl });
|
|
456
|
-
res.end();
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
// Check for LINEAR_CLIENT_ID
|
|
460
|
-
const clientId = process.env.LINEAR_CLIENT_ID;
|
|
461
|
-
if (!clientId) {
|
|
462
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
463
|
-
res.end("LINEAR_CLIENT_ID environment variable is required for direct OAuth");
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
// Generate state for CSRF protection
|
|
467
|
-
const state = randomUUID();
|
|
468
|
-
// Store state with expiration (10 minutes)
|
|
469
|
-
this.oauthStates.set(state, {
|
|
470
|
-
createdAt: Date.now(),
|
|
471
|
-
redirectUri: `${this.getBaseUrl()}/callback`,
|
|
472
|
-
});
|
|
473
|
-
// Clean up expired states (older than 10 minutes)
|
|
474
|
-
const now = Date.now();
|
|
475
|
-
for (const [stateKey, stateData] of this.oauthStates) {
|
|
476
|
-
if (now - stateData.createdAt > 10 * 60 * 1000) {
|
|
477
|
-
this.oauthStates.delete(stateKey);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
// Build Linear OAuth URL
|
|
481
|
-
const authUrl = new URL("https://linear.app/oauth/authorize");
|
|
482
|
-
authUrl.searchParams.set("client_id", clientId);
|
|
483
|
-
authUrl.searchParams.set("redirect_uri", `${this.getBaseUrl()}/callback`);
|
|
484
|
-
authUrl.searchParams.set("response_type", "code");
|
|
485
|
-
authUrl.searchParams.set("state", state);
|
|
486
|
-
authUrl.searchParams.set("scope", "read,write,app:assignable,app:mentionable");
|
|
487
|
-
authUrl.searchParams.set("actor", "app");
|
|
488
|
-
authUrl.searchParams.set("prompt", "consent");
|
|
489
|
-
console.log(`đ Redirecting to Linear OAuth: ${authUrl.toString()}`);
|
|
490
|
-
// Redirect to Linear OAuth
|
|
491
|
-
res.writeHead(302, { Location: authUrl.toString() });
|
|
492
|
-
res.end();
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
console.error("đ OAuth authorize error:", error);
|
|
496
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
497
|
-
res.end("Internal Server Error");
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Handle direct Linear OAuth callback (exchange code for token)
|
|
502
|
-
*/
|
|
503
|
-
async handleDirectLinearCallback(_req, res, url) {
|
|
504
|
-
try {
|
|
505
|
-
const code = url.searchParams.get("code");
|
|
506
|
-
const state = url.searchParams.get("state");
|
|
507
|
-
if (!code || !state) {
|
|
508
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
509
|
-
res.end("Missing code or state parameter");
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
// Validate state
|
|
513
|
-
const stateData = this.oauthStates.get(state);
|
|
514
|
-
if (!stateData) {
|
|
515
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
516
|
-
res.end("Invalid or expired state");
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
// Delete state after use
|
|
520
|
-
this.oauthStates.delete(state);
|
|
521
|
-
// Exchange code for token
|
|
522
|
-
const tokenResponse = await this.exchangeCodeForToken(code);
|
|
523
|
-
// Get workspace info using the token
|
|
524
|
-
const workspaceInfo = await this.getWorkspaceInfo(tokenResponse.access_token);
|
|
525
|
-
// Success! Return the Linear credentials
|
|
526
|
-
const linearCredentials = {
|
|
527
|
-
linearToken: tokenResponse.access_token,
|
|
528
|
-
linearWorkspaceId: workspaceInfo.organization.id,
|
|
529
|
-
linearWorkspaceName: workspaceInfo.organization.name,
|
|
530
|
-
};
|
|
531
|
-
// Send success response
|
|
532
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
533
|
-
res.end(`
|
|
534
|
-
<!DOCTYPE html>
|
|
535
|
-
<html>
|
|
536
|
-
<head>
|
|
537
|
-
<meta charset="UTF-8">
|
|
538
|
-
<title>Authorization Successful</title>
|
|
539
|
-
</head>
|
|
540
|
-
<body style="font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px;">
|
|
541
|
-
<h1>â
Authorization Successful!</h1>
|
|
542
|
-
<p>You can close this window and return to the terminal.</p>
|
|
543
|
-
<p>Your Linear workspace <strong>${workspaceInfo.organization.name}</strong> has been connected.</p>
|
|
544
|
-
<p style="margin-top: 30px;">
|
|
545
|
-
<a href="${this.getBaseUrl()}/oauth/authorize"
|
|
546
|
-
style="padding: 10px 20px; background: #5E6AD2; color: white; text-decoration: none; border-radius: 5px;">
|
|
547
|
-
Connect Another Workspace
|
|
548
|
-
</a>
|
|
549
|
-
</p>
|
|
550
|
-
<script>setTimeout(() => window.close(), 10000)</script>
|
|
551
|
-
</body>
|
|
552
|
-
</html>
|
|
553
|
-
`);
|
|
554
|
-
console.log(`đ Direct OAuth callback received for workspace: ${workspaceInfo.organization.name}`);
|
|
555
|
-
// Resolve any waiting promises
|
|
556
|
-
if (this.oauthCallbacks.size > 0) {
|
|
557
|
-
const callback = this.oauthCallbacks.values().next().value;
|
|
558
|
-
if (callback) {
|
|
559
|
-
callback.resolve(linearCredentials);
|
|
560
|
-
this.oauthCallbacks.delete(callback.id);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
// Call the registered OAuth callback handler
|
|
564
|
-
if (this.oauthCallbackHandler) {
|
|
565
|
-
try {
|
|
566
|
-
await this.oauthCallbackHandler(tokenResponse.access_token, workspaceInfo.organization.id, workspaceInfo.organization.name);
|
|
567
|
-
}
|
|
568
|
-
catch (error) {
|
|
569
|
-
console.error("đ Error in OAuth callback handler:", error);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
console.error("đ Direct Linear callback error:", error);
|
|
575
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
576
|
-
res.end(`OAuth failed: ${error.message}`);
|
|
577
|
-
// Reject any waiting promises
|
|
578
|
-
for (const [id, callback] of this.oauthCallbacks) {
|
|
579
|
-
callback.reject(error);
|
|
580
|
-
this.oauthCallbacks.delete(id);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Exchange authorization code for access token
|
|
586
|
-
*/
|
|
587
|
-
async exchangeCodeForToken(code) {
|
|
588
|
-
const clientId = process.env.LINEAR_CLIENT_ID;
|
|
589
|
-
const clientSecret = process.env.LINEAR_CLIENT_SECRET;
|
|
590
|
-
if (!clientId || !clientSecret) {
|
|
591
|
-
throw new Error("LINEAR_CLIENT_ID and LINEAR_CLIENT_SECRET are required");
|
|
592
|
-
}
|
|
593
|
-
const response = await fetch("https://api.linear.app/oauth/token", {
|
|
594
|
-
method: "POST",
|
|
595
|
-
headers: {
|
|
596
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
597
|
-
},
|
|
598
|
-
body: new URLSearchParams({
|
|
599
|
-
grant_type: "authorization_code",
|
|
600
|
-
client_id: clientId,
|
|
601
|
-
client_secret: clientSecret,
|
|
602
|
-
redirect_uri: `${this.getBaseUrl()}/callback`,
|
|
603
|
-
code: code,
|
|
604
|
-
}),
|
|
605
|
-
});
|
|
606
|
-
if (!response.ok) {
|
|
607
|
-
const error = await response.text();
|
|
608
|
-
throw new Error(`Token exchange failed: ${error}`);
|
|
609
|
-
}
|
|
610
|
-
return await response.json();
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Get workspace information using access token
|
|
614
|
-
*/
|
|
615
|
-
async getWorkspaceInfo(accessToken) {
|
|
616
|
-
const response = await fetch("https://api.linear.app/graphql", {
|
|
617
|
-
method: "POST",
|
|
618
|
-
headers: {
|
|
619
|
-
"Content-Type": "application/json",
|
|
620
|
-
Authorization: `Bearer ${accessToken}`,
|
|
621
|
-
},
|
|
622
|
-
body: JSON.stringify({
|
|
623
|
-
query: `
|
|
624
|
-
query {
|
|
625
|
-
viewer {
|
|
626
|
-
id
|
|
627
|
-
name
|
|
628
|
-
email
|
|
629
|
-
organization {
|
|
630
|
-
id
|
|
631
|
-
name
|
|
632
|
-
urlKey
|
|
633
|
-
teams {
|
|
634
|
-
nodes {
|
|
635
|
-
id
|
|
636
|
-
key
|
|
637
|
-
name
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
`,
|
|
644
|
-
}),
|
|
645
|
-
});
|
|
646
|
-
if (!response.ok) {
|
|
647
|
-
throw new Error("Failed to get workspace info");
|
|
648
|
-
}
|
|
649
|
-
const data = (await response.json());
|
|
650
|
-
if (data.errors) {
|
|
651
|
-
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
|
|
652
|
-
}
|
|
653
|
-
return {
|
|
654
|
-
userId: data.data.viewer.id,
|
|
655
|
-
userEmail: data.data.viewer.email,
|
|
656
|
-
organization: data.data.viewer.organization,
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Escape HTML special characters to prevent XSS attacks
|
|
661
|
-
*/
|
|
662
|
-
escapeHtml(unsafe) {
|
|
663
|
-
return unsafe
|
|
664
|
-
.replace(/&/g, "&")
|
|
665
|
-
.replace(/</g, "<")
|
|
666
|
-
.replace(/>/g, ">")
|
|
667
|
-
.replace(/"/g, """)
|
|
668
|
-
.replace(/'/g, "'");
|
|
669
|
-
}
|
|
670
214
|
/**
|
|
671
215
|
* Register an approval request and get approval URL
|
|
672
216
|
*/
|
|
@@ -689,224 +233,9 @@ export class SharedApplicationServer {
|
|
|
689
233
|
});
|
|
690
234
|
});
|
|
691
235
|
// Generate approval URL
|
|
692
|
-
const url =
|
|
236
|
+
const url = `http://${this.host}:${this.port}/approval?session=${encodeURIComponent(sessionId)}`;
|
|
693
237
|
console.log(`đ Registered approval request for session ${sessionId}: ${url}`);
|
|
694
238
|
return { promise, url };
|
|
695
239
|
}
|
|
696
|
-
/**
|
|
697
|
-
* Handle approval requests
|
|
698
|
-
*/
|
|
699
|
-
async handleApprovalRequest(_req, res, url) {
|
|
700
|
-
try {
|
|
701
|
-
const sessionId = url.searchParams.get("session");
|
|
702
|
-
const action = url.searchParams.get("action"); // "approve" or "reject"
|
|
703
|
-
const feedback = url.searchParams.get("feedback");
|
|
704
|
-
if (!sessionId) {
|
|
705
|
-
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
706
|
-
res.end(`
|
|
707
|
-
<!DOCTYPE html>
|
|
708
|
-
<html>
|
|
709
|
-
<head>
|
|
710
|
-
<meta charset="UTF-8">
|
|
711
|
-
<title>Invalid Request</title>
|
|
712
|
-
</head>
|
|
713
|
-
<body style="font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px;">
|
|
714
|
-
<h1>â Invalid Request</h1>
|
|
715
|
-
<p>Missing session parameter.</p>
|
|
716
|
-
</body>
|
|
717
|
-
</html>
|
|
718
|
-
`);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
const approval = this.pendingApprovals.get(sessionId);
|
|
722
|
-
// If no action specified, show approval UI
|
|
723
|
-
if (!action) {
|
|
724
|
-
const approvalExists = !!approval;
|
|
725
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
726
|
-
res.end(`
|
|
727
|
-
<!DOCTYPE html>
|
|
728
|
-
<html>
|
|
729
|
-
<head>
|
|
730
|
-
<meta charset="UTF-8">
|
|
731
|
-
<title>Approval Required</title>
|
|
732
|
-
<style>
|
|
733
|
-
body {
|
|
734
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
735
|
-
max-width: 700px;
|
|
736
|
-
margin: 50px auto;
|
|
737
|
-
padding: 20px;
|
|
738
|
-
background: #f5f5f5;
|
|
739
|
-
}
|
|
740
|
-
.card {
|
|
741
|
-
background: white;
|
|
742
|
-
padding: 30px;
|
|
743
|
-
border-radius: 8px;
|
|
744
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
745
|
-
}
|
|
746
|
-
h1 {
|
|
747
|
-
margin-top: 0;
|
|
748
|
-
color: #333;
|
|
749
|
-
}
|
|
750
|
-
.status {
|
|
751
|
-
padding: 15px;
|
|
752
|
-
border-radius: 5px;
|
|
753
|
-
margin: 20px 0;
|
|
754
|
-
}
|
|
755
|
-
.status.pending {
|
|
756
|
-
background: #fff3cd;
|
|
757
|
-
border-left: 4px solid #ffc107;
|
|
758
|
-
}
|
|
759
|
-
.status.resolved {
|
|
760
|
-
background: #d4edda;
|
|
761
|
-
border-left: 4px solid #28a745;
|
|
762
|
-
}
|
|
763
|
-
.buttons {
|
|
764
|
-
display: flex;
|
|
765
|
-
gap: 10px;
|
|
766
|
-
margin-top: 20px;
|
|
767
|
-
}
|
|
768
|
-
button {
|
|
769
|
-
padding: 12px 24px;
|
|
770
|
-
font-size: 16px;
|
|
771
|
-
border: none;
|
|
772
|
-
border-radius: 5px;
|
|
773
|
-
cursor: pointer;
|
|
774
|
-
transition: opacity 0.2s;
|
|
775
|
-
}
|
|
776
|
-
button:hover:not(:disabled) {
|
|
777
|
-
opacity: 0.9;
|
|
778
|
-
}
|
|
779
|
-
button:disabled {
|
|
780
|
-
opacity: 0.5;
|
|
781
|
-
cursor: not-allowed;
|
|
782
|
-
}
|
|
783
|
-
.approve-btn {
|
|
784
|
-
background: #28a745;
|
|
785
|
-
color: white;
|
|
786
|
-
flex: 1;
|
|
787
|
-
}
|
|
788
|
-
.reject-btn {
|
|
789
|
-
background: #dc3545;
|
|
790
|
-
color: white;
|
|
791
|
-
flex: 1;
|
|
792
|
-
}
|
|
793
|
-
textarea {
|
|
794
|
-
width: 100%;
|
|
795
|
-
padding: 10px;
|
|
796
|
-
border: 1px solid #ddd;
|
|
797
|
-
border-radius: 5px;
|
|
798
|
-
font-family: inherit;
|
|
799
|
-
margin-top: 10px;
|
|
800
|
-
resize: vertical;
|
|
801
|
-
}
|
|
802
|
-
label {
|
|
803
|
-
display: block;
|
|
804
|
-
margin-top: 15px;
|
|
805
|
-
color: #666;
|
|
806
|
-
font-size: 14px;
|
|
807
|
-
}
|
|
808
|
-
</style>
|
|
809
|
-
</head>
|
|
810
|
-
<body>
|
|
811
|
-
<div class="card">
|
|
812
|
-
${approvalExists
|
|
813
|
-
? `
|
|
814
|
-
<h1>đ Approval Required</h1>
|
|
815
|
-
<div class="status pending">
|
|
816
|
-
<strong>Status:</strong> Waiting for your decision
|
|
817
|
-
</div>
|
|
818
|
-
<p>The agent is requesting your approval to proceed with the next step of the workflow.</p>
|
|
819
|
-
|
|
820
|
-
<label for="feedback">Optional feedback or instructions:</label>
|
|
821
|
-
<textarea id="feedback" rows="3" placeholder="Enter any feedback or additional instructions..."></textarea>
|
|
822
|
-
|
|
823
|
-
<div class="buttons">
|
|
824
|
-
<button class="approve-btn" onclick="handleAction('approve')">
|
|
825
|
-
â
Approve
|
|
826
|
-
</button>
|
|
827
|
-
<button class="reject-btn" onclick="handleAction('reject')">
|
|
828
|
-
â Reject
|
|
829
|
-
</button>
|
|
830
|
-
</div>
|
|
831
|
-
`
|
|
832
|
-
: `
|
|
833
|
-
<h1>âšī¸ Approval Already Processed</h1>
|
|
834
|
-
<div class="status resolved">
|
|
835
|
-
This approval request has already been processed or has expired.
|
|
836
|
-
</div>
|
|
837
|
-
<p>You can close this window.</p>
|
|
838
|
-
`}
|
|
839
|
-
</div>
|
|
840
|
-
|
|
841
|
-
<script>
|
|
842
|
-
async function handleAction(action) {
|
|
843
|
-
const feedback = document.getElementById('feedback')?.value || '';
|
|
844
|
-
const url = new URL(window.location.href);
|
|
845
|
-
url.searchParams.set('action', action);
|
|
846
|
-
if (feedback) {
|
|
847
|
-
url.searchParams.set('feedback', feedback);
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// Disable buttons
|
|
851
|
-
document.querySelectorAll('button').forEach(btn => btn.disabled = true);
|
|
852
|
-
|
|
853
|
-
// Navigate to confirmation
|
|
854
|
-
window.location.href = url.toString();
|
|
855
|
-
}
|
|
856
|
-
</script>
|
|
857
|
-
</body>
|
|
858
|
-
</html>
|
|
859
|
-
`);
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
// Handle approval/rejection
|
|
863
|
-
if (!approval) {
|
|
864
|
-
res.writeHead(410, { "Content-Type": "text/html; charset=utf-8" });
|
|
865
|
-
res.end(`
|
|
866
|
-
<!DOCTYPE html>
|
|
867
|
-
<html>
|
|
868
|
-
<head>
|
|
869
|
-
<meta charset="UTF-8">
|
|
870
|
-
<title>Approval Expired</title>
|
|
871
|
-
</head>
|
|
872
|
-
<body style="font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px;">
|
|
873
|
-
<h1>â° Approval Expired</h1>
|
|
874
|
-
<p>This approval request has already been processed or has expired.</p>
|
|
875
|
-
<p>You can close this window.</p>
|
|
876
|
-
</body>
|
|
877
|
-
</html>
|
|
878
|
-
`);
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
// Process the approval/rejection
|
|
882
|
-
const approved = action === "approve";
|
|
883
|
-
approval.resolve(approved, feedback || undefined);
|
|
884
|
-
this.pendingApprovals.delete(sessionId);
|
|
885
|
-
console.log(`đ Approval ${approved ? "granted" : "rejected"} for session ${sessionId}${feedback ? ` with feedback: ${feedback}` : ""}`);
|
|
886
|
-
// Send success response
|
|
887
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
888
|
-
res.end(`
|
|
889
|
-
<!DOCTYPE html>
|
|
890
|
-
<html>
|
|
891
|
-
<head>
|
|
892
|
-
<meta charset="UTF-8">
|
|
893
|
-
<title>Approval ${approved ? "Granted" : "Rejected"}</title>
|
|
894
|
-
</head>
|
|
895
|
-
<body style="font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px;">
|
|
896
|
-
<h1>${approved ? "â
Approval Granted" : "â Approval Rejected"}</h1>
|
|
897
|
-
<p>Your decision has been recorded. The agent will ${approved ? "proceed with the next step" : "stop the current workflow"}.</p>
|
|
898
|
-
${feedback ? `<p><strong>Feedback provided:</strong> ${this.escapeHtml(feedback)}</p>` : ""}
|
|
899
|
-
<p style="margin-top: 30px; color: #666;">You can close this window and return to Linear.</p>
|
|
900
|
-
<script>setTimeout(() => window.close(), 5000)</script>
|
|
901
|
-
</body>
|
|
902
|
-
</html>
|
|
903
|
-
`);
|
|
904
|
-
}
|
|
905
|
-
catch (error) {
|
|
906
|
-
console.error("đ Approval request error:", error);
|
|
907
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
908
|
-
res.end("Internal Server Error");
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
240
|
}
|
|
912
241
|
//# sourceMappingURL=SharedApplicationServer.js.map
|