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.
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+
3
+ const WebSocket = require("ws");
4
+
5
+ function usage() {
6
+ process.stdout.write(
7
+ [
8
+ "Usage:",
9
+ " copilot-proxy-ws --url wss://proxy.example.com/ws --sessionId default",
10
+ "",
11
+ "Options:",
12
+ " --url URL WebSocket endpoint (wss://.../ws)",
13
+ " --sessionId ID Session id to attach",
14
+ " --token TOKEN AUTH_TOKEN (or set AUTH_TOKEN env)",
15
+ " --origin ORIGIN Set Origin header (for servers that require it)",
16
+ " --header KEY:VALUE Add custom request header (repeatable)",
17
+ " --cf-access-id ID Cloudflare Access Client ID",
18
+ " --cf-access-secret ID Cloudflare Access Client Secret",
19
+ " --text TEXT Send a text command (adds CR on server)",
20
+ " --keys KEYS Send raw keys",
21
+ " --resize COLSxROWS Send resize event once",
22
+ " --no-stdin Do not forward stdin",
23
+ ].join("\n") + "\n"
24
+ );
25
+ }
26
+
27
+ function parseArgs(argv) {
28
+ const out = {
29
+ url: "",
30
+ sessionId: "",
31
+ token: "",
32
+ text: "",
33
+ keys: "",
34
+ resize: "",
35
+ noStdin: false,
36
+ headers: [],
37
+ cfAccessId: "",
38
+ cfAccessSecret: "",
39
+ };
40
+ for (let i = 0; i < argv.length; i += 1) {
41
+ const arg = argv[i];
42
+ if (arg === "--url" && i + 1 < argv.length) {
43
+ out.url = argv[i + 1];
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (arg === "--sessionId" && i + 1 < argv.length) {
48
+ out.sessionId = argv[i + 1];
49
+ i += 1;
50
+ continue;
51
+ }
52
+ if (arg === "--token" && i + 1 < argv.length) {
53
+ out.token = argv[i + 1];
54
+ i += 1;
55
+ continue;
56
+ }
57
+ if (arg === "--origin" && i + 1 < argv.length) {
58
+ out.origin = argv[i + 1];
59
+ i += 1;
60
+ continue;
61
+ }
62
+ if (arg === "--header" && i + 1 < argv.length) {
63
+ out.headers.push(argv[i + 1]);
64
+ i += 1;
65
+ continue;
66
+ }
67
+ if (arg === "--cf-access-id" && i + 1 < argv.length) {
68
+ out.cfAccessId = argv[i + 1];
69
+ i += 1;
70
+ continue;
71
+ }
72
+ if (arg === "--cf-access-secret" && i + 1 < argv.length) {
73
+ out.cfAccessSecret = argv[i + 1];
74
+ i += 1;
75
+ continue;
76
+ }
77
+ if (arg === "--text" && i + 1 < argv.length) {
78
+ out.text = argv[i + 1];
79
+ i += 1;
80
+ continue;
81
+ }
82
+ if (arg === "--keys" && i + 1 < argv.length) {
83
+ out.keys = argv[i + 1];
84
+ i += 1;
85
+ continue;
86
+ }
87
+ if (arg === "--resize" && i + 1 < argv.length) {
88
+ out.resize = argv[i + 1];
89
+ i += 1;
90
+ continue;
91
+ }
92
+ if (arg === "--no-stdin") {
93
+ out.noStdin = true;
94
+ continue;
95
+ }
96
+ if (arg === "--help" || arg === "-h") {
97
+ usage();
98
+ process.exit(0);
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function buildUrl(baseUrl, sessionId) {
105
+ if (!sessionId) return baseUrl;
106
+ const hasQuery = baseUrl.includes("?");
107
+ const joiner = hasQuery ? "&" : "?";
108
+ return `${baseUrl}${joiner}sessionId=${encodeURIComponent(sessionId)}`;
109
+ }
110
+
111
+ function encodeAuthProtocol(token) {
112
+ if (!token) return null;
113
+ return `auth.${Buffer.from(token, "utf8").toString("base64url")}`;
114
+ }
115
+
116
+ function parseResize(value) {
117
+ if (!value) return null;
118
+ const match = value.match(/^(\d+)x(\d+)$/);
119
+ if (!match) return null;
120
+ const cols = Number(match[1]);
121
+ const rows = Number(match[2]);
122
+ if (!Number.isFinite(cols) || !Number.isFinite(rows)) return null;
123
+ return { cols, rows };
124
+ }
125
+
126
+ function sendBinary(ws, type, text) {
127
+ const payload = Buffer.from(text, "utf8");
128
+ const buf = Buffer.alloc(payload.length + 1);
129
+ buf[0] = type;
130
+ payload.copy(buf, 1);
131
+ ws.send(buf);
132
+ }
133
+
134
+ function parseHeaders(entries, origin) {
135
+ const customHeaders = {};
136
+ for (const entry of entries || []) {
137
+ const idx = entry.indexOf(":");
138
+ if (idx > 0) {
139
+ const key = entry.slice(0, idx).trim();
140
+ const value = entry.slice(idx + 1).trim();
141
+ if (key) customHeaders[key] = value;
142
+ }
143
+ }
144
+ if (!origin && Object.keys(customHeaders).length === 0) return undefined;
145
+ return { ...(origin ? { Origin: origin } : {}), ...customHeaders };
146
+ }
147
+
148
+ if (require.main !== module) {
149
+ module.exports = {
150
+ parseArgs,
151
+ parseHeaders,
152
+ };
153
+ } else {
154
+ const args = parseArgs(process.argv.slice(2));
155
+ const token = args.token || process.env.AUTH_TOKEN || "";
156
+ if (!args.url) {
157
+ usage();
158
+ process.exit(1);
159
+ }
160
+
161
+ const url = buildUrl(args.url, args.sessionId);
162
+ const protocols = [];
163
+ const authProto = encodeAuthProtocol(token);
164
+ if (authProto) protocols.push(authProto);
165
+
166
+ const headers = parseHeaders(args.headers, args.origin) || {};
167
+ if (args.cfAccessId) headers["CF-Access-Client-Id"] = args.cfAccessId;
168
+ if (args.cfAccessSecret) headers["CF-Access-Client-Secret"] = args.cfAccessSecret;
169
+
170
+ const ws = new WebSocket(url, protocols.length > 0 ? protocols : undefined, {
171
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
172
+ });
173
+
174
+ ws.on("open", () => {
175
+ if (args.text) {
176
+ ws.send(JSON.stringify({ type: "text", data: args.text }));
177
+ }
178
+ if (args.keys) {
179
+ ws.send(JSON.stringify({ type: "keys", data: args.keys }));
180
+ }
181
+ const resize = parseResize(args.resize);
182
+ if (resize) {
183
+ ws.send(JSON.stringify({ type: "resize", cols: resize.cols, rows: resize.rows }));
184
+ }
185
+ if (!args.noStdin) {
186
+ if (process.stdin.isTTY) {
187
+ process.stdin.setRawMode(true);
188
+ }
189
+ process.stdin.resume();
190
+ process.stdin.on("data", (data) => {
191
+ if (data && data.includes && data.includes("\u0003")) {
192
+ ws.close();
193
+ process.exit(0);
194
+ }
195
+ sendBinary(ws, 1, data.toString());
196
+ });
197
+ }
198
+ if (process.stdout.isTTY) {
199
+ process.stdout.on("resize", () => {
200
+ ws.send(
201
+ JSON.stringify({
202
+ type: "resize",
203
+ cols: process.stdout.columns || 80,
204
+ rows: process.stdout.rows || 24,
205
+ })
206
+ );
207
+ });
208
+ }
209
+ });
210
+
211
+ ws.on("message", (data, isBinary) => {
212
+ if (isBinary) {
213
+ process.stdout.write(Buffer.from(data));
214
+ } else {
215
+ process.stdout.write(data.toString());
216
+ }
217
+ });
218
+
219
+ ws.on("close", () => {
220
+ process.exit(0);
221
+ });
222
+
223
+ ws.on("error", (err) => {
224
+ process.stderr.write(`[copilot-proxy-ws] ${err.message}\n`);
225
+ process.exit(1);
226
+ });
227
+ }