pubblue 0.4.1 → 0.4.3

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,456 @@
1
+ import {
2
+ CHANNELS,
3
+ CONTROL_CHANNEL,
4
+ decodeMessage,
5
+ encodeMessage
6
+ } from "./chunk-56IKFMJ2.js";
7
+
8
+ // src/lib/tunnel-daemon.ts
9
+ import * as fs from "fs";
10
+ import * as net from "net";
11
+ import * as path from "path";
12
+ var OFFER_TIMEOUT_MS = 1e4;
13
+ var SIGNAL_POLL_WAITING_MS = 500;
14
+ var SIGNAL_POLL_CONNECTED_MS = 2e3;
15
+ var RECOVERY_DELAY_MS = 1e3;
16
+ var NOT_CONNECTED_WRITE_ERROR = "No browser connected. Ask the user to open the tunnel URL first, then retry.";
17
+ function getTunnelWriteReadinessError(isConnected) {
18
+ return isConnected ? null : NOT_CONNECTED_WRITE_ERROR;
19
+ }
20
+ async function startDaemon(config) {
21
+ const { tunnelId, apiClient, socketPath, infoPath } = config;
22
+ const ndc = await import("node-datachannel");
23
+ const buffer = { messages: [] };
24
+ const startTime = Date.now();
25
+ let stopped = false;
26
+ let connected = false;
27
+ let recovering = false;
28
+ let remoteDescriptionApplied = false;
29
+ let lastBrowserCandidateCount = 0;
30
+ let lastSentCandidateCount = 0;
31
+ const pendingRemoteCandidates = [];
32
+ const localCandidates = [];
33
+ let peer = null;
34
+ let channels = /* @__PURE__ */ new Map();
35
+ let pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
36
+ let pollingTimer = null;
37
+ let localCandidateInterval = null;
38
+ let localCandidateStopTimer = null;
39
+ let recoveryTimer = null;
40
+ function clearPollingTimer() {
41
+ if (pollingTimer) {
42
+ clearTimeout(pollingTimer);
43
+ pollingTimer = null;
44
+ }
45
+ }
46
+ function clearLocalCandidateTimers() {
47
+ if (localCandidateInterval) {
48
+ clearInterval(localCandidateInterval);
49
+ localCandidateInterval = null;
50
+ }
51
+ if (localCandidateStopTimer) {
52
+ clearTimeout(localCandidateStopTimer);
53
+ localCandidateStopTimer = null;
54
+ }
55
+ }
56
+ function clearRecoveryTimer() {
57
+ if (recoveryTimer) {
58
+ clearTimeout(recoveryTimer);
59
+ recoveryTimer = null;
60
+ }
61
+ }
62
+ function setupChannel(name, dc) {
63
+ channels.set(name, dc);
64
+ dc.onMessage((data) => {
65
+ if (typeof data === "string") {
66
+ const msg = decodeMessage(data);
67
+ if (!msg) return;
68
+ if (msg.type === "binary" && !msg.data) {
69
+ pendingInboundBinaryMeta.set(name, msg);
70
+ return;
71
+ }
72
+ buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
73
+ return;
74
+ }
75
+ const pendingMeta = pendingInboundBinaryMeta.get(name);
76
+ if (pendingMeta) pendingInboundBinaryMeta.delete(name);
77
+ const binMsg = pendingMeta ? {
78
+ id: pendingMeta.id,
79
+ type: "binary",
80
+ data: data.toString("base64"),
81
+ meta: { ...pendingMeta.meta, size: data.length }
82
+ } : {
83
+ id: `bin-${Date.now()}`,
84
+ type: "binary",
85
+ data: data.toString("base64"),
86
+ meta: { size: data.length }
87
+ };
88
+ buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
89
+ });
90
+ }
91
+ function openDataChannel(name) {
92
+ if (!peer) throw new Error("PeerConnection not initialized");
93
+ const existing = channels.get(name);
94
+ if (existing) return existing;
95
+ const dc = peer.createDataChannel(name, { ordered: true });
96
+ setupChannel(name, dc);
97
+ return dc;
98
+ }
99
+ async function waitForChannelOpen(dc, timeoutMs = 5e3) {
100
+ if (dc.isOpen()) return;
101
+ await new Promise((resolve, reject) => {
102
+ let settled = false;
103
+ const timeout = setTimeout(() => {
104
+ if (settled) return;
105
+ settled = true;
106
+ reject(new Error("DataChannel open timed out"));
107
+ }, timeoutMs);
108
+ dc.onOpen(() => {
109
+ if (settled) return;
110
+ settled = true;
111
+ clearTimeout(timeout);
112
+ resolve();
113
+ });
114
+ });
115
+ }
116
+ function resetNegotiationState() {
117
+ connected = false;
118
+ remoteDescriptionApplied = false;
119
+ lastBrowserCandidateCount = 0;
120
+ lastSentCandidateCount = 0;
121
+ pendingRemoteCandidates.length = 0;
122
+ localCandidates.length = 0;
123
+ clearLocalCandidateTimers();
124
+ }
125
+ function startLocalCandidateFlush() {
126
+ clearLocalCandidateTimers();
127
+ localCandidateInterval = setInterval(async () => {
128
+ if (localCandidates.length <= lastSentCandidateCount) return;
129
+ const newOnes = localCandidates.slice(lastSentCandidateCount);
130
+ lastSentCandidateCount = localCandidates.length;
131
+ await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
132
+ });
133
+ }, 500);
134
+ localCandidateStopTimer = setTimeout(() => {
135
+ clearLocalCandidateTimers();
136
+ }, 3e4);
137
+ }
138
+ function attachPeerHandlers(currentPeer) {
139
+ currentPeer.onLocalCandidate((candidate, mid) => {
140
+ if (stopped || currentPeer !== peer) return;
141
+ localCandidates.push(JSON.stringify({ candidate, sdpMid: mid }));
142
+ });
143
+ currentPeer.onStateChange((state) => {
144
+ if (stopped || currentPeer !== peer) return;
145
+ if (state === "connected") {
146
+ connected = true;
147
+ return;
148
+ }
149
+ if (state === "disconnected" || state === "failed") {
150
+ connected = false;
151
+ scheduleRecovery();
152
+ }
153
+ });
154
+ currentPeer.onDataChannel((dc) => {
155
+ if (stopped || currentPeer !== peer) return;
156
+ setupChannel(dc.getLabel(), dc);
157
+ });
158
+ }
159
+ function createPeer() {
160
+ const nextPeer = new ndc.PeerConnection("agent", {
161
+ iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
162
+ });
163
+ peer = nextPeer;
164
+ channels = /* @__PURE__ */ new Map();
165
+ pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
166
+ attachPeerHandlers(nextPeer);
167
+ openDataChannel(CONTROL_CHANNEL);
168
+ openDataChannel(CHANNELS.CHAT);
169
+ openDataChannel(CHANNELS.CANVAS);
170
+ }
171
+ function closeCurrentPeer() {
172
+ for (const dc of channels.values()) {
173
+ try {
174
+ dc.close();
175
+ } catch {
176
+ }
177
+ }
178
+ channels.clear();
179
+ pendingInboundBinaryMeta.clear();
180
+ if (peer) {
181
+ try {
182
+ peer.close();
183
+ } catch {
184
+ }
185
+ peer = null;
186
+ }
187
+ }
188
+ function scheduleNextPoll(delayMs) {
189
+ if (stopped) return;
190
+ clearPollingTimer();
191
+ pollingTimer = setTimeout(() => {
192
+ void runPollingLoop();
193
+ }, delayMs);
194
+ }
195
+ async function pollSignalingOnce() {
196
+ const tunnel = await apiClient.get(tunnelId);
197
+ if (tunnel.browserAnswer && !remoteDescriptionApplied) {
198
+ if (!peer) return;
199
+ try {
200
+ const answer = JSON.parse(tunnel.browserAnswer);
201
+ peer.setRemoteDescription(answer.sdp, answer.type);
202
+ remoteDescriptionApplied = true;
203
+ while (pendingRemoteCandidates.length > 0) {
204
+ const next = pendingRemoteCandidates.shift();
205
+ if (!next) break;
206
+ try {
207
+ peer.addRemoteCandidate(next.candidate, next.sdpMid);
208
+ } catch {
209
+ }
210
+ }
211
+ } catch {
212
+ }
213
+ }
214
+ if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
215
+ const newCandidates = tunnel.browserCandidates.slice(lastBrowserCandidateCount);
216
+ lastBrowserCandidateCount = tunnel.browserCandidates.length;
217
+ for (const c of newCandidates) {
218
+ try {
219
+ const parsed = JSON.parse(c);
220
+ if (typeof parsed.candidate !== "string") continue;
221
+ const sdpMid = typeof parsed.sdpMid === "string" ? parsed.sdpMid : "0";
222
+ if (!remoteDescriptionApplied) {
223
+ pendingRemoteCandidates.push({ candidate: parsed.candidate, sdpMid });
224
+ continue;
225
+ }
226
+ if (!peer) continue;
227
+ peer.addRemoteCandidate(parsed.candidate, sdpMid);
228
+ } catch {
229
+ }
230
+ }
231
+ }
232
+ }
233
+ async function runPollingLoop() {
234
+ if (stopped) return;
235
+ try {
236
+ await pollSignalingOnce();
237
+ } catch {
238
+ }
239
+ scheduleNextPoll(remoteDescriptionApplied ? SIGNAL_POLL_CONNECTED_MS : SIGNAL_POLL_WAITING_MS);
240
+ }
241
+ async function runNegotiationCycle() {
242
+ if (!peer) throw new Error("PeerConnection not initialized");
243
+ resetNegotiationState();
244
+ const offer = await generateOffer(peer, OFFER_TIMEOUT_MS);
245
+ await apiClient.signal(tunnelId, { offer });
246
+ startLocalCandidateFlush();
247
+ }
248
+ async function recoverPeer() {
249
+ if (stopped || recovering) return;
250
+ recovering = true;
251
+ try {
252
+ closeCurrentPeer();
253
+ createPeer();
254
+ await runNegotiationCycle();
255
+ } finally {
256
+ recovering = false;
257
+ }
258
+ }
259
+ function scheduleRecovery(delayMs = RECOVERY_DELAY_MS) {
260
+ if (stopped || recovering || recoveryTimer) return;
261
+ recoveryTimer = setTimeout(() => {
262
+ recoveryTimer = null;
263
+ if (stopped || connected) return;
264
+ void recoverPeer().catch(() => {
265
+ if (!stopped) scheduleRecovery(delayMs);
266
+ });
267
+ }, delayMs);
268
+ }
269
+ if (fs.existsSync(socketPath)) {
270
+ let stale = true;
271
+ try {
272
+ const raw = fs.readFileSync(infoPath, "utf-8");
273
+ const info = JSON.parse(raw);
274
+ process.kill(info.pid, 0);
275
+ stale = false;
276
+ } catch {
277
+ stale = true;
278
+ }
279
+ if (stale) {
280
+ try {
281
+ fs.unlinkSync(socketPath);
282
+ } catch {
283
+ }
284
+ } else {
285
+ throw new Error(`Daemon already running (socket: ${socketPath})`);
286
+ }
287
+ }
288
+ createPeer();
289
+ const ipcServer = net.createServer((conn) => {
290
+ let data = "";
291
+ conn.on("data", (chunk) => {
292
+ data += chunk.toString();
293
+ const newlineIdx = data.indexOf("\n");
294
+ if (newlineIdx === -1) return;
295
+ const line = data.slice(0, newlineIdx);
296
+ data = data.slice(newlineIdx + 1);
297
+ let request;
298
+ try {
299
+ request = JSON.parse(line);
300
+ } catch {
301
+ conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
302
+ `);
303
+ return;
304
+ }
305
+ handleIpcRequest(request).then((response) => conn.write(`${JSON.stringify(response)}
306
+ `)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: String(err) })}
307
+ `));
308
+ });
309
+ });
310
+ ipcServer.listen(socketPath);
311
+ const infoDir = path.dirname(infoPath);
312
+ if (!fs.existsSync(infoDir)) fs.mkdirSync(infoDir, { recursive: true });
313
+ fs.writeFileSync(
314
+ infoPath,
315
+ JSON.stringify({ pid: process.pid, tunnelId, socketPath, startedAt: startTime })
316
+ );
317
+ scheduleNextPoll(0);
318
+ try {
319
+ await runNegotiationCycle();
320
+ } catch (error) {
321
+ const message = error instanceof Error ? error.message : String(error);
322
+ await cleanup();
323
+ throw new Error(`Failed to generate WebRTC offer: ${message}`);
324
+ }
325
+ async function cleanup() {
326
+ if (stopped) return;
327
+ stopped = true;
328
+ clearPollingTimer();
329
+ clearLocalCandidateTimers();
330
+ clearRecoveryTimer();
331
+ closeCurrentPeer();
332
+ ipcServer.close();
333
+ try {
334
+ fs.unlinkSync(socketPath);
335
+ } catch {
336
+ }
337
+ try {
338
+ fs.unlinkSync(infoPath);
339
+ } catch {
340
+ }
341
+ await apiClient.close(tunnelId).catch(() => {
342
+ });
343
+ }
344
+ async function shutdown() {
345
+ await cleanup();
346
+ process.exit(0);
347
+ }
348
+ process.on("SIGTERM", () => {
349
+ void shutdown();
350
+ });
351
+ process.on("SIGINT", () => {
352
+ void shutdown();
353
+ });
354
+ async function handleIpcRequest(req) {
355
+ switch (req.method) {
356
+ case "write": {
357
+ const channel = req.params.channel || CHANNELS.CHAT;
358
+ const readinessError = getTunnelWriteReadinessError(connected);
359
+ if (readinessError) return { ok: false, error: readinessError };
360
+ const msg = req.params.msg;
361
+ const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
362
+ let targetDc = channels.get(channel);
363
+ if (!targetDc) targetDc = openDataChannel(channel);
364
+ try {
365
+ await waitForChannelOpen(targetDc);
366
+ } catch (error) {
367
+ const message = error instanceof Error ? error.message : String(error);
368
+ return { ok: false, error: `Channel "${channel}" not open: ${message}` };
369
+ }
370
+ if (msg.type === "binary" && binaryBase64) {
371
+ const payload = Buffer.from(binaryBase64, "base64");
372
+ targetDc.sendMessage(
373
+ encodeMessage({
374
+ ...msg,
375
+ meta: {
376
+ ...msg.meta || {},
377
+ size: payload.length
378
+ }
379
+ })
380
+ );
381
+ targetDc.sendMessageBinary(payload);
382
+ } else {
383
+ targetDc.sendMessage(encodeMessage(msg));
384
+ }
385
+ return { ok: true };
386
+ }
387
+ case "read": {
388
+ const channel = req.params.channel;
389
+ let msgs;
390
+ if (channel) {
391
+ msgs = buffer.messages.filter((m) => m.channel === channel);
392
+ buffer.messages = buffer.messages.filter((m) => m.channel !== channel);
393
+ } else {
394
+ msgs = [...buffer.messages];
395
+ buffer.messages = [];
396
+ }
397
+ return { ok: true, messages: msgs };
398
+ }
399
+ case "channels": {
400
+ const chList = [...channels.keys()].map((name) => ({ name, direction: "bidi" }));
401
+ return { ok: true, channels: chList };
402
+ }
403
+ case "status": {
404
+ return {
405
+ ok: true,
406
+ connected,
407
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
408
+ channels: [...channels.keys()],
409
+ bufferedMessages: buffer.messages.length
410
+ };
411
+ }
412
+ case "close": {
413
+ void shutdown();
414
+ return { ok: true };
415
+ }
416
+ default:
417
+ return { ok: false, error: `Unknown method: ${req.method}` };
418
+ }
419
+ }
420
+ }
421
+ function generateOffer(peer, timeoutMs) {
422
+ return new Promise((resolve, reject) => {
423
+ let resolved = false;
424
+ const done = (sdp, type) => {
425
+ if (resolved) return;
426
+ resolved = true;
427
+ clearTimeout(timeout);
428
+ resolve(JSON.stringify({ sdp, type }));
429
+ };
430
+ peer.onLocalDescription((sdp, type) => {
431
+ done(sdp, type);
432
+ });
433
+ peer.onGatheringStateChange((state) => {
434
+ if (state === "complete" && !resolved) {
435
+ const desc = peer.localDescription();
436
+ if (desc) done(desc.sdp, desc.type);
437
+ }
438
+ });
439
+ const timeout = setTimeout(() => {
440
+ if (resolved) return;
441
+ const desc = peer.localDescription();
442
+ if (desc) {
443
+ done(desc.sdp, desc.type);
444
+ } else {
445
+ resolved = true;
446
+ reject(new Error(`Timed out after ${timeoutMs}ms`));
447
+ }
448
+ }, timeoutMs);
449
+ peer.setLocalDescription();
450
+ });
451
+ }
452
+
453
+ export {
454
+ getTunnelWriteReadinessError,
455
+ startDaemon
456
+ };
package/dist/index.js CHANGED
@@ -215,19 +215,22 @@ function isDaemonRunning(tunnelId) {
215
215
  return false;
216
216
  }
217
217
  }
218
+ function getFollowReadDelayMs(disconnected, consecutiveFailures) {
219
+ if (!disconnected) return 1e3;
220
+ return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
221
+ }
218
222
  function registerTunnelCommands(program2) {
219
223
  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) => {
224
+ 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
225
  await ensureNodeDatachannelAvailable();
222
226
  const apiClient = createApiClient();
223
227
  const result = await apiClient.create({
224
- title: opts.title,
225
228
  expiresIn: opts.expires
226
229
  });
227
230
  const socketPath = getSocketPath(result.tunnelId);
228
231
  const infoPath = tunnelInfoPath(result.tunnelId);
229
232
  if (opts.foreground) {
230
- const { startDaemon } = await import("./tunnel-daemon-QBJSX4JM.js");
233
+ const { startDaemon } = await import("./tunnel-daemon-QPXIGRW7.js");
231
234
  console.log(`Tunnel started: ${result.url}`);
232
235
  console.log(`Tunnel ID: ${result.tunnelId}`);
233
236
  console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
@@ -335,21 +338,33 @@ function registerTunnelCommands(program2) {
335
338
  const tunnelId = tunnelIdArg || await resolveActiveTunnel();
336
339
  const socketPath = getSocketPath(tunnelId);
337
340
  if (opts.follow) {
341
+ let consecutiveFailures = 0;
342
+ let warnedDisconnected = false;
338
343
  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));
344
+ try {
345
+ const response = await ipcCall(socketPath, {
346
+ method: "read",
347
+ params: { channel: opts.channel }
348
+ });
349
+ if (warnedDisconnected) {
350
+ console.error("Daemon reconnected.");
351
+ warnedDisconnected = false;
352
+ }
353
+ consecutiveFailures = 0;
354
+ if (response.messages && response.messages.length > 0) {
355
+ for (const m of response.messages) {
356
+ console.log(JSON.stringify(m));
357
+ }
358
+ }
359
+ } catch {
360
+ consecutiveFailures += 1;
361
+ if (!warnedDisconnected) {
362
+ console.error("Daemon disconnected. Waiting for recovery...");
363
+ warnedDisconnected = true;
350
364
  }
351
365
  }
352
- await new Promise((r) => setTimeout(r, 1e3));
366
+ const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
367
+ await new Promise((r) => setTimeout(r, delayMs));
353
368
  }
354
369
  } else {
355
370
  const response = await ipcCall(socketPath, {
@@ -395,9 +410,7 @@ function registerTunnelCommands(program2) {
395
410
  const age = Math.floor((Date.now() - t.createdAt) / 6e4);
396
411
  const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
397
412
  const conn = t.hasConnection ? "connected" : "waiting";
398
- console.log(
399
- ` ${t.tunnelId} ${t.title || "(untitled)"} ${conn} ${running} ${age}m ago`
400
- );
413
+ console.log(` ${t.tunnelId} ${conn} ${running} ${age}m ago`);
401
414
  }
402
415
  });
403
416
  tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
@@ -568,6 +581,14 @@ async function resolveConfigureApiKey(opts) {
568
581
  }
569
582
  return readApiKeyFromPrompt();
570
583
  }
584
+ function resolveVisibilityFlags(opts) {
585
+ if (opts.public && opts.private) {
586
+ throw new Error(`Use only one of --public or --private for ${opts.commandName}.`);
587
+ }
588
+ if (opts.public) return true;
589
+ if (opts.private) return false;
590
+ return void 0;
591
+ }
571
592
  function readFile(filePath) {
572
593
  const resolved = path3.resolve(filePath);
573
594
  if (!fs3.existsSync(resolved)) {
@@ -579,7 +600,7 @@ function readFile(filePath) {
579
600
  basename: path3.basename(resolved)
580
601
  };
581
602
  }
582
- program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.1");
603
+ program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.3");
583
604
  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
605
  try {
585
606
  const apiKey = await resolveConfigureApiKey(opts);
@@ -591,7 +612,7 @@ program.command("configure").description("Configure the CLI with your API key").
591
612
  process.exit(1);
592
613
  }
593
614
  });
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(
615
+ 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
616
  async (fileArg, opts) => {
596
617
  const client = createClient();
597
618
  let content;
@@ -603,12 +624,17 @@ program.command("create").description("Create a new publication").argument("[fil
603
624
  } else {
604
625
  content = await readFromStdin();
605
626
  }
627
+ const resolvedVisibility = resolveVisibilityFlags({
628
+ public: opts.public,
629
+ private: opts.private,
630
+ commandName: "create"
631
+ });
606
632
  const result = await client.create({
607
633
  content,
608
634
  filename,
609
635
  title: opts.title,
610
636
  slug: opts.slug,
611
- isPublic: false,
637
+ isPublic: resolvedVisibility ?? false,
612
638
  expiresIn: opts.expires
613
639
  });
614
640
  console.log(`Created: ${result.url}`);
@@ -633,7 +659,7 @@ program.command("get").description("Get details of a publication").argument("<sl
633
659
  console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
634
660
  console.log(` Size: ${pub.content.length} bytes`);
635
661
  });
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(
662
+ 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
663
  async (slug, opts) => {
638
664
  const client = createClient();
639
665
  let content;
@@ -643,8 +669,11 @@ program.command("update").description("Update a publication's content and/or met
643
669
  content = file.content;
644
670
  filename = file.basename;
645
671
  }
646
- let isPublic;
647
- if (opts.private) isPublic = false;
672
+ const isPublic = resolveVisibilityFlags({
673
+ public: opts.public,
674
+ private: opts.private,
675
+ commandName: "update"
676
+ });
648
677
  const result = await client.update({
649
678
  slug,
650
679
  content,
@@ -0,0 +1,9 @@
1
+ import {
2
+ getTunnelWriteReadinessError,
3
+ startDaemon
4
+ } from "./chunk-YHFY3TW5.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-77HFJKLW.js";
6
+ } from "./chunk-YHFY3TW5.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.1",
3
+ "version": "0.4.3",
4
4
  "description": "CLI tool for publishing static content via pub.blue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,342 +0,0 @@
1
- import {
2
- CHANNELS,
3
- CONTROL_CHANNEL,
4
- decodeMessage,
5
- encodeMessage
6
- } from "./chunk-56IKFMJ2.js";
7
-
8
- // src/lib/tunnel-daemon.ts
9
- import * as fs from "fs";
10
- import * as net from "net";
11
- import * as path from "path";
12
- var OFFER_TIMEOUT_MS = 1e4;
13
- async function startDaemon(config) {
14
- const { tunnelId, apiClient, socketPath, infoPath } = config;
15
- const ndc = await import("node-datachannel");
16
- const buffer = { messages: [] };
17
- const startTime = Date.now();
18
- let connected = false;
19
- let pollingInterval = null;
20
- let lastBrowserCandidateCount = 0;
21
- let remoteDescriptionApplied = false;
22
- const pendingRemoteCandidates = [];
23
- const peer = new ndc.PeerConnection("agent", {
24
- iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
25
- });
26
- const channels = /* @__PURE__ */ new Map();
27
- const pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
28
- function openDataChannel(name) {
29
- const existing = channels.get(name);
30
- if (existing) return existing;
31
- const dc = peer.createDataChannel(name, { ordered: true });
32
- setupChannel(name, dc);
33
- return dc;
34
- }
35
- async function waitForChannelOpen(dc, timeoutMs = 5e3) {
36
- if (dc.isOpen()) return;
37
- await new Promise((resolve, reject) => {
38
- let settled = false;
39
- const timeout = setTimeout(() => {
40
- if (settled) return;
41
- settled = true;
42
- reject(new Error("DataChannel open timed out"));
43
- }, timeoutMs);
44
- dc.onOpen(() => {
45
- if (settled) return;
46
- settled = true;
47
- clearTimeout(timeout);
48
- resolve();
49
- });
50
- });
51
- }
52
- function setupChannel(name, dc) {
53
- channels.set(name, dc);
54
- dc.onMessage((data) => {
55
- if (typeof data === "string") {
56
- const msg = decodeMessage(data);
57
- if (msg) {
58
- if (msg.type === "binary" && !msg.data) {
59
- pendingInboundBinaryMeta.set(name, msg);
60
- return;
61
- }
62
- buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
63
- }
64
- } else {
65
- const pendingMeta = pendingInboundBinaryMeta.get(name);
66
- if (pendingMeta) pendingInboundBinaryMeta.delete(name);
67
- const binMsg = pendingMeta ? {
68
- id: pendingMeta.id,
69
- type: "binary",
70
- data: data.toString("base64"),
71
- meta: { ...pendingMeta.meta, size: data.length }
72
- } : {
73
- id: `bin-${Date.now()}`,
74
- type: "binary",
75
- data: data.toString("base64"),
76
- meta: { size: data.length }
77
- };
78
- buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
79
- }
80
- });
81
- }
82
- openDataChannel(CONTROL_CHANNEL);
83
- openDataChannel(CHANNELS.CHAT);
84
- openDataChannel(CHANNELS.CANVAS);
85
- const localCandidates = [];
86
- peer.onLocalCandidate((candidate, mid) => {
87
- localCandidates.push(JSON.stringify({ candidate, sdpMid: mid }));
88
- });
89
- peer.onStateChange((state) => {
90
- if (state === "connected") {
91
- connected = true;
92
- if (pollingInterval) {
93
- clearInterval(pollingInterval);
94
- pollingInterval = null;
95
- }
96
- } else if (state === "disconnected" || state === "failed") {
97
- connected = false;
98
- }
99
- });
100
- peer.onDataChannel((dc) => {
101
- setupChannel(dc.getLabel(), dc);
102
- });
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
- `));
141
- });
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
- }
180
- await apiClient.signal(tunnelId, { offer });
181
- setTimeout(async () => {
182
- if (localCandidates.length > 0) {
183
- await apiClient.signal(tunnelId, { candidates: localCandidates }).catch(() => {
184
- });
185
- }
186
- }, 1e3);
187
- let lastSentCandidateCount = 0;
188
- const candidateInterval = setInterval(async () => {
189
- if (localCandidates.length > lastSentCandidateCount) {
190
- const newOnes = localCandidates.slice(lastSentCandidateCount);
191
- lastSentCandidateCount = localCandidates.length;
192
- await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
193
- });
194
- }
195
- }, 500);
196
- setTimeout(() => clearInterval(candidateInterval), 3e4);
197
- pollingInterval = setInterval(async () => {
198
- try {
199
- const tunnel = await apiClient.get(tunnelId);
200
- if (tunnel.browserAnswer && !remoteDescriptionApplied) {
201
- try {
202
- const answer = JSON.parse(tunnel.browserAnswer);
203
- peer.setRemoteDescription(answer.sdp, answer.type);
204
- remoteDescriptionApplied = true;
205
- while (pendingRemoteCandidates.length > 0) {
206
- const next = pendingRemoteCandidates.shift();
207
- if (!next) break;
208
- try {
209
- peer.addRemoteCandidate(next.candidate, next.sdpMid);
210
- } catch {
211
- }
212
- }
213
- } catch {
214
- }
215
- }
216
- if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
217
- const newCandidates = tunnel.browserCandidates.slice(lastBrowserCandidateCount);
218
- lastBrowserCandidateCount = tunnel.browserCandidates.length;
219
- for (const c of newCandidates) {
220
- try {
221
- const parsed = JSON.parse(c);
222
- if (typeof parsed.candidate !== "string") continue;
223
- const sdpMid = typeof parsed.sdpMid === "string" ? parsed.sdpMid : "0";
224
- if (!remoteDescriptionApplied) {
225
- pendingRemoteCandidates.push({ candidate: parsed.candidate, sdpMid });
226
- continue;
227
- }
228
- peer.addRemoteCandidate(parsed.candidate, sdpMid);
229
- } catch {
230
- }
231
- }
232
- }
233
- } catch {
234
- }
235
- }, 500);
236
- async function handleIpcRequest(req) {
237
- switch (req.method) {
238
- case "write": {
239
- const channel = req.params.channel || CHANNELS.CHAT;
240
- const msg = req.params.msg;
241
- const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
242
- const dc = channels.get(channel);
243
- let targetDc = dc;
244
- if (!targetDc) {
245
- const newDc = openDataChannel(channel);
246
- targetDc = newDc;
247
- }
248
- try {
249
- await waitForChannelOpen(targetDc);
250
- } catch (error) {
251
- const message = error instanceof Error ? error.message : String(error);
252
- return { ok: false, error: `Channel "${channel}" not open: ${message}` };
253
- }
254
- if (msg.type === "binary" && binaryBase64) {
255
- const payload = Buffer.from(binaryBase64, "base64");
256
- targetDc.sendMessage(
257
- encodeMessage({
258
- ...msg,
259
- meta: {
260
- ...msg.meta || {},
261
- size: payload.length
262
- }
263
- })
264
- );
265
- targetDc.sendMessageBinary(payload);
266
- } else {
267
- targetDc.sendMessage(encodeMessage(msg));
268
- }
269
- return { ok: true };
270
- }
271
- case "read": {
272
- const channel = req.params.channel;
273
- let msgs;
274
- if (channel) {
275
- msgs = buffer.messages.filter((m) => m.channel === channel);
276
- buffer.messages = buffer.messages.filter((m) => m.channel !== channel);
277
- } else {
278
- msgs = [...buffer.messages];
279
- buffer.messages = [];
280
- }
281
- return { ok: true, messages: msgs };
282
- }
283
- case "channels": {
284
- const chList = [...channels.keys()].map((name) => ({
285
- name,
286
- direction: "bidi"
287
- }));
288
- return { ok: true, channels: chList };
289
- }
290
- case "status": {
291
- return {
292
- ok: true,
293
- connected,
294
- uptime: Math.floor((Date.now() - startTime) / 1e3),
295
- channels: [...channels.keys()],
296
- bufferedMessages: buffer.messages.length
297
- };
298
- }
299
- case "close": {
300
- void shutdown();
301
- return { ok: true };
302
- }
303
- default:
304
- return { ok: false, error: `Unknown method: ${req.method}` };
305
- }
306
- }
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);
319
- });
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
- });
338
- }
339
-
340
- export {
341
- startDaemon
342
- };
@@ -1,7 +0,0 @@
1
- import {
2
- startDaemon
3
- } from "./chunk-77HFJKLW.js";
4
- import "./chunk-56IKFMJ2.js";
5
- export {
6
- startDaemon
7
- };