pubblue 0.3.0 → 0.4.0
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.
- package/dist/chunk-56IKFMJ2.js +40 -0
- package/dist/chunk-BV423NLA.js +57 -0
- package/dist/chunk-OLY5PC4A.js +311 -0
- package/dist/index.js +434 -68
- package/dist/tunnel-daemon-CHNV373I.js +7 -0
- package/dist/tunnel-daemon-entry.d.ts +2 -0
- package/dist/tunnel-daemon-entry.js +24 -0
- package/package.json +7 -5
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
var MAX_TUNNEL_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
32
|
+
var DEFAULT_TUNNEL_EXPIRY_MS = 24 * 60 * 60 * 1e3;
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
CONTROL_CHANNEL,
|
|
36
|
+
CHANNELS,
|
|
37
|
+
generateMessageId,
|
|
38
|
+
encodeMessage,
|
|
39
|
+
decodeMessage
|
|
40
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/lib/tunnel-api.ts
|
|
2
|
+
var TunnelApiClient = class {
|
|
3
|
+
constructor(baseUrl, apiKey) {
|
|
4
|
+
this.baseUrl = baseUrl;
|
|
5
|
+
this.apiKey = apiKey;
|
|
6
|
+
}
|
|
7
|
+
async request(path, options = {}) {
|
|
8
|
+
const url = new URL(path, this.baseUrl);
|
|
9
|
+
const res = await fetch(url, {
|
|
10
|
+
...options,
|
|
11
|
+
headers: {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
14
|
+
...options.headers
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = await res.json();
|
|
20
|
+
} catch {
|
|
21
|
+
data = {};
|
|
22
|
+
}
|
|
23
|
+
if (!res.ok) throw new Error(data.error || `Request failed: ${res.status}`);
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
async create(opts) {
|
|
27
|
+
return this.request("/api/v1/tunnels", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
body: JSON.stringify(opts)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async get(tunnelId) {
|
|
33
|
+
const data = await this.request(
|
|
34
|
+
`/api/v1/tunnels/${encodeURIComponent(tunnelId)}`
|
|
35
|
+
);
|
|
36
|
+
return data.tunnel;
|
|
37
|
+
}
|
|
38
|
+
async list() {
|
|
39
|
+
const data = await this.request("/api/v1/tunnels/");
|
|
40
|
+
return data.tunnels;
|
|
41
|
+
}
|
|
42
|
+
async signal(tunnelId, opts) {
|
|
43
|
+
await this.request(`/api/v1/tunnels/${encodeURIComponent(tunnelId)}/signal`, {
|
|
44
|
+
method: "PATCH",
|
|
45
|
+
body: JSON.stringify(opts)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async close(tunnelId) {
|
|
49
|
+
await this.request(`/api/v1/tunnels/${encodeURIComponent(tunnelId)}`, {
|
|
50
|
+
method: "DELETE"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
TunnelApiClient
|
|
57
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
async function startDaemon(config) {
|
|
13
|
+
const { tunnelId, apiClient, socketPath, infoPath } = config;
|
|
14
|
+
const ndc = await import("node-datachannel");
|
|
15
|
+
const buffer = { messages: [] };
|
|
16
|
+
const startTime = Date.now();
|
|
17
|
+
let connected = false;
|
|
18
|
+
let pollingInterval = null;
|
|
19
|
+
let lastBrowserCandidateCount = 0;
|
|
20
|
+
let remoteDescriptionApplied = false;
|
|
21
|
+
const pendingRemoteCandidates = [];
|
|
22
|
+
const peer = new ndc.PeerConnection("agent", {
|
|
23
|
+
iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
|
|
24
|
+
});
|
|
25
|
+
const channels = /* @__PURE__ */ new Map();
|
|
26
|
+
const pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
|
|
27
|
+
function openDataChannel(name) {
|
|
28
|
+
const existing = channels.get(name);
|
|
29
|
+
if (existing) return existing;
|
|
30
|
+
const dc = peer.createDataChannel(name, { ordered: true });
|
|
31
|
+
setupChannel(name, dc);
|
|
32
|
+
return dc;
|
|
33
|
+
}
|
|
34
|
+
async function waitForChannelOpen(dc, timeoutMs = 5e3) {
|
|
35
|
+
if (dc.isOpen()) return;
|
|
36
|
+
await new Promise((resolve, reject) => {
|
|
37
|
+
let settled = false;
|
|
38
|
+
const timeout = setTimeout(() => {
|
|
39
|
+
if (settled) return;
|
|
40
|
+
settled = true;
|
|
41
|
+
reject(new Error("DataChannel open timed out"));
|
|
42
|
+
}, timeoutMs);
|
|
43
|
+
dc.onOpen(() => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function setupChannel(name, dc) {
|
|
52
|
+
channels.set(name, dc);
|
|
53
|
+
dc.onMessage((data) => {
|
|
54
|
+
if (typeof data === "string") {
|
|
55
|
+
const msg = decodeMessage(data);
|
|
56
|
+
if (msg) {
|
|
57
|
+
if (msg.type === "binary" && !msg.data) {
|
|
58
|
+
pendingInboundBinaryMeta.set(name, msg);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
buffer.messages.push({ channel: name, msg, timestamp: Date.now() });
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const pendingMeta = pendingInboundBinaryMeta.get(name);
|
|
65
|
+
if (pendingMeta) pendingInboundBinaryMeta.delete(name);
|
|
66
|
+
const binMsg = pendingMeta ? {
|
|
67
|
+
id: pendingMeta.id,
|
|
68
|
+
type: "binary",
|
|
69
|
+
data: data.toString("base64"),
|
|
70
|
+
meta: { ...pendingMeta.meta, size: data.length }
|
|
71
|
+
} : {
|
|
72
|
+
id: `bin-${Date.now()}`,
|
|
73
|
+
type: "binary",
|
|
74
|
+
data: data.toString("base64"),
|
|
75
|
+
meta: { size: data.length }
|
|
76
|
+
};
|
|
77
|
+
buffer.messages.push({ channel: name, msg: binMsg, timestamp: Date.now() });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
openDataChannel(CONTROL_CHANNEL);
|
|
82
|
+
openDataChannel(CHANNELS.CHAT);
|
|
83
|
+
openDataChannel(CHANNELS.CANVAS);
|
|
84
|
+
const localCandidates = [];
|
|
85
|
+
peer.onLocalCandidate((candidate, mid) => {
|
|
86
|
+
localCandidates.push(JSON.stringify({ candidate, sdpMid: mid }));
|
|
87
|
+
});
|
|
88
|
+
peer.onStateChange((state) => {
|
|
89
|
+
if (state === "connected") {
|
|
90
|
+
connected = true;
|
|
91
|
+
if (pollingInterval) {
|
|
92
|
+
clearInterval(pollingInterval);
|
|
93
|
+
pollingInterval = null;
|
|
94
|
+
}
|
|
95
|
+
} else if (state === "disconnected" || state === "failed") {
|
|
96
|
+
connected = false;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
peer.onDataChannel((dc) => {
|
|
100
|
+
setupChannel(dc.getLabel(), dc);
|
|
101
|
+
});
|
|
102
|
+
const offer = await new Promise((resolve) => {
|
|
103
|
+
peer.onLocalDescription((sdp, type) => {
|
|
104
|
+
resolve(JSON.stringify({ sdp, type }));
|
|
105
|
+
});
|
|
106
|
+
peer.setLocalDescription();
|
|
107
|
+
});
|
|
108
|
+
await apiClient.signal(tunnelId, { offer });
|
|
109
|
+
setTimeout(async () => {
|
|
110
|
+
if (localCandidates.length > 0) {
|
|
111
|
+
await apiClient.signal(tunnelId, { candidates: localCandidates }).catch(() => {
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}, 1e3);
|
|
115
|
+
let lastSentCandidateCount = 0;
|
|
116
|
+
const candidateInterval = setInterval(async () => {
|
|
117
|
+
if (localCandidates.length > lastSentCandidateCount) {
|
|
118
|
+
const newOnes = localCandidates.slice(lastSentCandidateCount);
|
|
119
|
+
lastSentCandidateCount = localCandidates.length;
|
|
120
|
+
await apiClient.signal(tunnelId, { candidates: newOnes }).catch(() => {
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}, 500);
|
|
124
|
+
setTimeout(() => clearInterval(candidateInterval), 3e4);
|
|
125
|
+
pollingInterval = setInterval(async () => {
|
|
126
|
+
try {
|
|
127
|
+
const tunnel = await apiClient.get(tunnelId);
|
|
128
|
+
if (tunnel.browserAnswer && !remoteDescriptionApplied) {
|
|
129
|
+
try {
|
|
130
|
+
const answer = JSON.parse(tunnel.browserAnswer);
|
|
131
|
+
peer.setRemoteDescription(answer.sdp, answer.type);
|
|
132
|
+
remoteDescriptionApplied = true;
|
|
133
|
+
while (pendingRemoteCandidates.length > 0) {
|
|
134
|
+
const next = pendingRemoteCandidates.shift();
|
|
135
|
+
if (!next) break;
|
|
136
|
+
try {
|
|
137
|
+
peer.addRemoteCandidate(next.candidate, next.sdpMid);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (tunnel.browserCandidates.length > lastBrowserCandidateCount) {
|
|
145
|
+
const newCandidates = tunnel.browserCandidates.slice(lastBrowserCandidateCount);
|
|
146
|
+
lastBrowserCandidateCount = tunnel.browserCandidates.length;
|
|
147
|
+
for (const c of newCandidates) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(c);
|
|
150
|
+
if (typeof parsed.candidate !== "string") continue;
|
|
151
|
+
const sdpMid = typeof parsed.sdpMid === "string" ? parsed.sdpMid : "0";
|
|
152
|
+
if (!remoteDescriptionApplied) {
|
|
153
|
+
pendingRemoteCandidates.push({ candidate: parsed.candidate, sdpMid });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
peer.addRemoteCandidate(parsed.candidate, sdpMid);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}, 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
|
+
async function handleIpcRequest(req) {
|
|
207
|
+
switch (req.method) {
|
|
208
|
+
case "write": {
|
|
209
|
+
const channel = req.params.channel || CHANNELS.CHAT;
|
|
210
|
+
const msg = req.params.msg;
|
|
211
|
+
const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
|
|
212
|
+
const dc = channels.get(channel);
|
|
213
|
+
let targetDc = dc;
|
|
214
|
+
if (!targetDc) {
|
|
215
|
+
const newDc = openDataChannel(channel);
|
|
216
|
+
targetDc = newDc;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await waitForChannelOpen(targetDc);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
return { ok: false, error: `Channel "${channel}" not open: ${message}` };
|
|
223
|
+
}
|
|
224
|
+
if (msg.type === "binary" && binaryBase64) {
|
|
225
|
+
const payload = Buffer.from(binaryBase64, "base64");
|
|
226
|
+
targetDc.sendMessage(
|
|
227
|
+
encodeMessage({
|
|
228
|
+
...msg,
|
|
229
|
+
meta: {
|
|
230
|
+
...msg.meta || {},
|
|
231
|
+
size: payload.length
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
targetDc.sendMessageBinary(payload);
|
|
236
|
+
} else {
|
|
237
|
+
targetDc.sendMessage(encodeMessage(msg));
|
|
238
|
+
}
|
|
239
|
+
return { ok: true };
|
|
240
|
+
}
|
|
241
|
+
case "read": {
|
|
242
|
+
const channel = req.params.channel;
|
|
243
|
+
let msgs;
|
|
244
|
+
if (channel) {
|
|
245
|
+
msgs = buffer.messages.filter((m) => m.channel === channel);
|
|
246
|
+
buffer.messages = buffer.messages.filter((m) => m.channel !== channel);
|
|
247
|
+
} else {
|
|
248
|
+
msgs = [...buffer.messages];
|
|
249
|
+
buffer.messages = [];
|
|
250
|
+
}
|
|
251
|
+
return { ok: true, messages: msgs };
|
|
252
|
+
}
|
|
253
|
+
case "channels": {
|
|
254
|
+
const chList = [...channels.keys()].map((name) => ({
|
|
255
|
+
name,
|
|
256
|
+
direction: "bidi"
|
|
257
|
+
}));
|
|
258
|
+
return { ok: true, channels: chList };
|
|
259
|
+
}
|
|
260
|
+
case "status": {
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
connected,
|
|
264
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
265
|
+
channels: [...channels.keys()],
|
|
266
|
+
bufferedMessages: buffer.messages.length
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
case "close": {
|
|
270
|
+
void shutdown();
|
|
271
|
+
return { ok: true };
|
|
272
|
+
}
|
|
273
|
+
default:
|
|
274
|
+
return { ok: false, error: `Unknown method: ${req.method}` };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
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(() => {
|
|
291
|
+
});
|
|
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
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export {
|
|
310
|
+
startDaemon
|
|
311
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,439 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
TunnelApiClient
|
|
4
|
+
} from "./chunk-BV423NLA.js";
|
|
5
|
+
import {
|
|
6
|
+
generateMessageId
|
|
7
|
+
} from "./chunk-56IKFMJ2.js";
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
10
|
+
import * as fs3 from "fs";
|
|
11
|
+
import * as path3 from "path";
|
|
6
12
|
import { createInterface } from "readline/promises";
|
|
7
13
|
import { Command } from "commander";
|
|
8
14
|
|
|
15
|
+
// src/commands/tunnel.ts
|
|
16
|
+
import { fork } from "child_process";
|
|
17
|
+
import * as fs2 from "fs";
|
|
18
|
+
import * as path2 from "path";
|
|
19
|
+
|
|
20
|
+
// src/lib/config.ts
|
|
21
|
+
import * as fs from "fs";
|
|
22
|
+
import * as os from "os";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
|
|
25
|
+
function getConfigDir(homeDir) {
|
|
26
|
+
const home = homeDir || os.homedir();
|
|
27
|
+
return path.join(home, ".config", "pubblue");
|
|
28
|
+
}
|
|
29
|
+
function getConfigPath(homeDir) {
|
|
30
|
+
const dir = getConfigDir(homeDir);
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
32
|
+
try {
|
|
33
|
+
fs.chmodSync(dir, 448);
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
return path.join(dir, "config.json");
|
|
37
|
+
}
|
|
38
|
+
function loadConfig(homeDir) {
|
|
39
|
+
const configPath = getConfigPath(homeDir);
|
|
40
|
+
if (!fs.existsSync(configPath)) return null;
|
|
41
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
function saveConfig(config, homeDir) {
|
|
45
|
+
const configPath = getConfigPath(homeDir);
|
|
46
|
+
fs.writeFileSync(configPath, `${JSON.stringify({ apiKey: config.apiKey }, null, 2)}
|
|
47
|
+
`, {
|
|
48
|
+
mode: 384
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
fs.chmodSync(configPath, 384);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getConfig(homeDir) {
|
|
56
|
+
const envKey = process.env.PUBBLUE_API_KEY;
|
|
57
|
+
const envUrl = process.env.PUBBLUE_URL;
|
|
58
|
+
const baseUrl = envUrl || DEFAULT_BASE_URL;
|
|
59
|
+
if (envKey) {
|
|
60
|
+
return { apiKey: envKey, baseUrl };
|
|
61
|
+
}
|
|
62
|
+
const saved = loadConfig(homeDir);
|
|
63
|
+
if (!saved) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
apiKey: saved.apiKey,
|
|
70
|
+
baseUrl
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/lib/tunnel-ipc.ts
|
|
75
|
+
import * as net from "net";
|
|
76
|
+
function getSocketPath(tunnelId) {
|
|
77
|
+
return `/tmp/pubblue-${tunnelId}.sock`;
|
|
78
|
+
}
|
|
79
|
+
async function ipcCall(socketPath, request) {
|
|
80
|
+
return new Promise((resolve3, reject) => {
|
|
81
|
+
let settled = false;
|
|
82
|
+
let timeoutId = null;
|
|
83
|
+
const finish = (fn) => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
settled = true;
|
|
86
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
87
|
+
fn();
|
|
88
|
+
};
|
|
89
|
+
const client = net.createConnection(socketPath, () => {
|
|
90
|
+
client.write(`${JSON.stringify(request)}
|
|
91
|
+
`);
|
|
92
|
+
});
|
|
93
|
+
let data = "";
|
|
94
|
+
client.on("data", (chunk) => {
|
|
95
|
+
data += chunk.toString();
|
|
96
|
+
const newlineIdx = data.indexOf("\n");
|
|
97
|
+
if (newlineIdx !== -1) {
|
|
98
|
+
const line = data.slice(0, newlineIdx);
|
|
99
|
+
client.end();
|
|
100
|
+
try {
|
|
101
|
+
finish(() => resolve3(JSON.parse(line)));
|
|
102
|
+
} catch {
|
|
103
|
+
finish(() => reject(new Error("Invalid response from daemon")));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
client.on("error", (err) => {
|
|
108
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
109
|
+
finish(() => reject(new Error("Daemon not running. Is the tunnel still active?")));
|
|
110
|
+
} else {
|
|
111
|
+
finish(() => reject(err));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
client.on("end", () => {
|
|
115
|
+
if (!data.includes("\n")) {
|
|
116
|
+
finish(() => reject(new Error("Daemon closed connection unexpectedly")));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
timeoutId = setTimeout(() => {
|
|
120
|
+
client.destroy();
|
|
121
|
+
finish(() => reject(new Error("Daemon request timed out")));
|
|
122
|
+
}, 1e4);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/commands/tunnel.ts
|
|
127
|
+
var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
128
|
+
".txt",
|
|
129
|
+
".md",
|
|
130
|
+
".markdown",
|
|
131
|
+
".json",
|
|
132
|
+
".csv",
|
|
133
|
+
".xml",
|
|
134
|
+
".yaml",
|
|
135
|
+
".yml",
|
|
136
|
+
".js",
|
|
137
|
+
".mjs",
|
|
138
|
+
".cjs",
|
|
139
|
+
".ts",
|
|
140
|
+
".tsx",
|
|
141
|
+
".jsx",
|
|
142
|
+
".css",
|
|
143
|
+
".scss",
|
|
144
|
+
".sass",
|
|
145
|
+
".less",
|
|
146
|
+
".log"
|
|
147
|
+
]);
|
|
148
|
+
function getMimeType(filePath) {
|
|
149
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
150
|
+
const mimeByExt = {
|
|
151
|
+
".html": "text/html; charset=utf-8",
|
|
152
|
+
".htm": "text/html; charset=utf-8",
|
|
153
|
+
".txt": "text/plain; charset=utf-8",
|
|
154
|
+
".md": "text/markdown; charset=utf-8",
|
|
155
|
+
".markdown": "text/markdown; charset=utf-8",
|
|
156
|
+
".json": "application/json",
|
|
157
|
+
".csv": "text/csv; charset=utf-8",
|
|
158
|
+
".xml": "application/xml",
|
|
159
|
+
".yaml": "application/x-yaml",
|
|
160
|
+
".yml": "application/x-yaml",
|
|
161
|
+
".png": "image/png",
|
|
162
|
+
".jpg": "image/jpeg",
|
|
163
|
+
".jpeg": "image/jpeg",
|
|
164
|
+
".gif": "image/gif",
|
|
165
|
+
".webp": "image/webp",
|
|
166
|
+
".svg": "image/svg+xml",
|
|
167
|
+
".pdf": "application/pdf",
|
|
168
|
+
".zip": "application/zip",
|
|
169
|
+
".mp3": "audio/mpeg",
|
|
170
|
+
".wav": "audio/wav",
|
|
171
|
+
".mp4": "video/mp4"
|
|
172
|
+
};
|
|
173
|
+
return mimeByExt[ext] || "application/octet-stream";
|
|
174
|
+
}
|
|
175
|
+
function tunnelInfoDir() {
|
|
176
|
+
const dir = path2.join(
|
|
177
|
+
process.env.HOME || process.env.USERPROFILE || "/tmp",
|
|
178
|
+
".config",
|
|
179
|
+
"pubblue",
|
|
180
|
+
"tunnels"
|
|
181
|
+
);
|
|
182
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
183
|
+
return dir;
|
|
184
|
+
}
|
|
185
|
+
function tunnelInfoPath(tunnelId) {
|
|
186
|
+
return path2.join(tunnelInfoDir(), `${tunnelId}.json`);
|
|
187
|
+
}
|
|
188
|
+
function createApiClient() {
|
|
189
|
+
const config = getConfig();
|
|
190
|
+
return new TunnelApiClient(config.baseUrl, config.apiKey);
|
|
191
|
+
}
|
|
192
|
+
async function ensureNodeDatachannelAvailable() {
|
|
193
|
+
try {
|
|
194
|
+
await import("node-datachannel");
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
console.error("node-datachannel native module is not available.");
|
|
198
|
+
console.error("Run `pnpm rebuild node-datachannel` in the cli package and retry.");
|
|
199
|
+
console.error(`Details: ${message}`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function isDaemonRunning(tunnelId) {
|
|
204
|
+
const infoPath = tunnelInfoPath(tunnelId);
|
|
205
|
+
if (!fs2.existsSync(infoPath)) return false;
|
|
206
|
+
try {
|
|
207
|
+
const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
208
|
+
process.kill(info.pid, 0);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
try {
|
|
212
|
+
fs2.unlinkSync(infoPath);
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function registerTunnelCommands(program2) {
|
|
219
|
+
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) => {
|
|
221
|
+
await ensureNodeDatachannelAvailable();
|
|
222
|
+
const apiClient = createApiClient();
|
|
223
|
+
const result = await apiClient.create({
|
|
224
|
+
title: opts.title,
|
|
225
|
+
expiresIn: opts.expires
|
|
226
|
+
});
|
|
227
|
+
const socketPath = getSocketPath(result.tunnelId);
|
|
228
|
+
const infoPath = tunnelInfoPath(result.tunnelId);
|
|
229
|
+
if (opts.foreground) {
|
|
230
|
+
const { startDaemon } = await import("./tunnel-daemon-CHNV373I.js");
|
|
231
|
+
console.log(`Tunnel started: ${result.url}`);
|
|
232
|
+
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
233
|
+
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
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
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
const daemonScript = path2.join(import.meta.dirname, "..", "tunnel-daemon-entry.js");
|
|
243
|
+
const config = getConfig();
|
|
244
|
+
const child = fork(daemonScript, [], {
|
|
245
|
+
detached: true,
|
|
246
|
+
stdio: "ignore",
|
|
247
|
+
env: {
|
|
248
|
+
...process.env,
|
|
249
|
+
PUBBLUE_DAEMON_TUNNEL_ID: result.tunnelId,
|
|
250
|
+
PUBBLUE_DAEMON_BASE_URL: config.baseUrl,
|
|
251
|
+
PUBBLUE_DAEMON_API_KEY: config.apiKey,
|
|
252
|
+
PUBBLUE_DAEMON_SOCKET: socketPath,
|
|
253
|
+
PUBBLUE_DAEMON_INFO: infoPath
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
child.unref();
|
|
257
|
+
console.log(`Tunnel started: ${result.url}`);
|
|
258
|
+
console.log(`Tunnel ID: ${result.tunnelId}`);
|
|
259
|
+
console.log(`Expires: ${new Date(result.expiresAt).toISOString()}`);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
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(
|
|
263
|
+
async (messageArg, opts) => {
|
|
264
|
+
let msg;
|
|
265
|
+
let binaryBase64;
|
|
266
|
+
if (opts.file) {
|
|
267
|
+
const filePath = path2.resolve(opts.file);
|
|
268
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
269
|
+
const bytes = fs2.readFileSync(filePath);
|
|
270
|
+
const filename = path2.basename(filePath);
|
|
271
|
+
if (ext === ".html" || ext === ".htm") {
|
|
272
|
+
msg = {
|
|
273
|
+
id: generateMessageId(),
|
|
274
|
+
type: "html",
|
|
275
|
+
data: bytes.toString("utf-8"),
|
|
276
|
+
meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
|
|
277
|
+
};
|
|
278
|
+
} else if (TEXT_FILE_EXTENSIONS.has(ext)) {
|
|
279
|
+
msg = {
|
|
280
|
+
id: generateMessageId(),
|
|
281
|
+
type: "text",
|
|
282
|
+
data: bytes.toString("utf-8"),
|
|
283
|
+
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
284
|
+
};
|
|
285
|
+
} else {
|
|
286
|
+
msg = {
|
|
287
|
+
id: generateMessageId(),
|
|
288
|
+
type: "binary",
|
|
289
|
+
meta: { filename, mime: getMimeType(filePath), size: bytes.length }
|
|
290
|
+
};
|
|
291
|
+
binaryBase64 = bytes.toString("base64");
|
|
292
|
+
}
|
|
293
|
+
} else if (messageArg) {
|
|
294
|
+
msg = {
|
|
295
|
+
id: generateMessageId(),
|
|
296
|
+
type: "text",
|
|
297
|
+
data: messageArg
|
|
298
|
+
};
|
|
299
|
+
} else {
|
|
300
|
+
const chunks = [];
|
|
301
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
302
|
+
msg = {
|
|
303
|
+
id: generateMessageId(),
|
|
304
|
+
type: "text",
|
|
305
|
+
data: Buffer.concat(chunks).toString("utf-8").trim()
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const tunnelId = opts.tunnel || await resolveActiveTunnel();
|
|
309
|
+
const socketPath = getSocketPath(tunnelId);
|
|
310
|
+
const response = await ipcCall(socketPath, {
|
|
311
|
+
method: "write",
|
|
312
|
+
params: { channel: opts.channel, msg, binaryBase64 }
|
|
313
|
+
});
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
console.error(`Failed: ${response.error}`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
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(
|
|
321
|
+
async (tunnelIdArg, opts) => {
|
|
322
|
+
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
323
|
+
const socketPath = getSocketPath(tunnelId);
|
|
324
|
+
if (opts.follow) {
|
|
325
|
+
while (true) {
|
|
326
|
+
const response = await ipcCall(socketPath, {
|
|
327
|
+
method: "read",
|
|
328
|
+
params: { channel: opts.channel }
|
|
329
|
+
}).catch(() => null);
|
|
330
|
+
if (!response) {
|
|
331
|
+
console.error("Daemon disconnected.");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
if (response.messages && response.messages.length > 0) {
|
|
335
|
+
for (const m of response.messages) {
|
|
336
|
+
console.log(JSON.stringify(m));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
const response = await ipcCall(socketPath, {
|
|
343
|
+
method: "read",
|
|
344
|
+
params: { channel: opts.channel }
|
|
345
|
+
});
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
console.error(`Failed: ${response.error}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
console.log(JSON.stringify(response.messages || [], null, 2));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").action(async (tunnelIdArg) => {
|
|
355
|
+
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
356
|
+
const socketPath = getSocketPath(tunnelId);
|
|
357
|
+
const response = await ipcCall(socketPath, { method: "channels", params: {} });
|
|
358
|
+
if (response.channels) {
|
|
359
|
+
for (const ch of response.channels) {
|
|
360
|
+
console.log(` ${ch.name} [${ch.direction}]`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").action(async (tunnelIdArg) => {
|
|
365
|
+
const tunnelId = tunnelIdArg || await resolveActiveTunnel();
|
|
366
|
+
const socketPath = getSocketPath(tunnelId);
|
|
367
|
+
const response = await ipcCall(socketPath, { method: "status", params: {} });
|
|
368
|
+
console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
|
|
369
|
+
console.log(` Uptime: ${response.uptime}s`);
|
|
370
|
+
const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
|
|
371
|
+
console.log(` Channels: ${chNames.join(", ")}`);
|
|
372
|
+
console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
|
|
373
|
+
});
|
|
374
|
+
tunnel.command("list").description("List active tunnels").action(async () => {
|
|
375
|
+
const apiClient = createApiClient();
|
|
376
|
+
const tunnels = await apiClient.list();
|
|
377
|
+
if (tunnels.length === 0) {
|
|
378
|
+
console.log("No active tunnels.");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
for (const t of tunnels) {
|
|
382
|
+
const age = Math.floor((Date.now() - t.createdAt) / 6e4);
|
|
383
|
+
const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
|
|
384
|
+
const conn = t.hasConnection ? "connected" : "waiting";
|
|
385
|
+
console.log(
|
|
386
|
+
` ${t.tunnelId} ${t.title || "(untitled)"} ${conn} ${running} ${age}m ago`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
|
|
391
|
+
const socketPath = getSocketPath(tunnelId);
|
|
392
|
+
let closedByDaemon = false;
|
|
393
|
+
try {
|
|
394
|
+
const daemonResult = await ipcCall(socketPath, { method: "close", params: {} });
|
|
395
|
+
closedByDaemon = daemonResult.ok;
|
|
396
|
+
} catch {
|
|
397
|
+
closedByDaemon = false;
|
|
398
|
+
}
|
|
399
|
+
if (!closedByDaemon) {
|
|
400
|
+
const apiClient = createApiClient();
|
|
401
|
+
try {
|
|
402
|
+
await apiClient.close(tunnelId);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
405
|
+
console.error(`Failed to close tunnel ${tunnelId}: ${message}`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
console.log(`Closed: ${tunnelId}`);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
async function resolveActiveTunnel() {
|
|
413
|
+
const dir = tunnelInfoDir();
|
|
414
|
+
const files = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
415
|
+
const active = [];
|
|
416
|
+
for (const f of files) {
|
|
417
|
+
const tunnelId = f.replace(".json", "");
|
|
418
|
+
if (isDaemonRunning(tunnelId)) active.push(tunnelId);
|
|
419
|
+
}
|
|
420
|
+
if (active.length === 0) {
|
|
421
|
+
console.error("No active tunnels. Run `pubblue tunnel start` first.");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
if (active.length === 1) return active[0];
|
|
425
|
+
console.error(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
9
429
|
// src/lib/api.ts
|
|
10
430
|
var PubApiClient = class {
|
|
11
431
|
constructor(baseUrl, apiKey) {
|
|
12
432
|
this.baseUrl = baseUrl;
|
|
13
433
|
this.apiKey = apiKey;
|
|
14
434
|
}
|
|
15
|
-
async request(
|
|
16
|
-
const url = new URL(
|
|
435
|
+
async request(path4, options = {}) {
|
|
436
|
+
const url = new URL(path4, this.baseUrl);
|
|
17
437
|
const res = await fetch(url, {
|
|
18
438
|
...options,
|
|
19
439
|
headers: {
|
|
@@ -71,60 +491,6 @@ var PubApiClient = class {
|
|
|
71
491
|
}
|
|
72
492
|
};
|
|
73
493
|
|
|
74
|
-
// src/lib/config.ts
|
|
75
|
-
import * as fs from "fs";
|
|
76
|
-
import * as os from "os";
|
|
77
|
-
import * as path from "path";
|
|
78
|
-
var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
|
|
79
|
-
function getConfigDir(homeDir) {
|
|
80
|
-
const home = homeDir || os.homedir();
|
|
81
|
-
return path.join(home, ".config", "pubblue");
|
|
82
|
-
}
|
|
83
|
-
function getConfigPath(homeDir) {
|
|
84
|
-
const dir = getConfigDir(homeDir);
|
|
85
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
86
|
-
try {
|
|
87
|
-
fs.chmodSync(dir, 448);
|
|
88
|
-
} catch {
|
|
89
|
-
}
|
|
90
|
-
return path.join(dir, "config.json");
|
|
91
|
-
}
|
|
92
|
-
function loadConfig(homeDir) {
|
|
93
|
-
const configPath = getConfigPath(homeDir);
|
|
94
|
-
if (!fs.existsSync(configPath)) return null;
|
|
95
|
-
const raw = fs.readFileSync(configPath, "utf-8");
|
|
96
|
-
return JSON.parse(raw);
|
|
97
|
-
}
|
|
98
|
-
function saveConfig(config, homeDir) {
|
|
99
|
-
const configPath = getConfigPath(homeDir);
|
|
100
|
-
fs.writeFileSync(configPath, `${JSON.stringify({ apiKey: config.apiKey }, null, 2)}
|
|
101
|
-
`, {
|
|
102
|
-
mode: 384
|
|
103
|
-
});
|
|
104
|
-
try {
|
|
105
|
-
fs.chmodSync(configPath, 384);
|
|
106
|
-
} catch {
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function getConfig(homeDir) {
|
|
110
|
-
const envKey = process.env.PUBBLUE_API_KEY;
|
|
111
|
-
const envUrl = process.env.PUBBLUE_URL;
|
|
112
|
-
const baseUrl = envUrl || DEFAULT_BASE_URL;
|
|
113
|
-
if (envKey) {
|
|
114
|
-
return { apiKey: envKey, baseUrl };
|
|
115
|
-
}
|
|
116
|
-
const saved = loadConfig(homeDir);
|
|
117
|
-
if (!saved) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
"Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
apiKey: saved.apiKey,
|
|
124
|
-
baseUrl
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
494
|
// src/index.ts
|
|
129
495
|
var program = new Command();
|
|
130
496
|
function createClient() {
|
|
@@ -173,17 +539,17 @@ async function resolveConfigureApiKey(opts) {
|
|
|
173
539
|
return readApiKeyFromPrompt();
|
|
174
540
|
}
|
|
175
541
|
function readFile(filePath) {
|
|
176
|
-
const resolved =
|
|
177
|
-
if (!
|
|
542
|
+
const resolved = path3.resolve(filePath);
|
|
543
|
+
if (!fs3.existsSync(resolved)) {
|
|
178
544
|
console.error(`File not found: ${resolved}`);
|
|
179
545
|
process.exit(1);
|
|
180
546
|
}
|
|
181
547
|
return {
|
|
182
|
-
content:
|
|
183
|
-
basename:
|
|
548
|
+
content: fs3.readFileSync(resolved, "utf-8"),
|
|
549
|
+
basename: path3.basename(resolved)
|
|
184
550
|
};
|
|
185
551
|
}
|
|
186
|
-
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.
|
|
552
|
+
program.name("pubblue").description("Publish static content and get shareable URLs").version("0.4.0");
|
|
187
553
|
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) => {
|
|
188
554
|
try {
|
|
189
555
|
const apiKey = await resolveConfigureApiKey(opts);
|
|
@@ -195,7 +561,7 @@ program.command("configure").description("Configure the CLI with your API key").
|
|
|
195
561
|
process.exit(1);
|
|
196
562
|
}
|
|
197
563
|
});
|
|
198
|
-
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("--
|
|
564
|
+
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(
|
|
199
565
|
async (fileArg, opts) => {
|
|
200
566
|
const client = createClient();
|
|
201
567
|
let content;
|
|
@@ -212,7 +578,7 @@ program.command("create").description("Create a new publication").argument("[fil
|
|
|
212
578
|
filename,
|
|
213
579
|
title: opts.title,
|
|
214
580
|
slug: opts.slug,
|
|
215
|
-
isPublic:
|
|
581
|
+
isPublic: false,
|
|
216
582
|
expiresIn: opts.expires
|
|
217
583
|
});
|
|
218
584
|
console.log(`Created: ${result.url}`);
|
|
@@ -237,7 +603,7 @@ program.command("get").description("Get details of a publication").argument("<sl
|
|
|
237
603
|
console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
|
|
238
604
|
console.log(` Size: ${pub.content.length} bytes`);
|
|
239
605
|
});
|
|
240
|
-
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("--
|
|
606
|
+
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(
|
|
241
607
|
async (slug, opts) => {
|
|
242
608
|
const client = createClient();
|
|
243
609
|
let content;
|
|
@@ -248,8 +614,7 @@ program.command("update").description("Update a publication's content and/or met
|
|
|
248
614
|
filename = file.basename;
|
|
249
615
|
}
|
|
250
616
|
let isPublic;
|
|
251
|
-
if (opts.
|
|
252
|
-
else if (opts.private) isPublic = false;
|
|
617
|
+
if (opts.private) isPublic = false;
|
|
253
618
|
const result = await client.update({
|
|
254
619
|
slug,
|
|
255
620
|
content,
|
|
@@ -283,4 +648,5 @@ program.command("delete").description("Delete a publication").argument("<slug>",
|
|
|
283
648
|
await client.remove(slug);
|
|
284
649
|
console.log(`Deleted: ${slug}`);
|
|
285
650
|
});
|
|
651
|
+
registerTunnelCommands(program);
|
|
286
652
|
program.parse();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TunnelApiClient
|
|
3
|
+
} from "./chunk-BV423NLA.js";
|
|
4
|
+
import {
|
|
5
|
+
startDaemon
|
|
6
|
+
} from "./chunk-OLY5PC4A.js";
|
|
7
|
+
import "./chunk-56IKFMJ2.js";
|
|
8
|
+
|
|
9
|
+
// src/tunnel-daemon-entry.ts
|
|
10
|
+
var tunnelId = process.env.PUBBLUE_DAEMON_TUNNEL_ID;
|
|
11
|
+
var baseUrl = process.env.PUBBLUE_DAEMON_BASE_URL;
|
|
12
|
+
var apiKey = process.env.PUBBLUE_DAEMON_API_KEY;
|
|
13
|
+
var socketPath = process.env.PUBBLUE_DAEMON_SOCKET;
|
|
14
|
+
var infoPath = process.env.PUBBLUE_DAEMON_INFO;
|
|
15
|
+
if (!tunnelId || !baseUrl || !apiKey || !socketPath || !infoPath) {
|
|
16
|
+
console.error("Missing required env vars for daemon.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
var apiClient = new TunnelApiClient(baseUrl, apiKey);
|
|
20
|
+
void startDaemon({ tunnelId, apiClient, socketPath, infoPath }).catch((error) => {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
console.error(`Tunnel daemon failed to start: ${message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
package/package.json
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pubblue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI tool for publishing static content via pub.blue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pubblue": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
11
|
-
"dev": "tsup src/index.ts --format esm --watch",
|
|
10
|
+
"build": "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --dts --clean",
|
|
11
|
+
"dev": "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --watch",
|
|
12
12
|
"test": "vitest run",
|
|
13
13
|
"test:watch": "vitest",
|
|
14
14
|
"lint": "tsc --noEmit"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"commander": "^13.0.0"
|
|
17
|
+
"commander": "^13.0.0",
|
|
18
|
+
"node-datachannel": "^0.32.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@types/node": "22.10.2",
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
},
|
|
36
37
|
"pnpm": {
|
|
37
38
|
"onlyBuiltDependencies": [
|
|
38
|
-
"esbuild"
|
|
39
|
+
"esbuild",
|
|
40
|
+
"node-datachannel"
|
|
39
41
|
]
|
|
40
42
|
}
|
|
41
43
|
}
|