ugly-app 0.1.518 → 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 (120) 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/Email.d.ts.map +1 -1
  39. package/dist/server/Email.js +7 -1
  40. package/dist/server/Email.js.map +1 -1
  41. package/dist/server/Logging.d.ts +5 -8
  42. package/dist/server/Logging.d.ts.map +1 -1
  43. package/dist/server/Logging.js +21 -97
  44. package/dist/server/Logging.js.map +1 -1
  45. package/dist/server/adapter/workers/cf-types.d.ts +7 -0
  46. package/dist/server/adapter/workers/cf-types.d.ts.map +1 -1
  47. package/dist/server/adapter/workers/createWorkersApp.js +1 -0
  48. package/dist/server/adapter/workers/createWorkersApp.js.map +1 -1
  49. package/dist/server/auth/magicLinkProvider.d.ts.map +1 -1
  50. package/dist/server/auth/magicLinkProvider.js +55 -6
  51. package/dist/server/auth/magicLinkProvider.js.map +1 -1
  52. package/dist/server/auth/workersMagicLink.d.ts.map +1 -1
  53. package/dist/server/auth/workersMagicLink.js +117 -13
  54. package/dist/server/auth/workersMagicLink.js.map +1 -1
  55. package/dist/server/bootstrap.d.ts +5 -21
  56. package/dist/server/bootstrap.d.ts.map +1 -1
  57. package/dist/server/bootstrap.js +5 -27
  58. package/dist/server/bootstrap.js.map +1 -1
  59. package/dist/shared/DB.d.ts +0 -9
  60. package/dist/shared/DB.d.ts.map +1 -1
  61. package/dist/shared/DB.js +46 -10
  62. package/dist/shared/DB.js.map +1 -1
  63. package/dist/vite/index.d.ts +0 -1
  64. package/dist/vite/index.d.ts.map +1 -1
  65. package/dist/vite/index.js +0 -1
  66. package/dist/vite/index.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/cli/dev.ts +0 -4
  69. package/src/cli/doctor.ts +31 -63
  70. package/src/cli/index.ts +9 -81
  71. package/src/cli/migrate.ts +0 -4
  72. package/src/cli/schemaGen.ts +0 -4
  73. package/src/cli/schemaStatus.ts +0 -4
  74. package/src/cli/serverLogQueryApi.ts +53 -136
  75. package/src/cli/version.ts +1 -1
  76. package/src/client/auth/MagicLinkCallback.tsx +10 -2
  77. package/src/client/auth/MagicLinkForm.tsx +7 -1
  78. package/src/server/App.ts +1 -2
  79. package/src/server/Email.ts +7 -1
  80. package/src/server/Logging.ts +21 -97
  81. package/src/server/adapter/workers/cf-types.ts +8 -0
  82. package/src/server/adapter/workers/createWorkersApp.ts +1 -0
  83. package/src/server/auth/magicLinkProvider.ts +61 -6
  84. package/src/server/auth/workersMagicLink.ts +117 -13
  85. package/src/server/bootstrap.ts +5 -29
  86. package/src/shared/DB.ts +46 -17
  87. package/src/vite/index.ts +0 -1
  88. package/.pgdata/base/1/pg_internal.init +0 -0
  89. package/.pgdata/base/17702/pg_internal.init +0 -0
  90. package/.pgdata/base/5/pg_internal.init +0 -0
  91. package/dist/cli/devTunnel.d.ts +0 -37
  92. package/dist/cli/devTunnel.d.ts.map +0 -1
  93. package/dist/cli/devTunnel.js +0 -470
  94. package/dist/cli/devTunnel.js.map +0 -1
  95. package/dist/cli/ensureInfra.d.ts +0 -9
  96. package/dist/cli/ensureInfra.d.ts.map +0 -1
  97. package/dist/cli/ensureInfra.js +0 -86
  98. package/dist/cli/ensureInfra.js.map +0 -1
  99. package/dist/cli/sidecar.d.ts +0 -21
  100. package/dist/cli/sidecar.d.ts.map +0 -1
  101. package/dist/cli/sidecar.js +0 -294
  102. package/dist/cli/sidecar.js.map +0 -1
  103. package/dist/cli/sidecarHealth.d.ts +0 -13
  104. package/dist/cli/sidecarHealth.d.ts.map +0 -1
  105. package/dist/cli/sidecarHealth.js +0 -42
  106. package/dist/cli/sidecarHealth.js.map +0 -1
  107. package/dist/cli/sidecarProtocol.d.ts +0 -41
  108. package/dist/cli/sidecarProtocol.d.ts.map +0 -1
  109. package/dist/cli/sidecarProtocol.js +0 -61
  110. package/dist/cli/sidecarProtocol.js.map +0 -1
  111. package/dist/vite/tunnelPlugin.d.ts +0 -10
  112. package/dist/vite/tunnelPlugin.d.ts.map +0 -1
  113. package/dist/vite/tunnelPlugin.js +0 -30
  114. package/dist/vite/tunnelPlugin.js.map +0 -1
  115. package/src/cli/devTunnel.ts +0 -589
  116. package/src/cli/ensureInfra.ts +0 -90
  117. package/src/cli/sidecar.ts +0 -350
  118. package/src/cli/sidecarHealth.ts +0 -58
  119. package/src/cli/sidecarProtocol.ts +0 -68
  120. package/src/vite/tunnelPlugin.ts +0 -32
@@ -1,68 +1,7 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import { getUglyBotUrl } from '../shared/uglyBotUrl.js';
5
- import { readUglyAppConfig } from './uglyappConfig.js';
1
+ import { pgQuery } from '../server/Pg.js';
2
+ import { closePg, initPg } from '../server/NodePool.js';
6
3
 
7
- interface AuthConfig {
8
- token: string;
9
- userId: string;
10
- serverUrl: string;
11
- }
12
-
13
- function readAuth(): AuthConfig {
14
- const authPath = path.join(os.homedir(), '.ugly-bot', 'auth.json');
15
- if (!fs.existsSync(authPath)) {
16
- throw new Error('Not logged in. Run: ugly-app login');
17
- }
18
- return JSON.parse(fs.readFileSync(authPath, 'utf-8')) as AuthConfig;
19
- }
20
-
21
- /** Returns the currently-logged-in developer's userId, used as devTunnelId for error:dev. */
22
- export function getCurrentDevTunnelId(): string {
23
- return readAuth().userId;
24
- }
25
-
26
- function getAppApiUrl(): string {
27
- return getUglyBotUrl();
28
- }
29
-
30
- function getProjectId(): string | undefined {
31
- const config = readUglyAppConfig(process.cwd());
32
- return config?.projectId ?? undefined;
33
- }
34
-
35
- async function adminRequest<T>(
36
- op: string,
37
- input: Record<string, unknown>,
38
- ): Promise<T> {
39
- const auth = readAuth();
40
- const appApiUrl = getAppApiUrl();
41
- const res = await fetch(`${appApiUrl}/request`, {
42
- method: 'POST',
43
- headers: {
44
- 'Authorization': `Bearer ${auth.token}`,
45
- 'Content-Type': 'application/json',
46
- },
47
- body: JSON.stringify({ function: op, input }),
48
- });
49
-
50
- if (!res.ok) {
51
- const body = await res.text().catch(() => '');
52
- if (res.status === 401) {
53
- throw new Error(
54
- 'Authentication failed — token may be expired. Run: ugly-app login',
55
- );
56
- }
57
- throw new Error(`${op} failed: ${res.status} ${body}`);
58
- }
59
-
60
- // app's /request route sends the raw handler return value via res.json(result)
61
- // (see app/server/backend/Request.ts:510). Don't unwrap a `.result` field —
62
- // it doesn't exist.
63
- const data = (await res.json()) as T;
64
- return data;
65
- }
4
+ type AdminCollection = 'errorLog' | 'perfLog' | 'feedbackReport';
66
5
 
67
6
  function formatTimestamp(created: unknown): string {
68
7
  if (created instanceof Date) {
@@ -79,44 +18,47 @@ function formatTimestamp(created: unknown): string {
79
18
 
80
19
  function formatErrorDoc(doc: Record<string, unknown>): string {
81
20
  const ts = formatTimestamp(doc['created']);
82
- const level = String(doc['level'] ?? 'error').toUpperCase().padEnd(5);
83
- const source = doc['source'] ? `[${doc['source']}]` : '';
84
- const message = String(doc['message'] ?? '');
85
- const userId = doc['userId'] ? ` user:${doc['userId']}` : '';
86
- const stack = doc['stack']
87
- ? `\n ${String(doc['stack']).split('\n').slice(0, 3).join('\n ')}`
21
+ const data = (doc['data'] ?? {}) as Record<string, unknown>;
22
+ const level = String(data['level'] ?? 'error').toUpperCase().padEnd(5);
23
+ const source = data['source'] ? `[${data['source']}]` : '';
24
+ const message = String(data['message'] ?? '');
25
+ const userId = data['userId'] ? ` user:${data['userId']}` : '';
26
+ const stack = data['stack']
27
+ ? `\n ${String(data['stack']).split('\n').slice(0, 3).join('\n ')}`
88
28
  : '';
89
29
  return `${ts} ${level} ${source} ${message}${userId}${stack}`;
90
30
  }
91
31
 
92
32
  function formatPerfDoc(doc: Record<string, unknown>): string {
93
33
  const ts = formatTimestamp(doc['created']);
94
- const op = String(doc['operation'] ?? '');
95
- const ms = doc['durationMs'] != null ? `${doc['durationMs']}ms` : '';
96
- const userId = doc['userId'] ? ` user:${doc['userId']}` : '';
34
+ const data = (doc['data'] ?? {}) as Record<string, unknown>;
35
+ const op = String(data['operation'] ?? '');
36
+ const ms = data['durationMs'] != null ? `${data['durationMs']}ms` : '';
37
+ const userId = data['userId'] ? ` user:${data['userId']}` : '';
97
38
  return `${ts} ${op} ${ms}${userId}`;
98
39
  }
99
40
 
100
41
  function formatFeedbackDoc(doc: Record<string, unknown>): string {
101
42
  const ts = formatTimestamp(doc['created']);
102
- const type = String(doc['type'] ?? '').toUpperCase().padEnd(7);
103
- const message = String(doc['description'] ?? doc['message'] ?? '');
43
+ const data = (doc['data'] ?? {}) as Record<string, unknown>;
44
+ const type = String(data['type'] ?? '').toUpperCase().padEnd(7);
45
+ const message = String(data['description'] ?? data['message'] ?? '');
104
46
  const lines = [`${ts} [${type}] ${message}`];
105
47
 
106
- if (doc['status']) lines.push(` Status: ${doc['status']}`);
107
- if (doc['userId']) lines.push(` User: ${doc['userId']}`);
108
- if (doc['url']) lines.push(` URL: ${doc['url']}`);
109
- if (doc['screenshotUri']) lines.push(` Screenshot: ${doc['screenshotUri']}`);
48
+ if (data['status']) lines.push(` Status: ${data['status']}`);
49
+ if (data['userId']) lines.push(` User: ${data['userId']}`);
50
+ if (data['url']) lines.push(` URL: ${data['url']}`);
51
+ if (data['screenshotUri']) lines.push(` Screenshot: ${data['screenshotUri']}`);
110
52
 
111
- const ctx = doc['context'];
53
+ const ctx = data['context'];
112
54
  if (ctx && typeof ctx === 'object') {
113
55
  const ctxStr = JSON.stringify(ctx);
114
56
  if (ctxStr !== '{}') lines.push(` Context: ${ctxStr}`);
115
57
  }
116
58
 
117
- if (doc['resolution']) lines.push(` Resolution: ${doc['resolution']}`);
59
+ if (data['resolution']) lines.push(` Resolution: ${data['resolution']}`);
118
60
 
119
- const previousLogs = doc['logs'] ?? doc['previousLogs'];
61
+ const previousLogs = data['logs'] ?? data['previousLogs'];
120
62
  if (Array.isArray(previousLogs) && previousLogs.length > 0) {
121
63
  lines.push(` Console logs (${previousLogs.length}):`);
122
64
  for (const entry of previousLogs as Record<string, unknown>[]) {
@@ -136,74 +78,49 @@ const formatters: Record<string, (doc: Record<string, unknown>) => string> = {
136
78
  feedbackReport: formatFeedbackDoc,
137
79
  };
138
80
 
139
- type AdminCollection = 'errorLog' | 'perfLog' | 'feedbackReport';
140
-
81
+ /**
82
+ * Query a log collection from the project's own Postgres. DATABASE_URL must
83
+ * point at the DB you want to inspect (local dev DB or a production Neon DB
84
+ * — set DATABASE_URL accordingly before running the command).
85
+ */
141
86
  export async function queryServerLogsApi(
142
87
  collection: AdminCollection,
143
88
  options: {
144
89
  limit?: number;
145
90
  level?: string;
146
91
  json?: boolean;
147
- /** When set, filter errorLog rows by devTunnelId (string) or 'none' for deployed-only. */
148
- devTunnelId?: string | 'none';
149
92
  },
150
93
  ): Promise<void> {
151
94
  const limit = options.limit ?? 50;
152
- const projectId = getProjectId();
153
-
154
- let items: Record<string, unknown>[];
155
95
 
156
- if (collection === 'errorLog') {
157
- const input: Record<string, unknown> = { limit };
158
- if (options.level) input['level'] = options.level;
159
- if (projectId) input['projectId'] = projectId;
160
- if (options.devTunnelId !== undefined) {
161
- input['devTunnelId'] = options.devTunnelId;
96
+ await initPg();
97
+ try {
98
+ const params: unknown[] = [limit];
99
+ let where = '';
100
+ if (collection === 'errorLog' && options.level) {
101
+ where = `WHERE data->'meta'->>'level' = $2`;
102
+ params.push(options.level);
162
103
  }
163
- const result = await adminRequest<{ errors: Record<string, unknown>[]; nextCursor: number | null }>(
164
- 'errorLogGetList',
165
- input,
104
+ const res = await pgQuery<Record<string, unknown>>(
105
+ `SELECT _id, data, created FROM "${collection}" ${where} ORDER BY created DESC LIMIT $1`,
106
+ params,
166
107
  );
167
- items = result.errors;
168
- } else if (collection === 'feedbackReport') {
169
- const input: Record<string, unknown> = { limit };
170
- if (projectId) input['projectId'] = projectId;
171
- if (options.devTunnelId !== undefined) {
172
- input['devTunnelId'] = options.devTunnelId;
173
- }
174
- const result = await adminRequest<{
175
- items: Record<string, unknown>[];
176
- nextCursor: number | null;
177
- }>('adminFeedbackList', input);
178
- items = result.items;
179
- } else {
180
- // perfLog
181
- const input: Record<string, unknown> = { limit };
182
- if (projectId) input['projectId'] = projectId;
183
- if (options.devTunnelId !== undefined) {
184
- input['devTunnelId'] = options.devTunnelId;
185
- }
186
- const result = await adminRequest<{
187
- items: Record<string, unknown>[];
188
- nextCursor: number | null;
189
- }>('perfLogGetList', input);
190
- items = result.items;
191
- }
108
+ const items = res.rows.slice().reverse();
192
109
 
193
- // Reverse so oldest first (server returns newest first)
194
- items.reverse();
195
-
196
- if (items.length === 0) {
197
- console.log(`No entries found in ${collection}.`);
198
- return;
199
- }
110
+ if (items.length === 0) {
111
+ console.log(`No entries found in ${collection}.`);
112
+ return;
113
+ }
200
114
 
201
- for (const doc of items) {
202
- if (options.json) {
203
- console.log(JSON.stringify(doc));
204
- } else {
205
- const fmt = formatters[collection] ?? formatErrorDoc;
206
- console.log(fmt(doc));
115
+ for (const doc of items) {
116
+ if (options.json) {
117
+ console.log(JSON.stringify(doc));
118
+ } else {
119
+ const fmt = formatters[collection] ?? formatErrorDoc;
120
+ console.log(fmt(doc));
121
+ }
207
122
  }
123
+ } finally {
124
+ closePg();
208
125
  }
209
126
  }
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.518";
2
+ export const CLI_VERSION = "0.1.521";
@@ -19,6 +19,7 @@ export function MagicLinkCallback() {
19
19
  const params = new URLSearchParams(window.location.search);
20
20
  const id = params.get('id');
21
21
  const sig = params.get('sig');
22
+ const returnTo = params.get('returnTo') ?? '';
22
23
  if (!id || !sig) {
23
24
  setState('error');
24
25
  return;
@@ -28,15 +29,22 @@ export function MagicLinkCallback() {
28
29
  const r = await fetch('/auth/magic-link/verify', {
29
30
  method: 'POST',
30
31
  headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify({ id, sig }),
32
+ body: JSON.stringify({ id, sig, returnTo }),
32
33
  });
33
34
  if (!r.ok) {
34
35
  setState('error');
35
36
  return;
36
37
  }
38
+ // Server returns a same-origin-validated `returnTo` in the
39
+ // success body — trust that over the raw URL parameter so a
40
+ // tampered click-through can't post-redirect off-site.
41
+ const data = (await r.json().catch(() => ({}))) as {
42
+ returnTo?: string;
43
+ };
44
+ const dest = data.returnTo || returnTo || '/';
37
45
  setState('success');
38
46
  setTimeout(() => {
39
- window.location.href = '/';
47
+ window.location.href = dest;
40
48
  }, 500);
41
49
  } catch {
42
50
  setState('error');
@@ -16,10 +16,16 @@ export function MagicLinkForm() {
16
16
  if (!email) return;
17
17
  setState('requesting');
18
18
  try {
19
+ // Capture where the user is right now so the verify step can
20
+ // resume on the same page (with query string + hash preserved).
21
+ // Server-side `validateReturnTo` strips off-origin URLs to `/`,
22
+ // so this is safe to send even when the user follows a deep link.
23
+ const returnTo =
24
+ typeof window !== 'undefined' ? window.location.href : '';
19
25
  const r = await fetch('/auth/magic-link/request', {
20
26
  method: 'POST',
21
27
  headers: { 'Content-Type': 'application/json' },
22
- body: JSON.stringify({ email }),
28
+ body: JSON.stringify({ email, returnTo }),
23
29
  });
24
30
  if (!r.ok) {
25
31
  setState('error');
package/src/server/App.ts CHANGED
@@ -1466,7 +1466,6 @@ export function createApp<
1466
1466
  const { createServer: createViteServer } = await import("vite");
1467
1467
  type PluginOption = import("vite").PluginOption;
1468
1468
  const hmrEnabled = process.env["UGLY_APP_HMR"] !== "false";
1469
- const { tunnelPlugin } = await import("../vite/tunnelPlugin.js");
1470
1469
  // Park Vite's dep-optimization cache OUTSIDE the project tree.
1471
1470
  // Default is `<cwd>/node_modules/.vite/` — fine for direct
1472
1471
  // `npm run dev`, lethal under ugly-studio's per-session
@@ -1603,7 +1602,7 @@ export function createApp<
1603
1602
  target: "esnext",
1604
1603
  },
1605
1604
  },
1606
- plugins: [...userPlugins, tunnelPlugin()],
1605
+ plugins: userPlugins,
1607
1606
  appType: "custom",
1608
1607
  });
1609
1608
  expressApp.use(vite.middlewares);
@@ -59,8 +59,14 @@ export function configureEmail(config: EmailConfig): void {
59
59
  }
60
60
 
61
61
  function resolveConfig(): { url: string; token: string; timeoutMs: number } {
62
- const url =
62
+ const raw =
63
63
  _config.url ?? process.env['EMAIL_PROXY_URL'] ?? DEFAULT_EMAIL_PROXY_URL;
64
+ // Match the AI/push proxy convention: EMAIL_PROXY_URL is the BASE
65
+ // (e.g. `https://ugly.bot/v1/email`) and the client appends the
66
+ // endpoint suffix. Tolerate the legacy form that includes `/send`
67
+ // so existing deploys keep working without an env-var rotation.
68
+ const stripped = raw.replace(/\/+$/, '');
69
+ const url = stripped.endsWith('/send') ? stripped : `${stripped}/send`;
64
70
  const token = _config.token ?? process.env['EMAIL_PROXY_TOKEN'] ?? '';
65
71
  const timeoutMs = _config.timeoutMs ?? 30_000;
66
72
  return { url, token, timeoutMs };
@@ -5,11 +5,10 @@ import path from 'node:path';
5
5
  import type { TypedDB } from '../shared/DB.js';
6
6
  import type { StorageClient } from './Storage.js';
7
7
  // Note: ./Pg.js and ./PostgresDoc.js are loaded LAZILY inside
8
- // `insertLogRows` only. Hosts that route logging via EVENT_LOG_URL
9
- // (e.g. Ugly Studio's sidecar) never need the pg driver keeping these
10
- // imports lazy stops the ~80ms `pg` module load on `import './Logging.js'`.
11
- // `randomId` from PostgresDoc.js is small and inlined below so we
12
- // don't transitively load the whole collection ORM either.
8
+ // `insertLogRows` only. Keeping these imports lazy stops the ~80ms `pg`
9
+ // module load on `import './Logging.js'` for code paths that never log.
10
+ // `randomId` from PostgresDoc.js is small and inlined below so we don't
11
+ // transitively load the whole collection ORM either.
13
12
  const RANDOM_ID_ALPHABET =
14
13
  'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';
15
14
  function randomId(): string {
@@ -22,73 +21,23 @@ function randomId(): string {
22
21
  return out;
23
22
  }
24
23
 
25
- // ─── Data-proxy capture forwarding ──────────────────────────────────────────
24
+ // ─── Log persistence ────────────────────────────────────────────────────────
26
25
  //
27
- // errorLog and perfLog writes go through the data-proxy's restricted-collection
28
- // RPCs (db.captureError / db.capturePerf). The data-proxy stamps projectId from
29
- // the JWT (claims.sub) and devTunnelId from the JWT (claims.devTunnelId), so
30
- // nothing the client sends can spoof identity. There is no project/userId
31
- // argument to these helpers — both come from the JWT the StorageClient holds.
26
+ // errorLog, perfLog, and feedbackReport writes go directly to the project's
27
+ // own Postgres. Each project owns these tables in its own DB (created by
28
+ // bootstrap.ts on startup). Hosts like Ugly Studio that want to surface logs
29
+ // query the project's DB through their own channels.
32
30
  //
33
- // studioSessionId is different: it's a sub-partition inside one developer's
34
- // dev tunnel (so N coding-agent sessions from the same user can show only
35
- // their own logs in the ugly-studio panels / AI tools). It is NOT a security
36
- // gate devTunnelId already isolates between users. We read it from
37
- // `UGLY_STUDIO_SESSION_ID`, an env var ugly-studio sets on every per-session
38
- // `npm run dev` child it spawns (see studio/server/devServerMulti.ts
39
- // startInstance). Absent = legacy single-session dev, no stamp.
31
+ // studioSessionId sub-partitions logs by ugly-studio coding-agent session so
32
+ // per-session panels / AI tools can scope what they show. We read it from
33
+ // `UGLY_STUDIO_SESSION_ID`, set by Studio on every per-session `npm run dev`
34
+ // child (see studio/server/devServerMulti.ts startInstance). Absent = no stamp.
40
35
 
41
36
  function getStudioSessionId(): string | undefined {
42
37
  const raw = process.env['UGLY_STUDIO_SESSION_ID'];
43
38
  return raw && raw.length > 0 ? raw : undefined;
44
39
  }
45
40
 
46
- /**
47
- * HTTPS event-log forwarding. When `EVENT_LOG_URL` is set (e.g. ugly-studio's
48
- * sidecar boot, which sets EVENT_LOG_URL=https://api.ugly.bot/v1/events and
49
- * EVENT_LOG_TOKEN=<jwt>) we POST batches to `${EVENT_LOG_URL}/${kind}` with
50
- * a bearer token, bypassing the direct-PG path entirely. The receiving
51
- * ugly.bot route stamps projectId + devTunnelId from the JWT.
52
- *
53
- * When EVENT_LOG_URL is unset we fall back to the legacy direct-PG path
54
- * which is gated on NODE_ENV=production (so local `npm run dev` and tests
55
- * still drop entries on the floor — keeps shared errorLog/perfLog tables
56
- * free of dev noise).
57
- */
58
- const EVENT_LOG_URL = process.env['EVENT_LOG_URL'] ?? '';
59
- const EVENT_LOG_TOKEN = process.env['EVENT_LOG_TOKEN'] ?? '';
60
- const HTTPS_LOG_ENABLED = EVENT_LOG_URL.length > 0;
61
- const LOG_TO_REMOTE =
62
- HTTPS_LOG_ENABLED || process.env['NODE_ENV'] === 'production';
63
-
64
- async function sendBatchOverHttps(
65
- kind: 'error' | 'perf' | 'feedback',
66
- entries: unknown[],
67
- ): Promise<void> {
68
- if (!HTTPS_LOG_ENABLED) return;
69
- if (entries.length === 0) return;
70
- const url = `${EVENT_LOG_URL.replace(/\/+$/, '')}/${kind}`;
71
- try {
72
- const res = await globalThis.fetch(url, {
73
- method: 'POST',
74
- headers: {
75
- 'Authorization': `Bearer ${EVENT_LOG_TOKEN}`,
76
- 'Content-Type': 'application/json',
77
- },
78
- body: JSON.stringify({ entries }),
79
- });
80
- if (!res.ok) {
81
- const text = await res.text().catch(() => '');
82
- console.warn(
83
- `[Logging] HTTPS ${kind} batch failed (${res.status}): ${text.slice(0, 200)}`,
84
- );
85
- }
86
- } catch (err) {
87
- const msg = err instanceof Error ? err.message : String(err);
88
- console.warn(`[Logging] HTTPS ${kind} batch network error: ${msg}`);
89
- }
90
- }
91
-
92
41
  interface PendingErrorEntry {
93
42
  level: string;
94
43
  message: string;
@@ -125,22 +74,9 @@ async function insertLogRows(
125
74
  entries: Record<string, unknown>[],
126
75
  ): Promise<void> {
127
76
  if (entries.length === 0) return;
128
- // Prefer HTTPS forwarding when configured (Studio sidecar, hosts without
129
- // direct PG access). ugly.bot stamps projectId+devTunnelId from the JWT.
130
- if (HTTPS_LOG_ENABLED) {
131
- const kind: 'error' | 'perf' | 'feedback' =
132
- collection === 'errorLog'
133
- ? 'error'
134
- : collection === 'perfLog'
135
- ? 'perf'
136
- : 'feedback';
137
- await sendBatchOverHttps(kind, entries);
138
- return;
139
- }
140
- // Best-effort row inserts. If the table doesn't exist yet (e.g. before
141
- // db:init / db:migrate ran), pg throws — we swallow because logging
142
- // must never throw. `pgQuery` is loaded lazily so hosts routing logs
143
- // via `EVENT_LOG_URL` (e.g. Ugly Studio) never pay the pg load cost.
77
+ // Best-effort row inserts into the project's own Postgres. If the table
78
+ // doesn't exist yet (e.g. before bootstrap created it), pg throws — we
79
+ // swallow because logging must never throw.
144
80
  const { pgQuery } = await import('./Pg.js');
145
81
  for (const entry of entries) {
146
82
  const now = new Date();
@@ -166,7 +102,6 @@ async function insertLogRows(
166
102
  }
167
103
 
168
104
  async function flushErrorEntries(): Promise<void> {
169
- if (!LOG_TO_REMOTE) return;
170
105
  if (_pendingErrorEntries.length === 0) return;
171
106
  const batch = _pendingErrorEntries.splice(0, 50);
172
107
  try {
@@ -194,7 +129,6 @@ function sendErrorToApi(
194
129
  context?: Record<string, unknown> | null;
195
130
  },
196
131
  ): void {
197
- if (!LOG_TO_REMOTE) return;
198
132
  const studioSessionId = getStudioSessionId();
199
133
  _pendingErrorEntries.push({
200
134
  level,
@@ -214,8 +148,7 @@ function sendErrorToApi(
214
148
  /**
215
149
  * Capture a batch of client-originated error entries (e.g. browser errors
216
150
  * forwarded via an HTTP endpoint). Buffers them into the same queue that
217
- * server-side console errors use; the data-proxy stamps projectId +
218
- * devTunnelId from the JWT on flush.
151
+ * server-side console errors use and flushes to the project's own Postgres.
219
152
  */
220
153
  export function captureClientErrors(
221
154
  entries: {
@@ -230,7 +163,6 @@ export function captureClientErrors(
230
163
  context?: Record<string, unknown> | null;
231
164
  }[],
232
165
  ): void {
233
- if (!LOG_TO_REMOTE) return;
234
166
  const studioSessionId = getStudioSessionId();
235
167
  for (const e of entries) {
236
168
  _pendingErrorEntries.push({
@@ -264,7 +196,6 @@ const _perfLogBuffer: PerfLogEntry[] = [];
264
196
  let _perfLogFlushTimer: ReturnType<typeof setInterval> | null = null;
265
197
 
266
198
  function flushPerfLogBuffer(): void {
267
- if (!LOG_TO_REMOTE) return;
268
199
  if (_perfLogBuffer.length === 0) return;
269
200
  const entries = _perfLogBuffer.splice(0, _perfLogBuffer.length);
270
201
  void insertLogRows(
@@ -285,12 +216,10 @@ function ensurePerfLogFlushTimer(): void {
285
216
  }
286
217
 
287
218
  /**
288
- * Record a perf entry. Buffered and flushed to the data-proxy on a 5s timer
289
- * (or immediately when the buffer reaches 50). The data-proxy stamps
290
- * projectId + devTunnelId from its JWT.
219
+ * Record a perf entry. Buffered and flushed to the project's own Postgres on
220
+ * a 5s timer (or immediately when the buffer reaches 50).
291
221
  */
292
222
  export function recordPerf(operation: string, durationMs: number, userId = ''): void {
293
- if (!LOG_TO_REMOTE) return;
294
223
  const studioSessionId = getStudioSessionId();
295
224
  _perfLogBuffer.push({
296
225
  operation,
@@ -304,7 +233,6 @@ export function recordPerf(operation: string, durationMs: number, userId = ''):
304
233
 
305
234
  /** Force-flush any buffered perf entries to Postgres. */
306
235
  export async function flushPerf(): Promise<void> {
307
- if (!LOG_TO_REMOTE) return;
308
236
  if (_perfLogBuffer.length === 0) return;
309
237
  const entries = _perfLogBuffer.splice(0, _perfLogBuffer.length);
310
238
  await insertLogRows(
@@ -314,9 +242,8 @@ export async function flushPerf(): Promise<void> {
314
242
  }
315
243
 
316
244
  /**
317
- * Send a feedback entry to the data-proxy. The proxy stamps projectId and
318
- * devTunnelId from the JWT the caller can't spoof identity. All
319
- * descriptive fields default to null when omitted.
245
+ * Persist a feedback entry to the project's own Postgres. All descriptive
246
+ * fields default to null when omitted.
320
247
  */
321
248
  export async function recordFeedback(input: {
322
249
  type: 'bug' | 'design' | 'feature';
@@ -410,9 +337,6 @@ export function createLogger(
410
337
  },
411
338
 
412
339
  async feedback({ type, message, screenshotTempKey }) {
413
- // The data-proxy capture RPC stamps projectId + devTunnelId from the JWT.
414
- // We send a minimal feedbackReport-shaped entry; nullable schema fields
415
- // default to null on the read side.
416
340
  let screenshotUri: string | null = null;
417
341
  if (screenshotTempKey) {
418
342
  try {
@@ -86,6 +86,14 @@ export interface WorkersEnv {
86
86
  */
87
87
  UGLY_APP_AUTH_MODE?: 'uglybot' | 'self';
88
88
 
89
+ /**
90
+ * When `'1'`, magic-link request handler logs the verify URL to the
91
+ * worker console AND returns it inside `{ debug: { link } }` so dev /
92
+ * E2E flows can complete sign-in without a working email proxy.
93
+ * NEVER set in production.
94
+ */
95
+ UGLY_APP_MAGIC_LINK_DEBUG?: string;
96
+
89
97
  // Per-app project id (used to namespace DO ids)
90
98
  PROJECT_ID: string;
91
99
 
@@ -1229,6 +1229,7 @@ function ensureAdapter(env: WorkersEnv): void {
1229
1229
  'SEARCH_PROXY_URL',
1230
1230
  'SEARCH_PROXY_TOKEN',
1231
1231
  'UGLY_APP_AUTH_MODE',
1232
+ 'UGLY_APP_MAGIC_LINK_DEBUG',
1232
1233
  'AUTH_SECRET',
1233
1234
  'DATABASE_URL',
1234
1235
  'PROJECT_ID',