uplink-cli 0.1.0-alpha.3 → 0.1.0-alpha.5

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 CHANGED
@@ -18,13 +18,13 @@ function getApiToken(apiBase: string): string | undefined {
18
18
  return process.env.AGENTCLOUD_TOKEN || undefined;
19
19
  }
20
20
 
21
- // Local dev convenience:
21
+ // Local dev:
22
22
  // - Prefer AGENTCLOUD_TOKEN if set
23
- // - Otherwise allow AGENTCLOUD_TOKEN_DEV / dev-token
23
+ // - Otherwise allow AGENTCLOUD_TOKEN_DEV (no hardcoded default for security)
24
24
  return (
25
25
  process.env.AGENTCLOUD_TOKEN ||
26
26
  process.env.AGENTCLOUD_TOKEN_DEV ||
27
- "dev-token"
27
+ undefined
28
28
  );
29
29
  }
30
30
 
@@ -79,12 +79,20 @@ function colorRed(text: string) {
79
79
  return `${c.red}${text}${c.reset}`;
80
80
  }
81
81
 
82
+ const TOKEN_DOMAIN = process.env.TUNNEL_DOMAIN || "x.uplink.spot";
83
+ const ALIAS_DOMAIN = process.env.ALIAS_DOMAIN || "uplink.spot";
84
+ const URL_SCHEME = (process.env.TUNNEL_URL_SCHEME || "https").toLowerCase();
85
+
82
86
  function colorMagenta(text: string) {
83
87
  return `${c.magenta}${text}${c.reset}`;
84
88
  }
85
89
 
90
+ function colorWhite(text: string) {
91
+ return `${c.brightWhite}${text}${c.reset}`;
92
+ }
93
+
86
94
  // ASCII banner with color styling
87
- const ASCII_UPLINK = colorCyan([
95
+ const ASCII_UPLINK = colorWhite([
88
96
  "██╗ ██╗██████╗ ██╗ ██╗███╗ ██╗██╗ ██╗",
89
97
  "██║ ██║██╔══██╗██║ ██║████╗ ██║██║ ██╔╝",
90
98
  "██║ ██║██████╔╝██║ ██║██╔██╗ ██║█████╔╝ ",
@@ -141,6 +149,7 @@ async function inlineSelect(
141
149
  let branchColor: string;
142
150
 
143
151
  if (isSelected) {
152
+ // Selected: cyan highlight
144
153
  branchColor = colorCyan(branch);
145
154
  if (opt.label === "Back") {
146
155
  label = colorDim(opt.label);
@@ -148,11 +157,12 @@ async function inlineSelect(
148
157
  label = colorCyan(opt.label);
149
158
  }
150
159
  } else {
151
- branchColor = colorDim(branch);
160
+ // Not selected: white
161
+ branchColor = colorWhite(branch);
152
162
  if (opt.label === "Back") {
153
163
  label = colorDim(opt.label);
154
164
  } else {
155
- label = opt.label;
165
+ label = colorWhite(opt.label);
156
166
  }
157
167
  }
158
168
 
@@ -167,8 +177,8 @@ async function inlineSelect(
167
177
  allOptions.forEach((opt, idx) => {
168
178
  const isLast = idx === allOptions.length - 1;
169
179
  const branch = isLast ? "└─" : "├─";
170
- const branchColor = idx === 0 ? colorCyan(branch) : colorDim(branch);
171
- const label = idx === 0 ? colorCyan(opt.label) : (opt.label === "Back" ? colorDim(opt.label) : opt.label);
180
+ const branchColor = idx === 0 ? colorCyan(branch) : colorWhite(branch);
181
+ const label = idx === 0 ? colorCyan(opt.label) : (opt.label === "Back" ? colorDim(opt.label) : colorWhite(opt.label));
172
182
  console.log(`${branchColor} ${label}`);
173
183
  });
174
184
 
@@ -652,6 +662,100 @@ export const menuCommand = new Command("menu")
652
662
  }
653
663
  },
654
664
  },
665
+ {
666
+ label: "Set Permanent Alias",
667
+ action: async () => {
668
+ const data = await apiRequest("GET", "/v1/tunnels");
669
+ const tunnels = data.tunnels || [];
670
+ if (!tunnels.length) return "No tunnels found.";
671
+
672
+ const options: SelectOption[] = tunnels.map((t: any) => {
673
+ const token = truncate(t.token || "", 10);
674
+ const alias = t.alias ? colorGreen(t.alias) : colorDim("none");
675
+ const port = t.target_port ?? t.targetPort ?? "-";
676
+ return {
677
+ label: `${token.padEnd(12)} port ${String(port).padEnd(5)} alias ${alias}`,
678
+ value: t.id,
679
+ };
680
+ });
681
+
682
+ const choice = await inlineSelect("Select tunnel for alias", options, true);
683
+ if (choice === null) return "";
684
+
685
+ try {
686
+ process.stdin.setRawMode(false);
687
+ } catch {
688
+ /* ignore */
689
+ }
690
+ const aliasInput = await promptLine("Enter alias (e.g. thomas): ");
691
+ restoreRawMode();
692
+ const alias = aliasInput.trim();
693
+ if (!alias) return "Alias not set (empty).";
694
+
695
+ try {
696
+ const result = await apiRequest("POST", `/v1/tunnels/${choice.value}/alias`, {
697
+ alias,
698
+ });
699
+ const aliasUrl = result.aliasUrl || `${URL_SCHEME}://${alias}.${ALIAS_DOMAIN}`;
700
+ const tokenUrl = result.url || `${URL_SCHEME}://${result.token}.${TOKEN_DOMAIN}`;
701
+ return [
702
+ "✓ Alias updated",
703
+ `→ Alias URL ${aliasUrl}`,
704
+ `→ Token URL ${tokenUrl}`,
705
+ ].join("\n");
706
+ } catch (err: any) {
707
+ const errMsg = err?.message || String(err);
708
+ // Check for premium feature errors
709
+ if (errMsg.includes("ALIAS_NOT_ENABLED")) {
710
+ try {
711
+ const parsed = JSON.parse(errMsg);
712
+ const userId = parsed?.error?.details?.user_id || "(check your token)";
713
+ return [
714
+ "",
715
+ colorYellow("🔒 Permanent Aliases - Premium Feature"),
716
+ "",
717
+ "Permanent aliases give you stable URLs like:",
718
+ ` ${colorGreen(`https://myapp.${ALIAS_DOMAIN}`)}`,
719
+ "",
720
+ "Instead of regenerating tokens each time.",
721
+ "",
722
+ "To unlock this feature:",
723
+ ` → Join our Discord: ${colorCyan("https://uplink.spot")}`,
724
+ ` → Share your user ID: ${colorDim(userId)}`,
725
+ "",
726
+ "We'll enable it for your account!",
727
+ "",
728
+ ].join("\n");
729
+ } catch {
730
+ return colorYellow("🔒 Aliases are a premium feature. Contact us at uplink.spot to upgrade.");
731
+ }
732
+ }
733
+ if (errMsg.includes("ALIAS_LIMIT_REACHED")) {
734
+ return colorYellow("⚠️ You've reached your alias limit. Contact us to increase it.");
735
+ }
736
+ throw err; // Re-throw other errors
737
+ }
738
+ },
739
+ },
740
+ {
741
+ label: "Remove Alias",
742
+ action: async () => {
743
+ const data = await apiRequest("GET", "/v1/tunnels");
744
+ const tunnels = (data.tunnels || []).filter((t: any) => !!t.alias);
745
+ if (!tunnels.length) return "No tunnels with aliases.";
746
+
747
+ const options: SelectOption[] = tunnels.map((t: any) => ({
748
+ label: `${truncate(t.token || "", 10).padEnd(12)} alias ${colorGreen(t.alias)}`,
749
+ value: t.id,
750
+ }));
751
+
752
+ const choice = await inlineSelect("Select tunnel to remove alias", options, true);
753
+ if (choice === null) return "";
754
+
755
+ await apiRequest("DELETE", `/v1/tunnels/${choice.value}/alias`);
756
+ return "✓ Alias removed";
757
+ },
758
+ },
655
759
  {
656
760
  label: "View Connected (with IPs)",
657
761
  action: async () => {
@@ -705,27 +809,38 @@ export const menuCommand = new Command("menu")
705
809
  if (!tunnels || tunnels.length === 0) {
706
810
  return "No tunnels found.";
707
811
  }
708
-
709
- const lines = tunnels.map(
710
- (t: any) => {
711
- const token = t.token || "";
712
- const connectedFromApi = t.connected ?? false;
713
- const connectedLocal = runningClients.some((c) => c.token === token);
714
- const connectionStatus = isAdmin
715
- ? (connectedFromApi ? "connected" : "disconnected")
716
- : (connectedLocal ? "connected" : "unknown");
717
-
718
- return `${truncate(t.id, 12)} ${truncate(token, 10).padEnd(12)} ${String(
812
+ const lines = tunnels.map((t: any) => {
813
+ const token = t.token || "";
814
+ const alias = t.alias || "-";
815
+ const tokenUrl = t.url || `${URL_SCHEME}://${token}.${TOKEN_DOMAIN}`;
816
+ const aliasUrl =
817
+ t.aliasUrl || (t.alias ? `${URL_SCHEME}://${t.alias}.${ALIAS_DOMAIN}` : "-");
818
+ const connectedFromApi = t.connected ?? false;
819
+ const connectedLocal = runningClients.some((c) => c.token === token);
820
+ const connectionStatus = isAdmin
821
+ ? connectedFromApi
822
+ ? "connected"
823
+ : "disconnected"
824
+ : connectedLocal
825
+ ? "connected"
826
+ : "unknown";
827
+
828
+ return [
829
+ `${truncate(t.id, 12)} ${truncate(token, 10).padEnd(12)} ${String(
719
830
  t.target_port ?? t.targetPort ?? "-"
720
831
  ).padEnd(5)} ${connectionStatus.padEnd(12)} ${truncate(
721
832
  t.created_at ?? t.createdAt ?? "",
722
833
  19
723
- )}`;
724
- }
725
- );
726
- return ["ID Token Port Connection Created", "-".repeat(70), ...lines].join(
727
- "\n"
728
- );
834
+ )}`,
835
+ ` url: ${tokenUrl}`,
836
+ ` alias: ${aliasUrl} (${alias})`,
837
+ ].join("\n");
838
+ });
839
+ return [
840
+ "ID Token Port Connection Created",
841
+ "-".repeat(90),
842
+ ...lines,
843
+ ].join("\n\n");
729
844
  },
730
845
  },
731
846
  {
@@ -853,6 +968,71 @@ export const menuCommand = new Command("menu")
853
968
  }
854
969
  },
855
970
  },
971
+ {
972
+ label: "Grant Alias Access",
973
+ action: async () => {
974
+ try {
975
+ // Fetch available tokens to show users
976
+ const result = await apiRequest("GET", "/v1/admin/tokens");
977
+ const tokens = result.tokens || [];
978
+
979
+ if (tokens.length === 0) {
980
+ restoreRawMode();
981
+ return "No tokens found.";
982
+ }
983
+
984
+ // Build options from tokens (group by user_id)
985
+ const userMap = new Map<string, any>();
986
+ for (const t of tokens) {
987
+ const userId = t.user_id || t.userId;
988
+ if (userId && !userMap.has(userId)) {
989
+ userMap.set(userId, t);
990
+ }
991
+ }
992
+
993
+ const options: SelectOption[] = Array.from(userMap.entries()).map(([userId, t]) => ({
994
+ label: `${truncate(userId, 20)} ${colorDim(`${t.role || "user"} - ${t.label || "no label"}`)}`,
995
+ value: userId,
996
+ }));
997
+
998
+ const selected = await inlineSelect("Select user to grant alias access", options, true);
999
+
1000
+ if (selected === null) {
1001
+ restoreRawMode();
1002
+ return "";
1003
+ }
1004
+
1005
+ const userId = selected.value as string;
1006
+
1007
+ // Prompt for limit
1008
+ try {
1009
+ process.stdin.setRawMode(false);
1010
+ } catch {
1011
+ /* ignore */
1012
+ }
1013
+ const limitAnswer = await promptLine("Alias limit (1-10, or -1 for unlimited, default 1): ");
1014
+ restoreRawMode();
1015
+
1016
+ const aliasLimit = limitAnswer.trim() ? parseInt(limitAnswer.trim(), 10) : 1;
1017
+ if (isNaN(aliasLimit) || aliasLimit < -1 || aliasLimit > 100) {
1018
+ return "Invalid limit. Must be -1 (unlimited) or 0-100.";
1019
+ }
1020
+
1021
+ await apiRequest("POST", "/v1/admin/grant-alias", { userId, aliasLimit });
1022
+
1023
+ const limitDesc = aliasLimit === -1 ? "unlimited" : String(aliasLimit);
1024
+ return [
1025
+ "✓ Alias access granted",
1026
+ "",
1027
+ `→ User ${userId}`,
1028
+ `→ Limit ${limitDesc} alias(es)`,
1029
+ ].join("\n");
1030
+ } catch (err: any) {
1031
+ restoreRawMode();
1032
+ throw err;
1033
+ }
1034
+ },
1035
+ },
856
1036
  ],
857
1037
  });
858
1038
 
@@ -903,11 +1083,8 @@ export const menuCommand = new Command("menu")
903
1083
  if (clients.length === 0) {
904
1084
  cachedActiveTunnels = "";
905
1085
  } else {
906
- const domain = process.env.TUNNEL_DOMAIN || "t.uplink.spot";
907
- const scheme = (process.env.TUNNEL_URL_SCHEME || "https").toLowerCase();
908
-
909
1086
  const tunnelLines = clients.map((client, idx) => {
910
- const url = `${scheme}://${client.token}.${domain}`;
1087
+ const url = `${URL_SCHEME}://${client.token}.${TOKEN_DOMAIN}`;
911
1088
  const isLast = idx === clients.length - 1;
912
1089
  const branch = isLast ? "└─" : "├─";
913
1090
  return colorDim(branch) + " " + colorGreen(url) + colorDim(" → ") + `localhost:${client.port}`;
@@ -1001,6 +1178,7 @@ export const menuCommand = new Command("menu")
1001
1178
  let branchColor: string;
1002
1179
 
1003
1180
  if (isSelected) {
1181
+ // Selected: cyan highlight
1004
1182
  branchColor = colorCyan(branch);
1005
1183
  if (cleanLabel.toLowerCase().includes("exit")) {
1006
1184
  label = colorDim(cleanLabel);
@@ -1012,7 +1190,8 @@ export const menuCommand = new Command("menu")
1012
1190
  label = colorCyan(cleanLabel);
1013
1191
  }
1014
1192
  } else {
1015
- branchColor = colorDim(branch);
1193
+ // Not selected: white text
1194
+ branchColor = colorWhite(branch);
1016
1195
  if (cleanLabel.toLowerCase().includes("exit")) {
1017
1196
  label = colorDim(cleanLabel);
1018
1197
  } else if (cleanLabel.toLowerCase().includes("stop all") || cleanLabel.toLowerCase().includes("kill")) {
@@ -1020,7 +1199,7 @@ export const menuCommand = new Command("menu")
1020
1199
  } else if (cleanLabel.toLowerCase().includes("get started")) {
1021
1200
  label = colorGreen(cleanLabel);
1022
1201
  } else {
1023
- label = cleanLabel;
1202
+ label = colorWhite(cleanLabel);
1024
1203
  }
1025
1204
  }
1026
1205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplink-cli",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.5",
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",