uplink-cli 0.1.0-alpha.4 → 0.1.0-alpha.6
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/cli/src/http.ts +84 -20
- package/package.json +3 -1
package/cli/src/http.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import fetch from "node-fetch";
|
|
1
|
+
import fetch, { AbortError } from "node-fetch";
|
|
2
|
+
|
|
3
|
+
// Configuration
|
|
4
|
+
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
|
5
|
+
const MAX_RETRIES = 3;
|
|
6
|
+
const INITIAL_RETRY_DELAY = 1000; // 1 second
|
|
2
7
|
|
|
3
8
|
function getApiBase(): string {
|
|
4
9
|
return process.env.AGENTCLOUD_API_BASE ?? "https://api.uplink.spot";
|
|
@@ -18,16 +23,40 @@ function getApiToken(apiBase: string): string | undefined {
|
|
|
18
23
|
return process.env.AGENTCLOUD_TOKEN || undefined;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
// Local dev
|
|
26
|
+
// Local dev:
|
|
22
27
|
// - Prefer AGENTCLOUD_TOKEN if set
|
|
23
|
-
// - Otherwise allow AGENTCLOUD_TOKEN_DEV
|
|
28
|
+
// - Otherwise allow AGENTCLOUD_TOKEN_DEV (no hardcoded default for security)
|
|
24
29
|
return (
|
|
25
30
|
process.env.AGENTCLOUD_TOKEN ||
|
|
26
31
|
process.env.AGENTCLOUD_TOKEN_DEV ||
|
|
27
|
-
|
|
32
|
+
undefined
|
|
28
33
|
);
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
// Check if error is retryable (network issues, 5xx errors)
|
|
37
|
+
function isRetryable(error: unknown, statusCode?: number): boolean {
|
|
38
|
+
// Network errors are retryable
|
|
39
|
+
if (error instanceof AbortError) return true;
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
const msg = error.message.toLowerCase();
|
|
42
|
+
if (msg.includes("econnrefused") || msg.includes("enotfound") ||
|
|
43
|
+
msg.includes("etimedout") || msg.includes("econnreset") ||
|
|
44
|
+
msg.includes("socket hang up")) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 5xx server errors are retryable
|
|
49
|
+
if (statusCode && statusCode >= 500 && statusCode < 600) return true;
|
|
50
|
+
// 429 Too Many Requests is retryable
|
|
51
|
+
if (statusCode === 429) return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Sleep helper
|
|
56
|
+
function sleep(ms: number): Promise<void> {
|
|
57
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
export async function apiRequest(
|
|
32
61
|
method: string,
|
|
33
62
|
path: string,
|
|
@@ -39,22 +68,57 @@ export async function apiRequest(
|
|
|
39
68
|
throw new Error("Missing AGENTCLOUD_TOKEN");
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
let lastError: Error | null = null;
|
|
72
|
+
|
|
73
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
74
|
+
// Create abort controller for timeout
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`${apiBase}${path}`, {
|
|
80
|
+
method,
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
Authorization: `Bearer ${apiToken}`,
|
|
84
|
+
},
|
|
85
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
58
88
|
|
|
89
|
+
clearTimeout(timeoutId);
|
|
59
90
|
|
|
91
|
+
const json = await response.json().catch(() => ({}));
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
// Check if this error is retryable
|
|
95
|
+
if (isRetryable(null, response.status) && attempt < MAX_RETRIES - 1) {
|
|
96
|
+
const delay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
|
|
97
|
+
await sleep(delay);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(JSON.stringify(json, null, 2));
|
|
101
|
+
}
|
|
60
102
|
|
|
103
|
+
return json;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
107
|
+
|
|
108
|
+
// Check if we should retry
|
|
109
|
+
if (isRetryable(error) && attempt < MAX_RETRIES - 1) {
|
|
110
|
+
const delay = INITIAL_RETRY_DELAY * Math.pow(2, attempt);
|
|
111
|
+
await sleep(delay);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Not retryable or out of retries
|
|
116
|
+
if (error instanceof AbortError) {
|
|
117
|
+
throw new Error(`Request timeout after ${REQUEST_TIMEOUT}ms`);
|
|
118
|
+
}
|
|
119
|
+
throw lastError;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw lastError || new Error("Request failed after retries");
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uplink-cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"description": "Localhost to public URL in seconds. No signup forms, no browser - everything in your terminal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tunnel",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"better-sqlite3": "^11.10.0",
|
|
51
51
|
"body-parser": "^1.20.3",
|
|
52
52
|
"commander": "^12.1.0",
|
|
53
|
+
"compression": "^1.7.4",
|
|
53
54
|
"dotenv": "^16.6.1",
|
|
54
55
|
"express": "^4.19.2",
|
|
55
56
|
"express-rate-limit": "^8.2.1",
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"zod": "^4.2.1"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
66
|
+
"@types/compression": "^1.7.5",
|
|
65
67
|
"@types/express": "^4.17.21",
|
|
66
68
|
"@types/express-rate-limit": "^5.1.3",
|
|
67
69
|
"@types/node": "^22.7.4",
|