pubblue 0.4.0 → 0.4.2

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,11 @@ 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;
13
+ var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the tunnel URL first, then retry.";
14
+ function getTunnelWriteReadinessError(isConnected) {
15
+ return isConnected ? null : NOT_CONNECTED_WRITE_ERROR;
16
+ }
12
17
  async function startDaemon(config) {
13
18
  const { tunnelId, apiClient, socketPath, infoPath } = config;
14
19
  const ndc = await import("node-datachannel");
@@ -99,12 +104,83 @@ async function startDaemon(config) {
99
104
  peer.onDataChannel((dc) => {
100
105
  setupChannel(dc.getLabel(), dc);
101
106
  });
102
- const offer = await new Promise((resolve) => {
103
- peer.onLocalDescription((sdp, type) => {
104
- resolve(JSON.stringify({ sdp, type }));
107
+ if (fs.existsSync(socketPath)) {
108
+ let stale = true;
109
+ try {
110
+ const raw = fs.readFileSync(infoPath, "utf-8");
111
+ const info = JSON.parse(raw);
112
+ process.kill(info.pid, 0);
113
+ stale = false;
114
+ } catch {
115
+ stale = true;
116
+ }
117
+ if (stale) {
118
+ try {
119
+ fs.unlinkSync(socketPath);
120
+ } catch {
121
+ }
122
+ } else {
123
+ throw new Error(`Daemon already running (socket: ${socketPath})`);
124
+ }
125
+ }
126
+ const ipcServer = net.createServer((conn) => {
127
+ let data = "";
128
+ conn.on("data", (chunk) => {
129
+ data += chunk.toString();
130
+ const newlineIdx = data.indexOf("\n");
131
+ if (newlineIdx === -1) return;
132
+ const line = data.slice(0, newlineIdx);
133
+ data = data.slice(newlineIdx + 1);
134
+ let request;
135
+ try {
136
+ request = JSON.parse(line);
137
+ } catch {
138
+ conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
139
+ `);
140
+ return;
141
+ }
142
+ handleIpcRequest(request).then((response) => conn.write(`${JSON.stringify(response)}
143
+ `)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: String(err) })}
144
+ `));
105
145
  });
106
- peer.setLocalDescription();
107
146
  });
147
+ ipcServer.listen(socketPath);
148
+ const infoDir = path.dirname(infoPath);
149
+ if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
150
+ fs.writeFileSync(
151
+ infoPath,
152
+ JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
153
+ );
154
+ async function cleanup() {
155
+ if (pollingInterval) clearInterval(pollingInterval);
156
+ for (const dc of channels.values()) dc.close();
157
+ peer.close();
158
+ ipcServer.close();
159
+ try {
160
+ fs.unlinkSync(socketPath);
161
+ } catch {
162
+ }
163
+ try {
164
+ fs.unlinkSync(infoPath);
165
+ } catch {
166
+ }
167
+ await apiClient.close(tunnelId).catch(() => {
168
+ });
169
+ }
170
+ async function shutdown() {
171
+ await cleanup();
172
+ process.exit(0);
173
+ }
174
+ process.on("SIGTERM", () => void shutdown());
175
+ process.on("SIGINT", () => void shutdown());
176
+ let offer;
177
+ try {
178
+ offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
179
+ } catch (error) {
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ await cleanup();
182
+ throw new Error(`Failed to generate WebRTC offer: ${message}`);
183
+ }
108
184
  await apiClient.signal(tunnelId, { offer });
109
185
  setTimeout(async () => {
110
186
  if (localCandidates.length > 0) {
@@ -161,52 +237,12 @@ async function startDaemon(config) {
161
237
  } catch {
162
238
  }
163
239
  }, 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
240
  async function handleIpcRequest(req) {
207
241
  switch (req.method) {
208
242
  case "write": {
209
243
  const channel = req.params.channel || CHANNELS.CHAT;
244
+ const readinessError = getTunnelWriteReadinessError(connected);
245
+ if (readinessError) return { ok: false, error: readinessError };
210
246
  const msg = req.params.msg;
211
247
  const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
212
248
  const dc = channels.get(channel);
@@ -274,38 +310,40 @@ async function startDaemon(config) {
274
310
  return { ok: false, error: `Unknown method: ${req.method}` };
275
311
  }
276
312
  }
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(() => {
313
+ }
314
+ function generateOffer(peer, timeoutMs) {
315
+ return new Promise((resolve, reject) => {
316
+ let resolved = false;
317
+ const done = (sdp, type) => {
318
+ if (resolved) return;
319
+ resolved = true;
320
+ clearTimeout(timeout);
321
+ resolve(JSON.stringify({ sdp, type }));
322
+ };
323
+ peer.onLocalDescription((sdp, type) => {
324
+ done(sdp, type);
291
325
  });
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
- );
326
+ peer.onGatheringStateChange((state) => {
327
+ if (state === "complete" && !resolved) {
328
+ const desc = peer.localDescription();
329
+ if (desc) done(desc.sdp, desc.type);
330
+ }
331
+ });
332
+ const timeout = setTimeout(() => {
333
+ if (resolved) return;
334
+ const desc = peer.localDescription();
335
+ if (desc) {
336
+ done(desc.sdp, desc.type);
337
+ } else {
338
+ resolved = true;
339
+ reject(new Error(`Timed out after ${timeoutMs}ms`));
340
+ }
341
+ }, timeoutMs);
342
+ peer.setLocalDescription();
343
+ });
307
344
  }
308
345
 
309
346
  export {
347
+ getTunnelWriteReadinessError,
310
348
  startDaemon
311
349
  };
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-K7Z7FUFN.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.2");
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);
@@ -0,0 +1,9 @@
1
+ import {
2
+ getTunnelWriteReadinessError,
3
+ startDaemon
4
+ } from "./chunk-3RFMAQOM.js";
5
+ import "./chunk-56IKFMJ2.js";
6
+ export {
7
+ getTunnelWriteReadinessError,
8
+ startDaemon
9
+ };
@@ -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-3RFMAQOM.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.2",
4
4
  "description": "CLI tool for publishing static content via pub.blue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +0,0 @@
1
- import {
2
- startDaemon
3
- } from "./chunk-OLY5PC4A.js";
4
- import "./chunk-56IKFMJ2.js";
5
- export {
6
- startDaemon
7
- };