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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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-
|
|
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
|
-
|
|
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.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);
|
package/package.json
CHANGED