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.
Files changed (2) hide show
  1. package/cli/src/http.ts +84 -20
  2. 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 convenience:
26
+ // Local dev:
22
27
  // - Prefer AGENTCLOUD_TOKEN if set
23
- // - Otherwise allow AGENTCLOUD_TOKEN_DEV / dev-token
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
- "dev-token"
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
- const response = await fetch(`${apiBase}${path}`, {
43
- method,
44
- headers: {
45
- "Content-Type": "application/json",
46
- Authorization: `Bearer ${apiToken}`,
47
- },
48
- body: body ? JSON.stringify(body) : undefined,
49
- });
50
-
51
- const json = await response.json().catch(() => ({}));
52
- if (!response.ok) {
53
- throw new Error(JSON.stringify(json, null, 2));
54
- }
55
-
56
- return json;
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.4",
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",