pubblue 0.4.0 → 0.4.1
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.
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as net from "net";
|
|
11
11
|
import * as path from "path";
|
|
12
|
+
var OFFER_TIMEOUT_MS = 1e4;
|
|
12
13
|
async function startDaemon(config) {
|
|
13
14
|
const { tunnelId, apiClient, socketPath, infoPath } = config;
|
|
14
15
|
const ndc = await import("node-datachannel");
|
|
@@ -99,12 +100,83 @@ async function startDaemon(config) {
|
|
|
99
100
|
peer.onDataChannel((dc) => {
|
|
100
101
|
setupChannel(dc.getLabel(), dc);
|
|
101
102
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
if (fs.existsSync(socketPath)) {
|
|
104
|
+
let stale = true;
|
|
105
|
+
try {
|
|
106
|
+
const raw = fs.readFileSync(infoPath, "utf-8");
|
|
107
|
+
const info = JSON.parse(raw);
|
|
108
|
+
process.kill(info.pid, 0);
|
|
109
|
+
stale = false;
|
|
110
|
+
} catch {
|
|
111
|
+
stale = true;
|
|
112
|
+
}
|
|
113
|
+
if (stale) {
|
|
114
|
+
try {
|
|
115
|
+
fs.unlinkSync(socketPath);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error(`Daemon already running (socket: ${socketPath})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const ipcServer = net.createServer((conn) => {
|
|
123
|
+
let data = "";
|
|
124
|
+
conn.on("data", (chunk) => {
|
|
125
|
+
data += chunk.toString();
|
|
126
|
+
const newlineIdx = data.indexOf("\n");
|
|
127
|
+
if (newlineIdx === -1) return;
|
|
128
|
+
const line = data.slice(0, newlineIdx);
|
|
129
|
+
data = data.slice(newlineIdx + 1);
|
|
130
|
+
let request;
|
|
131
|
+
try {
|
|
132
|
+
request = JSON.parse(line);
|
|
133
|
+
} catch {
|
|
134
|
+
conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
|
|
135
|
+
`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
handleIpcRequest(request).then((response) => conn.write(`${JSON.stringify(response)}
|
|
139
|
+
`)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: String(err) })}
|
|
140
|
+
`));
|
|
105
141
|
});
|
|
106
|
-
peer.setLocalDescription();
|
|
107
142
|
});
|
|
143
|
+
ipcServer.listen(socketPath);
|
|
144
|
+
const infoDir = path.dirname(infoPath);
|
|
145
|
+
if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
infoPath,
|
|
148
|
+
JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
|
|
149
|
+
);
|
|
150
|
+
async function cleanup() {
|
|
151
|
+
if (pollingInterval) clearInterval(pollingInterval);
|
|
152
|
+
for (const dc of channels.values()) dc.close();
|
|
153
|
+
peer.close();
|
|
154
|
+
ipcServer.close();
|
|
155
|
+
try {
|
|
156
|
+
fs.unlinkSync(socketPath);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
fs.unlinkSync(infoPath);
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
await apiClient.close(tunnelId).catch(() => {
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function shutdown() {
|
|
167
|
+
await cleanup();
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
process.on("SIGTERM", () => void shutdown());
|
|
171
|
+
process.on("SIGINT", () => void shutdown());
|
|
172
|
+
let offer;
|
|
173
|
+
try {
|
|
174
|
+
offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
await cleanup();
|
|
178
|
+
throw new Error(`Failed to generate WebRTC offer: ${message}`);
|
|
179
|
+
}
|
|
108
180
|
await apiClient.signal(tunnelId, { offer });
|
|
109
181
|
setTimeout(async () => {
|
|
110
182
|
if (localCandidates.length > 0) {
|
|
@@ -161,48 +233,6 @@ async function startDaemon(config) {
|
|
|
161
233
|
} catch {
|
|
162
234
|
}
|
|
163
235
|
}, 500);
|
|
164
|
-
if (fs.existsSync(socketPath)) {
|
|
165
|
-
const infoFile = infoPath;
|
|
166
|
-
let stale = true;
|
|
167
|
-
try {
|
|
168
|
-
const raw = fs.readFileSync(infoFile, "utf-8");
|
|
169
|
-
const info = JSON.parse(raw);
|
|
170
|
-
process.kill(info.pid, 0);
|
|
171
|
-
stale = false;
|
|
172
|
-
} catch {
|
|
173
|
-
stale = true;
|
|
174
|
-
}
|
|
175
|
-
if (stale) {
|
|
176
|
-
try {
|
|
177
|
-
fs.unlinkSync(socketPath);
|
|
178
|
-
} catch {
|
|
179
|
-
}
|
|
180
|
-
} else {
|
|
181
|
-
throw new Error(`Daemon already running (socket: ${socketPath})`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const ipcServer = net.createServer((conn) => {
|
|
185
|
-
let data = "";
|
|
186
|
-
conn.on("data", (chunk) => {
|
|
187
|
-
data += chunk.toString();
|
|
188
|
-
const newlineIdx = data.indexOf("\n");
|
|
189
|
-
if (newlineIdx === -1) return;
|
|
190
|
-
const line = data.slice(0, newlineIdx);
|
|
191
|
-
data = data.slice(newlineIdx + 1);
|
|
192
|
-
let request;
|
|
193
|
-
try {
|
|
194
|
-
request = JSON.parse(line);
|
|
195
|
-
} catch {
|
|
196
|
-
conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
|
|
197
|
-
`);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
handleIpcRequest(request).then((response) => conn.write(`${JSON.stringify(response)}
|
|
201
|
-
`)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: String(err) })}
|
|
202
|
-
`));
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
ipcServer.listen(socketPath);
|
|
206
236
|
async function handleIpcRequest(req) {
|
|
207
237
|
switch (req.method) {
|
|
208
238
|
case "write": {
|
|
@@ -274,36 +304,37 @@ async function startDaemon(config) {
|
|
|
274
304
|
return { ok: false, error: `Unknown method: ${req.method}` };
|
|
275
305
|
}
|
|
276
306
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
await apiClient.close(tunnelId).catch(() => {
|
|
307
|
+
}
|
|
308
|
+
function generateOffer(peer, timeoutMs) {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
let resolved = false;
|
|
311
|
+
const done = (sdp, type) => {
|
|
312
|
+
if (resolved) return;
|
|
313
|
+
resolved = true;
|
|
314
|
+
clearTimeout(timeout);
|
|
315
|
+
resolve(JSON.stringify({ sdp, type }));
|
|
316
|
+
};
|
|
317
|
+
peer.onLocalDescription((sdp, type) => {
|
|
318
|
+
done(sdp, type);
|
|
291
319
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
320
|
+
peer.onGatheringStateChange((state) => {
|
|
321
|
+
if (state === "complete" && !resolved) {
|
|
322
|
+
const desc = peer.localDescription();
|
|
323
|
+
if (desc) done(desc.sdp, desc.type);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
const timeout = setTimeout(() => {
|
|
327
|
+
if (resolved) return;
|
|
328
|
+
const desc = peer.localDescription();
|
|
329
|
+
if (desc) {
|
|
330
|
+
done(desc.sdp, desc.type);
|
|
331
|
+
} else {
|
|
332
|
+
resolved = true;
|
|
333
|
+
reject(new Error(`Timed out after ${timeoutMs}ms`));
|
|
334
|
+
}
|
|
335
|
+
}, timeoutMs);
|
|
336
|
+
peer.setLocalDescription();
|
|
337
|
+
});
|
|
307
338
|
}
|
|
308
339
|
|
|
309
340
|
export {
|
package/dist/index.js
CHANGED
|
@@ -227,19 +227,25 @@ function registerTunnelCommands(program2) {
|
|
|
227
227
|
const socketPath = getSocketPath(result.tunnelId);
|
|
228
228
|
const infoPath = tunnelInfoPath(result.tunnelId);
|
|
229
229
|
if (opts.foreground) {
|
|
230
|
-
const { startDaemon } = await import("./tunnel-daemon-
|
|
230
|
+
const { startDaemon } = await import("./tunnel-daemon-QBJSX4JM.js");
|
|
231
231
|
console.log(`Tunnel started: ${result.url}`);
|
|
232
232
|
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
233
233
|
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
234
234
|
console.log("Running in foreground. Press Ctrl+C to stop.");
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
235
|
+
try {
|
|
236
|
+
await startDaemon({
|
|
237
|
+
tunnelId: result.tunnelId,
|
|
238
|
+
apiClient,
|
|
239
|
+
socketPath,
|
|
240
|
+
infoPath
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
244
|
+
console.error(`Daemon failed: ${message}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
241
247
|
} else {
|
|
242
|
-
const daemonScript = path2.join(import.meta.dirname, "
|
|
248
|
+
const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
|
|
243
249
|
const config = getConfig();
|
|
244
250
|
const child = fork(daemonScript, [], {
|
|
245
251
|
detached: true,
|
|
@@ -254,6 +260,13 @@ function registerTunnelCommands(program2) {
|
|
|
254
260
|
}
|
|
255
261
|
});
|
|
256
262
|
child.unref();
|
|
263
|
+
const ready = await waitForDaemonReady(infoPath, child, 5e3);
|
|
264
|
+
if (!ready) {
|
|
265
|
+
console.error("Daemon failed to start. Cleaning up tunnel...");
|
|
266
|
+
await apiClient.close(result.tunnelId).catch(() => {
|
|
267
|
+
});
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
257
270
|
console.log(`Tunnel started: ${result.url}`);
|
|
258
271
|
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
259
272
|
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
@@ -425,6 +438,23 @@ async function resolveActiveTunnel() {
|
|
|
425
438
|
console.error(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
|
|
426
439
|
process.exit(1);
|
|
427
440
|
}
|
|
441
|
+
function waitForDaemonReady(infoPath, child, timeoutMs) {
|
|
442
|
+
return new Promise((resolve3) => {
|
|
443
|
+
let settled = false;
|
|
444
|
+
const done = (value) => {
|
|
445
|
+
if (settled) return;
|
|
446
|
+
settled = true;
|
|
447
|
+
clearInterval(poll);
|
|
448
|
+
clearTimeout(timeout);
|
|
449
|
+
resolve3(value);
|
|
450
|
+
};
|
|
451
|
+
child.on("exit", () => done(false));
|
|
452
|
+
const poll = setInterval(() => {
|
|
453
|
+
if (fs2.existsSync(infoPath)) done(true);
|
|
454
|
+
}, 100);
|
|
455
|
+
const timeout = setTimeout(() => done(false), timeoutMs);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
428
458
|
|
|
429
459
|
// src/lib/api.ts
|
|
430
460
|
var PubApiClient = class {
|
|
@@ -549,7 +579,7 @@ function readFile(filePath) {
|
|
|
549
579
|
basename: path3.basename(resolved)
|
|
550
580
|
};
|
|
551
581
|
}
|
|
552
|
-
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.
|
|
582
|
+
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.1");
|
|
553
583
|
program.command("configure").description("Configure the CLI with your API key").option("--api-key <key>", "Your API key (less secure: appears in shell history)").option("--api-key-stdin", "Read API key from stdin").action(async (opts) => {
|
|
554
584
|
try {
|
|
555
585
|
const apiKey = await resolveConfigureApiKey(opts);
|