ugly-app 0.1.203 → 0.1.204

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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.203";
1
+ export declare const CLI_VERSION = "0.1.204";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.203";
2
+ export const CLI_VERSION = "0.1.204";
3
3
  //# sourceMappingURL=version.js.map
@@ -1,2 +1,3 @@
1
1
  export { dataSourcePlugin } from './dataSourcePlugin.js';
2
+ export { tunnelPlugin } from './tunnelPlugin.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export { dataSourcePlugin } from './dataSourcePlugin.js';
2
+ export { tunnelPlugin } from './tunnelPlugin.js';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Plugin } from 'vite';
2
+ interface TunnelPluginOptions {
3
+ /** The ugly.bot server URL. Defaults to 'wss://ugly.bot'. */
4
+ serverUrl?: string;
5
+ /** Project ID. If omitted, read from package.json name field. */
6
+ projectId?: string;
7
+ }
8
+ /**
9
+ * Vite plugin that auto-connects a dev tunnel to ugly.bot on startup.
10
+ * Relays visitor HTTP requests and WebSocket connections to the local dev server.
11
+ */
12
+ export declare function tunnelPlugin(options?: TunnelPluginOptions): Plugin;
13
+ export {};
14
+ //# sourceMappingURL=tunnelPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnelPlugin.d.ts","sourceRoot":"","sources":["../../src/vite/tunnelPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAMlD,UAAU,mBAAmB;IAC3B,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CA2QtE"}
@@ -0,0 +1,221 @@
1
+ import { readFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import WebSocket from 'ws';
5
+ /**
6
+ * Vite plugin that auto-connects a dev tunnel to ugly.bot on startup.
7
+ * Relays visitor HTTP requests and WebSocket connections to the local dev server.
8
+ */
9
+ export function tunnelPlugin(options = {}) {
10
+ let server;
11
+ let ws = null;
12
+ let reconnectTimer = null;
13
+ let reconnectDelay = 1000;
14
+ let registered = false;
15
+ let subdomain = '';
16
+ function getAuthToken() {
17
+ try {
18
+ const authPath = join(homedir(), '.ugly-app', 'auth.json');
19
+ const auth = JSON.parse(readFileSync(authPath, 'utf-8'));
20
+ return auth.token ?? null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function getProjectId() {
27
+ if (options.projectId)
28
+ return options.projectId;
29
+ try {
30
+ const pkgPath = join(process.cwd(), 'package.json');
31
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
32
+ return (pkg.name ?? 'unknown')
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9-]/g, '')
35
+ .slice(0, 30);
36
+ }
37
+ catch {
38
+ return 'unknown';
39
+ }
40
+ }
41
+ function connect() {
42
+ const token = getAuthToken();
43
+ if (!token) {
44
+ server.config.logger.warn('\x1b[33m ➜ Tunnel: not connected (run `npx ugly-app login` first)\x1b[0m');
45
+ return;
46
+ }
47
+ const serverUrl = options.serverUrl ?? 'wss://ugly.bot';
48
+ try {
49
+ ws = new WebSocket(`${serverUrl}/proxy-tunnel`, {
50
+ headers: { Authorization: `Bearer ${token}` },
51
+ });
52
+ }
53
+ catch {
54
+ scheduleReconnect();
55
+ return;
56
+ }
57
+ ws.on('open', () => {
58
+ reconnectDelay = 1000; // Reset backoff on successful connection
59
+ ws.send(JSON.stringify({ type: 'register', projectId: getProjectId() }));
60
+ });
61
+ ws.on('message', (rawData) => {
62
+ try {
63
+ const msg = JSON.parse(rawData.toString());
64
+ if (msg.type === 'registered') {
65
+ registered = true;
66
+ subdomain = msg.subdomain;
67
+ server.config.logger.info(`\x1b[36m ➜ Tunnel: https://${subdomain}.ugly.bot\x1b[0m`);
68
+ return;
69
+ }
70
+ if (msg.type === 'http-request') {
71
+ handleHttpRequest(msg);
72
+ return;
73
+ }
74
+ if (msg.type === 'ws-open') {
75
+ handleWsOpen(msg);
76
+ return;
77
+ }
78
+ if (msg.type === 'ws-frame') {
79
+ handleWsFrame(msg);
80
+ return;
81
+ }
82
+ if (msg.type === 'ws-close') {
83
+ handleWsClose(msg);
84
+ return;
85
+ }
86
+ }
87
+ catch {
88
+ // Ignore malformed messages
89
+ }
90
+ });
91
+ ws.on('close', (code) => {
92
+ registered = false;
93
+ if (code === 4001) {
94
+ server.config.logger.warn('\x1b[33m ➜ Tunnel: replaced by another session\x1b[0m');
95
+ }
96
+ scheduleReconnect();
97
+ });
98
+ ws.on('error', () => {
99
+ // error event is always followed by close, reconnect handled there
100
+ });
101
+ }
102
+ function scheduleReconnect() {
103
+ if (reconnectTimer)
104
+ return;
105
+ reconnectTimer = setTimeout(() => {
106
+ reconnectTimer = null;
107
+ reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
108
+ connect();
109
+ }, reconnectDelay);
110
+ }
111
+ // --- Local WebSocket connections for WS relay ---
112
+ const localWsSockets = new Map();
113
+ function handleHttpRequest(msg) {
114
+ const port = server.config.server.port ?? 5173;
115
+ const url = `http://localhost:${port}${msg.url}`;
116
+ const headers = { ...msg.headers };
117
+ // Remove host header — let fetch set it for localhost
118
+ delete headers['host'];
119
+ const fetchOptions = {
120
+ method: msg.method,
121
+ headers,
122
+ body: msg.body ? Buffer.from(msg.body, 'base64') : undefined,
123
+ };
124
+ fetch(url, fetchOptions)
125
+ .then(async (res) => {
126
+ const responseHeaders = {};
127
+ res.headers.forEach((value, key) => {
128
+ responseHeaders[key] = value;
129
+ });
130
+ const bodyBuffer = await res.arrayBuffer();
131
+ const body = bodyBuffer.byteLength > 0
132
+ ? Buffer.from(bodyBuffer).toString('base64')
133
+ : undefined;
134
+ ws?.send(JSON.stringify({
135
+ type: 'http-response',
136
+ id: msg.id,
137
+ status: res.status,
138
+ headers: responseHeaders,
139
+ body,
140
+ }));
141
+ })
142
+ .catch(() => {
143
+ ws?.send(JSON.stringify({
144
+ type: 'http-response',
145
+ id: msg.id,
146
+ status: 502,
147
+ headers: {},
148
+ body: Buffer.from('Local server error').toString('base64'),
149
+ }));
150
+ });
151
+ }
152
+ function handleWsOpen(msg) {
153
+ const port = server.config.server.port ?? 5173;
154
+ const url = `ws://localhost:${port}${msg.url}`;
155
+ const localWs = new WebSocket(url);
156
+ localWs.on('open', () => {
157
+ localWsSockets.set(msg.id, localWs);
158
+ ws?.send(JSON.stringify({ type: 'ws-open-ok', id: msg.id }));
159
+ });
160
+ localWs.on('error', () => {
161
+ ws?.send(JSON.stringify({ type: 'ws-open-error', id: msg.id, status: 502 }));
162
+ });
163
+ localWs.on('message', (data, isBinary) => {
164
+ const encoded = isBinary
165
+ ? data.toString('base64')
166
+ : data.toString();
167
+ ws?.send(JSON.stringify({
168
+ type: 'ws-frame',
169
+ id: msg.id,
170
+ data: encoded,
171
+ binary: isBinary,
172
+ }));
173
+ });
174
+ localWs.on('close', (code) => {
175
+ localWsSockets.delete(msg.id);
176
+ ws?.send(JSON.stringify({ type: 'ws-close', id: msg.id, code }));
177
+ });
178
+ }
179
+ function handleWsFrame(msg) {
180
+ const localWs = localWsSockets.get(msg.id);
181
+ if (!localWs || localWs.readyState !== WebSocket.OPEN)
182
+ return;
183
+ if (msg.binary) {
184
+ localWs.send(Buffer.from(msg.data, 'base64'));
185
+ }
186
+ else {
187
+ localWs.send(msg.data);
188
+ }
189
+ }
190
+ function handleWsClose(msg) {
191
+ const localWs = localWsSockets.get(msg.id);
192
+ if (localWs) {
193
+ localWsSockets.delete(msg.id);
194
+ localWs.close(msg.code ?? 1000);
195
+ }
196
+ }
197
+ return {
198
+ name: 'ugly-app-tunnel',
199
+ apply: 'serve', // Only runs in dev mode
200
+ configureServer(devServer) {
201
+ server = devServer;
202
+ // Connect after server is listening
203
+ devServer.httpServer?.once('listening', () => {
204
+ connect();
205
+ });
206
+ },
207
+ buildEnd() {
208
+ if (ws && ws.readyState <= 1) {
209
+ ws.close(1000, 'Dev server stopped');
210
+ }
211
+ if (reconnectTimer) {
212
+ clearTimeout(reconnectTimer);
213
+ }
214
+ for (const localWs of localWsSockets.values()) {
215
+ localWs.close(1000);
216
+ }
217
+ localWsSockets.clear();
218
+ },
219
+ };
220
+ }
221
+ //# sourceMappingURL=tunnelPlugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnelPlugin.js","sourceRoot":"","sources":["../../src/vite/tunnelPlugin.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.203",
3
+ "version": "0.1.204",
4
4
  "type": "module",
5
5
  "main": "./dist/server/index.js",
6
6
  "exports": {
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.203";
2
+ export const CLI_VERSION = "0.1.204";
package/src/vite/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { dataSourcePlugin } from './dataSourcePlugin.js';
2
+ export { tunnelPlugin } from './tunnelPlugin.js';
@@ -0,0 +1,285 @@
1
+ import type { Plugin, ViteDevServer } from 'vite';
2
+ import { readFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ import WebSocket from 'ws';
6
+
7
+ interface TunnelPluginOptions {
8
+ /** The ugly.bot server URL. Defaults to 'wss://ugly.bot'. */
9
+ serverUrl?: string;
10
+ /** Project ID. If omitted, read from package.json name field. */
11
+ projectId?: string;
12
+ }
13
+
14
+ /**
15
+ * Vite plugin that auto-connects a dev tunnel to ugly.bot on startup.
16
+ * Relays visitor HTTP requests and WebSocket connections to the local dev server.
17
+ */
18
+ export function tunnelPlugin(options: TunnelPluginOptions = {}): Plugin {
19
+ let server: ViteDevServer;
20
+ let ws: WebSocket | null = null;
21
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
22
+ let reconnectDelay = 1000;
23
+ let registered = false;
24
+ let subdomain = '';
25
+
26
+ function getAuthToken(): string | null {
27
+ try {
28
+ const authPath = join(homedir(), '.ugly-app', 'auth.json');
29
+ const auth = JSON.parse(readFileSync(authPath, 'utf-8'));
30
+ return auth.token ?? null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function getProjectId(): string {
37
+ if (options.projectId) return options.projectId;
38
+ try {
39
+ const pkgPath = join(process.cwd(), 'package.json');
40
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
41
+ return (pkg.name ?? 'unknown')
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9-]/g, '')
44
+ .slice(0, 30);
45
+ } catch {
46
+ return 'unknown';
47
+ }
48
+ }
49
+
50
+ function connect(): void {
51
+ const token = getAuthToken();
52
+ if (!token) {
53
+ server.config.logger.warn(
54
+ '\x1b[33m ➜ Tunnel: not connected (run `npx ugly-app login` first)\x1b[0m',
55
+ );
56
+ return;
57
+ }
58
+
59
+ const serverUrl = options.serverUrl ?? 'wss://ugly.bot';
60
+
61
+ try {
62
+ ws = new WebSocket(`${serverUrl}/proxy-tunnel`, {
63
+ headers: { Authorization: `Bearer ${token}` },
64
+ });
65
+ } catch {
66
+ scheduleReconnect();
67
+ return;
68
+ }
69
+
70
+ ws.on('open', () => {
71
+ reconnectDelay = 1000; // Reset backoff on successful connection
72
+ ws!.send(JSON.stringify({ type: 'register', projectId: getProjectId() }));
73
+ });
74
+
75
+ ws.on('message', (rawData) => {
76
+ try {
77
+ const msg = JSON.parse(rawData.toString());
78
+
79
+ if (msg.type === 'registered') {
80
+ registered = true;
81
+ subdomain = msg.subdomain;
82
+ server.config.logger.info(
83
+ `\x1b[36m ➜ Tunnel: https://${subdomain}.ugly.bot\x1b[0m`,
84
+ );
85
+ return;
86
+ }
87
+
88
+ if (msg.type === 'http-request') {
89
+ handleHttpRequest(msg);
90
+ return;
91
+ }
92
+
93
+ if (msg.type === 'ws-open') {
94
+ handleWsOpen(msg);
95
+ return;
96
+ }
97
+
98
+ if (msg.type === 'ws-frame') {
99
+ handleWsFrame(msg);
100
+ return;
101
+ }
102
+
103
+ if (msg.type === 'ws-close') {
104
+ handleWsClose(msg);
105
+ return;
106
+ }
107
+ } catch {
108
+ // Ignore malformed messages
109
+ }
110
+ });
111
+
112
+ ws.on('close', (code) => {
113
+ registered = false;
114
+ if (code === 4001) {
115
+ server.config.logger.warn(
116
+ '\x1b[33m ➜ Tunnel: replaced by another session\x1b[0m',
117
+ );
118
+ }
119
+ scheduleReconnect();
120
+ });
121
+
122
+ ws.on('error', () => {
123
+ // error event is always followed by close, reconnect handled there
124
+ });
125
+ }
126
+
127
+ function scheduleReconnect(): void {
128
+ if (reconnectTimer) return;
129
+ reconnectTimer = setTimeout(() => {
130
+ reconnectTimer = null;
131
+ reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
132
+ connect();
133
+ }, reconnectDelay);
134
+ }
135
+
136
+ // --- Local WebSocket connections for WS relay ---
137
+ const localWsSockets = new Map<string, WebSocket>();
138
+
139
+ function handleHttpRequest(msg: {
140
+ id: string;
141
+ method: string;
142
+ url: string;
143
+ headers: Record<string, string>;
144
+ body?: string;
145
+ }): void {
146
+ const port = server.config.server.port ?? 5173;
147
+ const url = `http://localhost:${port}${msg.url}`;
148
+
149
+ const headers = { ...msg.headers };
150
+ // Remove host header — let fetch set it for localhost
151
+ delete headers['host'];
152
+
153
+ const fetchOptions: RequestInit = {
154
+ method: msg.method,
155
+ headers,
156
+ body: msg.body ? Buffer.from(msg.body, 'base64') : undefined,
157
+ };
158
+
159
+ fetch(url, fetchOptions)
160
+ .then(async (res) => {
161
+ const responseHeaders: Record<string, string> = {};
162
+ res.headers.forEach((value, key) => {
163
+ responseHeaders[key] = value;
164
+ });
165
+
166
+ const bodyBuffer = await res.arrayBuffer();
167
+ const body =
168
+ bodyBuffer.byteLength > 0
169
+ ? Buffer.from(bodyBuffer).toString('base64')
170
+ : undefined;
171
+
172
+ ws?.send(
173
+ JSON.stringify({
174
+ type: 'http-response',
175
+ id: msg.id,
176
+ status: res.status,
177
+ headers: responseHeaders,
178
+ body,
179
+ }),
180
+ );
181
+ })
182
+ .catch(() => {
183
+ ws?.send(
184
+ JSON.stringify({
185
+ type: 'http-response',
186
+ id: msg.id,
187
+ status: 502,
188
+ headers: {},
189
+ body: Buffer.from('Local server error').toString('base64'),
190
+ }),
191
+ );
192
+ });
193
+ }
194
+
195
+ function handleWsOpen(msg: {
196
+ id: string;
197
+ url: string;
198
+ headers: Record<string, string>;
199
+ }): void {
200
+ const port = server.config.server.port ?? 5173;
201
+ const url = `ws://localhost:${port}${msg.url}`;
202
+
203
+ const localWs = new WebSocket(url);
204
+
205
+ localWs.on('open', () => {
206
+ localWsSockets.set(msg.id, localWs);
207
+ ws?.send(JSON.stringify({ type: 'ws-open-ok', id: msg.id }));
208
+ });
209
+
210
+ localWs.on('error', () => {
211
+ ws?.send(
212
+ JSON.stringify({ type: 'ws-open-error', id: msg.id, status: 502 }),
213
+ );
214
+ });
215
+
216
+ localWs.on('message', (data, isBinary) => {
217
+ const encoded = isBinary
218
+ ? (data as Buffer).toString('base64')
219
+ : data.toString();
220
+ ws?.send(
221
+ JSON.stringify({
222
+ type: 'ws-frame',
223
+ id: msg.id,
224
+ data: encoded,
225
+ binary: isBinary,
226
+ }),
227
+ );
228
+ });
229
+
230
+ localWs.on('close', (code) => {
231
+ localWsSockets.delete(msg.id);
232
+ ws?.send(JSON.stringify({ type: 'ws-close', id: msg.id, code }));
233
+ });
234
+ }
235
+
236
+ function handleWsFrame(msg: {
237
+ id: string;
238
+ data: string;
239
+ binary: boolean;
240
+ }): void {
241
+ const localWs = localWsSockets.get(msg.id);
242
+ if (!localWs || localWs.readyState !== WebSocket.OPEN) return;
243
+
244
+ if (msg.binary) {
245
+ localWs.send(Buffer.from(msg.data, 'base64'));
246
+ } else {
247
+ localWs.send(msg.data);
248
+ }
249
+ }
250
+
251
+ function handleWsClose(msg: { id: string; code?: number }): void {
252
+ const localWs = localWsSockets.get(msg.id);
253
+ if (localWs) {
254
+ localWsSockets.delete(msg.id);
255
+ localWs.close(msg.code ?? 1000);
256
+ }
257
+ }
258
+
259
+ return {
260
+ name: 'ugly-app-tunnel',
261
+ apply: 'serve', // Only runs in dev mode
262
+
263
+ configureServer(devServer) {
264
+ server = devServer;
265
+
266
+ // Connect after server is listening
267
+ devServer.httpServer?.once('listening', () => {
268
+ connect();
269
+ });
270
+ },
271
+
272
+ buildEnd() {
273
+ if (ws && ws.readyState <= 1) {
274
+ ws.close(1000, 'Dev server stopped');
275
+ }
276
+ if (reconnectTimer) {
277
+ clearTimeout(reconnectTimer);
278
+ }
279
+ for (const localWs of localWsSockets.values()) {
280
+ localWs.close(1000);
281
+ }
282
+ localWsSockets.clear();
283
+ },
284
+ };
285
+ }