pubblue 0.4.2 → 0.4.4

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.
@@ -0,0 +1,60 @@
1
+ // src/lib/bridge-protocol.ts
2
+ var CONTROL_CHANNEL = "_control";
3
+ var CHANNELS = {
4
+ CHAT: "chat",
5
+ CANVAS: "canvas",
6
+ AUDIO: "audio",
7
+ MEDIA: "media",
8
+ FILE: "file"
9
+ };
10
+ var idCounter = 0;
11
+ function generateMessageId() {
12
+ const ts = Date.now().toString(36);
13
+ const seq = (idCounter++).toString(36);
14
+ const rand = Math.random().toString(36).slice(2, 6);
15
+ return `${ts}-${seq}-${rand}`;
16
+ }
17
+ function encodeMessage(msg) {
18
+ return JSON.stringify(msg);
19
+ }
20
+ function decodeMessage(raw) {
21
+ try {
22
+ const parsed = JSON.parse(raw);
23
+ if (parsed && typeof parsed.id === "string" && typeof parsed.type === "string") {
24
+ return parsed;
25
+ }
26
+ return null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function makeEventMessage(event, meta) {
32
+ return { id: generateMessageId(), type: "event", data: event, meta };
33
+ }
34
+ function makeAckMessage(messageId, channel) {
35
+ return makeEventMessage("ack", { messageId, channel, receivedAt: Date.now() });
36
+ }
37
+ function parseAckMessage(msg) {
38
+ if (msg.type !== "event" || msg.data !== "ack" || !msg.meta) return null;
39
+ const messageId = typeof msg.meta.messageId === "string" ? msg.meta.messageId : null;
40
+ const channel = typeof msg.meta.channel === "string" ? msg.meta.channel : null;
41
+ if (!messageId || !channel) return null;
42
+ const receivedAt = typeof msg.meta.receivedAt === "number" ? msg.meta.receivedAt : void 0;
43
+ return { messageId, channel, receivedAt };
44
+ }
45
+ function shouldAcknowledgeMessage(channel, msg) {
46
+ return channel !== CONTROL_CHANNEL && parseAckMessage(msg) === null;
47
+ }
48
+ var MAX_TUNNEL_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
49
+ var DEFAULT_TUNNEL_EXPIRY_MS = 24 * 60 * 60 * 1e3;
50
+
51
+ export {
52
+ CONTROL_CHANNEL,
53
+ CHANNELS,
54
+ generateMessageId,
55
+ encodeMessage,
56
+ decodeMessage,
57
+ makeAckMessage,
58
+ parseAckMessage,
59
+ shouldAcknowledgeMessage
60
+ };
package/dist/index.js CHANGED
@@ -3,8 +3,9 @@ import {
3
3
  TunnelApiClient
4
4
  } from "./chunk-BV423NLA.js";
5
5
  import {
6
+ CHANNELS,
6
7
  generateMessageId
7
- } from "./chunk-56IKFMJ2.js";
8
+ } from "./chunk-MW35LBNH.js";
8
9
 
9
10
  // src/index.ts
10
11
  import * as fs3 from "fs";
@@ -185,6 +186,9 @@ function tunnelInfoDir() {
185
186
  function tunnelInfoPath(tunnelId) {
186
187
  return path2.join(tunnelInfoDir(), `${tunnelId}.json`);
187
188
  }
189
+ function tunnelLogPath(tunnelId) {
190
+ return path2.join(tunnelInfoDir(), `${tunnelId}.log`);
191
+ }
188
192
  function createApiClient() {
189
193
  const config = getConfig();
190
194
  return new TunnelApiClient(config.baseUrl, config.apiKey);
@@ -215,19 +219,25 @@ function isDaemonRunning(tunnelId) {
215
219
  return false;
216
220
  }
217
221
  }
222
+ function getFollowReadDelayMs(disconnected, consecutiveFailures) {
223
+ if (!disconnected) return 1e3;
224
+ return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
225
+ }
226
+ function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
227
+ return tunnelOpt || tunnelIdArg;
228
+ }
218
229
  function registerTunnelCommands(program2) {
219
230
  const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
220
- tunnel.command("start").description("Start a new tunnel (spawns background daemon)").option("--title <title>", "Tunnel title").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
231
+ tunnel.command("start").description("Start a new tunnel (spawns background daemon)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
221
232
  await ensureNodeDatachannelAvailable();
222
233
  const apiClient = createApiClient();
223
234
  const result = await apiClient.create({
224
- title: opts.title,
225
235
  expiresIn: opts.expires
226
236
  });
227
237
  const socketPath = getSocketPath(result.tunnelId);
228
238
  const infoPath = tunnelInfoPath(result.tunnelId);
229
239
  if (opts.foreground) {
230
- const { startDaemon } = await import("./tunnel-daemon-K7Z7FUFN.js");
240
+ const { startDaemon } = await import("./tunnel-daemon-DR4A65ME.js");
231
241
  console.log(`Tunnel started: ${result.url}`);
232
242
  console.log(`Tunnel ID: ${result.tunnelId}`);
233
243
  console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
@@ -247,9 +257,11 @@ function registerTunnelCommands(program2) {
247
257
  } else {
248
258
  const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
249
259
  const config = getConfig();
260
+ const logPath = tunnelLogPath(result.tunnelId);
261
+ const daemonLogFd = fs2.openSync(logPath, "a");
250
262
  const child = fork(daemonScript, [], {
251
263
  detached: true,
252
- stdio: "ignore",
264
+ stdio: ["ignore", daemonLogFd, daemonLogFd],
253
265
  env: {
254
266
  ...process.env,
255
267
  PUBBLUE_DAEMON_TUNNEL_ID: result.tunnelId,
@@ -259,10 +271,12 @@ function registerTunnelCommands(program2) {
259
271
  PUBBLUE_DAEMON_INFO: infoPath
260
272
  }
261
273
  });
274
+ fs2.closeSync(daemonLogFd);
262
275
  child.unref();
263
276
  const ready = await waitForDaemonReady(infoPath, child, 5e3);
264
277
  if (!ready) {
265
278
  console.error("Daemon failed to start. Cleaning up tunnel...");
279
+ console.error(`Daemon log: ${logPath}`);
266
280
  await apiClient.close(result.tunnelId).catch(() => {
267
281
  });
268
282
  process.exit(1);
@@ -270,6 +284,7 @@ function registerTunnelCommands(program2) {
270
284
  console.log(`Tunnel started: ${result.url}`);
271
285
  console.log(`Tunnel ID: ${result.tunnelId}`);
272
286
  console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
287
+ console.log(`Daemon log: ${logPath}`);
273
288
  }
274
289
  });
275
290
  tunnel.command("write").description("Write data to a channel").argument("[message]", "Text message (or use --file)").option("-t, --tunnel <tunnelId>", "Tunnel ID (auto-detected if one active)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(
@@ -330,31 +345,50 @@ function registerTunnelCommands(program2) {
330
345
  }
331
346
  }
332
347
  );
333
- tunnel.command("read").description("Read buffered messages from channels").argument("[tunnelId]", "Tunnel ID (auto-detected if one active)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").action(
348
+ tunnel.command("read").description("Read buffered messages from channels").argument("[tunnelId]", "Tunnel ID (auto-detected if one active)").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(
334
349
  async (tunnelIdArg, opts) => {
335
- const tunnelId = tunnelIdArg || await resolveActiveTunnel();
350
+ const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
336
351
  const socketPath = getSocketPath(tunnelId);
352
+ const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
337
353
  if (opts.follow) {
354
+ if (!opts.channel && !opts.all) {
355
+ console.error(
356
+ "Following chat channel by default. Use `--all` to include binary/file channels."
357
+ );
358
+ }
359
+ let consecutiveFailures = 0;
360
+ let warnedDisconnected = false;
338
361
  while (true) {
339
- const response = await ipcCall(socketPath, {
340
- method: "read",
341
- params: { channel: opts.channel }
342
- }).catch(() => null);
343
- if (!response) {
344
- console.error("Daemon disconnected.");
345
- process.exit(1);
346
- }
347
- if (response.messages && response.messages.length > 0) {
348
- for (const m of response.messages) {
349
- console.log(JSON.stringify(m));
362
+ try {
363
+ const response = await ipcCall(socketPath, {
364
+ method: "read",
365
+ params: { channel: readChannel }
366
+ });
367
+ if (warnedDisconnected) {
368
+ console.error("Daemon reconnected.");
369
+ warnedDisconnected = false;
370
+ }
371
+ consecutiveFailures = 0;
372
+ if (response.messages && response.messages.length > 0) {
373
+ for (const m of response.messages) {
374
+ console.log(JSON.stringify(m));
375
+ }
376
+ }
377
+ } catch (error) {
378
+ consecutiveFailures += 1;
379
+ if (!warnedDisconnected) {
380
+ const detail = error instanceof Error ? ` ${error.message}` : "";
381
+ console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
382
+ warnedDisconnected = true;
350
383
  }
351
384
  }
352
- await new Promise((r) => setTimeout(r, 1e3));
385
+ const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
386
+ await new Promise((r) => setTimeout(r, delayMs));
353
387
  }
354
388
  } else {
355
389
  const response = await ipcCall(socketPath, {
356
390
  method: "read",
357
- params: { channel: opts.channel }
391
+ params: { channel: readChannel }
358
392
  });
359
393
  if (!response.ok) {
360
394
  console.error(`Failed: ${response.error}`);
@@ -364,8 +398,8 @@ function registerTunnelCommands(program2) {
364
398
  }
365
399
  }
366
400
  );
367
- tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").action(async (tunnelIdArg) => {
368
- const tunnelId = tunnelIdArg || await resolveActiveTunnel();
401
+ tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
402
+ const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
369
403
  const socketPath = getSocketPath(tunnelId);
370
404
  const response = await ipcCall(socketPath, { method: "channels", params: {} });
371
405
  if (response.channels) {
@@ -374,8 +408,8 @@ function registerTunnelCommands(program2) {
374
408
  }
375
409
  }
376
410
  });
377
- tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").action(async (tunnelIdArg) => {
378
- const tunnelId = tunnelIdArg || await resolveActiveTunnel();
411
+ tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
412
+ const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
379
413
  const socketPath = getSocketPath(tunnelId);
380
414
  const response = await ipcCall(socketPath, { method: "status", params: {} });
381
415
  console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
@@ -383,6 +417,13 @@ function registerTunnelCommands(program2) {
383
417
  const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
384
418
  console.log(` Channels: ${chNames.join(", ")}`);
385
419
  console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
420
+ if (typeof response.lastError === "string" && response.lastError.length > 0) {
421
+ console.log(` Last error: ${response.lastError}`);
422
+ }
423
+ const logPath = tunnelLogPath(tunnelId);
424
+ if (fs2.existsSync(logPath)) {
425
+ console.log(` Log: ${logPath}`);
426
+ }
386
427
  });
387
428
  tunnel.command("list").description("List active tunnels").action(async () => {
388
429
  const apiClient = createApiClient();
@@ -395,9 +436,7 @@ function registerTunnelCommands(program2) {
395
436
  const age = Math.floor((Date.now() - t.createdAt) / 6e4);
396
437
  const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
397
438
  const conn = t.hasConnection ? "connected" : "waiting";
398
- console.log(
399
- ` ${t.tunnelId} ${t.title || "(untitled)"} ${conn} ${running} ${age}m ago`
400
- );
439
+ console.log(` ${t.tunnelId} ${conn} ${running} ${age}m ago`);
401
440
  }
402
441
  });
403
442
  tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
@@ -568,6 +607,14 @@ async function resolveConfigureApiKey(opts) {
568
607
  }
569
608
  return readApiKeyFromPrompt();
570
609
  }
610
+ function resolveVisibilityFlags(opts) {
611
+ if (opts.public && opts.private) {
612
+ throw new Error(`Use only one of --public or --private for ${opts.commandName}.`);
613
+ }
614
+ if (opts.public) return true;
615
+ if (opts.private) return false;
616
+ return void 0;
617
+ }
571
618
  function readFile(filePath) {
572
619
  const resolved = path3.resolve(filePath);
573
620
  if (!fs3.existsSync(resolved)) {
@@ -579,7 +626,7 @@ function readFile(filePath) {
579
626
  basename: path3.basename(resolved)
580
627
  };
581
628
  }
582
- program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.2");
629
+ program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.4");
583
630
  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) => {
584
631
  try {
585
632
  const apiKey = await resolveConfigureApiKey(opts);
@@ -591,7 +638,7 @@ program.command("configure").description("Configure the CLI with your API key").
591
638
  process.exit(1);
592
639
  }
593
640
  });
594
- program.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
641
+ program.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--public", "Make the publication public").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
595
642
  async (fileArg, opts) => {
596
643
  const client = createClient();
597
644
  let content;
@@ -603,12 +650,17 @@ program.command("create").description("Create a new publication").argument("[fil
603
650
  } else {
604
651
  content = await readFromStdin();
605
652
  }
653
+ const resolvedVisibility = resolveVisibilityFlags({
654
+ public: opts.public,
655
+ private: opts.private,
656
+ commandName: "create"
657
+ });
606
658
  const result = await client.create({
607
659
  content,
608
660
  filename,
609
661
  title: opts.title,
610
662
  slug: opts.slug,
611
- isPublic: false,
663
+ isPublic: resolvedVisibility ?? false,
612
664
  expiresIn: opts.expires
613
665
  });
614
666
  console.log(`Created: ${result.url}`);
@@ -633,7 +685,7 @@ program.command("get").description("Get details of a publication").argument("<sl
633
685
  console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
634
686
  console.log(` Size: ${pub.content.length} bytes`);
635
687
  });
636
- program.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
688
+ program.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the publication public").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
637
689
  async (slug, opts) => {
638
690
  const client = createClient();
639
691
  let content;
@@ -643,8 +695,11 @@ program.command("update").description("Update a publication's content and/or met
643
695
  content = file.content;
644
696
  filename = file.basename;
645
697
  }
646
- let isPublic;
647
- if (opts.private) isPublic = false;
698
+ const isPublic = resolveVisibilityFlags({
699
+ public: opts.public,
700
+ private: opts.private,
701
+ commandName: "update"
702
+ });
648
703
  const result = await client.update({
649
704
  slug,
650
705
  content,
@@ -0,0 +1,11 @@
1
+ import {
2
+ getTunnelWriteReadinessError,
3
+ shouldRecoverForBrowserAnswerChange,
4
+ startDaemon
5
+ } from "./chunk-AIEPM67G.js";
6
+ import "./chunk-MW35LBNH.js";
7
+ export {
8
+ getTunnelWriteReadinessError,
9
+ shouldRecoverForBrowserAnswerChange,
10
+ startDaemon
11
+ };
@@ -3,8 +3,8 @@ import {
3
3
  } from "./chunk-BV423NLA.js";
4
4
  import {
5
5
  startDaemon
6
- } from "./chunk-3RFMAQOM.js";
7
- import "./chunk-56IKFMJ2.js";
6
+ } from "./chunk-AIEPM67G.js";
7
+ import "./chunk-MW35LBNH.js";
8
8
 
9
9
  // src/tunnel-daemon-entry.ts
10
10
  var tunnelId = process.env.PUBBLUE_DAEMON_TUNNEL_ID;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pubblue",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "CLI tool for publishing static content via pub.blue",
5
5
  "type": "module",
6
6
  "bin": {