ugly-app 0.1.519 → 0.1.521

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.
Files changed (110) hide show
  1. package/.pgdata/base/16384/pg_internal.init +0 -0
  2. package/.pgdata/global/pg_control +0 -0
  3. package/.pgdata/global/pg_internal.init +0 -0
  4. package/.pgdata/pg_wal/000000010000000000000004 +0 -0
  5. package/.pgdata/postmaster.pid +2 -2
  6. package/dist/cli/dev.d.ts.map +1 -1
  7. package/dist/cli/dev.js +0 -3
  8. package/dist/cli/dev.js.map +1 -1
  9. package/dist/cli/doctor.d.ts.map +1 -1
  10. package/dist/cli/doctor.js +31 -66
  11. package/dist/cli/doctor.js.map +1 -1
  12. package/dist/cli/index.js +8 -66
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/migrate.d.ts.map +1 -1
  15. package/dist/cli/migrate.js +0 -4
  16. package/dist/cli/migrate.js.map +1 -1
  17. package/dist/cli/schemaGen.d.ts.map +1 -1
  18. package/dist/cli/schemaGen.js +0 -4
  19. package/dist/cli/schemaGen.js.map +1 -1
  20. package/dist/cli/schemaStatus.d.ts.map +1 -1
  21. package/dist/cli/schemaStatus.js +0 -4
  22. package/dist/cli/schemaStatus.js.map +1 -1
  23. package/dist/cli/serverLogQueryApi.d.ts +5 -4
  24. package/dist/cli/serverLogQueryApi.d.ts.map +1 -1
  25. package/dist/cli/serverLogQueryApi.js +55 -115
  26. package/dist/cli/serverLogQueryApi.js.map +1 -1
  27. package/dist/cli/version.d.ts +1 -1
  28. package/dist/cli/version.js +1 -1
  29. package/dist/client/auth/MagicLinkCallback.d.ts.map +1 -1
  30. package/dist/client/auth/MagicLinkCallback.js +8 -2
  31. package/dist/client/auth/MagicLinkCallback.js.map +1 -1
  32. package/dist/client/auth/MagicLinkForm.d.ts.map +1 -1
  33. package/dist/client/auth/MagicLinkForm.js +6 -1
  34. package/dist/client/auth/MagicLinkForm.js.map +1 -1
  35. package/dist/server/App.d.ts.map +1 -1
  36. package/dist/server/App.js +1 -2
  37. package/dist/server/App.js.map +1 -1
  38. package/dist/server/Logging.d.ts +5 -8
  39. package/dist/server/Logging.d.ts.map +1 -1
  40. package/dist/server/Logging.js +21 -97
  41. package/dist/server/Logging.js.map +1 -1
  42. package/dist/server/auth/magicLinkProvider.d.ts.map +1 -1
  43. package/dist/server/auth/magicLinkProvider.js +55 -6
  44. package/dist/server/auth/magicLinkProvider.js.map +1 -1
  45. package/dist/server/auth/workersMagicLink.d.ts.map +1 -1
  46. package/dist/server/auth/workersMagicLink.js +78 -6
  47. package/dist/server/auth/workersMagicLink.js.map +1 -1
  48. package/dist/server/bootstrap.d.ts +5 -21
  49. package/dist/server/bootstrap.d.ts.map +1 -1
  50. package/dist/server/bootstrap.js +5 -27
  51. package/dist/server/bootstrap.js.map +1 -1
  52. package/dist/shared/DB.d.ts +0 -9
  53. package/dist/shared/DB.d.ts.map +1 -1
  54. package/dist/shared/DB.js +46 -10
  55. package/dist/shared/DB.js.map +1 -1
  56. package/dist/vite/index.d.ts +0 -1
  57. package/dist/vite/index.d.ts.map +1 -1
  58. package/dist/vite/index.js +0 -1
  59. package/dist/vite/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/cli/dev.ts +0 -4
  62. package/src/cli/doctor.ts +31 -63
  63. package/src/cli/index.ts +9 -81
  64. package/src/cli/migrate.ts +0 -4
  65. package/src/cli/schemaGen.ts +0 -4
  66. package/src/cli/schemaStatus.ts +0 -4
  67. package/src/cli/serverLogQueryApi.ts +53 -136
  68. package/src/cli/version.ts +1 -1
  69. package/src/client/auth/MagicLinkCallback.tsx +10 -2
  70. package/src/client/auth/MagicLinkForm.tsx +7 -1
  71. package/src/server/App.ts +1 -2
  72. package/src/server/Logging.ts +21 -97
  73. package/src/server/auth/magicLinkProvider.ts +61 -6
  74. package/src/server/auth/workersMagicLink.ts +75 -6
  75. package/src/server/bootstrap.ts +5 -29
  76. package/src/shared/DB.ts +46 -17
  77. package/src/vite/index.ts +0 -1
  78. package/.pgdata/base/1/pg_internal.init +0 -0
  79. package/.pgdata/base/17702/pg_internal.init +0 -0
  80. package/.pgdata/base/5/pg_internal.init +0 -0
  81. package/dist/cli/devTunnel.d.ts +0 -37
  82. package/dist/cli/devTunnel.d.ts.map +0 -1
  83. package/dist/cli/devTunnel.js +0 -470
  84. package/dist/cli/devTunnel.js.map +0 -1
  85. package/dist/cli/ensureInfra.d.ts +0 -9
  86. package/dist/cli/ensureInfra.d.ts.map +0 -1
  87. package/dist/cli/ensureInfra.js +0 -86
  88. package/dist/cli/ensureInfra.js.map +0 -1
  89. package/dist/cli/sidecar.d.ts +0 -21
  90. package/dist/cli/sidecar.d.ts.map +0 -1
  91. package/dist/cli/sidecar.js +0 -294
  92. package/dist/cli/sidecar.js.map +0 -1
  93. package/dist/cli/sidecarHealth.d.ts +0 -13
  94. package/dist/cli/sidecarHealth.d.ts.map +0 -1
  95. package/dist/cli/sidecarHealth.js +0 -42
  96. package/dist/cli/sidecarHealth.js.map +0 -1
  97. package/dist/cli/sidecarProtocol.d.ts +0 -41
  98. package/dist/cli/sidecarProtocol.d.ts.map +0 -1
  99. package/dist/cli/sidecarProtocol.js +0 -61
  100. package/dist/cli/sidecarProtocol.js.map +0 -1
  101. package/dist/vite/tunnelPlugin.d.ts +0 -10
  102. package/dist/vite/tunnelPlugin.d.ts.map +0 -1
  103. package/dist/vite/tunnelPlugin.js +0 -30
  104. package/dist/vite/tunnelPlugin.js.map +0 -1
  105. package/src/cli/devTunnel.ts +0 -589
  106. package/src/cli/ensureInfra.ts +0 -90
  107. package/src/cli/sidecar.ts +0 -350
  108. package/src/cli/sidecarHealth.ts +0 -58
  109. package/src/cli/sidecarProtocol.ts +0 -68
  110. package/src/vite/tunnelPlugin.ts +0 -32
@@ -1,589 +0,0 @@
1
- /**
2
- * devTunnel.ts
3
- *
4
- * Combined dev tunnel client: multiplexes infrastructure TCP ports (Postgres,
5
- * NATS, S3, Qdrant) over binary frames AND handles proxy tunnel HTTP/WS relay
6
- * for public dev URLs — all on a single WebSocket to /dev-tunnel.
7
- *
8
- * Binary frames = infra tunnel (5-byte header + TCP payload)
9
- * Text frames = proxy tunnel (JSON messages for HTTP/WS relay)
10
- */
11
-
12
- import net from 'net';
13
- import WebSocket from 'ws';
14
- import { getUglyBotWsUrl } from '../shared/uglyBotUrl.js';
15
- import type { SidecarPorts } from './sidecarHealth.js';
16
- import {
17
- SERVICE,
18
- SERVICE_NAME,
19
- FLAGS,
20
- type ServiceId,
21
- encodeFrame,
22
- decodeFrame,
23
- } from './sidecarProtocol.js';
24
-
25
- export interface StartDevTunnelOpts {
26
- projectId: string;
27
- token: string;
28
- /** Defaults to UGLY_BOT_WS_URL */
29
- serverUrl?: string;
30
- /** When set, enables proxy tunnel relay to this local port */
31
- vitePort?: number;
32
- /**
33
- * Optional studio session id (`ws_x:sess_y`). When set, the server
34
- * scopes the proxy subdomain to this session so two concurrent dev
35
- * servers on the same project don't collide on the same registration.
36
- */
37
- sessionId?: string;
38
- }
39
-
40
- export interface DevTunnelResult extends SidecarPorts {
41
- /** Public subdomain if proxy tunnel registered (e.g. 'proxy-user-myapp') */
42
- subdomain?: string;
43
- }
44
-
45
- const BACKPRESSURE_THRESHOLD = 1024 * 1024; // 1 MB
46
- // Only NATS needs a tunnel proxy — PG/R2/Qdrant go through the centralized data proxy
47
- const SERVICE_LIST: ServiceId[] = [
48
- SERVICE.NATS,
49
- ];
50
-
51
- // Headers to strip from localhost responses (avoid double-decompression)
52
- const STRIP_RESPONSE_HEADERS = new Set([
53
- 'content-encoding',
54
- 'content-length',
55
- 'transfer-encoding',
56
- ]);
57
-
58
- interface DevTunnelState {
59
- ws: WebSocket | null;
60
- servers: Map<ServiceId, net.Server>;
61
- streams: Map<number, net.Socket>;
62
- nextStreamId: number;
63
- reconnectTimer: ReturnType<typeof setTimeout> | null;
64
- reconnectDelay: number;
65
- connected: boolean;
66
- stopped: boolean;
67
- subdomain: string | null;
68
- localWsSockets: Map<string, WebSocket>;
69
- }
70
-
71
- let state: DevTunnelState | null = null;
72
-
73
- /**
74
- * Start the dev tunnel: opens WSS connection and binds ephemeral TCP ports.
75
- * If vitePort is provided, also registers a proxy tunnel for public URL access.
76
- */
77
- export async function startDevTunnel(opts: StartDevTunnelOpts): Promise<DevTunnelResult> {
78
- if (state && !state.stopped) {
79
- throw new Error('Dev tunnel already running');
80
- }
81
-
82
- state = {
83
- ws: null,
84
- servers: new Map(),
85
- streams: new Map(),
86
- nextStreamId: 1,
87
- reconnectTimer: null,
88
- reconnectDelay: 1000,
89
- connected: false,
90
- stopped: false,
91
- subdomain: null,
92
- localWsSockets: new Map(),
93
- };
94
-
95
- // Create TCP servers for each service on port 0 (ephemeral)
96
- const portResults = new Map<ServiceId, number>();
97
-
98
- for (const serviceId of SERVICE_LIST) {
99
- const server = net.createServer((socket) => {
100
- handleTcpConnection(serviceId, socket);
101
- });
102
-
103
- await new Promise<void>((resolve, reject) => {
104
- server.listen(0, '127.0.0.1', () => resolve());
105
- server.on('error', reject);
106
- });
107
-
108
- const addr = server.address() as net.AddressInfo;
109
- portResults.set(serviceId, addr.port);
110
- state.servers.set(serviceId, server);
111
- }
112
-
113
- // Connect WebSocket
114
- await connectWs(opts);
115
-
116
- const ports: DevTunnelResult = {
117
- postgres: 0, // No longer proxied — uses data proxy
118
- nats: portResults.get(SERVICE.NATS)!,
119
- s3: 0, // No longer proxied — uses data proxy
120
- qdrantHttp: 0, // No longer proxied — uses data proxy
121
- qdrantGrpc: 0, // No longer proxied — uses data proxy
122
- subdomain: state.subdomain ?? undefined,
123
- };
124
-
125
- console.log('[ugly-app] Dev tunnel listening:');
126
- for (const [serviceId, port] of portResults) {
127
- console.log(` ${SERVICE_NAME[serviceId]}: localhost:${port}`);
128
- }
129
-
130
- if (ports.subdomain) {
131
- console.log(`\x1b[36m ➜ Tunnel: https://${ports.subdomain}.ugly.bot\x1b[0m`);
132
- }
133
-
134
- return ports;
135
- }
136
-
137
- function connectWs(opts: StartDevTunnelOpts): Promise<void> {
138
- if (!state || state.stopped) return Promise.resolve();
139
-
140
- const serverUrl = opts.serverUrl ?? getUglyBotWsUrl();
141
-
142
- return new Promise<void>((resolve, reject) => {
143
- let resolved = false;
144
-
145
- try {
146
- const ws = new WebSocket(`${serverUrl}/dev-tunnel`, {
147
- headers: {
148
- 'Authorization': `Bearer ${opts.token}`,
149
- 'X-Project-Id': opts.projectId,
150
- },
151
- });
152
-
153
- ws.binaryType = 'nodebuffer';
154
-
155
- ws.on('open', () => {
156
- if (!state) return;
157
- state.ws = ws;
158
- // Don't reset reconnectDelay here — reset on 'ready' only,
159
- // so rapid disconnect/reconnect cycles see growing delays.
160
- });
161
-
162
- ws.on('message', (data: Buffer | string, isBinary: boolean) => {
163
- if (!state) return;
164
-
165
- if (!isBinary) {
166
- // Text frame — JSON control/proxy messages
167
- try {
168
- const msg = JSON.parse(typeof data === 'string' ? data : data.toString('utf8')) as Record<string, unknown>;
169
- handleTextMessage(msg, opts, ws, () => {
170
- if (!resolved) {
171
- resolved = true;
172
- resolve();
173
- }
174
- });
175
- } catch {
176
- // Not valid JSON — ignore
177
- }
178
- return;
179
- }
180
-
181
- // Binary frame — infra tunnel
182
- try {
183
- const { streamId, flags, payload } = decodeFrame(Buffer.from(data as Buffer));
184
-
185
- if (flags & FLAGS.RST) {
186
- const socket = state.streams.get(streamId);
187
- if (socket) {
188
- state.streams.delete(streamId);
189
- socket.destroy();
190
- }
191
- return;
192
- }
193
-
194
- if (payload.length > 0) {
195
- const socket = state.streams.get(streamId);
196
- if (socket && !socket.destroyed) {
197
- socket.write(payload);
198
- }
199
- }
200
-
201
- if (flags & FLAGS.FIN) {
202
- const socket = state.streams.get(streamId);
203
- if (socket) {
204
- state.streams.delete(streamId);
205
- socket.end();
206
- }
207
- return;
208
- }
209
- } catch {
210
- // Ignore malformed frames
211
- }
212
- });
213
-
214
- ws.on('close', (code: number, reason: Buffer) => {
215
- if (!state || state.stopped) return;
216
- state.connected = false;
217
- state.ws = null;
218
-
219
- // RST all active streams
220
- for (const [id, socket] of state.streams) {
221
- socket.destroy();
222
- state.streams.delete(id);
223
- }
224
-
225
- // Close all local WS relays
226
- for (const localWs of state.localWsSockets.values()) {
227
- localWs.close(1001);
228
- }
229
- state.localWsSockets.clear();
230
-
231
- if (!resolved) {
232
- resolved = true;
233
- cleanupServers();
234
- const reasonStr = reason?.toString() || '';
235
- if (code === 4001) {
236
- reject(new Error(`[ugly-app] Authentication failed (4001). Run: npx ugly-app login${process.env['UGLY_BOT_LOCAL'] === '1' ? ' --local' : ''}`));
237
- } else if (code === 4500) {
238
- reject(new Error(`[ugly-app] Failed to provision resources (4500): ${reasonStr}`));
239
- } else {
240
- reject(new Error(`[ugly-app] Cannot reach ugly.bot (close code ${code}): ${reasonStr}`));
241
- }
242
- return;
243
- }
244
-
245
- // 4004 = "Replaced by new tunnel" — our own reconnect created a new connection,
246
- // so the server killed this old one. Don't reconnect again (the new one IS our reconnect).
247
- if (code === 4004) {
248
- console.log('[ugly-app] Old tunnel closed (replaced by new connection)');
249
- return;
250
- }
251
-
252
- console.warn('[ugly-app] Tunnel disconnected, reconnecting...');
253
- scheduleReconnect(opts);
254
- });
255
-
256
- ws.on('error', () => {
257
- // error is always followed by close — handled there
258
- });
259
- } catch (err) {
260
- if (!resolved) {
261
- resolved = true;
262
- cleanupServers();
263
- reject(err instanceof Error ? err : new Error(String(err)));
264
- }
265
- }
266
- });
267
- }
268
-
269
- function handleTextMessage(
270
- msg: Record<string, unknown>,
271
- opts: StartDevTunnelOpts,
272
- ws: WebSocket,
273
- onReady: () => void,
274
- ): void {
275
- if (!state) return;
276
-
277
- switch (msg.type) {
278
- case 'ready':
279
- state.connected = true;
280
- state.reconnectDelay = 1000; // Reset backoff only after successful ready handshake
281
- // Register proxy tunnel if vitePort is set
282
- if (opts.vitePort) {
283
- ws.send(
284
- JSON.stringify({
285
- type: 'register',
286
- projectId: opts.projectId,
287
- sessionId: opts.sessionId,
288
- }),
289
- );
290
- }
291
- onReady();
292
- break;
293
-
294
- case 'provisioning':
295
- console.log('[ugly-app] Cloud DB being provisioned — first-time setup...');
296
- break;
297
-
298
- case 'registered':
299
- state.subdomain = msg.subdomain as string;
300
- console.log(`\x1b[36m ➜ Tunnel: https://${state.subdomain}.ugly.bot\x1b[0m`);
301
- break;
302
-
303
- case 'http-request':
304
- handleHttpRequest(msg as { id: string; method: string; url: string; headers: Record<string, string>; body?: string }, opts, ws);
305
- break;
306
-
307
- case 'ws-open':
308
- handleWsOpen(msg as { id: string; url: string; headers: Record<string, string> }, opts, ws);
309
- break;
310
-
311
- case 'ws-frame':
312
- handleWsFrame(msg as { id: string; data: string; binary: boolean });
313
- break;
314
-
315
- case 'ws-close':
316
- handleWsClose(msg as { id: string; code?: number });
317
- break;
318
-
319
- case 'tunnel:ping':
320
- // Respond to server heartbeat
321
- ws.send(JSON.stringify({ type: 'tunnel:pong' }));
322
- break;
323
- }
324
- }
325
-
326
- // ── Proxy tunnel: HTTP relay ────────────────────────────────────────────────
327
-
328
- function handleHttpRequest(
329
- msg: { id: string; method: string; url: string; headers: Record<string, string>; body?: string },
330
- opts: StartDevTunnelOpts,
331
- ws: WebSocket,
332
- ): void {
333
- const port = opts.vitePort ?? 5173;
334
- const url = `http://localhost:${port}${msg.url}`;
335
-
336
- const headers = { ...msg.headers };
337
- delete headers['host'];
338
- delete headers['accept-encoding'];
339
-
340
- const fetchOptions: RequestInit = {
341
- method: msg.method,
342
- headers,
343
- body: msg.body ? Buffer.from(msg.body, 'base64') : undefined,
344
- };
345
-
346
- fetch(url, fetchOptions)
347
- .then(async (res) => {
348
- const responseHeaders: Record<string, string> = {};
349
- res.headers.forEach((value, key) => {
350
- if (!STRIP_RESPONSE_HEADERS.has(key)) {
351
- responseHeaders[key] = value;
352
- }
353
- });
354
-
355
- const bodyBuffer = await res.arrayBuffer();
356
- const body =
357
- bodyBuffer.byteLength > 0
358
- ? Buffer.from(bodyBuffer).toString('base64')
359
- : undefined;
360
-
361
- ws.send(
362
- JSON.stringify({
363
- type: 'http-response',
364
- id: msg.id,
365
- status: res.status,
366
- headers: responseHeaders,
367
- body,
368
- }),
369
- );
370
- })
371
- .catch(() => {
372
- ws.send(
373
- JSON.stringify({
374
- type: 'http-response',
375
- id: msg.id,
376
- status: 502,
377
- headers: {},
378
- body: Buffer.from('Local server error').toString('base64'),
379
- }),
380
- );
381
- });
382
- }
383
-
384
- // ── Proxy tunnel: WebSocket relay ───────────────────────────────────────────
385
-
386
- function handleWsOpen(
387
- msg: { id: string; url: string; headers: Record<string, string> },
388
- opts: StartDevTunnelOpts,
389
- ws: WebSocket,
390
- ): void {
391
- if (!state) return;
392
-
393
- const port = opts.vitePort ?? 5173;
394
- const url = `ws://localhost:${port}${msg.url}`;
395
-
396
- const wsHeaders = { ...msg.headers };
397
- delete wsHeaders['host'];
398
- delete wsHeaders['upgrade'];
399
- delete wsHeaders['connection'];
400
- delete wsHeaders['sec-websocket-key'];
401
- delete wsHeaders['sec-websocket-version'];
402
- delete wsHeaders['sec-websocket-extensions'];
403
- const protocols = msg.headers['sec-websocket-protocol']
404
- ?.split(',')
405
- .map((p) => p.trim())
406
- .filter(Boolean);
407
- const localWs = new WebSocket(url, protocols, { headers: wsHeaders });
408
-
409
- localWs.on('open', () => {
410
- if (!state) return;
411
- state.localWsSockets.set(msg.id, localWs);
412
- ws.send(JSON.stringify({ type: 'ws-open-ok', id: msg.id }));
413
- });
414
-
415
- localWs.on('error', () => {
416
- ws.send(
417
- JSON.stringify({ type: 'ws-open-error', id: msg.id, status: 502 }),
418
- );
419
- });
420
-
421
- localWs.on('message', (data, isBinary) => {
422
- const encoded = isBinary
423
- ? (data as Buffer).toString('base64')
424
- : data.toString();
425
- ws.send(
426
- JSON.stringify({
427
- type: 'ws-frame',
428
- id: msg.id,
429
- data: encoded,
430
- binary: isBinary,
431
- }),
432
- );
433
- });
434
-
435
- localWs.on('close', (code) => {
436
- state?.localWsSockets.delete(msg.id);
437
- ws.send(JSON.stringify({ type: 'ws-close', id: msg.id, code }));
438
- });
439
- }
440
-
441
- function handleWsFrame(msg: { id: string; data: string; binary: boolean }): void {
442
- if (!state) return;
443
- const localWs = state.localWsSockets.get(msg.id);
444
- if (localWs?.readyState !== WebSocket.OPEN) return;
445
-
446
- if (msg.binary) {
447
- localWs.send(Buffer.from(msg.data, 'base64'));
448
- } else {
449
- localWs.send(msg.data);
450
- }
451
- }
452
-
453
- function handleWsClose(msg: { id: string; code?: number }): void {
454
- if (!state) return;
455
- const localWs = state.localWsSockets.get(msg.id);
456
- if (localWs) {
457
- state.localWsSockets.delete(msg.id);
458
- localWs.close(msg.code ?? 1000);
459
- }
460
- }
461
-
462
- // ── Infra tunnel: TCP multiplexing ──────────────────────────────────────────
463
-
464
- function cleanupServers(): void {
465
- if (!state) return;
466
- for (const server of state.servers.values()) {
467
- server.close();
468
- }
469
- state.servers.clear();
470
- for (const socket of state.streams.values()) {
471
- socket.destroy();
472
- }
473
- state.streams.clear();
474
- for (const localWs of state.localWsSockets.values()) {
475
- localWs.close(1001);
476
- }
477
- state.localWsSockets.clear();
478
- state = null;
479
- }
480
-
481
- function scheduleReconnect(opts: StartDevTunnelOpts): void {
482
- if (!state || state.stopped || state.reconnectTimer) return;
483
-
484
- state.reconnectTimer = setTimeout(() => {
485
- if (!state || state.stopped) return;
486
- state.reconnectTimer = null;
487
- state.reconnectDelay = Math.min(state.reconnectDelay * 2, 30_000);
488
- connectWs(opts).then(() => {
489
- console.log('[ugly-app] Tunnel reconnected');
490
- }).catch(() => {
491
- scheduleReconnect(opts);
492
- });
493
- }, state.reconnectDelay);
494
- }
495
-
496
- function handleTcpConnection(serviceId: ServiceId, socket: net.Socket): void {
497
- if (!state) { socket.destroy(); return; }
498
-
499
- if (!state.connected || !state.ws) {
500
- console.warn(`[ugly-app] Tunnel reconnecting — rejecting ${SERVICE_NAME[serviceId]} connection`);
501
- socket.destroy();
502
- return;
503
- }
504
-
505
- const streamId = state.nextStreamId++;
506
- if (state.nextStreamId > 0xFFFFFFFF) {
507
- state.nextStreamId = 1;
508
- }
509
-
510
- state.streams.set(streamId, socket);
511
- const serviceFlags = serviceId << 2;
512
-
513
- // Send empty data frame to signal "stream opened"
514
- if (state.ws?.readyState === WebSocket.OPEN) {
515
- state.ws.send(encodeFrame(streamId, serviceFlags, Buffer.alloc(0)));
516
- }
517
-
518
- socket.on('data', (chunk: Buffer) => {
519
- if (state?.ws?.readyState !== WebSocket.OPEN) return;
520
-
521
- const frame = encodeFrame(streamId, serviceFlags, chunk);
522
- state.ws.send(frame);
523
-
524
- if (state.ws.bufferedAmount > BACKPRESSURE_THRESHOLD) {
525
- socket.pause();
526
- const checkDrain = () => {
527
- if (!state?.ws) return;
528
- if (state.ws.bufferedAmount <= BACKPRESSURE_THRESHOLD) {
529
- socket.resume();
530
- } else {
531
- setTimeout(checkDrain, 50);
532
- }
533
- };
534
- setTimeout(checkDrain, 50);
535
- }
536
- });
537
-
538
- socket.on('end', () => {
539
- if (!state) return;
540
- state.streams.delete(streamId);
541
- if (state.ws?.readyState === WebSocket.OPEN) {
542
- state.ws.send(encodeFrame(streamId, serviceFlags | FLAGS.FIN, Buffer.alloc(0)));
543
- }
544
- });
545
-
546
- socket.on('error', () => {
547
- if (!state) return;
548
- state.streams.delete(streamId);
549
- if (state.ws?.readyState === WebSocket.OPEN) {
550
- state.ws.send(encodeFrame(streamId, serviceFlags | FLAGS.RST, Buffer.alloc(0)));
551
- }
552
- });
553
-
554
- socket.on('close', () => {
555
- state?.streams.delete(streamId);
556
- });
557
- }
558
-
559
- /** Stop the dev tunnel: close WS, all TCP servers and sockets. */
560
- export function stopDevTunnel(): void {
561
- if (!state) return;
562
- state.stopped = true;
563
-
564
- if (state.reconnectTimer) {
565
- clearTimeout(state.reconnectTimer);
566
- state.reconnectTimer = null;
567
- }
568
-
569
- if (state.ws && state.ws.readyState <= WebSocket.OPEN) {
570
- state.ws.close(1000, 'Dev tunnel stopped');
571
- }
572
-
573
- for (const socket of state.streams.values()) {
574
- socket.destroy();
575
- }
576
- state.streams.clear();
577
-
578
- for (const server of state.servers.values()) {
579
- server.close();
580
- }
581
- state.servers.clear();
582
-
583
- for (const localWs of state.localWsSockets.values()) {
584
- localWs.close(1000);
585
- }
586
- state.localWsSockets.clear();
587
-
588
- state = null;
589
- }
@@ -1,90 +0,0 @@
1
- import { getLocalPort } from '../shared/Defaults.js';
2
- import { getUglyBotUrl } from '../shared/uglyBotUrl.js';
3
- import { readAuthToken, readLocalToken } from './authStore.js';
4
- import { readUglyAppConfig } from './uglyappConfig.js';
5
-
6
- function isLocal(): boolean {
7
- return process.env['UGLY_BOT_LOCAL'] === '1';
8
- }
9
-
10
- /**
11
- * Ensure infrastructure env vars (DATABASE_URL, NATS_URL, etc.) are set.
12
- *
13
- * Starts the sidecar tunnel if env vars are not already present.
14
- * The tunnel connects to ugly.bot (cloud or local depending on UGLY_BOT_LOCAL).
15
- * If not logged in or token expired, auto-triggers the login flow.
16
- */
17
- export async function ensureInfra(): Promise<void> {
18
- const config = readUglyAppConfig(process.cwd());
19
-
20
- // Inject basic config-derived env vars
21
- if (config) {
22
- if (!process.env['NATS_PREFIX']) process.env['NATS_PREFIX'] = config.projectId;
23
- if (!process.env['STORAGE_KEY_PREFIX']) process.env['STORAGE_KEY_PREFIX'] = config.projectId;
24
- // Use the right project token for the current mode
25
- // Prefer ~/.ugly-bot/ auth store, fall back to .uglyapp for legacy
26
- const projectToken = isLocal()
27
- ? (readLocalToken(config.projectId) ?? config.localToken)
28
- : (readAuthToken(config.projectId) ?? config.uglyBotToken);
29
- if (projectToken && !process.env['UGLY_BOT_TOKEN']) {
30
- process.env['UGLY_BOT_TOKEN'] = projectToken;
31
- }
32
- }
33
-
34
- // If a Postgres URL is already set (e.g. by a parent process, a deploy
35
- // script, or an E2E test), skip the tunnel-bootstrap path. `DATABASE_URL`
36
- // is the canonical name `Pg.ts:getPool()` reads; `POSTGRES_URL` is the
37
- // legacy name kept for older deploys until they migrate.
38
- if (process.env['DATABASE_URL'] || process.env['POSTGRES_URL']) return;
39
-
40
- if (!config) {
41
- throw new Error('[ugly-app] No .uglyapp config found. Run: npx ugly-app init');
42
- }
43
-
44
- // Ensure we have valid user auth — auto-trigger login if needed
45
- const { ensureUglyBotAuth } = await import('./uglyBotLogin.js');
46
- const auth = await ensureUglyBotAuth(getUglyBotUrl());
47
-
48
- // Ensure we have a valid project token for this mode
49
- const storedToken = isLocal()
50
- ? (readLocalToken(config.projectId) ?? config.localToken)
51
- : (readAuthToken(config.projectId) ?? config.uglyBotToken);
52
- if (!storedToken) {
53
- console.log(`[ugly-app] No project token for ${isLocal() ? 'local' : 'cloud'} — registering...`);
54
- const path = await import('path');
55
- const { runUglyBotSetup } = await import('./uglyBotSetup.js');
56
- await runUglyBotSetup(config.projectId, path.join(process.cwd(), '.uglyapp'));
57
- // Re-read token from auth store (runUglyBotSetup now writes there)
58
- const newToken = isLocal()
59
- ? readLocalToken(config.projectId)
60
- : readAuthToken(config.projectId);
61
- if (newToken) {
62
- process.env['UGLY_BOT_TOKEN'] = newToken;
63
- }
64
- }
65
-
66
- const { startDevTunnel } = await import('./devTunnel.js');
67
-
68
- // Studio sessions set this so each session's proxy subdomain is unique;
69
- // unset (regular `npm run dev`) keeps the legacy `proxy-{username}-{projectId}` URL.
70
- const sessionId = process.env['UGLY_STUDIO_SESSION_ID'];
71
-
72
- const vitePort = getLocalPort();
73
- try {
74
- const ports = await startDevTunnel({ projectId: config.projectId, token: auth.token, vitePort, sessionId });
75
- process.env['NATS_URL'] = `nats://localhost:${ports.nats}`;
76
- } catch (err) {
77
- const msg = (err as Error).message;
78
- if (msg.includes('4001')) {
79
- // Auth failed — force re-login and retry
80
- console.log('[ugly-app] Auth expired, re-authenticating...');
81
- const { runUglyBotLogin } = await import('./uglyBotLogin.js');
82
- const freshAuth = await runUglyBotLogin(getUglyBotUrl());
83
- const ports = await startDevTunnel({ projectId: config.projectId, token: freshAuth.token, vitePort, sessionId });
84
- process.env['NATS_URL'] = `nats://localhost:${ports.nats}`;
85
- process.env['MINIO_ENDPOINT'] = `http://localhost:${ports.s3}`;
86
- } else {
87
- throw err;
88
- }
89
- }
90
- }