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
- const offer = await new Promise((resolve) => {
103
- peer.onLocalDescription((sdp, type) => {
104
- resolve(JSON.stringify({ sdp, type }));
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
- async function shutdown() {
278
- if (pollingInterval) clearInterval(pollingInterval);
279
- for (const dc of channels.values()) dc.close();
280
- peer.close();
281
- ipcServer.close();
282
- try {
283
- fs.unlinkSync(socketPath);
284
- } catch {
285
- }
286
- try {
287
- fs.unlinkSync(infoPath);
288
- } catch {
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
- process.exit(0);
293
- }
294
- process.on("SIGTERM", () => void shutdown());
295
- process.on("SIGINT", () => void shutdown());
296
- const infoDir = path.dirname(infoPath);
297
- if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
298
- fs.writeFileSync(
299
- infoPath,
300
- JSON.stringify({
301
- pid: process.pid,
302
- tunnelId,
303
- socketPath,
304
- startedAt: startTime
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-CHNV373I.js");
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
- await startDaemon({
236
- tunnelId: result.tunnelId,
237
- apiClient,
238
- socketPath,
239
- infoPath
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, "..", "tunnel-daemon-entry.js");
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.0");
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);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startDaemon
3
- } from "./chunk-OLY5PC4A.js";
3
+ } from "./chunk-77HFJKLW.js";
4
4
  import "./chunk-56IKFMJ2.js";
5
5
  export {
6
6
  startDaemon
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-BV423NLA.js";
4
4
  import {
5
5
  startDaemon
6
- } from "./chunk-OLY5PC4A.js";
6
+ } from "./chunk-77HFJKLW.js";
7
7
  import "./chunk-56IKFMJ2.js";
8
8
 
9
9
  // src/tunnel-daemon-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubblue",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI tool for publishing static content via pub.blue",
5
5
  "type": "module",
6
6
  "bin": {