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.
- package/.pgdata/base/16384/pg_internal.init +0 -0
- package/.pgdata/global/pg_control +0 -0
- package/.pgdata/global/pg_internal.init +0 -0
- package/.pgdata/pg_wal/000000010000000000000004 +0 -0
- package/.pgdata/postmaster.pid +2 -2
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +0 -3
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +31 -66
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +8 -66
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +0 -4
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/schemaGen.d.ts.map +1 -1
- package/dist/cli/schemaGen.js +0 -4
- package/dist/cli/schemaGen.js.map +1 -1
- package/dist/cli/schemaStatus.d.ts.map +1 -1
- package/dist/cli/schemaStatus.js +0 -4
- package/dist/cli/schemaStatus.js.map +1 -1
- package/dist/cli/serverLogQueryApi.d.ts +5 -4
- package/dist/cli/serverLogQueryApi.d.ts.map +1 -1
- package/dist/cli/serverLogQueryApi.js +55 -115
- package/dist/cli/serverLogQueryApi.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/auth/MagicLinkCallback.d.ts.map +1 -1
- package/dist/client/auth/MagicLinkCallback.js +8 -2
- package/dist/client/auth/MagicLinkCallback.js.map +1 -1
- package/dist/client/auth/MagicLinkForm.d.ts.map +1 -1
- package/dist/client/auth/MagicLinkForm.js +6 -1
- package/dist/client/auth/MagicLinkForm.js.map +1 -1
- package/dist/server/App.d.ts.map +1 -1
- package/dist/server/App.js +1 -2
- package/dist/server/App.js.map +1 -1
- package/dist/server/Email.d.ts.map +1 -1
- package/dist/server/Email.js +7 -1
- package/dist/server/Email.js.map +1 -1
- package/dist/server/Logging.d.ts +5 -8
- package/dist/server/Logging.d.ts.map +1 -1
- package/dist/server/Logging.js +21 -97
- package/dist/server/Logging.js.map +1 -1
- package/dist/server/adapter/workers/cf-types.d.ts +7 -0
- package/dist/server/adapter/workers/cf-types.d.ts.map +1 -1
- package/dist/server/adapter/workers/createWorkersApp.js +1 -0
- package/dist/server/adapter/workers/createWorkersApp.js.map +1 -1
- package/dist/server/auth/magicLinkProvider.d.ts.map +1 -1
- package/dist/server/auth/magicLinkProvider.js +55 -6
- package/dist/server/auth/magicLinkProvider.js.map +1 -1
- package/dist/server/auth/workersMagicLink.d.ts.map +1 -1
- package/dist/server/auth/workersMagicLink.js +117 -13
- package/dist/server/auth/workersMagicLink.js.map +1 -1
- package/dist/server/bootstrap.d.ts +5 -21
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +5 -27
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/shared/DB.d.ts +0 -9
- package/dist/shared/DB.d.ts.map +1 -1
- package/dist/shared/DB.js +46 -10
- package/dist/shared/DB.js.map +1 -1
- package/dist/vite/index.d.ts +0 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +0 -1
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/dev.ts +0 -4
- package/src/cli/doctor.ts +31 -63
- package/src/cli/index.ts +9 -81
- package/src/cli/migrate.ts +0 -4
- package/src/cli/schemaGen.ts +0 -4
- package/src/cli/schemaStatus.ts +0 -4
- package/src/cli/serverLogQueryApi.ts +53 -136
- package/src/cli/version.ts +1 -1
- package/src/client/auth/MagicLinkCallback.tsx +10 -2
- package/src/client/auth/MagicLinkForm.tsx +7 -1
- package/src/server/App.ts +1 -2
- package/src/server/Email.ts +7 -1
- package/src/server/Logging.ts +21 -97
- package/src/server/adapter/workers/cf-types.ts +8 -0
- package/src/server/adapter/workers/createWorkersApp.ts +1 -0
- package/src/server/auth/magicLinkProvider.ts +61 -6
- package/src/server/auth/workersMagicLink.ts +117 -13
- package/src/server/bootstrap.ts +5 -29
- package/src/shared/DB.ts +46 -17
- package/src/vite/index.ts +0 -1
- package/.pgdata/base/1/pg_internal.init +0 -0
- package/.pgdata/base/17702/pg_internal.init +0 -0
- package/.pgdata/base/5/pg_internal.init +0 -0
- package/dist/cli/devTunnel.d.ts +0 -37
- package/dist/cli/devTunnel.d.ts.map +0 -1
- package/dist/cli/devTunnel.js +0 -470
- package/dist/cli/devTunnel.js.map +0 -1
- package/dist/cli/ensureInfra.d.ts +0 -9
- package/dist/cli/ensureInfra.d.ts.map +0 -1
- package/dist/cli/ensureInfra.js +0 -86
- package/dist/cli/ensureInfra.js.map +0 -1
- package/dist/cli/sidecar.d.ts +0 -21
- package/dist/cli/sidecar.d.ts.map +0 -1
- package/dist/cli/sidecar.js +0 -294
- package/dist/cli/sidecar.js.map +0 -1
- package/dist/cli/sidecarHealth.d.ts +0 -13
- package/dist/cli/sidecarHealth.d.ts.map +0 -1
- package/dist/cli/sidecarHealth.js +0 -42
- package/dist/cli/sidecarHealth.js.map +0 -1
- package/dist/cli/sidecarProtocol.d.ts +0 -41
- package/dist/cli/sidecarProtocol.d.ts.map +0 -1
- package/dist/cli/sidecarProtocol.js +0 -61
- package/dist/cli/sidecarProtocol.js.map +0 -1
- package/dist/vite/tunnelPlugin.d.ts +0 -10
- package/dist/vite/tunnelPlugin.d.ts.map +0 -1
- package/dist/vite/tunnelPlugin.js +0 -30
- package/dist/vite/tunnelPlugin.js.map +0 -1
- package/src/cli/devTunnel.ts +0 -589
- package/src/cli/ensureInfra.ts +0 -90
- package/src/cli/sidecar.ts +0 -350
- package/src/cli/sidecarHealth.ts +0 -58
- package/src/cli/sidecarProtocol.ts +0 -68
- package/src/vite/tunnelPlugin.ts +0 -32
|
@@ -1,68 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
|
95
|
-
const
|
|
96
|
-
const
|
|
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
|
|
103
|
-
const
|
|
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 (
|
|
107
|
-
if (
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
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 =
|
|
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 (
|
|
59
|
+
if (data['resolution']) lines.push(` Resolution: ${data['resolution']}`);
|
|
118
60
|
|
|
119
|
-
const 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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (options.
|
|
161
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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 =
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
}
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
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:
|
|
1605
|
+
plugins: userPlugins,
|
|
1607
1606
|
appType: "custom",
|
|
1608
1607
|
});
|
|
1609
1608
|
expressApp.use(vite.middlewares);
|
package/src/server/Email.ts
CHANGED
|
@@ -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
|
|
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 };
|
package/src/server/Logging.ts
CHANGED
|
@@ -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.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
// ───
|
|
24
|
+
// ─── Log persistence ────────────────────────────────────────────────────────
|
|
26
25
|
//
|
|
27
|
-
// errorLog and
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
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
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
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
|
|
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
|
|
289
|
-
* (or immediately when the buffer reaches 50).
|
|
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
|
-
*
|
|
318
|
-
*
|
|
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
|
|