speqs 0.1.2 → 0.2.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/auth.d.ts +17 -0
- package/dist/auth.js +102 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +25 -0
- package/dist/index.js +30 -0
- package/dist/tunnel.js +127 -36
- package/package.json +1 -1
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-based authentication via the Speqs frontend plugin auth flow.
|
|
3
|
+
* Uses the existing /auth/plugin + /api/plugin/auth/poll infrastructure.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getAppUrl(): string;
|
|
6
|
+
export declare function getSupabaseUrl(): string;
|
|
7
|
+
export declare function getSupabaseAnonKey(): string;
|
|
8
|
+
export declare function decodeJwtExp(token: string): number;
|
|
9
|
+
export declare function isTokenExpired(token: string, bufferSeconds?: number): boolean;
|
|
10
|
+
export declare function login(appUrl?: string): Promise<{
|
|
11
|
+
accessToken: string;
|
|
12
|
+
refreshToken: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function refreshTokens(refreshToken: string, supabaseUrl?: string, anonKey?: string): Promise<{
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string;
|
|
17
|
+
}>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-based authentication via the Speqs frontend plugin auth flow.
|
|
3
|
+
* Uses the existing /auth/plugin + /api/plugin/auth/poll infrastructure.
|
|
4
|
+
*/
|
|
5
|
+
import * as crypto from "node:crypto";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
const POLL_INTERVAL = 2_000;
|
|
8
|
+
const LOGIN_TIMEOUT = 5 * 60 * 1000; // 5 minutes (matches server-side token TTL)
|
|
9
|
+
const DEFAULT_APP_URL = "https://app.speqs.io";
|
|
10
|
+
const DEFAULT_SUPABASE_URL = "https://hngymyxdyamokpbeakps.supabase.co";
|
|
11
|
+
const DEFAULT_SUPABASE_ANON_KEY = "sb_publishable_JlS-HfwNyDqLNbrfbrkUlw_PSdZJdo2";
|
|
12
|
+
export function getAppUrl() {
|
|
13
|
+
return process.env.SPEQS_APP_URL ?? DEFAULT_APP_URL;
|
|
14
|
+
}
|
|
15
|
+
export function getSupabaseUrl() {
|
|
16
|
+
return process.env.SPEQS_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
|
|
17
|
+
}
|
|
18
|
+
export function getSupabaseAnonKey() {
|
|
19
|
+
return process.env.SPEQS_SUPABASE_ANON_KEY ?? DEFAULT_SUPABASE_ANON_KEY;
|
|
20
|
+
}
|
|
21
|
+
// --- Browser open ---
|
|
22
|
+
function openBrowser(url) {
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
execFile("cmd", ["/c", "start", "", url]);
|
|
25
|
+
}
|
|
26
|
+
else if (process.platform === "darwin") {
|
|
27
|
+
execFile("open", [url]);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
execFile("xdg-open", [url]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// --- JWT decode ---
|
|
34
|
+
export function decodeJwtExp(token) {
|
|
35
|
+
try {
|
|
36
|
+
const payload = token.split(".")[1];
|
|
37
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
38
|
+
return decoded.exp;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function isTokenExpired(token, bufferSeconds = 300) {
|
|
45
|
+
const exp = decodeJwtExp(token);
|
|
46
|
+
if (!exp)
|
|
47
|
+
return true;
|
|
48
|
+
return Date.now() / 1000 >= exp - bufferSeconds;
|
|
49
|
+
}
|
|
50
|
+
// --- Login via browser polling ---
|
|
51
|
+
export async function login(appUrl) {
|
|
52
|
+
const url = appUrl ?? getAppUrl();
|
|
53
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
54
|
+
const loginUrl = `${url}/auth/plugin?state=${state}`;
|
|
55
|
+
console.log("Opening browser to sign in...");
|
|
56
|
+
console.log(`If the browser doesn't open, visit:\n ${loginUrl}\n`);
|
|
57
|
+
openBrowser(loginUrl);
|
|
58
|
+
console.log("Waiting for authentication...");
|
|
59
|
+
const deadline = Date.now() + LOGIN_TIMEOUT;
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
62
|
+
try {
|
|
63
|
+
const resp = await fetch(`${url}/api/plugin/auth/poll?state=${state}`, {
|
|
64
|
+
signal: AbortSignal.timeout(10_000),
|
|
65
|
+
});
|
|
66
|
+
if (resp.status === 200) {
|
|
67
|
+
const data = await resp.json();
|
|
68
|
+
if (data.status === "complete" && data.access_token && data.refresh_token) {
|
|
69
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 202 = pending, keep polling
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Network error, keep polling
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error("Login timed out. Please try again.");
|
|
79
|
+
}
|
|
80
|
+
// --- Token refresh ---
|
|
81
|
+
export async function refreshTokens(refreshToken, supabaseUrl, anonKey) {
|
|
82
|
+
const url = supabaseUrl ?? getSupabaseUrl();
|
|
83
|
+
const key = anonKey ?? getSupabaseAnonKey();
|
|
84
|
+
const resp = await fetch(`${url}/auth/v1/token?grant_type=refresh_token`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
apikey: key,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
91
|
+
signal: AbortSignal.timeout(10_000),
|
|
92
|
+
});
|
|
93
|
+
if (!resp.ok) {
|
|
94
|
+
const body = await resp.text().catch(() => "");
|
|
95
|
+
throw new Error(`Token refresh failed (HTTP ${resp.status}): ${body}`);
|
|
96
|
+
}
|
|
97
|
+
const data = await resp.json();
|
|
98
|
+
if (!data.access_token || !data.refresh_token) {
|
|
99
|
+
throw new Error("Token refresh response missing required fields");
|
|
100
|
+
}
|
|
101
|
+
return { accessToken: data.access_token, refreshToken: data.refresh_token };
|
|
102
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config persistence for ~/.speqs/config.json
|
|
3
|
+
*/
|
|
4
|
+
export interface SpeqsConfig {
|
|
5
|
+
access_token?: string;
|
|
6
|
+
refresh_token?: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
[key: string]: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function loadConfig(): SpeqsConfig;
|
|
11
|
+
export declare function saveConfig(config: SpeqsConfig): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config persistence for ~/.speqs/config.json
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), ".speqs");
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
try {
|
|
11
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
12
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Corrupted config — ignore
|
|
17
|
+
}
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
export function saveConfig(config) {
|
|
21
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
22
|
+
const tmp = CONFIG_FILE + ".tmp";
|
|
23
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
24
|
+
fs.renameSync(tmp, CONFIG_FILE);
|
|
25
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,42 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { program, Option } from "commander";
|
|
4
4
|
import { runTunnel } from "./tunnel.js";
|
|
5
|
+
import { login, getAppUrl } from "./auth.js";
|
|
6
|
+
import { loadConfig, saveConfig } from "./config.js";
|
|
5
7
|
const require = createRequire(import.meta.url);
|
|
6
8
|
const { version } = require("../package.json");
|
|
7
9
|
program
|
|
8
10
|
.name("speqs")
|
|
9
11
|
.description("Speqs CLI tools")
|
|
10
12
|
.version(version);
|
|
13
|
+
program
|
|
14
|
+
.command("login")
|
|
15
|
+
.description("Authenticate with Speqs via your browser")
|
|
16
|
+
.action(async () => {
|
|
17
|
+
try {
|
|
18
|
+
const tokens = await login(getAppUrl());
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
config.access_token = tokens.accessToken;
|
|
21
|
+
config.refresh_token = tokens.refreshToken;
|
|
22
|
+
saveConfig(config);
|
|
23
|
+
console.log("\nLogin successful!");
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error(`Login failed: ${e instanceof Error ? e.message : e}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
program
|
|
31
|
+
.command("logout")
|
|
32
|
+
.description("Remove saved authentication credentials")
|
|
33
|
+
.action(() => {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
delete config.access_token;
|
|
36
|
+
delete config.refresh_token;
|
|
37
|
+
delete config.token;
|
|
38
|
+
saveConfig(config);
|
|
39
|
+
console.log("Logged out.");
|
|
40
|
+
});
|
|
11
41
|
program
|
|
12
42
|
.command("tunnel")
|
|
13
43
|
.description("Expose your localhost to Speqs via a Cloudflare tunnel")
|
package/dist/tunnel.js
CHANGED
|
@@ -2,33 +2,16 @@
|
|
|
2
2
|
* Localhost tunnel CLI — wraps cloudflared and registers with Speqs backend.
|
|
3
3
|
*/
|
|
4
4
|
import { spawn, execSync } from "node:child_process";
|
|
5
|
-
import * as fs from "node:fs";
|
|
6
|
-
import * as path from "node:path";
|
|
7
|
-
import * as os from "node:os";
|
|
8
5
|
import * as readline from "node:readline";
|
|
6
|
+
import { loadConfig, saveConfig } from "./config.js";
|
|
7
|
+
import { refreshTokens, isTokenExpired, decodeJwtExp } from "./auth.js";
|
|
9
8
|
const TUNNEL_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
10
9
|
const HEARTBEAT_INTERVAL = 30_000;
|
|
11
10
|
const MAX_HEARTBEAT_FAILURES = 3;
|
|
12
11
|
const CLOUDFLARED_STARTUP_TIMEOUT = 30_000;
|
|
13
12
|
const DEFAULT_API_URL = "https://api.speqs.io";
|
|
14
13
|
const API_BASE = "/api/v1";
|
|
15
|
-
|
|
16
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
17
|
-
function loadConfig() {
|
|
18
|
-
try {
|
|
19
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
20
|
-
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
// Corrupted config — ignore
|
|
25
|
-
}
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
function saveConfig(config) {
|
|
29
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
30
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
31
|
-
}
|
|
14
|
+
// --- Token resolution ---
|
|
32
15
|
async function verifyToken(token, apiUrl) {
|
|
33
16
|
try {
|
|
34
17
|
const resp = await fetch(`${apiUrl}${API_BASE}/tunnel/active`, {
|
|
@@ -58,20 +41,62 @@ function prompt(question) {
|
|
|
58
41
|
});
|
|
59
42
|
});
|
|
60
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve an access token, refreshing if needed.
|
|
46
|
+
* Returns both the token and a mutable holder for runtime refresh.
|
|
47
|
+
*/
|
|
61
48
|
async function resolveToken(tokenArg, apiUrl) {
|
|
49
|
+
// 1. Explicit token argument
|
|
62
50
|
if (tokenArg)
|
|
63
|
-
return tokenArg;
|
|
51
|
+
return { token: tokenArg, refresh: null };
|
|
52
|
+
// 2. Environment variable
|
|
64
53
|
const envToken = process.env.SPEQS_TOKEN;
|
|
65
54
|
if (envToken)
|
|
66
|
-
return envToken;
|
|
55
|
+
return { token: envToken, refresh: null };
|
|
56
|
+
// 3. Saved config with refresh token
|
|
67
57
|
const config = loadConfig();
|
|
58
|
+
if (config.access_token && config.refresh_token) {
|
|
59
|
+
let accessToken = config.access_token;
|
|
60
|
+
// Refresh if expired or close to expiry
|
|
61
|
+
if (isTokenExpired(accessToken)) {
|
|
62
|
+
try {
|
|
63
|
+
console.log("Refreshing access token...");
|
|
64
|
+
const tokens = await refreshTokens(config.refresh_token);
|
|
65
|
+
accessToken = tokens.accessToken;
|
|
66
|
+
config.access_token = tokens.accessToken;
|
|
67
|
+
config.refresh_token = tokens.refreshToken;
|
|
68
|
+
saveConfig(config);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(`Token refresh failed: ${e instanceof Error ? e.message : e}`);
|
|
72
|
+
console.error('Run "speqs login" to re-authenticate.\n');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (await verifyToken(accessToken, apiUrl)) {
|
|
76
|
+
// Return with refresh capability for long-running tunnel
|
|
77
|
+
const doRefresh = async () => {
|
|
78
|
+
const cfg = loadConfig();
|
|
79
|
+
if (!cfg.refresh_token)
|
|
80
|
+
throw new Error("No refresh token");
|
|
81
|
+
const tokens = await refreshTokens(cfg.refresh_token);
|
|
82
|
+
cfg.access_token = tokens.accessToken;
|
|
83
|
+
cfg.refresh_token = tokens.refreshToken;
|
|
84
|
+
saveConfig(cfg);
|
|
85
|
+
return tokens.accessToken;
|
|
86
|
+
};
|
|
87
|
+
return { token: accessToken, refresh: doRefresh };
|
|
88
|
+
}
|
|
89
|
+
console.error('Saved token is invalid. Run "speqs login" to re-authenticate.\n');
|
|
90
|
+
}
|
|
91
|
+
// 4. Legacy saved token (no refresh token)
|
|
68
92
|
if (config.token) {
|
|
69
93
|
if (await verifyToken(config.token, apiUrl)) {
|
|
70
|
-
return config.token;
|
|
94
|
+
return { token: config.token, refresh: null };
|
|
71
95
|
}
|
|
72
96
|
console.error("Saved token is invalid or expired.\n");
|
|
73
97
|
}
|
|
74
|
-
// Interactive prompt
|
|
98
|
+
// 5. Interactive prompt (legacy fallback)
|
|
99
|
+
console.log('Tip: Run "speqs login" for browser-based authentication with auto-refresh.\n');
|
|
75
100
|
console.log("You can find your token in the simulation view.\n");
|
|
76
101
|
while (true) {
|
|
77
102
|
const token = await prompt("Paste your token: ");
|
|
@@ -82,7 +107,7 @@ async function resolveToken(tokenArg, apiUrl) {
|
|
|
82
107
|
if (await verifyToken(token, apiUrl)) {
|
|
83
108
|
config.token = token;
|
|
84
109
|
saveConfig(config);
|
|
85
|
-
return token;
|
|
110
|
+
return { token, refresh: null };
|
|
86
111
|
}
|
|
87
112
|
console.error("Invalid token. Try again.\n");
|
|
88
113
|
}
|
|
@@ -171,7 +196,7 @@ async function deregisterTunnel(apiUrl, token) {
|
|
|
171
196
|
console.error(`Warning: Failed to deregister tunnel: ${e}`);
|
|
172
197
|
}
|
|
173
198
|
}
|
|
174
|
-
function startHeartbeat(apiUrl,
|
|
199
|
+
function startHeartbeat(apiUrl, getToken, doRefresh, onTokenRefreshed, onFatal) {
|
|
175
200
|
let consecutiveFailures = 0;
|
|
176
201
|
let stopped = false;
|
|
177
202
|
const interval = setInterval(async () => {
|
|
@@ -180,9 +205,30 @@ function startHeartbeat(apiUrl, token, onFatal) {
|
|
|
180
205
|
try {
|
|
181
206
|
const resp = await fetch(`${apiUrl}${API_BASE}/tunnel/heartbeat`, {
|
|
182
207
|
method: "POST",
|
|
183
|
-
headers: { Authorization: `Bearer ${
|
|
208
|
+
headers: { Authorization: `Bearer ${getToken()}` },
|
|
184
209
|
signal: AbortSignal.timeout(10_000),
|
|
185
210
|
});
|
|
211
|
+
// If 401 and we can refresh, try once
|
|
212
|
+
if (resp.status === 401 && doRefresh) {
|
|
213
|
+
try {
|
|
214
|
+
const newToken = await doRefresh();
|
|
215
|
+
onTokenRefreshed(newToken);
|
|
216
|
+
console.log("Token refreshed.");
|
|
217
|
+
// Retry heartbeat with new token
|
|
218
|
+
const retry = await fetch(`${apiUrl}${API_BASE}/tunnel/heartbeat`, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: { Authorization: `Bearer ${newToken}` },
|
|
221
|
+
signal: AbortSignal.timeout(10_000),
|
|
222
|
+
});
|
|
223
|
+
if (!retry.ok)
|
|
224
|
+
throw new Error(`HTTP ${retry.status}`);
|
|
225
|
+
consecutiveFailures = 0;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
catch (refreshErr) {
|
|
229
|
+
console.error(`Token refresh failed: ${refreshErr}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
186
232
|
if (!resp.ok)
|
|
187
233
|
throw new Error(`HTTP ${resp.status}`);
|
|
188
234
|
consecutiveFailures = 0;
|
|
@@ -205,13 +251,55 @@ function startHeartbeat(apiUrl, token, onFatal) {
|
|
|
205
251
|
},
|
|
206
252
|
};
|
|
207
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Schedule a proactive token refresh before the JWT expires.
|
|
256
|
+
* Refreshes 10 minutes before expiry.
|
|
257
|
+
*/
|
|
258
|
+
function scheduleProactiveRefresh(token, doRefresh, onTokenRefreshed) {
|
|
259
|
+
if (!doRefresh)
|
|
260
|
+
return { stop: () => { } };
|
|
261
|
+
const exp = decodeJwtExp(token);
|
|
262
|
+
if (!exp)
|
|
263
|
+
return { stop: () => { } };
|
|
264
|
+
const refreshAt = (exp - 600) * 1000; // 10 min before expiry
|
|
265
|
+
const delay = refreshAt - Date.now();
|
|
266
|
+
if (delay <= 0)
|
|
267
|
+
return { stop: () => { } };
|
|
268
|
+
const timer = setTimeout(async () => {
|
|
269
|
+
try {
|
|
270
|
+
const newToken = await doRefresh();
|
|
271
|
+
onTokenRefreshed(newToken);
|
|
272
|
+
console.log("Token proactively refreshed.");
|
|
273
|
+
// Schedule next refresh for the new token
|
|
274
|
+
scheduleProactiveRefresh(newToken, doRefresh, onTokenRefreshed);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
console.error(`Proactive token refresh failed: ${e}`);
|
|
278
|
+
}
|
|
279
|
+
}, delay);
|
|
280
|
+
return { stop: () => clearTimeout(timer) };
|
|
281
|
+
}
|
|
208
282
|
// --- Main ---
|
|
209
283
|
export async function runTunnel(port, tokenArg, apiUrlArg) {
|
|
210
284
|
const apiUrl = resolveApiUrl(apiUrlArg);
|
|
211
285
|
if (apiUrl !== DEFAULT_API_URL) {
|
|
212
286
|
console.log(`Using API: ${apiUrl}`);
|
|
213
287
|
}
|
|
214
|
-
const
|
|
288
|
+
const resolved = await resolveToken(tokenArg, apiUrl);
|
|
289
|
+
let currentToken = resolved.token;
|
|
290
|
+
const onTokenRefreshed = (newToken) => {
|
|
291
|
+
currentToken = newToken;
|
|
292
|
+
};
|
|
293
|
+
// Serialize refresh calls to prevent concurrent use of single-use refresh tokens
|
|
294
|
+
let refreshInFlight = null;
|
|
295
|
+
const serializedRefresh = resolved.refresh
|
|
296
|
+
? async () => {
|
|
297
|
+
if (refreshInFlight)
|
|
298
|
+
return refreshInFlight;
|
|
299
|
+
refreshInFlight = resolved.refresh().finally(() => { refreshInFlight = null; });
|
|
300
|
+
return refreshInFlight;
|
|
301
|
+
}
|
|
302
|
+
: null;
|
|
215
303
|
checkCloudflared();
|
|
216
304
|
let cfResult;
|
|
217
305
|
try {
|
|
@@ -222,30 +310,33 @@ export async function runTunnel(port, tokenArg, apiUrlArg) {
|
|
|
222
310
|
process.exit(1);
|
|
223
311
|
}
|
|
224
312
|
const { process: cfProcess, tunnelUrl } = cfResult;
|
|
225
|
-
await registerTunnel(apiUrl,
|
|
313
|
+
await registerTunnel(apiUrl, currentToken, tunnelUrl, port);
|
|
226
314
|
let shuttingDown = false;
|
|
315
|
+
const heartbeat = startHeartbeat(apiUrl, () => currentToken, serializedRefresh, onTokenRefreshed, async () => {
|
|
316
|
+
await deregisterTunnel(apiUrl, currentToken);
|
|
317
|
+
cfProcess.kill();
|
|
318
|
+
process.exit(1);
|
|
319
|
+
});
|
|
320
|
+
const proactiveRefresh = scheduleProactiveRefresh(currentToken, serializedRefresh, onTokenRefreshed);
|
|
227
321
|
const shutdown = async () => {
|
|
228
322
|
if (shuttingDown)
|
|
229
323
|
process.exit(1);
|
|
230
324
|
shuttingDown = true;
|
|
231
325
|
console.log("\nShutting down...");
|
|
232
326
|
heartbeat.stop();
|
|
327
|
+
proactiveRefresh.stop();
|
|
233
328
|
cfProcess.kill();
|
|
234
|
-
await deregisterTunnel(apiUrl,
|
|
329
|
+
await deregisterTunnel(apiUrl, currentToken);
|
|
235
330
|
process.exit(0);
|
|
236
331
|
};
|
|
237
|
-
const heartbeat = startHeartbeat(apiUrl, token, async () => {
|
|
238
|
-
await deregisterTunnel(apiUrl, token);
|
|
239
|
-
cfProcess.kill();
|
|
240
|
-
process.exit(1);
|
|
241
|
-
});
|
|
242
332
|
process.on("SIGINT", shutdown);
|
|
243
333
|
process.on("SIGTERM", shutdown);
|
|
244
334
|
console.log("\nPress Ctrl+C to disconnect.\n");
|
|
245
335
|
cfProcess.on("exit", async () => {
|
|
246
336
|
if (!shuttingDown) {
|
|
247
337
|
heartbeat.stop();
|
|
248
|
-
|
|
338
|
+
proactiveRefresh.stop();
|
|
339
|
+
await deregisterTunnel(apiUrl, currentToken);
|
|
249
340
|
process.exit(0);
|
|
250
341
|
}
|
|
251
342
|
});
|