quvur-dev 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/dist/index.cjs +789 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/package.json +36 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/commands/index.ts
|
|
27
|
+
var import_commander7 = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/core/ws-client.ts
|
|
30
|
+
var import_ws = require("ws");
|
|
31
|
+
|
|
32
|
+
// src/utils/config.ts
|
|
33
|
+
var path = __toESM(require("path"), 1);
|
|
34
|
+
var fs = __toESM(require("fs"), 1);
|
|
35
|
+
var os = __toESM(require("os"), 1);
|
|
36
|
+
|
|
37
|
+
// src/utils/logger.ts
|
|
38
|
+
var import_chalk = __toESM(require("chalk"), 1);
|
|
39
|
+
var logger = {
|
|
40
|
+
info: (...msg) => console.log(import_chalk.default.blue("[\u2139]"), ...msg),
|
|
41
|
+
next: (...msg) => console.log(import_chalk.default.green("[\u2192]"), ...msg),
|
|
42
|
+
success: (...msg) => console.log(import_chalk.default.green("[\u2714]"), ...msg),
|
|
43
|
+
error: (...msg) => console.error(import_chalk.default.red("[\u2716]"), ...msg),
|
|
44
|
+
debug: (...msg) => console.debug(import_chalk.default.bgRed("[debug]"), ...msg),
|
|
45
|
+
warn: (...msg) => console.warn(import_chalk.default.bgYellowBright("[warn]"), ...msg),
|
|
46
|
+
log: (...msg) => console.log(...msg)
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/utils/config.ts
|
|
50
|
+
var localConfigPath = path.join(process.cwd(), "config.json");
|
|
51
|
+
var configDir = path.join(os.homedir(), ".quvur");
|
|
52
|
+
var configPath = path.join(os.homedir(), ".quvur", "config.json");
|
|
53
|
+
var loadConfig = () => {
|
|
54
|
+
if (!fs.existsSync(configPath)) {
|
|
55
|
+
if (fs.existsSync(localConfigPath)) {
|
|
56
|
+
return loadConfigFromLocal();
|
|
57
|
+
}
|
|
58
|
+
throw new Error("Config file not found. Run `quvur auth` first.");
|
|
59
|
+
}
|
|
60
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
61
|
+
try {
|
|
62
|
+
const config2 = JSON.parse(raw);
|
|
63
|
+
return config2;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error("Invalid config file. Run `quvur auth` first.");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var loadConfigFromLocal = () => {
|
|
69
|
+
const raw = fs.readFileSync(localConfigPath, "utf-8");
|
|
70
|
+
try {
|
|
71
|
+
const config2 = JSON.parse(raw);
|
|
72
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
73
|
+
fs.writeFileSync(configPath, raw);
|
|
74
|
+
return config2;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.error(error);
|
|
77
|
+
throw new Error("Invalid config file");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var saveConfig = (config2) => {
|
|
81
|
+
fs.writeFileSync(configPath, JSON.stringify(config2, null, 2));
|
|
82
|
+
};
|
|
83
|
+
var config = loadConfig();
|
|
84
|
+
|
|
85
|
+
// src/auth/token-manager.ts
|
|
86
|
+
var import_axios = __toESM(require("axios"), 1);
|
|
87
|
+
var TokenManager = class {
|
|
88
|
+
async getValidAccessToken() {
|
|
89
|
+
const { accessToken, refreshToken } = config.auth;
|
|
90
|
+
if (!accessToken) {
|
|
91
|
+
logger.error("No access token found. Please authenticate first.");
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await this.tokenExchange(accessToken);
|
|
96
|
+
logger.debug("Token exchanged");
|
|
97
|
+
return accessToken;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (refreshToken) {
|
|
100
|
+
try {
|
|
101
|
+
const newAccessToken = await this.refreshTokens(refreshToken);
|
|
102
|
+
logger.debug("Token refreshed");
|
|
103
|
+
return newAccessToken;
|
|
104
|
+
} catch (error2) {
|
|
105
|
+
if (error2 instanceof Error) {
|
|
106
|
+
logger.error(`Failed to refresh tokens: ${error2.message}`);
|
|
107
|
+
}
|
|
108
|
+
this.clearTokens();
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (error instanceof Error) {
|
|
113
|
+
logger.error(`Unauthorized. ${error.message}`);
|
|
114
|
+
} else {
|
|
115
|
+
logger.error("Error during token exchange");
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
saveTokens(accessToken, refreshToken) {
|
|
121
|
+
config.auth.accessToken = accessToken;
|
|
122
|
+
config.auth.refreshToken = refreshToken;
|
|
123
|
+
saveConfig(config);
|
|
124
|
+
}
|
|
125
|
+
clearTokens() {
|
|
126
|
+
config.auth.accessToken = null;
|
|
127
|
+
config.auth.refreshToken = null;
|
|
128
|
+
saveConfig(config);
|
|
129
|
+
}
|
|
130
|
+
async refreshTokens(refreshToken) {
|
|
131
|
+
try {
|
|
132
|
+
const response = await import_axios.default.post(
|
|
133
|
+
`${config.apiBaseUrl}/auth/refresh-token`,
|
|
134
|
+
{ refresh_token: refreshToken }
|
|
135
|
+
);
|
|
136
|
+
const { data: tokens } = response;
|
|
137
|
+
config.auth = {
|
|
138
|
+
accessToken: tokens.access_token,
|
|
139
|
+
refreshToken: tokens.refresh_token
|
|
140
|
+
};
|
|
141
|
+
saveConfig(config);
|
|
142
|
+
return tokens.access_token;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error instanceof import_axios.AxiosError) {
|
|
145
|
+
const errorData = error.response?.data;
|
|
146
|
+
if (!errorData) throw error;
|
|
147
|
+
throw new Error(errorData.error);
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async tokenExchange(accessToken) {
|
|
153
|
+
try {
|
|
154
|
+
const response = await import_axios.default.post(
|
|
155
|
+
`${config.apiBaseUrl}/auth/token-exchange`,
|
|
156
|
+
{
|
|
157
|
+
access_token: accessToken
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
const { data } = response;
|
|
161
|
+
return data.access_token;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof import_axios.AxiosError) {
|
|
164
|
+
const errorData = error.response?.data;
|
|
165
|
+
if (!errorData) throw error;
|
|
166
|
+
throw new Error(errorData.error);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var tokenManager = new TokenManager();
|
|
173
|
+
|
|
174
|
+
// src/core/ws-client.ts
|
|
175
|
+
var connectWebSocket = async (data) => {
|
|
176
|
+
const url = new URL(config.wsBaseUrl);
|
|
177
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
178
|
+
if (value) url.searchParams.set(key, String(value));
|
|
179
|
+
});
|
|
180
|
+
const accessToken = await tokenManager.getValidAccessToken();
|
|
181
|
+
if (!accessToken) return null;
|
|
182
|
+
const ws = new import_ws.WebSocket(url, {
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Bearer ${accessToken}`
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return ws;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/transport/http-tunnel.ts
|
|
191
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
192
|
+
var import_ws3 = require("ws");
|
|
193
|
+
|
|
194
|
+
// src/@protocol/binary-frame.ts
|
|
195
|
+
var encodeBinaryFrame = (frame) => {
|
|
196
|
+
const idBuf = Buffer.from(frame.requestId);
|
|
197
|
+
const idLen = Buffer.alloc(4);
|
|
198
|
+
idLen.writeUInt32BE(idBuf.length);
|
|
199
|
+
return Buffer.concat([idLen, idBuf, frame.chunk]);
|
|
200
|
+
};
|
|
201
|
+
var decodeBinaryFrame = (buffer) => {
|
|
202
|
+
const idLen = buffer.readUInt32BE(0);
|
|
203
|
+
const idStart = 4;
|
|
204
|
+
const idEnd = 4 + idLen;
|
|
205
|
+
if (buffer.length <= idEnd) return null;
|
|
206
|
+
const requestId = buffer.subarray(idStart, idEnd).toString();
|
|
207
|
+
const chunk = buffer.subarray(idEnd);
|
|
208
|
+
return { requestId, chunk };
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/@protocol/schemas/index.ts
|
|
212
|
+
var import_zod6 = __toESM(require("zod"), 1);
|
|
213
|
+
|
|
214
|
+
// src/@protocol/schemas/request.schema.ts
|
|
215
|
+
var import_zod2 = __toESM(require("zod"), 1);
|
|
216
|
+
|
|
217
|
+
// src/@protocol/schemas/base.schema.ts
|
|
218
|
+
var import_zod = __toESM(require("zod"), 1);
|
|
219
|
+
|
|
220
|
+
// src/@protocol/constants.ts
|
|
221
|
+
var PROTOCOL_VERSION = "1.0.0";
|
|
222
|
+
var JsonMessageType = /* @__PURE__ */ ((JsonMessageType2) => {
|
|
223
|
+
JsonMessageType2["CreateDeviceSession"] = "create-device-session";
|
|
224
|
+
JsonMessageType2["AuthSuccess"] = "auth-success";
|
|
225
|
+
JsonMessageType2["AuthError"] = "auth-error";
|
|
226
|
+
JsonMessageType2["RequestStart"] = "request-start";
|
|
227
|
+
JsonMessageType2["RequestEnd"] = "request-end";
|
|
228
|
+
JsonMessageType2["ResponseStart"] = "response-start";
|
|
229
|
+
JsonMessageType2["ResponseEnd"] = "response-end";
|
|
230
|
+
JsonMessageType2["TunnelCreated"] = "tunnel-created";
|
|
231
|
+
JsonMessageType2["Error"] = "error";
|
|
232
|
+
return JsonMessageType2;
|
|
233
|
+
})(JsonMessageType || {});
|
|
234
|
+
|
|
235
|
+
// src/@protocol/schemas/base.schema.ts
|
|
236
|
+
var baseSchema = import_zod.default.object({
|
|
237
|
+
version: import_zod.default.string().default(PROTOCOL_VERSION),
|
|
238
|
+
type: import_zod.default.enum(JsonMessageType)
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// src/@protocol/schemas/request.schema.ts
|
|
242
|
+
var requestStartSchema = baseSchema.extend({
|
|
243
|
+
type: import_zod2.default.literal("request-start" /* RequestStart */),
|
|
244
|
+
requestId: import_zod2.default.string(),
|
|
245
|
+
method: import_zod2.default.string(),
|
|
246
|
+
url: import_zod2.default.string(),
|
|
247
|
+
headers: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string())
|
|
248
|
+
});
|
|
249
|
+
var requestEndSchema = baseSchema.extend({
|
|
250
|
+
type: import_zod2.default.literal("request-end" /* RequestEnd */),
|
|
251
|
+
requestId: import_zod2.default.string()
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// src/@protocol/schemas/response.schema.ts
|
|
255
|
+
var import_zod3 = __toESM(require("zod"), 1);
|
|
256
|
+
var responseStartSchema = baseSchema.extend({
|
|
257
|
+
type: import_zod3.default.literal("response-start" /* ResponseStart */),
|
|
258
|
+
requestId: import_zod3.default.string(),
|
|
259
|
+
statusCode: import_zod3.default.number().int().min(100).max(599),
|
|
260
|
+
headers: import_zod3.default.record(import_zod3.default.string(), import_zod3.default.string())
|
|
261
|
+
});
|
|
262
|
+
var responseEndSchema = baseSchema.extend({
|
|
263
|
+
type: import_zod3.default.literal("response-end" /* ResponseEnd */),
|
|
264
|
+
requestId: import_zod3.default.string()
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// src/@protocol/schemas/tunnel.schema.ts
|
|
268
|
+
var import_zod4 = __toESM(require("zod"), 1);
|
|
269
|
+
var tunnelCreatedSchema = baseSchema.extend({
|
|
270
|
+
type: import_zod4.default.literal("tunnel-created" /* TunnelCreated */),
|
|
271
|
+
tunnelId: import_zod4.default.string(),
|
|
272
|
+
subdomain: import_zod4.default.string(),
|
|
273
|
+
host: import_zod4.default.string()
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/@protocol/schemas/error-schema.ts
|
|
277
|
+
var import_zod5 = __toESM(require("zod"), 1);
|
|
278
|
+
var errorSchema = baseSchema.extend({
|
|
279
|
+
type: import_zod5.default.literal("error" /* Error */),
|
|
280
|
+
message: import_zod5.default.string()
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// src/@protocol/schemas/index.ts
|
|
284
|
+
var jsonMessageSchema = import_zod6.default.discriminatedUnion("type", [
|
|
285
|
+
requestStartSchema,
|
|
286
|
+
requestEndSchema,
|
|
287
|
+
responseStartSchema,
|
|
288
|
+
responseEndSchema,
|
|
289
|
+
tunnelCreatedSchema,
|
|
290
|
+
errorSchema
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
// src/@protocol/logger/index.ts
|
|
294
|
+
var import_chalk2 = __toESM(require("chalk"), 1);
|
|
295
|
+
var protoLogger = {
|
|
296
|
+
info: (...msg) => console.log(
|
|
297
|
+
import_chalk2.default.blue("[protocol:info]"),
|
|
298
|
+
...msg,
|
|
299
|
+
(/* @__PURE__ */ new Date()).toLocaleString(),
|
|
300
|
+
"\n"
|
|
301
|
+
),
|
|
302
|
+
next: (...msg) => console.log(
|
|
303
|
+
import_chalk2.default.green("[protocol:next]"),
|
|
304
|
+
...msg,
|
|
305
|
+
(/* @__PURE__ */ new Date()).toLocaleString(),
|
|
306
|
+
"\n"
|
|
307
|
+
),
|
|
308
|
+
success: (...msg) => console.log(
|
|
309
|
+
import_chalk2.default.green("[protocol:success]"),
|
|
310
|
+
...msg,
|
|
311
|
+
(/* @__PURE__ */ new Date()).toLocaleString(),
|
|
312
|
+
"\n"
|
|
313
|
+
),
|
|
314
|
+
error: (...msg) => console.error(
|
|
315
|
+
import_chalk2.default.red("[protocol:error]"),
|
|
316
|
+
...msg,
|
|
317
|
+
(/* @__PURE__ */ new Date()).toLocaleString(),
|
|
318
|
+
"\n"
|
|
319
|
+
),
|
|
320
|
+
debug: (...msg) => console.debug(
|
|
321
|
+
import_chalk2.default.bgRed("[protocol:debug]"),
|
|
322
|
+
...msg,
|
|
323
|
+
(/* @__PURE__ */ new Date()).toLocaleString(),
|
|
324
|
+
"\n"
|
|
325
|
+
),
|
|
326
|
+
warn: (...msg) => console.warn(
|
|
327
|
+
import_chalk2.default.bgYellowBright("[protocol:warn]"),
|
|
328
|
+
...msg,
|
|
329
|
+
(/* @__PURE__ */ new Date()).toLocaleString(),
|
|
330
|
+
"\n"
|
|
331
|
+
),
|
|
332
|
+
log: (...msg) => console.log("[protocol:log]", ...msg, (/* @__PURE__ */ new Date()).toLocaleString(), "\n")
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// src/@protocol/parser.ts
|
|
336
|
+
var parseFrame = (data, isBinary) => {
|
|
337
|
+
if (isBinary) {
|
|
338
|
+
const decoded = decodeBinaryFrame(data);
|
|
339
|
+
if (!decoded) return null;
|
|
340
|
+
return { kind: "binary" /* Binary */, data: decoded };
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const parsed = JSON.parse(data.toString("utf-8"));
|
|
344
|
+
const validated = jsonMessageSchema.parse(parsed);
|
|
345
|
+
return {
|
|
346
|
+
kind: "json" /* Json */,
|
|
347
|
+
data: validated
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
protoLogger.error("Failed to parse frame:", error);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/@protocol/contract.ts
|
|
356
|
+
var import_ws2 = require("ws");
|
|
357
|
+
function createProtocolSide(incomingSchema, outgoingSchema) {
|
|
358
|
+
return {
|
|
359
|
+
validateIncoming: (data) => {
|
|
360
|
+
try {
|
|
361
|
+
return incomingSchema.parse(data);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
sendJson: (ws, message) => {
|
|
367
|
+
try {
|
|
368
|
+
const validated = outgoingSchema.parse(message);
|
|
369
|
+
ws.send(JSON.stringify(validated), { binary: false });
|
|
370
|
+
} catch (error) {
|
|
371
|
+
protoLogger.error("Failed to send JSON message:", error);
|
|
372
|
+
ws.close();
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
sendBinary(ws, frame) {
|
|
376
|
+
const buffer = encodeBinaryFrame(frame);
|
|
377
|
+
ws.send(buffer, { binary: true });
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/@protocol/directions/client-to-server.ts
|
|
383
|
+
var import_zod7 = __toESM(require("zod"), 1);
|
|
384
|
+
var clientToServerSchema = import_zod7.default.discriminatedUnion("type", [
|
|
385
|
+
responseStartSchema,
|
|
386
|
+
responseEndSchema,
|
|
387
|
+
errorSchema
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
// src/@protocol/directions/server-to-client.ts
|
|
391
|
+
var import_zod8 = __toESM(require("zod"), 1);
|
|
392
|
+
var serverToClientSchema = import_zod8.default.discriminatedUnion("type", [
|
|
393
|
+
tunnelCreatedSchema,
|
|
394
|
+
requestStartSchema,
|
|
395
|
+
requestEndSchema,
|
|
396
|
+
errorSchema
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
// src/@protocol/index.ts
|
|
400
|
+
var protocol = createProtocolSide(
|
|
401
|
+
serverToClientSchema,
|
|
402
|
+
// incoming
|
|
403
|
+
clientToServerSchema
|
|
404
|
+
// outgoing
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// src/utils/make-tunnel-url.ts
|
|
408
|
+
var makeTunnelUrl = (host) => {
|
|
409
|
+
return `http://${host}`;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/transport/http-tunnel.ts
|
|
413
|
+
var connections = /* @__PURE__ */ new Map();
|
|
414
|
+
var startHttpTransport = (ws, localPort) => {
|
|
415
|
+
ws.on("open", () => {
|
|
416
|
+
logger.info("Creating HTTP tunnel for", `localhost:${localPort}...`);
|
|
417
|
+
});
|
|
418
|
+
ws.on("message", (event, isBinary) => {
|
|
419
|
+
const frame = parseFrame(event, isBinary);
|
|
420
|
+
if (!frame) {
|
|
421
|
+
logger.warn("Invalid incoming frame");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (frame.kind === "binary" /* Binary */) {
|
|
425
|
+
const localRequest = connections.get(frame.data.requestId);
|
|
426
|
+
if (localRequest) {
|
|
427
|
+
localRequest.write(frame.data.chunk);
|
|
428
|
+
}
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const message = protocol.validateIncoming(frame.data);
|
|
432
|
+
if (!message) {
|
|
433
|
+
logger.warn("Invalid direction message");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (message.type === "error" /* Error */) {
|
|
437
|
+
logger.error(message.message);
|
|
438
|
+
return ws.close();
|
|
439
|
+
}
|
|
440
|
+
if (message.type === "tunnel-created" /* TunnelCreated */) {
|
|
441
|
+
logger.success("Tunnel ready");
|
|
442
|
+
logger.next("URL:", makeTunnelUrl(message.host));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (message.type === "request-start" /* RequestStart */) {
|
|
446
|
+
const localRequest = import_node_http.default.request({
|
|
447
|
+
hostname: "127.0.0.1",
|
|
448
|
+
port: localPort,
|
|
449
|
+
method: message.method,
|
|
450
|
+
headers: message.headers,
|
|
451
|
+
path: message.url
|
|
452
|
+
});
|
|
453
|
+
connections.set(message.requestId, localRequest);
|
|
454
|
+
localRequest.on("response", (localResponse) => {
|
|
455
|
+
protocol.sendJson(ws, {
|
|
456
|
+
type: "response-start" /* ResponseStart */,
|
|
457
|
+
version: PROTOCOL_VERSION,
|
|
458
|
+
requestId: message.requestId,
|
|
459
|
+
statusCode: localResponse.statusCode || 200,
|
|
460
|
+
headers: Object(localResponse.headers)
|
|
461
|
+
});
|
|
462
|
+
localResponse.on("data", (chunk) => {
|
|
463
|
+
protocol.sendBinary(ws, { requestId: message.requestId, chunk });
|
|
464
|
+
});
|
|
465
|
+
localResponse.on("end", () => {
|
|
466
|
+
protocol.sendJson(ws, {
|
|
467
|
+
type: "response-end" /* ResponseEnd */,
|
|
468
|
+
version: PROTOCOL_VERSION,
|
|
469
|
+
requestId: message.requestId
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
localRequest.on("error", (error) => {
|
|
474
|
+
logger.error(`HTTP request error: ${error.message}`);
|
|
475
|
+
protocol.sendJson(ws, {
|
|
476
|
+
type: "response-start" /* ResponseStart */,
|
|
477
|
+
version: PROTOCOL_VERSION,
|
|
478
|
+
requestId: message.requestId,
|
|
479
|
+
statusCode: 500,
|
|
480
|
+
headers: {}
|
|
481
|
+
});
|
|
482
|
+
protocol.sendJson(ws, {
|
|
483
|
+
type: "response-end" /* ResponseEnd */,
|
|
484
|
+
version: PROTOCOL_VERSION,
|
|
485
|
+
requestId: message.requestId
|
|
486
|
+
});
|
|
487
|
+
ws.close();
|
|
488
|
+
});
|
|
489
|
+
} else if (message.type === "request-end" /* RequestEnd */) {
|
|
490
|
+
const localRequest = connections.get(message.requestId);
|
|
491
|
+
if (localRequest) {
|
|
492
|
+
localRequest.end();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
ws.on("close", () => {
|
|
497
|
+
logger.info("WebSocket connection closed");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
});
|
|
500
|
+
ws.on("error", (error) => {
|
|
501
|
+
logger.error(`Websocket error: ${error.message || error}`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/core/tunnel.ts
|
|
507
|
+
async function startHttpTunnel(data) {
|
|
508
|
+
const ws = await connectWebSocket(data);
|
|
509
|
+
if (!ws) return;
|
|
510
|
+
startHttpTransport(ws, data.port);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/commands/validators/http.validator.ts
|
|
514
|
+
var import_commander = require("commander");
|
|
515
|
+
var validatePort = (value) => {
|
|
516
|
+
const parsed = Number(value);
|
|
517
|
+
if (!Number.isInteger(parsed)) {
|
|
518
|
+
throw new import_commander.InvalidArgumentError("port must be an integer");
|
|
519
|
+
}
|
|
520
|
+
if (parsed <= 0 || parsed > 65535) {
|
|
521
|
+
throw new import_commander.InvalidArgumentError("port must be between 1 and 65535");
|
|
522
|
+
}
|
|
523
|
+
return parsed;
|
|
524
|
+
};
|
|
525
|
+
var validateSubdomain = (value) => {
|
|
526
|
+
if (value.length < 3) {
|
|
527
|
+
throw new import_commander.InvalidArgumentError(
|
|
528
|
+
"subdomain must be at least 3 characters long"
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (value.length > 63) {
|
|
532
|
+
throw new import_commander.InvalidArgumentError("subdomain must be less than 63 characters");
|
|
533
|
+
}
|
|
534
|
+
const regex = /^[a-z0-9][a-z0-9-]*$/;
|
|
535
|
+
if (!regex.test(value)) {
|
|
536
|
+
throw new import_commander.InvalidArgumentError(
|
|
537
|
+
"subdomain must contain only lowercase letters, numbers, and hyphens"
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return value;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/commands/http.ts
|
|
544
|
+
var registerHttpCommand = (program2, config2) => {
|
|
545
|
+
program2.command("http").description(config2.description).usage(config2.usage).argument("<port>", "Local port to expose", validatePort).option(
|
|
546
|
+
"-s, --subdomain <name>",
|
|
547
|
+
"Request a custom subdomain",
|
|
548
|
+
validateSubdomain
|
|
549
|
+
).addHelpText("after", `Examples:
|
|
550
|
+
${config2.examples.join("\n ")}`).action(async (port, options) => {
|
|
551
|
+
await startHttpTunnel({
|
|
552
|
+
port,
|
|
553
|
+
subdomain: options.subdomain,
|
|
554
|
+
type: "http" /* http */
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// src/commands/help.ts
|
|
560
|
+
var import_commander2 = require("commander");
|
|
561
|
+
var registerHelpSubcommand = (program2) => {
|
|
562
|
+
program2.addHelpText(
|
|
563
|
+
"after",
|
|
564
|
+
`
|
|
565
|
+
${config.help.intro}
|
|
566
|
+
|
|
567
|
+
Examples:
|
|
568
|
+
${config.help.examples.join("\n ")}`
|
|
569
|
+
);
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// src/commands/tcp.ts
|
|
573
|
+
var import_commander3 = require("commander");
|
|
574
|
+
var registerTcpCommand = (program2, config2) => {
|
|
575
|
+
program2.command("tcp").description(config2.description).usage(config2.usage).argument("<port>", "Local TCP port to expose").option("-t <token>", "Override auth token").addHelpText("after", `
|
|
576
|
+
Examples:
|
|
577
|
+
${config2.examples.join("\n ")}`).action((port, options) => {
|
|
578
|
+
logger.success("TCP tunnel starting on port:", port);
|
|
579
|
+
logger.info("Options:", options);
|
|
580
|
+
});
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// src/commands/auth.ts
|
|
584
|
+
var import_commander4 = require("commander");
|
|
585
|
+
|
|
586
|
+
// src/auth/auth-service.ts
|
|
587
|
+
var import_axios2 = __toESM(require("axios"), 1);
|
|
588
|
+
|
|
589
|
+
// src/events/auth-emitter.ts
|
|
590
|
+
var import_events = require("events");
|
|
591
|
+
var AuthEventEmitter = class extends import_events.EventEmitter {
|
|
592
|
+
emit(event, payload) {
|
|
593
|
+
return super.emit(event, payload);
|
|
594
|
+
}
|
|
595
|
+
on(event, listener) {
|
|
596
|
+
return super.on(event, listener);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
var authEmitter = new AuthEventEmitter();
|
|
600
|
+
|
|
601
|
+
// src/auth/auth-service.ts
|
|
602
|
+
var AuthService = class {
|
|
603
|
+
async startLogin() {
|
|
604
|
+
try {
|
|
605
|
+
authEmitter.emit("auth:start");
|
|
606
|
+
const response = await import_axios2.default.post(
|
|
607
|
+
`${config.apiBaseUrl}/auth/github/device-start`
|
|
608
|
+
);
|
|
609
|
+
const { data } = response;
|
|
610
|
+
authEmitter.emit("auth:code", {
|
|
611
|
+
user_code: data.user_code,
|
|
612
|
+
verification_uri: data.verification_uri
|
|
613
|
+
});
|
|
614
|
+
this.startPolling(data.device_code, data.interval, data.expires_in);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
authEmitter.emit("auth:error", error);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async startPolling(deviceCode, interval, expiresIn) {
|
|
620
|
+
const expiresAt = /* @__PURE__ */ new Date();
|
|
621
|
+
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
622
|
+
while (Date.now() < expiresAt.getTime()) {
|
|
623
|
+
try {
|
|
624
|
+
const response = await import_axios2.default.post(
|
|
625
|
+
`${config.apiBaseUrl}/auth/github/device-poll`,
|
|
626
|
+
{ device_code: deviceCode }
|
|
627
|
+
);
|
|
628
|
+
const { data } = response;
|
|
629
|
+
authEmitter.emit("auth:success", {
|
|
630
|
+
accessToken: data.access_token,
|
|
631
|
+
refreshToken: data.refresh_token,
|
|
632
|
+
user: data.user
|
|
633
|
+
});
|
|
634
|
+
return authEmitter.emit("auth:completed");
|
|
635
|
+
} catch (error) {
|
|
636
|
+
if (error instanceof import_axios2.AxiosError) {
|
|
637
|
+
const errorData = error.response?.data;
|
|
638
|
+
if (!errorData) {
|
|
639
|
+
authEmitter.emit("auth:error", error);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (errorData.error === "authorization_pending" /* AUTHORIZATION_PENDING */) {
|
|
643
|
+
await this.wait(interval);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (errorData.error === "slow_down" /* SLOW_DOWN */ && errorData.interval) {
|
|
647
|
+
authEmitter.emit("auth:slow_down");
|
|
648
|
+
await this.wait(errorData.interval);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (errorData.error === "expired_token" /* EXPIRED_TOKEN */) {
|
|
652
|
+
authEmitter.emit("auth:expired");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
authEmitter.emit(
|
|
656
|
+
"auth:error",
|
|
657
|
+
new Error(errorData.error_description)
|
|
658
|
+
);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
return authEmitter.emit("auth:error", error);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async wait(seconds) {
|
|
666
|
+
await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
667
|
+
}
|
|
668
|
+
async logout(refreshToken) {
|
|
669
|
+
try {
|
|
670
|
+
const response = await import_axios2.default.post(
|
|
671
|
+
`${config.apiBaseUrl}/auth/logout`,
|
|
672
|
+
{
|
|
673
|
+
refresh_token: refreshToken
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
headers: {
|
|
677
|
+
Authorization: `Bearer ${config.auth.accessToken}`
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
);
|
|
681
|
+
return true;
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (error instanceof import_axios2.AxiosError) {
|
|
684
|
+
const errorData = error.response?.data;
|
|
685
|
+
if (!errorData) throw error;
|
|
686
|
+
throw new Error(`Logout failed: ${errorData.error}`);
|
|
687
|
+
}
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
var authService = new AuthService();
|
|
693
|
+
|
|
694
|
+
// src/commands/auth.ts
|
|
695
|
+
var import_chalk3 = __toESM(require("chalk"), 1);
|
|
696
|
+
var registerAuthCommand = (program2, commandConfig) => {
|
|
697
|
+
program2.command("auth").description(commandConfig.description).usage(commandConfig.usage).addHelpText(
|
|
698
|
+
"after",
|
|
699
|
+
`
|
|
700
|
+
Examples:
|
|
701
|
+
${commandConfig.examples.join("\n ")}`
|
|
702
|
+
).action(async () => {
|
|
703
|
+
authEmitter.on("auth:start", () => {
|
|
704
|
+
logger.info("Starting GitHub authentication...");
|
|
705
|
+
});
|
|
706
|
+
authEmitter.on("auth:code", ({ user_code, verification_uri }) => {
|
|
707
|
+
logger.info("Please visit the following URL to authorize the CLI:");
|
|
708
|
+
logger.log(import_chalk3.default.bold.cyan(verification_uri));
|
|
709
|
+
logger.info(`Enter the code: ${import_chalk3.default.bold.yellow(user_code)}`);
|
|
710
|
+
});
|
|
711
|
+
authEmitter.on("auth:success", ({ accessToken, refreshToken, user }) => {
|
|
712
|
+
tokenManager.saveTokens(accessToken, refreshToken);
|
|
713
|
+
logger.success(
|
|
714
|
+
`Successfully authenticated as ${import_chalk3.default.bold.green(`${user.name} (${user.username})`)}!`
|
|
715
|
+
);
|
|
716
|
+
});
|
|
717
|
+
authEmitter.on("auth:error", (error) => {
|
|
718
|
+
logger.error(`Authentication failed: ${error.message}`);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
});
|
|
721
|
+
authEmitter.on("auth:expired", () => {
|
|
722
|
+
logger.error("Authentication code expired. Please try again.");
|
|
723
|
+
process.exit(1);
|
|
724
|
+
});
|
|
725
|
+
authEmitter.on("auth:completed", () => {
|
|
726
|
+
process.exit(0);
|
|
727
|
+
});
|
|
728
|
+
await authService.startLogin();
|
|
729
|
+
});
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// src/commands/logout.ts
|
|
733
|
+
var import_commander5 = require("commander");
|
|
734
|
+
var registerLogoutCommand = (program2, commandConfig) => {
|
|
735
|
+
program2.command("logout").description(commandConfig.description).usage(commandConfig.usage).addHelpText("after", `Examples:
|
|
736
|
+
${commandConfig.examples.join("\n ")}`).action(async () => {
|
|
737
|
+
const refreshToken = config.auth.refreshToken;
|
|
738
|
+
if (refreshToken) {
|
|
739
|
+
try {
|
|
740
|
+
await authService.logout(refreshToken);
|
|
741
|
+
logger.success("Logged out successfully.");
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (error instanceof Error) {
|
|
744
|
+
logger.error("Logout failed:", error.message);
|
|
745
|
+
} else {
|
|
746
|
+
logger.error("Logout failed");
|
|
747
|
+
}
|
|
748
|
+
} finally {
|
|
749
|
+
tokenManager.clearTokens();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/commands/config.ts
|
|
756
|
+
var import_commander6 = require("commander");
|
|
757
|
+
var registerConfigCommand = (program2, config2) => {
|
|
758
|
+
program2.command("config").description(config2.description).usage(config2.usage).addHelpText("after", `Examples:
|
|
759
|
+
${config2.examples.join("\n ")}`).option("-r, --reset", "Reset config file options to default.").action((options) => {
|
|
760
|
+
if (options.reset) {
|
|
761
|
+
loadConfigFromLocal();
|
|
762
|
+
logger.success("Config file reset successfully.");
|
|
763
|
+
} else {
|
|
764
|
+
loadConfig();
|
|
765
|
+
logger.success("Config file loaded successfully.");
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// src/commands/index.ts
|
|
771
|
+
var program = new import_commander7.Command();
|
|
772
|
+
program.name(config.name).version(config.version).description(config.description).usage(config.usage).configureOutput({
|
|
773
|
+
writeOut: (message) => logger.info(message),
|
|
774
|
+
writeErr: (message) => logger.error(message)
|
|
775
|
+
});
|
|
776
|
+
registerHttpCommand(program, config.help.commands.http);
|
|
777
|
+
registerTcpCommand(program, config.help.commands.tcp);
|
|
778
|
+
registerAuthCommand(program, config.help.commands.auth);
|
|
779
|
+
registerLogoutCommand(program, config.help.commands.logout);
|
|
780
|
+
registerConfigCommand(program, config.help.commands.config);
|
|
781
|
+
registerHelpSubcommand(program);
|
|
782
|
+
|
|
783
|
+
// src/index.ts
|
|
784
|
+
process.on("SIGINT", () => {
|
|
785
|
+
logger.log("\n");
|
|
786
|
+
logger.warn("Shutting down tunnel...\n");
|
|
787
|
+
process.exit(0);
|
|
788
|
+
});
|
|
789
|
+
program.parse(process.argv);
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "quvur-dev",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "Muhammadali Yuldoshev <mukhammadaliweb@gmail.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"quvur": "dist/index.cjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "npx tsup src/index.ts --dts",
|
|
17
|
+
"start:dev": "tsx --watch src/index.ts",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/axios": "^0.9.36",
|
|
22
|
+
"@types/node": "^25.2.3",
|
|
23
|
+
"@types/ws": "^8.18.1",
|
|
24
|
+
"tsup": "^8.5.1",
|
|
25
|
+
"tsx": "^4.21.0",
|
|
26
|
+
"typescript": "^5.9.3"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"axios": "^1.13.5",
|
|
30
|
+
"chalk": "^5.6.2",
|
|
31
|
+
"commander": "^14.0.3",
|
|
32
|
+
"dotenv": "^17.3.1",
|
|
33
|
+
"ws": "^8.19.0",
|
|
34
|
+
"zod": "^4.3.6"
|
|
35
|
+
}
|
|
36
|
+
}
|