copilot-proxy-web 1.0.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/lib/cli.js ADDED
@@ -0,0 +1,273 @@
1
+ function parseArgs(argv) {
2
+ let logPath = "run.log";
3
+ let webEnabled = false;
4
+ let webPlain = false;
5
+ let apiEnabled = false;
6
+ let host = "127.0.0.1";
7
+ let port = 3000;
8
+ let apiPort = 3000;
9
+ let useShell = false;
10
+ let debugWs = false;
11
+ let debugWsPath = "";
12
+ let ptyCols = 80;
13
+ let ptyRows = 24;
14
+ let idleMs = 5000;
15
+ let idleLogPath = "";
16
+ let idleClean = true;
17
+ let idleMarkdown = false;
18
+ let idleMarkdownMaxChars = 0;
19
+ let idleBufferMaxChars = null;
20
+ let idleBurstMs = null;
21
+ let idlePacketMaxBytes = null;
22
+ let conversationContext = null;
23
+ let conversationProfile = null;
24
+ let authToken = null;
25
+ let useXForwardedFor = false;
26
+ let enforceAuthOnPublic = false;
27
+ let tgToken = null;
28
+ let tgChat = null;
29
+ let tgProxy = null;
30
+ let tgParseMode = null;
31
+ let tgRetry = null;
32
+ let tgBackoffMs = null;
33
+ let tgTimeoutMs = null;
34
+ let tgMaxChars = null;
35
+ let cleaned = [];
36
+ let noDefaultSession = false;
37
+ let noStdin = false;
38
+ let noStdout = false;
39
+
40
+ for (let i = 0; i < argv.length; i += 1) {
41
+ const arg = argv[i];
42
+ if (arg === "--log" && i + 1 < argv.length) {
43
+ logPath = argv[i + 1];
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (arg === "--web") {
48
+ webEnabled = true;
49
+ apiEnabled = true;
50
+ continue;
51
+ }
52
+ if (arg === "--api") {
53
+ apiEnabled = true;
54
+ continue;
55
+ }
56
+ if (arg === "--web-plain") {
57
+ webPlain = true;
58
+ continue;
59
+ }
60
+ if (arg === "--debug-ws") {
61
+ debugWs = true;
62
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
63
+ debugWsPath = argv[i + 1];
64
+ i += 1;
65
+ }
66
+ continue;
67
+ }
68
+ if (arg === "--pty-cols" && i + 1 < argv.length) {
69
+ ptyCols = Number(argv[i + 1]) || ptyCols;
70
+ i += 1;
71
+ continue;
72
+ }
73
+ if (arg === "--pty-rows" && i + 1 < argv.length) {
74
+ ptyRows = Number(argv[i + 1]) || ptyRows;
75
+ i += 1;
76
+ continue;
77
+ }
78
+ if (arg === "--idle-ms" && i + 1 < argv.length) {
79
+ const value = Number(argv[i + 1]);
80
+ if (Number.isFinite(value)) idleMs = value;
81
+ i += 1;
82
+ continue;
83
+ }
84
+ if (arg === "--idle-seconds" && i + 1 < argv.length) {
85
+ const value = Number(argv[i + 1]);
86
+ if (Number.isFinite(value)) idleMs = Math.max(0, value * 1000);
87
+ i += 1;
88
+ continue;
89
+ }
90
+ if (arg === "--idle-log" && i + 1 < argv.length) {
91
+ idleLogPath = argv[i + 1];
92
+ i += 1;
93
+ continue;
94
+ }
95
+ if (arg === "--idle-markdown") {
96
+ idleMarkdown = true;
97
+ continue;
98
+ }
99
+ if (arg === "--idle-markdown-max" && i + 1 < argv.length) {
100
+ idleMarkdownMaxChars = Number(argv[i + 1]) || idleMarkdownMaxChars;
101
+ i += 1;
102
+ continue;
103
+ }
104
+ if (arg === "--idle-buffer-max" && i + 1 < argv.length) {
105
+ const value = Number(argv[i + 1]);
106
+ if (Number.isFinite(value)) idleBufferMaxChars = value;
107
+ i += 1;
108
+ continue;
109
+ }
110
+ if (arg === "--idle-burst-ms" && i + 1 < argv.length) {
111
+ const value = Number(argv[i + 1]);
112
+ if (Number.isFinite(value)) idleBurstMs = value;
113
+ i += 1;
114
+ continue;
115
+ }
116
+ if (arg === "--idle-packet-max" && i + 1 < argv.length) {
117
+ const value = Number(argv[i + 1]);
118
+ if (Number.isFinite(value)) idlePacketMaxBytes = value;
119
+ i += 1;
120
+ continue;
121
+ }
122
+ if (arg === "--conversation-context" && i + 1 < argv.length) {
123
+ const value = Number(argv[i + 1]);
124
+ if (Number.isFinite(value)) conversationContext = value;
125
+ i += 1;
126
+ continue;
127
+ }
128
+ if (arg === "--conversation-profile" && i + 1 < argv.length) {
129
+ conversationProfile = argv[i + 1];
130
+ i += 1;
131
+ continue;
132
+ }
133
+ if (arg === "--tg-token" && i + 1 < argv.length) {
134
+ tgToken = argv[i + 1];
135
+ i += 1;
136
+ continue;
137
+ }
138
+ if (arg === "--tg-chat" && i + 1 < argv.length) {
139
+ tgChat = argv[i + 1];
140
+ i += 1;
141
+ continue;
142
+ }
143
+ if (arg === "--tg-proxy" && i + 1 < argv.length) {
144
+ tgProxy = argv[i + 1];
145
+ i += 1;
146
+ continue;
147
+ }
148
+ if (arg === "--tg-parse-mode" && i + 1 < argv.length) {
149
+ tgParseMode = argv[i + 1];
150
+ i += 1;
151
+ continue;
152
+ }
153
+ if (arg === "--tg-retry" && i + 1 < argv.length) {
154
+ tgRetry = Number(argv[i + 1]);
155
+ i += 1;
156
+ continue;
157
+ }
158
+ if (arg === "--tg-backoff-ms" && i + 1 < argv.length) {
159
+ tgBackoffMs = Number(argv[i + 1]);
160
+ i += 1;
161
+ continue;
162
+ }
163
+ if (arg === "--tg-timeout-ms" && i + 1 < argv.length) {
164
+ tgTimeoutMs = Number(argv[i + 1]);
165
+ i += 1;
166
+ continue;
167
+ }
168
+ if (arg === "--tg-max-chars" && i + 1 < argv.length) {
169
+ tgMaxChars = Number(argv[i + 1]);
170
+ i += 1;
171
+ continue;
172
+ }
173
+ if (arg === "--auth-token" && i + 1 < argv.length) {
174
+ authToken = argv[i + 1];
175
+ i += 1;
176
+ continue;
177
+ }
178
+ if (arg === "--use-x-forwarded-for") {
179
+ useXForwardedFor = true;
180
+ continue;
181
+ }
182
+ if (arg === "--enforce-auth-on-public") {
183
+ enforceAuthOnPublic = true;
184
+ continue;
185
+ }
186
+ if (arg === "--idle-raw") {
187
+ idleClean = false;
188
+ continue;
189
+ }
190
+ if (arg === "--port" && i + 1 < argv.length) {
191
+ port = Number(argv[i + 1]) || port;
192
+ apiPort = port;
193
+ i += 1;
194
+ continue;
195
+ }
196
+ if (arg === "--host" && i + 1 < argv.length) {
197
+ host = argv[i + 1];
198
+ i += 1;
199
+ continue;
200
+ }
201
+ if (arg === "--api-port" && i + 1 < argv.length) {
202
+ apiPort = Number(argv[i + 1]) || apiPort;
203
+ i += 1;
204
+ continue;
205
+ }
206
+ if (arg === "--shell") {
207
+ useShell = true;
208
+ continue;
209
+ }
210
+ if (arg === "--no-default-session") {
211
+ noDefaultSession = true;
212
+ continue;
213
+ }
214
+ if (arg === "--no-stdin") {
215
+ noStdin = true;
216
+ continue;
217
+ }
218
+ if (arg === "--no-stdout") {
219
+ noStdout = true;
220
+ continue;
221
+ }
222
+ cleaned.push(arg);
223
+ }
224
+
225
+ const sepIndex = cleaned.indexOf("--");
226
+ const cmdArgs = sepIndex >= 0 ? cleaned.slice(sepIndex + 1) : cleaned;
227
+ const [cmd, ...args] = cmdArgs.length > 0 ? cmdArgs : ["copilot"];
228
+
229
+ return {
230
+ logPath,
231
+ webEnabled,
232
+ webPlain,
233
+ apiEnabled,
234
+ host,
235
+ port,
236
+ apiPort,
237
+ useShell,
238
+ debugWs,
239
+ debugWsPath,
240
+ ptyCols,
241
+ ptyRows,
242
+ idleMs,
243
+ idleLogPath,
244
+ idleClean,
245
+ idleMarkdown,
246
+ idleMarkdownMaxChars,
247
+ idleBufferMaxChars,
248
+ idleBurstMs,
249
+ idlePacketMaxBytes,
250
+ conversationContext,
251
+ conversationProfile,
252
+ tgToken,
253
+ tgChat,
254
+ tgProxy,
255
+ tgParseMode,
256
+ tgRetry,
257
+ tgBackoffMs,
258
+ tgTimeoutMs,
259
+ tgMaxChars,
260
+ authToken,
261
+ useXForwardedFor,
262
+ enforceAuthOnPublic,
263
+ noDefaultSession,
264
+ noStdin,
265
+ noStdout,
266
+ cmd,
267
+ args,
268
+ };
269
+ }
270
+
271
+ module.exports = {
272
+ parseArgs,
273
+ };
@@ -0,0 +1,326 @@
1
+ function createCloudflareService({
2
+ ensureCloudflaredOrExit,
3
+ cfStateStore,
4
+ cfConfigFile,
5
+ fsLike,
6
+ spawnFn,
7
+ isProcessAlive,
8
+ sleepFn,
9
+ processLike,
10
+ consoleLike,
11
+ httpsLike,
12
+ httpLike,
13
+ getCloudflareArg,
14
+ normalizePath,
15
+ port,
16
+ cfTokenFile,
17
+ }) {
18
+ function exit(code) {
19
+ processLike.exit(code);
20
+ }
21
+
22
+ function cloudflareStart() {
23
+ const bin = ensureCloudflaredOrExit();
24
+ const existingPid = cfStateStore.readCfPid();
25
+ if (existingPid && isProcessAlive(existingPid)) {
26
+ consoleLike.log(`Cloudflare tunnel already running (pid ${existingPid}).`);
27
+ return exit(0);
28
+ }
29
+ if (!fsLike.existsSync(cfConfigFile)) {
30
+ consoleLike.error("Missing cloudflare config. Run: npx copilot-proxy-web cloudflare setup");
31
+ return exit(1);
32
+ }
33
+ const state = cfStateStore.readCfState();
34
+ let tokenValue = null;
35
+ if (state?.tokenFile && fsLike.existsSync(state.tokenFile)) {
36
+ try {
37
+ tokenValue = fsLike.readFileSync(state.tokenFile, "utf8").trim();
38
+ } catch {
39
+ tokenValue = null;
40
+ }
41
+ }
42
+ const args = tokenValue
43
+ ? ["tunnel", "run", "--token", tokenValue]
44
+ : ["tunnel", "--config", cfConfigFile, "run"];
45
+ const child = spawnFn(bin, args, {
46
+ stdio: "ignore",
47
+ detached: true,
48
+ });
49
+ child.unref();
50
+ cfStateStore.writeCfPid(child.pid);
51
+ consoleLike.log(`Cloudflare tunnel started (pid ${child.pid}).`);
52
+ return undefined;
53
+ }
54
+
55
+ function cloudflareStop() {
56
+ const pid = cfStateStore.readCfPid();
57
+ if (!pid) {
58
+ consoleLike.log("Cloudflare tunnel not running.");
59
+ return exit(0);
60
+ }
61
+ if (!isProcessAlive(pid)) {
62
+ cfStateStore.clearCfPid();
63
+ consoleLike.log("Cloudflare tunnel not running.");
64
+ return exit(0);
65
+ }
66
+ try {
67
+ processLike.kill(pid, "SIGTERM");
68
+ } catch {
69
+ cfStateStore.clearCfPid();
70
+ consoleLike.error("Failed to stop Cloudflare tunnel.");
71
+ return exit(1);
72
+ }
73
+ for (let i = 0; i < 20; i += 1) {
74
+ if (!isProcessAlive(pid)) break;
75
+ sleepFn(100);
76
+ }
77
+ if (isProcessAlive(pid)) {
78
+ try {
79
+ processLike.kill(pid, "SIGKILL");
80
+ } catch {
81
+ // ignore
82
+ }
83
+ }
84
+ cfStateStore.clearCfPid();
85
+ consoleLike.log("Cloudflare tunnel stopped.");
86
+ return undefined;
87
+ }
88
+
89
+ function cloudflareStatus({ diagnose } = {}) {
90
+ const pid = cfStateStore.readCfPid();
91
+ const state = cfStateStore.readCfState();
92
+ const warnings = [];
93
+ const tokenActive = Boolean(state?.tokenFile && fsLike.existsSync(state.tokenFile));
94
+ if (!tokenActive && state?.credentialsFile) {
95
+ if (!fsLike.existsSync(state.credentialsFile)) {
96
+ warnings.push(`credentials missing: ${state.credentialsFile}`);
97
+ }
98
+ if (state?.tunnelId) {
99
+ const expected = `${state.tunnelId}.json`;
100
+ if (!state.credentialsFile.endsWith(expected)) {
101
+ warnings.push(`credentials mismatch: expected ${expected}`);
102
+ warnings.push("open Zero Trust → Networks → Connectors → (tunnel) and copy the token/run command");
103
+ warnings.push("then run: npx copilot-proxy-web cloudflare token set --token <TOKEN>");
104
+ warnings.push("docs: https://developers.cloudflare.com/cloudflare-one/");
105
+ }
106
+ }
107
+ }
108
+ if (pid && isProcessAlive(pid)) {
109
+ consoleLike.log(`Cloudflare tunnel running (pid ${pid}).`);
110
+ if (state?.tunnelName) consoleLike.log(` tunnel name: ${state.tunnelName}`);
111
+ if (state?.tunnelId) consoleLike.log(` tunnel id: ${state.tunnelId}`);
112
+ if (state?.hostname) consoleLike.log(` hostname: ${state.hostname}`);
113
+ if (state?.localPort) consoleLike.log(` local port: ${state.localPort}`);
114
+ if (state?.configFile) consoleLike.log(` config: ${state.configFile}`);
115
+ if (tokenActive) {
116
+ consoleLike.log(` token: ${state.tokenFile}`);
117
+ consoleLike.log(" token mode: active (credentials ignored)");
118
+ } else if (state?.credentialsFile) {
119
+ consoleLike.log(` credentials: ${state.credentialsFile}`);
120
+ }
121
+ for (const warning of warnings) {
122
+ consoleLike.log(` warning: ${warning}`);
123
+ }
124
+ if (state?.hostname) {
125
+ const diagnoseFn = diagnose || cloudflareDiagnose;
126
+ diagnoseFn();
127
+ return undefined;
128
+ }
129
+ return exit(0);
130
+ }
131
+ consoleLike.log("Cloudflare tunnel stopped.");
132
+ if (state?.tunnelName) consoleLike.log(` tunnel name: ${state.tunnelName}`);
133
+ if (state?.tunnelId) consoleLike.log(` tunnel id: ${state.tunnelId}`);
134
+ if (state?.hostname) consoleLike.log(` hostname: ${state.hostname}`);
135
+ if (state?.localPort) consoleLike.log(` local port: ${state.localPort}`);
136
+ if (state?.configFile) consoleLike.log(` config: ${state.configFile}`);
137
+ if (tokenActive) {
138
+ consoleLike.log(` token: ${state.tokenFile}`);
139
+ consoleLike.log(" token mode: active (credentials ignored)");
140
+ } else if (state?.credentialsFile) {
141
+ consoleLike.log(` credentials: ${state.credentialsFile}`);
142
+ }
143
+ for (const warning of warnings) {
144
+ consoleLike.log(` warning: ${warning}`);
145
+ }
146
+ return exit(1);
147
+ }
148
+
149
+ function cloudflareCheck() {
150
+ const state = cfStateStore.readCfState();
151
+ if (!state?.hostname) {
152
+ consoleLike.error("Missing hostname in Cloudflare state. Run setup first.");
153
+ return exit(1);
154
+ }
155
+ const url = `https://${state.hostname}/api/sessions`;
156
+ const req = httpsLike.get(
157
+ url,
158
+ { timeout: 5000 },
159
+ (res) => {
160
+ res.resume();
161
+ if (res.statusCode < 500) {
162
+ consoleLike.log(`OK (${url})`);
163
+ exit(0);
164
+ return;
165
+ }
166
+ consoleLike.error(`Unhealthy (HTTP ${res.statusCode})`);
167
+ exit(1);
168
+ }
169
+ );
170
+ req.on("timeout", () => {
171
+ req.destroy();
172
+ consoleLike.error("Unhealthy (timeout)");
173
+ exit(1);
174
+ });
175
+ req.on("error", () => {
176
+ consoleLike.error("Unhealthy (connection failed)");
177
+ exit(1);
178
+ });
179
+ return undefined;
180
+ }
181
+
182
+ function cloudflareDiagnose() {
183
+ const state = cfStateStore.readCfState();
184
+ if (!state?.hostname) {
185
+ consoleLike.error("Missing hostname in Cloudflare state. Run setup first.");
186
+ return exit(1);
187
+ }
188
+ const targetPort = state?.localPort || port;
189
+ const localUrlArg = getCloudflareArg("--local-url");
190
+ const localPathArg = getCloudflareArg("--local-path");
191
+ let localPath = "/";
192
+ if (!localUrlArg && localPathArg) localPath = normalizePath(localPathArg);
193
+ const localUrl = localUrlArg || `http://127.0.0.1:${targetPort}${localPath}`;
194
+ let remotePath = localPath;
195
+ if (localUrlArg) {
196
+ try {
197
+ remotePath = new URL(localUrlArg).pathname || "/";
198
+ } catch {
199
+ remotePath = "/";
200
+ }
201
+ }
202
+ const remoteUrl = `https://${state.hostname}${remotePath}`;
203
+
204
+ function report(label, ok, detail) {
205
+ const status = ok ? "OK" : "FAIL";
206
+ consoleLike.log(`${label}: ${status}${detail ? ` (${detail})` : ""}`);
207
+ }
208
+
209
+ httpLike
210
+ .get(localUrl, { timeout: 3000 }, (res) => {
211
+ res.resume();
212
+ res.on("end", () => {
213
+ const ok = res.statusCode >= 200 && res.statusCode < 300;
214
+ const detail = `HTTP ${res.statusCode}: ${localUrl}`;
215
+ report("Local", ok, detail);
216
+ httpsLike
217
+ .get(remoteUrl, { timeout: 5000 }, (res2) => {
218
+ res2.resume();
219
+ const ok2 = res2.statusCode >= 200 && res2.statusCode < 300;
220
+ const isRedirect = res2.statusCode === 302;
221
+ if (ok2) {
222
+ report("Cloudflare", true, `HTTP ${res2.statusCode}`);
223
+ } else if (isRedirect) {
224
+ report("Cloudflare", true, "Access redirect (HTTP 302)");
225
+ } else {
226
+ report("Cloudflare", false, `HTTP ${res2.statusCode}`);
227
+ }
228
+ if (!ok2 && !isRedirect) {
229
+ if (res2.statusCode === 401 || res2.statusCode === 403) {
230
+ consoleLike.log("Hint: Access policy may be blocking this request.");
231
+ }
232
+ if (res2.statusCode === 404) {
233
+ consoleLike.log("Hint: Hostname or route may not match the tunnel.");
234
+ }
235
+ }
236
+ exit(ok && (ok2 || isRedirect) ? 0 : 1);
237
+ })
238
+ .on("timeout", () => {
239
+ report("Cloudflare", false, "timeout");
240
+ exit(1);
241
+ })
242
+ .on("error", () => {
243
+ report("Cloudflare", false, "connection failed");
244
+ exit(1);
245
+ });
246
+ });
247
+ })
248
+ .on("timeout", () => {
249
+ report("Local", false, `timeout: ${localUrl}`);
250
+ exit(1);
251
+ })
252
+ .on("error", () => {
253
+ report("Local", false, `connection failed: ${localUrl}`);
254
+ exit(1);
255
+ });
256
+ return undefined;
257
+ }
258
+
259
+ function cloudflareClean() {
260
+ try {
261
+ cfStateStore.clean();
262
+ } catch {
263
+ consoleLike.error("Failed to clean Cloudflare state.");
264
+ exit(1);
265
+ return;
266
+ }
267
+ consoleLike.log("Cloudflare state cleaned.");
268
+ exit(0);
269
+ }
270
+
271
+ function cloudflareToken(action, tokenValue) {
272
+ if (action === "set") {
273
+ if (!tokenValue) {
274
+ consoleLike.error("Missing token. Use: cloudflare token set --token <TOKEN>");
275
+ exit(1);
276
+ return;
277
+ }
278
+ cfStateStore.setToken(tokenValue);
279
+ if (cfStateStore.patchCfState) {
280
+ cfStateStore.patchCfState({
281
+ tokenFile: cfTokenFile,
282
+ tokenSetAt: new Date().toISOString(),
283
+ });
284
+ }
285
+ consoleLike.log(`Cloudflare token stored in ${cfTokenFile}.`);
286
+ exit(0);
287
+ return;
288
+ }
289
+ if (action === "clear") {
290
+ cfStateStore.clearToken();
291
+ if (cfStateStore.patchCfState) {
292
+ cfStateStore.patchCfState({ tokenFile: null, token: null });
293
+ }
294
+ consoleLike.log("Cloudflare token cleared.");
295
+ exit(0);
296
+ return;
297
+ }
298
+ if (action === "get") {
299
+ const state = cfStateStore.readCfState ? cfStateStore.readCfState() : null;
300
+ if (state?.tokenFile && fsLike.existsSync(state.tokenFile)) {
301
+ consoleLike.log("Cloudflare token is set.");
302
+ exit(0);
303
+ return;
304
+ }
305
+ consoleLike.log("Cloudflare token is not set.");
306
+ exit(1);
307
+ return;
308
+ }
309
+ consoleLike.error("Usage: cloudflare token <set|get|clear> [--token <TOKEN>]");
310
+ exit(1);
311
+ }
312
+
313
+ return {
314
+ cloudflareStart,
315
+ cloudflareStop,
316
+ cloudflareStatus,
317
+ cloudflareCheck,
318
+ cloudflareDiagnose,
319
+ cloudflareClean,
320
+ cloudflareToken,
321
+ };
322
+ }
323
+
324
+ module.exports = {
325
+ createCloudflareService,
326
+ };