browser-debug-mcp-bridge 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +101 -0
- package/apps/mcp-server/dist/db/connection.js +42 -0
- package/apps/mcp-server/dist/db/connection.js.map +1 -0
- package/apps/mcp-server/dist/db/error-fingerprints.js +21 -0
- package/apps/mcp-server/dist/db/error-fingerprints.js.map +1 -0
- package/apps/mcp-server/dist/db/events-repository.js +109 -0
- package/apps/mcp-server/dist/db/events-repository.js.map +1 -0
- package/apps/mcp-server/dist/db/index.js +4 -0
- package/apps/mcp-server/dist/db/index.js.map +1 -0
- package/apps/mcp-server/dist/db/migrations.js +101 -0
- package/apps/mcp-server/dist/db/migrations.js.map +1 -0
- package/apps/mcp-server/dist/db/schema.js +157 -0
- package/apps/mcp-server/dist/db/schema.js.map +1 -0
- package/apps/mcp-server/dist/main.js +384 -0
- package/apps/mcp-server/dist/main.js.map +1 -0
- package/apps/mcp-server/dist/mcp/server.js +1619 -0
- package/apps/mcp-server/dist/mcp/server.js.map +1 -0
- package/apps/mcp-server/dist/mcp-bridge.js +55 -0
- package/apps/mcp-server/dist/mcp-bridge.js.map +1 -0
- package/apps/mcp-server/dist/retention.js +841 -0
- package/apps/mcp-server/dist/retention.js.map +1 -0
- package/apps/mcp-server/dist/websocket/index.js +3 -0
- package/apps/mcp-server/dist/websocket/index.js.map +1 -0
- package/apps/mcp-server/dist/websocket/messages.js +150 -0
- package/apps/mcp-server/dist/websocket/messages.js.map +1 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js +302 -0
- package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -0
- package/apps/mcp-server/package.json +28 -0
- package/package.json +88 -0
- package/scripts/mcp-start.cjs +229 -0
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import JSZip from 'jszip';
|
|
4
|
+
const DEFAULT_SETTINGS = {
|
|
5
|
+
retentionDays: 30,
|
|
6
|
+
maxDbMb: 1024,
|
|
7
|
+
maxSessions: 10000,
|
|
8
|
+
cleanupIntervalMinutes: 60,
|
|
9
|
+
lastCleanupAt: null,
|
|
10
|
+
exportPathOverride: null,
|
|
11
|
+
};
|
|
12
|
+
const MAX_SNAPSHOT_DOM_BYTES = 512 * 1024;
|
|
13
|
+
const MAX_SNAPSHOT_STYLES_BYTES = 512 * 1024;
|
|
14
|
+
const MAX_SNAPSHOT_PNG_BYTES = 5 * 1024 * 1024;
|
|
15
|
+
const SNAPSHOT_ASSET_DIR = 'snapshot-assets';
|
|
16
|
+
export function getRetentionSettings(db) {
|
|
17
|
+
let row;
|
|
18
|
+
try {
|
|
19
|
+
row = db
|
|
20
|
+
.prepare(`SELECT retention_days, max_db_mb, max_sessions, cleanup_interval_minutes, last_cleanup_at, export_path_override
|
|
21
|
+
FROM server_settings
|
|
22
|
+
WHERE id = 1`)
|
|
23
|
+
.get();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return DEFAULT_SETTINGS;
|
|
27
|
+
}
|
|
28
|
+
if (!row) {
|
|
29
|
+
return DEFAULT_SETTINGS;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
retentionDays: row.retention_days,
|
|
33
|
+
maxDbMb: row.max_db_mb,
|
|
34
|
+
maxSessions: row.max_sessions,
|
|
35
|
+
cleanupIntervalMinutes: row.cleanup_interval_minutes,
|
|
36
|
+
lastCleanupAt: row.last_cleanup_at,
|
|
37
|
+
exportPathOverride: row.export_path_override,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function updateRetentionSettings(db, input) {
|
|
41
|
+
const current = getRetentionSettings(db);
|
|
42
|
+
const next = {
|
|
43
|
+
...current,
|
|
44
|
+
retentionDays: normalizePositiveInt(input.retentionDays, current.retentionDays, 1, 3650),
|
|
45
|
+
maxDbMb: normalizePositiveInt(input.maxDbMb, current.maxDbMb, 50, 102400),
|
|
46
|
+
maxSessions: normalizePositiveInt(input.maxSessions, current.maxSessions, 100, 1000000),
|
|
47
|
+
cleanupIntervalMinutes: normalizePositiveInt(input.cleanupIntervalMinutes, current.cleanupIntervalMinutes, 5, 1440),
|
|
48
|
+
exportPathOverride: normalizeExportPath(input.exportPathOverride, current.exportPathOverride),
|
|
49
|
+
};
|
|
50
|
+
db.prepare(`UPDATE server_settings
|
|
51
|
+
SET retention_days = ?, max_db_mb = ?, max_sessions = ?, cleanup_interval_minutes = ?, export_path_override = ?
|
|
52
|
+
WHERE id = 1`).run(next.retentionDays, next.maxDbMb, next.maxSessions, next.cleanupIntervalMinutes, next.exportPathOverride);
|
|
53
|
+
return getRetentionSettings(db);
|
|
54
|
+
}
|
|
55
|
+
export function shouldRunCleanup(settings, now = Date.now()) {
|
|
56
|
+
if (settings.lastCleanupAt === null) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return now - settings.lastCleanupAt >= settings.cleanupIntervalMinutes * 60_000;
|
|
60
|
+
}
|
|
61
|
+
export function runRetentionCleanup(db, dbPath, settings, trigger) {
|
|
62
|
+
const beforeMb = getDbSizeMb(dbPath);
|
|
63
|
+
let deletedByAge = 0;
|
|
64
|
+
let deletedByMaxSessions = 0;
|
|
65
|
+
let deletedByDbSize = 0;
|
|
66
|
+
let warning = null;
|
|
67
|
+
const ageThreshold = Date.now() - settings.retentionDays * 24 * 60 * 60 * 1000;
|
|
68
|
+
while (true) {
|
|
69
|
+
const sessionId = getOldestUnpinnedSession(db, 'created_at < ?', [ageThreshold]);
|
|
70
|
+
if (!sessionId) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
deleteSession(db, sessionId);
|
|
74
|
+
deletedByAge += 1;
|
|
75
|
+
}
|
|
76
|
+
while (getSessionCount(db) > settings.maxSessions) {
|
|
77
|
+
const sessionId = getOldestUnpinnedSession(db);
|
|
78
|
+
if (!sessionId) {
|
|
79
|
+
warning = 'Cleanup skipped some records because only pinned sessions remain.';
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
deleteSession(db, sessionId);
|
|
83
|
+
deletedByMaxSessions += 1;
|
|
84
|
+
}
|
|
85
|
+
let capSafety = 0;
|
|
86
|
+
while (getDbSizeMb(dbPath) > settings.maxDbMb) {
|
|
87
|
+
const sessionId = getOldestUnpinnedSession(db);
|
|
88
|
+
if (!sessionId) {
|
|
89
|
+
warning = 'Cleanup skipped some records because only pinned sessions remain.';
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
deleteSession(db, sessionId);
|
|
93
|
+
deletedByDbSize += 1;
|
|
94
|
+
capSafety += 1;
|
|
95
|
+
if (capSafety > 5000) {
|
|
96
|
+
warning = 'Cleanup reached safety stop while enforcing max DB size.';
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (deletedByDbSize > 0) {
|
|
101
|
+
db.exec('VACUUM');
|
|
102
|
+
}
|
|
103
|
+
pruneOrphanedSnapshotAssets(db, dbPath);
|
|
104
|
+
const ranAt = Date.now();
|
|
105
|
+
db.prepare('UPDATE server_settings SET last_cleanup_at = ? WHERE id = 1').run(ranAt);
|
|
106
|
+
const afterMb = getDbSizeMb(dbPath);
|
|
107
|
+
return {
|
|
108
|
+
trigger,
|
|
109
|
+
deletedSessions: deletedByAge + deletedByMaxSessions + deletedByDbSize,
|
|
110
|
+
deletedByAge,
|
|
111
|
+
deletedByMaxSessions,
|
|
112
|
+
deletedByDbSize,
|
|
113
|
+
pinnedProtected: warning !== null,
|
|
114
|
+
dbSizeBeforeMb: beforeMb,
|
|
115
|
+
dbSizeAfterMb: afterMb,
|
|
116
|
+
warning,
|
|
117
|
+
ranAt,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function writeSnapshot(db, dbPath, sessionId, input, triggerEventId = null) {
|
|
121
|
+
const session = db.prepare('SELECT 1 FROM sessions WHERE session_id = ?').get(sessionId);
|
|
122
|
+
if (!session) {
|
|
123
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
124
|
+
}
|
|
125
|
+
const timestamp = asTimestamp(input.timestamp, Date.now());
|
|
126
|
+
const createdAt = Date.now();
|
|
127
|
+
const snapshotId = `${sessionId}-snapshot-${timestamp}-${Math.random().toString(36).slice(2, 10)}`;
|
|
128
|
+
const trigger = normalizeSnapshotTrigger(input.trigger);
|
|
129
|
+
const selector = typeof input.selector === 'string' ? input.selector : null;
|
|
130
|
+
const url = typeof input.url === 'string' ? input.url : null;
|
|
131
|
+
const mode = normalizeSnapshotMode(input.mode);
|
|
132
|
+
const styleMode = normalizeStyleMode(input.mode);
|
|
133
|
+
const domJson = serializeBounded(input.snapshot?.dom, MAX_SNAPSHOT_DOM_BYTES, 'dom');
|
|
134
|
+
const stylesJson = serializeBounded(input.snapshot?.styles, MAX_SNAPSHOT_STYLES_BYTES, 'styles');
|
|
135
|
+
const domTruncated = Boolean(input.truncation?.dom);
|
|
136
|
+
const stylesTruncated = Boolean(input.truncation?.styles);
|
|
137
|
+
const pngTruncated = Boolean(input.truncation?.png);
|
|
138
|
+
const pngWrite = maybePersistPng(dbPath, sessionId, snapshotId, input.png);
|
|
139
|
+
db.prepare(`INSERT INTO snapshots (
|
|
140
|
+
snapshot_id, session_id, trigger_event_id, ts, trigger, selector, url, mode, style_mode,
|
|
141
|
+
dom_json, styles_json, png_path, png_mime, png_bytes,
|
|
142
|
+
dom_truncated, styles_truncated, png_truncated, created_at
|
|
143
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(snapshotId, sessionId, triggerEventId, timestamp, trigger, selector, url, mode, styleMode, domJson, stylesJson, pngWrite.relativePath, pngWrite.mime, pngWrite.byteLength, domTruncated ? 1 : 0, stylesTruncated ? 1 : 0, pngTruncated ? 1 : 0, createdAt);
|
|
144
|
+
return { snapshotId };
|
|
145
|
+
}
|
|
146
|
+
export function listSnapshots(db, sessionId, limitInput, offsetInput) {
|
|
147
|
+
const limit = normalizePositiveInt(limitInput, 50, 1, 200);
|
|
148
|
+
const offset = normalizePositiveInt(offsetInput, 0, 0, 1_000_000);
|
|
149
|
+
const rows = db.prepare(`SELECT
|
|
150
|
+
snapshot_id, session_id, ts, trigger, selector, url, mode, style_mode,
|
|
151
|
+
dom_json, styles_json, png_path, png_mime, png_bytes,
|
|
152
|
+
dom_truncated, styles_truncated, png_truncated, created_at
|
|
153
|
+
FROM snapshots
|
|
154
|
+
WHERE session_id = ?
|
|
155
|
+
ORDER BY ts DESC
|
|
156
|
+
LIMIT ? OFFSET ?`).all(sessionId, limit + 1, offset);
|
|
157
|
+
const hasMore = rows.length > limit;
|
|
158
|
+
const page = rows.slice(0, limit);
|
|
159
|
+
return {
|
|
160
|
+
limit,
|
|
161
|
+
offset,
|
|
162
|
+
hasMore,
|
|
163
|
+
nextOffset: hasMore ? offset + limit : null,
|
|
164
|
+
snapshots: page.map((row) => ({
|
|
165
|
+
snapshotId: row.snapshot_id,
|
|
166
|
+
sessionId: row.session_id,
|
|
167
|
+
timestamp: row.ts,
|
|
168
|
+
trigger: row.trigger,
|
|
169
|
+
selector: row.selector,
|
|
170
|
+
url: row.url,
|
|
171
|
+
mode: row.mode,
|
|
172
|
+
styleMode: row.style_mode,
|
|
173
|
+
dom: parseJsonOrNull(row.dom_json),
|
|
174
|
+
styles: parseJsonOrNull(row.styles_json),
|
|
175
|
+
pngPath: row.png_path,
|
|
176
|
+
pngMime: row.png_mime,
|
|
177
|
+
pngBytes: row.png_bytes,
|
|
178
|
+
truncation: {
|
|
179
|
+
dom: row.dom_truncated === 1,
|
|
180
|
+
styles: row.styles_truncated === 1,
|
|
181
|
+
png: row.png_truncated === 1,
|
|
182
|
+
},
|
|
183
|
+
createdAt: row.created_at,
|
|
184
|
+
})),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export function pruneOrphanedSnapshotAssets(db, dbPath) {
|
|
188
|
+
const assetRoot = getSnapshotAssetsRoot(dbPath);
|
|
189
|
+
if (!existsSync(assetRoot)) {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
const referencedPaths = new Set(db.prepare('SELECT png_path FROM snapshots WHERE png_path IS NOT NULL').all().map((row) => normalizeAssetPath(row.png_path)));
|
|
193
|
+
const files = collectFiles(assetRoot);
|
|
194
|
+
let removed = 0;
|
|
195
|
+
for (const filePath of files) {
|
|
196
|
+
const relativePath = normalizeAssetPath(filePath.slice(assetRoot.length + 1));
|
|
197
|
+
if (referencedPaths.has(relativePath)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
rmSync(filePath, { force: true });
|
|
201
|
+
removed += 1;
|
|
202
|
+
}
|
|
203
|
+
return removed;
|
|
204
|
+
}
|
|
205
|
+
export function setSessionPinned(db, sessionId, pinned) {
|
|
206
|
+
const result = db.prepare('UPDATE sessions SET pinned = ? WHERE session_id = ?').run(pinned ? 1 : 0, sessionId);
|
|
207
|
+
return result.changes > 0;
|
|
208
|
+
}
|
|
209
|
+
export function exportSessionToJson(db, dbPath, sessionId, projectRoot, exportPathOverride, options = {}) {
|
|
210
|
+
const compatibilityMode = options.compatibilityMode !== false;
|
|
211
|
+
const payload = buildSessionExportPayload(db, sessionId, dbPath, {
|
|
212
|
+
compatibilityMode,
|
|
213
|
+
includePngBase64: options.includePngBase64 === true,
|
|
214
|
+
});
|
|
215
|
+
const baseDir = exportPathOverride && exportPathOverride.trim().length > 0
|
|
216
|
+
? resolve(exportPathOverride)
|
|
217
|
+
: resolve(join(projectRoot, 'exports'));
|
|
218
|
+
mkdirSync(baseDir, { recursive: true });
|
|
219
|
+
const safeSessionId = sessionId.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
220
|
+
const filePath = join(baseDir, `${safeSessionId}.json`);
|
|
221
|
+
writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
222
|
+
return {
|
|
223
|
+
filePath,
|
|
224
|
+
format: 'json',
|
|
225
|
+
compatibilityMode,
|
|
226
|
+
events: payload.events.length,
|
|
227
|
+
network: payload.network.length,
|
|
228
|
+
fingerprints: payload.fingerprints.length,
|
|
229
|
+
snapshots: payload.snapshots.length,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export async function exportSessionToZip(db, dbPath, sessionId, projectRoot, exportPathOverride) {
|
|
233
|
+
const payload = buildSessionExportPayload(db, sessionId, dbPath, {
|
|
234
|
+
compatibilityMode: false,
|
|
235
|
+
includePngBase64: false,
|
|
236
|
+
});
|
|
237
|
+
const zip = new JSZip();
|
|
238
|
+
zip.file('manifest.json', JSON.stringify(payload, null, 2));
|
|
239
|
+
for (const snapshot of payload.snapshots) {
|
|
240
|
+
const assetPath = snapshot.png.assetPath;
|
|
241
|
+
if (!assetPath) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const absolutePath = resolve(join(resolve(dbPath, '..'), assetPath));
|
|
245
|
+
if (!existsSync(absolutePath)) {
|
|
246
|
+
throw new Error(`Snapshot export failed: missing asset file ${assetPath}.`);
|
|
247
|
+
}
|
|
248
|
+
const buffer = readFileSync(absolutePath);
|
|
249
|
+
assertPngBuffer(buffer, `Snapshot export failed: corrupt PNG asset ${assetPath}.`);
|
|
250
|
+
zip.file(assetPath, buffer);
|
|
251
|
+
}
|
|
252
|
+
const baseDir = exportPathOverride && exportPathOverride.trim().length > 0
|
|
253
|
+
? resolve(exportPathOverride)
|
|
254
|
+
: resolve(join(projectRoot, 'exports'));
|
|
255
|
+
mkdirSync(baseDir, { recursive: true });
|
|
256
|
+
const safeSessionId = sessionId.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
257
|
+
const filePath = join(baseDir, `${safeSessionId}.zip`);
|
|
258
|
+
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
259
|
+
writeFileSync(filePath, zipBuffer);
|
|
260
|
+
return {
|
|
261
|
+
filePath,
|
|
262
|
+
format: 'zip',
|
|
263
|
+
compatibilityMode: false,
|
|
264
|
+
events: payload.events.length,
|
|
265
|
+
network: payload.network.length,
|
|
266
|
+
fingerprints: payload.fingerprints.length,
|
|
267
|
+
snapshots: payload.snapshots.length,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
export async function importSessionFromZipBase64(db, dbPath, archiveBase64) {
|
|
271
|
+
const archive = Buffer.from(archiveBase64, 'base64');
|
|
272
|
+
if (archive.byteLength === 0) {
|
|
273
|
+
throw new Error('Import archive is empty or invalid base64.');
|
|
274
|
+
}
|
|
275
|
+
const zip = await JSZip.loadAsync(archive);
|
|
276
|
+
const manifestEntry = zip.file('manifest.json');
|
|
277
|
+
if (!manifestEntry) {
|
|
278
|
+
throw new Error('Import archive missing manifest.json.');
|
|
279
|
+
}
|
|
280
|
+
const manifestText = await manifestEntry.async('text');
|
|
281
|
+
let payload;
|
|
282
|
+
try {
|
|
283
|
+
payload = JSON.parse(manifestText);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
throw new Error('Import archive manifest.json is invalid JSON.');
|
|
287
|
+
}
|
|
288
|
+
const snapshotAssets = new Map();
|
|
289
|
+
const root = asObject(payload, 'Import payload must be an object');
|
|
290
|
+
const snapshotsValue = root.snapshots;
|
|
291
|
+
if (Array.isArray(snapshotsValue)) {
|
|
292
|
+
for (let i = 0; i < snapshotsValue.length; i += 1) {
|
|
293
|
+
const snapshot = asObject(snapshotsValue[i], `Snapshot at index ${i} must be an object`);
|
|
294
|
+
const png = snapshot.png;
|
|
295
|
+
if (!png || typeof png !== 'object' || Array.isArray(png)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const assetPath = asNullableString(png.assetPath);
|
|
299
|
+
if (!assetPath) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const normalizedPath = normalizeAssetPath(assetPath);
|
|
303
|
+
const assetEntry = zip.file(normalizedPath);
|
|
304
|
+
if (!assetEntry) {
|
|
305
|
+
throw new Error(`Import archive missing PNG asset ${normalizedPath}.`);
|
|
306
|
+
}
|
|
307
|
+
const buffer = await assetEntry.async('nodebuffer');
|
|
308
|
+
assertPngBuffer(buffer, `Import archive contains corrupt PNG asset ${normalizedPath}.`);
|
|
309
|
+
snapshotAssets.set(normalizedPath, buffer);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return importSessionFromJson(db, payload, { dbPath, snapshotAssets });
|
|
313
|
+
}
|
|
314
|
+
export function importSessionFromJson(db, payload, options = {}) {
|
|
315
|
+
const parsed = normalizeImportPayload(payload);
|
|
316
|
+
const requestedSessionId = parsed.requestedSessionId;
|
|
317
|
+
const sessionId = resolveImportedSessionId(db, requestedSessionId);
|
|
318
|
+
const remappedSessionId = sessionId !== requestedSessionId;
|
|
319
|
+
const importedAt = Date.now();
|
|
320
|
+
const insertSession = db.prepare(`INSERT INTO sessions (
|
|
321
|
+
session_id, created_at, ended_at, tab_id, window_id, url_start, url_last,
|
|
322
|
+
user_agent, viewport_w, viewport_h, dpr, safe_mode, allowlist_hash, pinned
|
|
323
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
324
|
+
const insertEvent = db.prepare(`INSERT INTO events (event_id, session_id, ts, type, payload_json)
|
|
325
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
326
|
+
const insertNetwork = db.prepare(`INSERT INTO network (
|
|
327
|
+
request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class, response_size_est
|
|
328
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
329
|
+
const insertFingerprint = db.prepare(`INSERT INTO error_fingerprints (
|
|
330
|
+
fingerprint, session_id, count, sample_message, sample_stack, first_seen_at, last_seen_at
|
|
331
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
332
|
+
const insertSnapshot = db.prepare(`INSERT INTO snapshots (
|
|
333
|
+
snapshot_id, session_id, trigger_event_id, ts, trigger, selector, url, mode, style_mode,
|
|
334
|
+
dom_json, styles_json, png_path, png_mime, png_bytes,
|
|
335
|
+
dom_truncated, styles_truncated, png_truncated, created_at
|
|
336
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
337
|
+
const runImport = db.transaction(() => {
|
|
338
|
+
insertSession.run(sessionId, parsed.session.createdAt, parsed.session.endedAt, parsed.session.tabId, parsed.session.windowId, parsed.session.urlStart, parsed.session.urlLast, parsed.session.userAgent, parsed.session.viewportW, parsed.session.viewportH, parsed.session.dpr, parsed.session.safeMode, parsed.session.allowlistHash, parsed.session.pinned);
|
|
339
|
+
for (let i = 0; i < parsed.events.length; i += 1) {
|
|
340
|
+
const row = parsed.events[i];
|
|
341
|
+
const eventId = `${sessionId}-import-event-${importedAt}-${i}`;
|
|
342
|
+
insertEvent.run(eventId, sessionId, row.ts, row.type, row.payloadJson);
|
|
343
|
+
}
|
|
344
|
+
for (let i = 0; i < parsed.network.length; i += 1) {
|
|
345
|
+
const row = parsed.network[i];
|
|
346
|
+
const requestId = `${sessionId}-import-network-${importedAt}-${i}`;
|
|
347
|
+
insertNetwork.run(requestId, sessionId, row.tsStart, row.durationMs, row.method, row.url, row.status, row.initiator, row.errorClass, row.responseSizeEst);
|
|
348
|
+
}
|
|
349
|
+
for (let i = 0; i < parsed.fingerprints.length; i += 1) {
|
|
350
|
+
const row = parsed.fingerprints[i];
|
|
351
|
+
const fingerprint = `${sessionId}::${row.fingerprint}`;
|
|
352
|
+
insertFingerprint.run(fingerprint, sessionId, row.count, row.sampleMessage, row.sampleStack, row.firstSeenAt, row.lastSeenAt);
|
|
353
|
+
}
|
|
354
|
+
const sortedSnapshots = [...parsed.snapshots].sort((a, b) => a.timestamp - b.timestamp);
|
|
355
|
+
for (let i = 0; i < sortedSnapshots.length; i += 1) {
|
|
356
|
+
const row = sortedSnapshots[i];
|
|
357
|
+
const snapshotId = `${sessionId}-import-snapshot-${importedAt}-${i}`;
|
|
358
|
+
let pngPath = null;
|
|
359
|
+
let pngMime = null;
|
|
360
|
+
let pngBytes = null;
|
|
361
|
+
if (row.png.assetPath || row.png.base64) {
|
|
362
|
+
if (!options.dbPath) {
|
|
363
|
+
throw new Error('Snapshot import requires dbPath when snapshot PNG data is present.');
|
|
364
|
+
}
|
|
365
|
+
let pngBuffer = null;
|
|
366
|
+
if (row.png.assetPath) {
|
|
367
|
+
const normalizedPath = normalizeAssetPath(row.png.assetPath);
|
|
368
|
+
const fromArchive = options.snapshotAssets?.get(normalizedPath);
|
|
369
|
+
if (!fromArchive) {
|
|
370
|
+
throw new Error(`Import payload references missing PNG asset ${normalizedPath}.`);
|
|
371
|
+
}
|
|
372
|
+
pngBuffer = fromArchive;
|
|
373
|
+
}
|
|
374
|
+
else if (row.png.base64) {
|
|
375
|
+
pngBuffer = Buffer.from(row.png.base64, 'base64');
|
|
376
|
+
}
|
|
377
|
+
if (!pngBuffer || pngBuffer.byteLength === 0) {
|
|
378
|
+
throw new Error('Snapshot PNG payload is missing or invalid.');
|
|
379
|
+
}
|
|
380
|
+
assertPngBuffer(pngBuffer, 'Snapshot PNG payload is corrupt.');
|
|
381
|
+
const persisted = persistSnapshotPngBuffer(options.dbPath, sessionId, snapshotId, pngBuffer);
|
|
382
|
+
pngPath = persisted.relativePath;
|
|
383
|
+
pngMime = persisted.mime;
|
|
384
|
+
pngBytes = persisted.byteLength;
|
|
385
|
+
}
|
|
386
|
+
insertSnapshot.run(snapshotId, sessionId, null, row.timestamp, row.trigger, row.selector, row.url, row.mode, row.styleMode, row.domJson, row.stylesJson, pngPath, pngMime, pngBytes, row.truncation.dom ? 1 : 0, row.truncation.styles ? 1 : 0, row.truncation.png ? 1 : 0, row.createdAt);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
runImport();
|
|
390
|
+
return {
|
|
391
|
+
sessionId,
|
|
392
|
+
requestedSessionId,
|
|
393
|
+
remappedSessionId,
|
|
394
|
+
events: parsed.events.length,
|
|
395
|
+
network: parsed.network.length,
|
|
396
|
+
fingerprints: parsed.fingerprints.length,
|
|
397
|
+
snapshots: parsed.snapshots.length,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function buildSessionExportPayload(db, sessionId, dbPath, options) {
|
|
401
|
+
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId);
|
|
402
|
+
if (!session) {
|
|
403
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
404
|
+
}
|
|
405
|
+
const events = db.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY ts ASC').all(sessionId);
|
|
406
|
+
const network = db.prepare('SELECT * FROM network WHERE session_id = ? ORDER BY ts_start ASC').all(sessionId);
|
|
407
|
+
const fingerprints = db
|
|
408
|
+
.prepare('SELECT * FROM error_fingerprints WHERE session_id = ? ORDER BY count DESC, last_seen_at DESC')
|
|
409
|
+
.all(sessionId);
|
|
410
|
+
const rows = db.prepare(`SELECT
|
|
411
|
+
snapshot_id, session_id, trigger_event_id, ts, trigger, selector, url, mode, style_mode,
|
|
412
|
+
dom_json, styles_json, png_path, png_mime, png_bytes,
|
|
413
|
+
dom_truncated, styles_truncated, png_truncated, created_at
|
|
414
|
+
FROM snapshots
|
|
415
|
+
WHERE session_id = ?
|
|
416
|
+
ORDER BY ts ASC, created_at ASC`).all(sessionId);
|
|
417
|
+
const snapshots = rows.map((row) => {
|
|
418
|
+
let pngBase64;
|
|
419
|
+
if (options.includePngBase64 && row.png_path) {
|
|
420
|
+
const absolutePath = resolve(join(resolve(dbPath, '..'), row.png_path));
|
|
421
|
+
if (!existsSync(absolutePath)) {
|
|
422
|
+
throw new Error(`Snapshot export failed: missing asset file ${row.png_path}.`);
|
|
423
|
+
}
|
|
424
|
+
const pngBuffer = readFileSync(absolutePath);
|
|
425
|
+
assertPngBuffer(pngBuffer, `Snapshot export failed: corrupt PNG asset ${row.png_path}.`);
|
|
426
|
+
pngBase64 = pngBuffer.toString('base64');
|
|
427
|
+
}
|
|
428
|
+
const png = {
|
|
429
|
+
path: row.png_path,
|
|
430
|
+
mime: row.png_mime,
|
|
431
|
+
bytes: row.png_bytes,
|
|
432
|
+
};
|
|
433
|
+
if (options.compatibilityMode) {
|
|
434
|
+
if (pngBase64) {
|
|
435
|
+
png.base64 = pngBase64;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else if (row.png_path) {
|
|
439
|
+
png.assetPath = normalizeAssetPath(row.png_path);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
snapshotId: row.snapshot_id,
|
|
443
|
+
sessionId: row.session_id,
|
|
444
|
+
triggerEventId: row.trigger_event_id,
|
|
445
|
+
timestamp: row.ts,
|
|
446
|
+
trigger: row.trigger,
|
|
447
|
+
selector: row.selector,
|
|
448
|
+
url: row.url,
|
|
449
|
+
mode: row.mode,
|
|
450
|
+
styleMode: row.style_mode,
|
|
451
|
+
dom: parseJsonOrNull(row.dom_json),
|
|
452
|
+
styles: parseJsonOrNull(row.styles_json),
|
|
453
|
+
truncation: {
|
|
454
|
+
dom: row.dom_truncated === 1,
|
|
455
|
+
styles: row.styles_truncated === 1,
|
|
456
|
+
png: row.png_truncated === 1,
|
|
457
|
+
},
|
|
458
|
+
createdAt: row.created_at,
|
|
459
|
+
png,
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
exportedAt: new Date().toISOString(),
|
|
464
|
+
session,
|
|
465
|
+
events,
|
|
466
|
+
network,
|
|
467
|
+
fingerprints,
|
|
468
|
+
snapshots,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function assertPngBuffer(buffer, errorPrefix) {
|
|
472
|
+
if (buffer.byteLength === 0) {
|
|
473
|
+
throw new Error(errorPrefix);
|
|
474
|
+
}
|
|
475
|
+
if (buffer.byteLength > MAX_SNAPSHOT_PNG_BYTES) {
|
|
476
|
+
throw new Error(`${errorPrefix} PNG exceeds ${MAX_SNAPSHOT_PNG_BYTES} bytes.`);
|
|
477
|
+
}
|
|
478
|
+
const pngSignature = '89504e470d0a1a0a';
|
|
479
|
+
if (buffer.subarray(0, 8).toString('hex') !== pngSignature) {
|
|
480
|
+
throw new Error(errorPrefix);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function persistSnapshotPngBuffer(dbPath, sessionId, snapshotId, buffer) {
|
|
484
|
+
if (buffer.byteLength > MAX_SNAPSHOT_PNG_BYTES) {
|
|
485
|
+
throw new Error(`Snapshot png payload exceeds max bytes (${buffer.byteLength} > ${MAX_SNAPSHOT_PNG_BYTES}).`);
|
|
486
|
+
}
|
|
487
|
+
const safeSession = sessionId.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
488
|
+
const relativePath = normalizeAssetPath(join(SNAPSHOT_ASSET_DIR, safeSession, `${snapshotId}.png`));
|
|
489
|
+
const absolutePath = resolve(join(resolve(dbPath, '..'), relativePath));
|
|
490
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
491
|
+
writeFileSync(absolutePath, buffer);
|
|
492
|
+
return {
|
|
493
|
+
relativePath,
|
|
494
|
+
mime: 'image/png',
|
|
495
|
+
byteLength: buffer.byteLength,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function normalizePositiveInt(value, fallback, min, max) {
|
|
499
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
500
|
+
return fallback;
|
|
501
|
+
}
|
|
502
|
+
const num = Math.floor(value);
|
|
503
|
+
if (num < min) {
|
|
504
|
+
return min;
|
|
505
|
+
}
|
|
506
|
+
if (num > max) {
|
|
507
|
+
return max;
|
|
508
|
+
}
|
|
509
|
+
return num;
|
|
510
|
+
}
|
|
511
|
+
function normalizeSnapshotTrigger(value) {
|
|
512
|
+
if (value === 'click' || value === 'manual' || value === 'navigation' || value === 'error') {
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
return 'manual';
|
|
516
|
+
}
|
|
517
|
+
function normalizeSnapshotMode(value) {
|
|
518
|
+
const mode = value;
|
|
519
|
+
const dom = Boolean(mode?.dom);
|
|
520
|
+
const png = Boolean(mode?.png);
|
|
521
|
+
if (dom && png) {
|
|
522
|
+
return 'both';
|
|
523
|
+
}
|
|
524
|
+
if (png) {
|
|
525
|
+
return 'png';
|
|
526
|
+
}
|
|
527
|
+
return 'dom';
|
|
528
|
+
}
|
|
529
|
+
function normalizeStyleMode(value) {
|
|
530
|
+
const mode = value;
|
|
531
|
+
return mode?.styleMode === 'computed-full' ? 'computed-full' : 'computed-lite';
|
|
532
|
+
}
|
|
533
|
+
function serializeBounded(value, maxBytes, label) {
|
|
534
|
+
if (value === undefined || value === null) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const text = JSON.stringify(value);
|
|
538
|
+
const bytes = Buffer.byteLength(text, 'utf-8');
|
|
539
|
+
if (bytes > maxBytes) {
|
|
540
|
+
throw new Error(`Snapshot ${label} payload exceeds max bytes (${bytes} > ${maxBytes}).`);
|
|
541
|
+
}
|
|
542
|
+
return text;
|
|
543
|
+
}
|
|
544
|
+
function maybePersistPng(dbPath, sessionId, snapshotId, input) {
|
|
545
|
+
if (!input || input.captured !== true || typeof input.dataUrl !== 'string') {
|
|
546
|
+
return { relativePath: null, mime: null, byteLength: null };
|
|
547
|
+
}
|
|
548
|
+
const match = /^data:(image\/png);base64,(.+)$/u.exec(input.dataUrl);
|
|
549
|
+
if (!match) {
|
|
550
|
+
throw new Error('Snapshot png payload must be a PNG data URL.');
|
|
551
|
+
}
|
|
552
|
+
const mime = match[1] ?? 'image/png';
|
|
553
|
+
const base64 = match[2] ?? '';
|
|
554
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
555
|
+
if (buffer.byteLength > MAX_SNAPSHOT_PNG_BYTES) {
|
|
556
|
+
throw new Error(`Snapshot png payload exceeds max bytes (${buffer.byteLength} > ${MAX_SNAPSHOT_PNG_BYTES}).`);
|
|
557
|
+
}
|
|
558
|
+
const safeSession = sessionId.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
559
|
+
const relativePath = normalizeAssetPath(join(SNAPSHOT_ASSET_DIR, safeSession, `${snapshotId}.png`));
|
|
560
|
+
const absolutePath = resolve(join(resolve(dbPath, '..'), relativePath));
|
|
561
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
562
|
+
writeFileSync(absolutePath, buffer);
|
|
563
|
+
return {
|
|
564
|
+
relativePath,
|
|
565
|
+
mime,
|
|
566
|
+
byteLength: buffer.byteLength,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function parseJsonOrNull(value) {
|
|
570
|
+
if (!value) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
return JSON.parse(value);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function getSnapshotAssetsRoot(dbPath) {
|
|
581
|
+
return resolve(join(resolve(dbPath, '..'), SNAPSHOT_ASSET_DIR));
|
|
582
|
+
}
|
|
583
|
+
function normalizeAssetPath(pathValue) {
|
|
584
|
+
return pathValue.replace(/\\/g, '/');
|
|
585
|
+
}
|
|
586
|
+
function collectFiles(root) {
|
|
587
|
+
const entries = readdirSync(root, { withFileTypes: true });
|
|
588
|
+
const files = [];
|
|
589
|
+
for (const entry of entries) {
|
|
590
|
+
const fullPath = join(root, entry.name);
|
|
591
|
+
if (entry.isDirectory()) {
|
|
592
|
+
files.push(...collectFiles(fullPath));
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
files.push(fullPath);
|
|
596
|
+
}
|
|
597
|
+
return files;
|
|
598
|
+
}
|
|
599
|
+
function normalizeExportPath(value, fallback) {
|
|
600
|
+
if (value === null) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
if (typeof value !== 'string') {
|
|
604
|
+
return fallback;
|
|
605
|
+
}
|
|
606
|
+
const trimmed = value.trim();
|
|
607
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
608
|
+
}
|
|
609
|
+
function normalizeImportPayload(payload) {
|
|
610
|
+
const root = asObject(payload, 'Import payload must be an object');
|
|
611
|
+
const sessionRoot = asObject(root.session, 'Import payload must include a session object');
|
|
612
|
+
const requestedSessionId = asNonEmptyString(sessionRoot.session_id ?? sessionRoot.sessionId, 'Import payload missing session_id');
|
|
613
|
+
const createdAt = asTimestamp(sessionRoot.created_at ?? sessionRoot.createdAt, Date.now());
|
|
614
|
+
const endedAt = asNullableTimestamp(sessionRoot.ended_at ?? sessionRoot.endedAt);
|
|
615
|
+
const rawEvents = asArray(root.events, 'Import payload events must be an array');
|
|
616
|
+
const rawNetwork = asArray(root.network, 'Import payload network must be an array');
|
|
617
|
+
const rawFingerprints = asArray(root.fingerprints, 'Import payload fingerprints must be an array');
|
|
618
|
+
const rawSnapshots = root.snapshots === undefined ? [] : asArray(root.snapshots, 'Import payload snapshots must be an array');
|
|
619
|
+
if (rawEvents.length > 100_000 || rawNetwork.length > 100_000 || rawFingerprints.length > 100_000) {
|
|
620
|
+
throw new Error('Import payload exceeds record limit (100000 per section)');
|
|
621
|
+
}
|
|
622
|
+
const allowedEventTypes = new Set(['console', 'error', 'network', 'nav', 'ui', 'element_ref']);
|
|
623
|
+
const allowedInitiators = new Set(['fetch', 'xhr', 'img', 'script', 'other']);
|
|
624
|
+
const allowedErrorClasses = new Set(['timeout', 'cors', 'dns', 'blocked', 'http_error', 'unknown']);
|
|
625
|
+
const events = rawEvents.map((entry, index) => {
|
|
626
|
+
const event = asObject(entry, `Event at index ${index} must be an object`);
|
|
627
|
+
const ts = asTimestamp(event.ts ?? event.timestamp, createdAt);
|
|
628
|
+
const rawType = asString(event.type, 'ui');
|
|
629
|
+
const type = allowedEventTypes.has(rawType) ? rawType : 'ui';
|
|
630
|
+
const payloadJson = toJsonString(event.payload_json ?? event.payload ?? {});
|
|
631
|
+
return { ts, type, payloadJson };
|
|
632
|
+
});
|
|
633
|
+
const network = rawNetwork.map((entry, index) => {
|
|
634
|
+
const row = asObject(entry, `Network row at index ${index} must be an object`);
|
|
635
|
+
const tsStart = asTimestamp(row.ts_start ?? row.tsStart ?? row.timestamp, createdAt);
|
|
636
|
+
const method = asString(row.method, 'GET') || 'GET';
|
|
637
|
+
const url = asString(row.url, '');
|
|
638
|
+
const initiatorCandidate = asNullableString(row.initiator);
|
|
639
|
+
const errorClassCandidate = asNullableString(row.error_class ?? row.errorClass);
|
|
640
|
+
return {
|
|
641
|
+
tsStart,
|
|
642
|
+
durationMs: asNullableInteger(row.duration_ms ?? row.durationMs),
|
|
643
|
+
method,
|
|
644
|
+
url,
|
|
645
|
+
status: asNullableInteger(row.status),
|
|
646
|
+
initiator: initiatorCandidate && allowedInitiators.has(initiatorCandidate) ? initiatorCandidate : null,
|
|
647
|
+
errorClass: errorClassCandidate && allowedErrorClasses.has(errorClassCandidate) ? errorClassCandidate : null,
|
|
648
|
+
responseSizeEst: asNullableInteger(row.response_size_est ?? row.responseSizeEst),
|
|
649
|
+
};
|
|
650
|
+
});
|
|
651
|
+
const fingerprints = rawFingerprints.map((entry, index) => {
|
|
652
|
+
const row = asObject(entry, `Fingerprint row at index ${index} must be an object`);
|
|
653
|
+
const rawFingerprint = asString(row.fingerprint, `imported-${index}`) || `imported-${index}`;
|
|
654
|
+
return {
|
|
655
|
+
fingerprint: rawFingerprint,
|
|
656
|
+
count: Math.max(1, asInteger(row.count, 1)),
|
|
657
|
+
sampleMessage: asString(row.sample_message ?? row.sampleMessage, 'Imported error fingerprint') || 'Imported error fingerprint',
|
|
658
|
+
sampleStack: asNullableString(row.sample_stack ?? row.sampleStack),
|
|
659
|
+
firstSeenAt: asTimestamp(row.first_seen_at ?? row.firstSeenAt, createdAt),
|
|
660
|
+
lastSeenAt: asTimestamp(row.last_seen_at ?? row.lastSeenAt, createdAt),
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
const snapshots = rawSnapshots.map((entry, index) => {
|
|
664
|
+
const row = asObject(entry, `Snapshot at index ${index} must be an object`);
|
|
665
|
+
const timestamp = asTimestamp(row.ts ?? row.timestamp, createdAt);
|
|
666
|
+
const trigger = normalizeSnapshotTrigger(row.trigger);
|
|
667
|
+
const selector = asNullableString(row.selector);
|
|
668
|
+
const url = asNullableString(row.url);
|
|
669
|
+
const mode = normalizeSnapshotMode(row.mode);
|
|
670
|
+
const styleMode = normalizeStyleMode({ styleMode: row.style_mode ?? row.styleMode });
|
|
671
|
+
const domJson = toNullableJsonString(row.dom_json ?? row.dom);
|
|
672
|
+
const stylesJson = toNullableJsonString(row.styles_json ?? row.styles);
|
|
673
|
+
const pngRoot = row.png && typeof row.png === 'object' && !Array.isArray(row.png)
|
|
674
|
+
? row.png
|
|
675
|
+
: {};
|
|
676
|
+
const assetPath = asNullableString(pngRoot.assetPath ?? row.png_asset_path);
|
|
677
|
+
const base64 = asNullableString(pngRoot.base64 ?? row.png_base64);
|
|
678
|
+
return {
|
|
679
|
+
timestamp,
|
|
680
|
+
trigger,
|
|
681
|
+
selector,
|
|
682
|
+
url,
|
|
683
|
+
mode,
|
|
684
|
+
styleMode,
|
|
685
|
+
domJson,
|
|
686
|
+
stylesJson,
|
|
687
|
+
truncation: {
|
|
688
|
+
dom: Boolean(row.dom_truncated ?? row.truncation?.dom),
|
|
689
|
+
styles: Boolean(row.styles_truncated ?? row.truncation?.styles),
|
|
690
|
+
png: Boolean(row.png_truncated ?? row.truncation?.png),
|
|
691
|
+
},
|
|
692
|
+
createdAt: asTimestamp(row.created_at ?? row.createdAt, createdAt),
|
|
693
|
+
png: {
|
|
694
|
+
assetPath,
|
|
695
|
+
base64,
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
return {
|
|
700
|
+
requestedSessionId,
|
|
701
|
+
session: {
|
|
702
|
+
createdAt,
|
|
703
|
+
endedAt,
|
|
704
|
+
tabId: asNullableInteger(sessionRoot.tab_id ?? sessionRoot.tabId),
|
|
705
|
+
windowId: asNullableInteger(sessionRoot.window_id ?? sessionRoot.windowId),
|
|
706
|
+
urlStart: asNullableString(sessionRoot.url_start ?? sessionRoot.urlStart),
|
|
707
|
+
urlLast: asNullableString(sessionRoot.url_last ?? sessionRoot.urlLast),
|
|
708
|
+
userAgent: asNullableString(sessionRoot.user_agent ?? sessionRoot.userAgent),
|
|
709
|
+
viewportW: asNullableInteger(sessionRoot.viewport_w ?? sessionRoot.viewportW),
|
|
710
|
+
viewportH: asNullableInteger(sessionRoot.viewport_h ?? sessionRoot.viewportH),
|
|
711
|
+
dpr: asNullableNumber(sessionRoot.dpr),
|
|
712
|
+
safeMode: asBooleanInt(sessionRoot.safe_mode ?? sessionRoot.safeMode),
|
|
713
|
+
allowlistHash: asNullableString(sessionRoot.allowlist_hash ?? sessionRoot.allowlistHash),
|
|
714
|
+
pinned: asBooleanInt(sessionRoot.pinned),
|
|
715
|
+
},
|
|
716
|
+
events,
|
|
717
|
+
network,
|
|
718
|
+
fingerprints,
|
|
719
|
+
snapshots,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function resolveImportedSessionId(db, requestedSessionId) {
|
|
723
|
+
const existing = db.prepare('SELECT 1 FROM sessions WHERE session_id = ?').get(requestedSessionId);
|
|
724
|
+
if (!existing) {
|
|
725
|
+
return requestedSessionId;
|
|
726
|
+
}
|
|
727
|
+
const safeId = requestedSessionId.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
728
|
+
return `${safeId}-import-${Date.now()}`;
|
|
729
|
+
}
|
|
730
|
+
function asObject(value, error) {
|
|
731
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
732
|
+
throw new Error(error);
|
|
733
|
+
}
|
|
734
|
+
return value;
|
|
735
|
+
}
|
|
736
|
+
function asArray(value, error) {
|
|
737
|
+
if (!Array.isArray(value)) {
|
|
738
|
+
throw new Error(error);
|
|
739
|
+
}
|
|
740
|
+
return value;
|
|
741
|
+
}
|
|
742
|
+
function asString(value, fallback) {
|
|
743
|
+
return typeof value === 'string' ? value : fallback;
|
|
744
|
+
}
|
|
745
|
+
function asNonEmptyString(value, error) {
|
|
746
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
747
|
+
throw new Error(error);
|
|
748
|
+
}
|
|
749
|
+
return value;
|
|
750
|
+
}
|
|
751
|
+
function asInteger(value, fallback) {
|
|
752
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
753
|
+
return fallback;
|
|
754
|
+
}
|
|
755
|
+
return Math.floor(value);
|
|
756
|
+
}
|
|
757
|
+
function asNullableInteger(value) {
|
|
758
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
return Math.floor(value);
|
|
762
|
+
}
|
|
763
|
+
function asNullableNumber(value) {
|
|
764
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
return value;
|
|
768
|
+
}
|
|
769
|
+
function asNullableString(value) {
|
|
770
|
+
if (typeof value !== 'string') {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
return value;
|
|
774
|
+
}
|
|
775
|
+
function asTimestamp(value, fallback) {
|
|
776
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
777
|
+
return fallback;
|
|
778
|
+
}
|
|
779
|
+
return Math.floor(value);
|
|
780
|
+
}
|
|
781
|
+
function asNullableTimestamp(value) {
|
|
782
|
+
if (value === null || value === undefined) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
return Math.floor(value);
|
|
789
|
+
}
|
|
790
|
+
function asBooleanInt(value) {
|
|
791
|
+
if (value === 1 || value === true) {
|
|
792
|
+
return 1;
|
|
793
|
+
}
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
function toJsonString(value) {
|
|
797
|
+
if (typeof value === 'string') {
|
|
798
|
+
try {
|
|
799
|
+
JSON.parse(value);
|
|
800
|
+
return value;
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return JSON.stringify({ value });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
return JSON.stringify(value ?? {});
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
return '{}';
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function toNullableJsonString(value) {
|
|
814
|
+
if (value === null || value === undefined) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
return toJsonString(value);
|
|
818
|
+
}
|
|
819
|
+
function getDbSizeMb(dbPath) {
|
|
820
|
+
try {
|
|
821
|
+
const bytes = statSync(dbPath).size;
|
|
822
|
+
return Number((bytes / (1024 * 1024)).toFixed(2));
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
return 0;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function getSessionCount(db) {
|
|
829
|
+
return db.prepare('SELECT COUNT(*) AS count FROM sessions').get().count;
|
|
830
|
+
}
|
|
831
|
+
function getOldestUnpinnedSession(db, clause, params = []) {
|
|
832
|
+
const whereClause = clause ? `AND ${clause}` : '';
|
|
833
|
+
const row = db
|
|
834
|
+
.prepare(`SELECT session_id FROM sessions WHERE pinned = 0 ${whereClause} ORDER BY created_at ASC LIMIT 1`)
|
|
835
|
+
.get(...params);
|
|
836
|
+
return row?.session_id ?? null;
|
|
837
|
+
}
|
|
838
|
+
function deleteSession(db, sessionId) {
|
|
839
|
+
db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId);
|
|
840
|
+
}
|
|
841
|
+
//# sourceMappingURL=retention.js.map
|