@mseep/anything-analyzer 3.6.50

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 (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,574 @@
1
+ import type Database from 'better-sqlite3'
2
+ import type {
3
+ Session,
4
+ CapturedRequest,
5
+ JsHookRecord,
6
+ StorageSnapshot,
7
+ AnalysisReport,
8
+ FingerprintProfile,
9
+ AiRequestLog,
10
+ InteractionEvent,
11
+ InteractionType,
12
+ } from '@shared/types'
13
+
14
+ // ============================================================
15
+ // Sessions Repository
16
+ // ============================================================
17
+
18
+ export class SessionsRepo {
19
+ private stmts: {
20
+ insert: Database.Statement
21
+ findById: Database.Statement
22
+ findAll: Database.Statement
23
+ updateStatus: Database.Statement
24
+ delete: Database.Statement
25
+ }
26
+
27
+ constructor(private db: Database.Database) {
28
+ this.stmts = {
29
+ insert: db.prepare(
30
+ `INSERT INTO sessions (id, name, target_url, status, created_at, stopped_at)
31
+ VALUES (@id, @name, @target_url, @status, @created_at, @stopped_at)`
32
+ ),
33
+ findById: db.prepare('SELECT * FROM sessions WHERE id = ?'),
34
+ findAll: db.prepare('SELECT * FROM sessions ORDER BY created_at DESC'),
35
+ updateStatus: db.prepare(
36
+ 'UPDATE sessions SET status = @status, stopped_at = @stopped_at WHERE id = @id'
37
+ ),
38
+ delete: db.prepare('DELETE FROM sessions WHERE id = ?')
39
+ }
40
+ }
41
+
42
+ insert(session: Session): void {
43
+ this.stmts.insert.run(session)
44
+ }
45
+
46
+ findById(id: string): Session | undefined {
47
+ return this.stmts.findById.get(id) as Session | undefined
48
+ }
49
+
50
+ findAll(): Session[] {
51
+ return this.stmts.findAll.all() as Session[]
52
+ }
53
+
54
+ updateStatus(id: string, status: string, stoppedAt: number | null = null): void {
55
+ this.stmts.updateStatus.run({ id, status, stopped_at: stoppedAt })
56
+ }
57
+
58
+ delete(id: string): void {
59
+ this.stmts.delete.run(id)
60
+ }
61
+ }
62
+
63
+ // ============================================================
64
+ // Requests Repository
65
+ // ============================================================
66
+
67
+ export class RequestsRepo {
68
+ private stmts: {
69
+ insert: Database.Statement
70
+ updateResponse: Database.Statement
71
+ findBySession: Database.Statement
72
+ findById: Database.Statement
73
+ getNextSequence: Database.Statement
74
+ deleteBySession: Database.Statement
75
+ }
76
+
77
+ constructor(private db: Database.Database) {
78
+ this.stmts = {
79
+ insert: db.prepare(
80
+ `INSERT INTO requests (id, session_id, sequence, timestamp, method, url, request_headers, request_body, content_type, initiator, source)
81
+ VALUES (@id, @session_id, @sequence, @timestamp, @method, @url, @request_headers, @request_body, @content_type, @initiator, @source)`
82
+ ),
83
+ updateResponse: db.prepare(
84
+ `UPDATE requests SET status_code = @status_code, response_headers = @response_headers,
85
+ response_body = @response_body, content_type = @content_type, duration_ms = @duration_ms,
86
+ is_streaming = @is_streaming, is_websocket = @is_websocket
87
+ WHERE id = @id`
88
+ ),
89
+ findBySession: db.prepare(
90
+ 'SELECT * FROM requests WHERE session_id = ? ORDER BY sequence ASC'
91
+ ),
92
+ findById: db.prepare('SELECT * FROM requests WHERE id = ?'),
93
+ getNextSequence: db.prepare(
94
+ 'SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM requests WHERE session_id = ?'
95
+ ),
96
+ deleteBySession: db.prepare('DELETE FROM requests WHERE session_id = ?')
97
+ }
98
+ }
99
+
100
+ insert(data: Partial<CapturedRequest> & { source?: string }): void {
101
+ this.stmts.insert.run({ ...data, source: data.source || 'cdp' })
102
+ }
103
+
104
+ updateResponse(data: {
105
+ id: string
106
+ status_code: number
107
+ response_headers: string
108
+ response_body: string | null
109
+ content_type: string | null
110
+ duration_ms: number
111
+ is_streaming: number // 0 or 1
112
+ is_websocket: number // 0 or 1
113
+ }): void {
114
+ this.stmts.updateResponse.run(data)
115
+ }
116
+
117
+ findBySession(sessionId: string): CapturedRequest[] {
118
+ return this.stmts.findBySession.all(sessionId) as CapturedRequest[]
119
+ }
120
+
121
+ findById(id: string): CapturedRequest | undefined {
122
+ return this.stmts.findById.get(id) as CapturedRequest | undefined
123
+ }
124
+
125
+ getNextSequence(sessionId: string): number {
126
+ const row = this.stmts.getNextSequence.get(sessionId) as { next_seq: number }
127
+ return row.next_seq
128
+ }
129
+
130
+ deleteBySession(sessionId: string): void {
131
+ this.stmts.deleteBySession.run(sessionId)
132
+ }
133
+
134
+ /**
135
+ * Find requests with dynamic filtering conditions.
136
+ */
137
+ findBySessionFiltered(sessionId: string, filters: {
138
+ method?: string
139
+ domain?: string
140
+ statusCode?: number
141
+ statusRange?: string
142
+ contentType?: string
143
+ urlPattern?: string
144
+ limit?: number
145
+ }): CapturedRequest[] {
146
+ const conditions: string[] = ['session_id = ?']
147
+ const params: unknown[] = [sessionId]
148
+
149
+ if (filters.method) {
150
+ conditions.push('method = ?')
151
+ params.push(filters.method.toUpperCase())
152
+ }
153
+
154
+ if (filters.domain) {
155
+ conditions.push('url LIKE ?')
156
+ params.push(`%${filters.domain}%`)
157
+ }
158
+
159
+ if (filters.statusCode != null) {
160
+ conditions.push('status_code = ?')
161
+ params.push(filters.statusCode)
162
+ } else if (filters.statusRange) {
163
+ const prefix = filters.statusRange.charAt(0)
164
+ if (/^[1-5]$/.test(prefix)) {
165
+ conditions.push('status_code >= ? AND status_code < ?')
166
+ params.push(Number(prefix) * 100, (Number(prefix) + 1) * 100)
167
+ }
168
+ }
169
+
170
+ if (filters.contentType) {
171
+ conditions.push('content_type LIKE ?')
172
+ params.push(`%${filters.contentType}%`)
173
+ }
174
+
175
+ if (filters.urlPattern) {
176
+ conditions.push('url LIKE ?')
177
+ params.push(`%${filters.urlPattern}%`)
178
+ }
179
+
180
+ const limit = filters.limit && filters.limit > 0 ? filters.limit : 50
181
+ const sql = `SELECT * FROM requests WHERE ${conditions.join(' AND ')} ORDER BY sequence ASC LIMIT ?`
182
+ params.push(limit)
183
+
184
+ return this.db.prepare(sql).all(...params) as CapturedRequest[]
185
+ }
186
+ }
187
+
188
+ // ============================================================
189
+ // JS Hooks Repository
190
+ // ============================================================
191
+
192
+ export class JsHooksRepo {
193
+ private stmts: {
194
+ insert: Database.Statement
195
+ findBySession: Database.Statement
196
+ deleteBySession: Database.Statement
197
+ }
198
+
199
+ constructor(private db: Database.Database) {
200
+ this.stmts = {
201
+ insert: db.prepare(
202
+ `INSERT INTO js_hooks (session_id, timestamp, hook_type, function_name, arguments, result, call_stack, related_request_id)
203
+ VALUES (@session_id, @timestamp, @hook_type, @function_name, @arguments, @result, @call_stack, @related_request_id)`
204
+ ),
205
+ findBySession: db.prepare(
206
+ 'SELECT * FROM js_hooks WHERE session_id = ? ORDER BY timestamp ASC'
207
+ ),
208
+ deleteBySession: db.prepare('DELETE FROM js_hooks WHERE session_id = ?')
209
+ }
210
+ }
211
+
212
+ insert(record: Omit<JsHookRecord, 'id'>): void {
213
+ this.stmts.insert.run(record)
214
+ }
215
+
216
+ findBySession(sessionId: string): JsHookRecord[] {
217
+ return this.stmts.findBySession.all(sessionId) as JsHookRecord[]
218
+ }
219
+
220
+ deleteBySession(sessionId: string): void {
221
+ this.stmts.deleteBySession.run(sessionId)
222
+ }
223
+ }
224
+
225
+ // ============================================================
226
+ // Storage Snapshots Repository
227
+ // ============================================================
228
+
229
+ export class StorageSnapshotsRepo {
230
+ private stmts: {
231
+ insert: Database.Statement
232
+ findBySession: Database.Statement
233
+ findLatest: Database.Statement
234
+ deleteBySession: Database.Statement
235
+ }
236
+
237
+ constructor(private db: Database.Database) {
238
+ this.stmts = {
239
+ insert: db.prepare(
240
+ `INSERT INTO storage_snapshots (session_id, timestamp, domain, storage_type, data)
241
+ VALUES (@session_id, @timestamp, @domain, @storage_type, @data)`
242
+ ),
243
+ findBySession: db.prepare(
244
+ 'SELECT * FROM storage_snapshots WHERE session_id = ? ORDER BY timestamp ASC'
245
+ ),
246
+ findLatest: db.prepare(
247
+ `SELECT * FROM storage_snapshots
248
+ WHERE session_id = ? AND storage_type = ?
249
+ ORDER BY timestamp DESC LIMIT 1`
250
+ ),
251
+ deleteBySession: db.prepare('DELETE FROM storage_snapshots WHERE session_id = ?')
252
+ }
253
+ }
254
+
255
+ insert(snapshot: Omit<StorageSnapshot, 'id'>): void {
256
+ this.stmts.insert.run(snapshot)
257
+ }
258
+
259
+ findBySession(sessionId: string): StorageSnapshot[] {
260
+ return this.stmts.findBySession.all(sessionId) as StorageSnapshot[]
261
+ }
262
+
263
+ findLatest(sessionId: string, storageType: string): StorageSnapshot | undefined {
264
+ return this.stmts.findLatest.get(sessionId, storageType) as StorageSnapshot | undefined
265
+ }
266
+
267
+ deleteBySession(sessionId: string): void {
268
+ this.stmts.deleteBySession.run(sessionId)
269
+ }
270
+ }
271
+
272
+ // ============================================================
273
+ // Analysis Reports Repository
274
+ // ============================================================
275
+
276
+ export class AnalysisReportsRepo {
277
+ private stmts: {
278
+ insert: Database.Statement
279
+ findBySession: Database.Statement
280
+ findById: Database.Statement
281
+ deleteBySession: Database.Statement
282
+ deleteById: Database.Statement
283
+ }
284
+
285
+ constructor(private db: Database.Database) {
286
+ this.stmts = {
287
+ insert: db.prepare(
288
+ `INSERT INTO analysis_reports (id, session_id, created_at, llm_provider, llm_model, prompt_tokens, completion_tokens, report_content, filter_prompt_tokens, filter_completion_tokens)
289
+ VALUES (@id, @session_id, @created_at, @llm_provider, @llm_model, @prompt_tokens, @completion_tokens, @report_content, @filter_prompt_tokens, @filter_completion_tokens)`
290
+ ),
291
+ findBySession: db.prepare(
292
+ 'SELECT * FROM analysis_reports WHERE session_id = ? ORDER BY created_at DESC'
293
+ ),
294
+ findById: db.prepare('SELECT * FROM analysis_reports WHERE id = ?'),
295
+ deleteBySession: db.prepare('DELETE FROM analysis_reports WHERE session_id = ?'),
296
+ deleteById: db.prepare('DELETE FROM analysis_reports WHERE id = ?')
297
+ }
298
+ }
299
+
300
+ insert(report: AnalysisReport): void {
301
+ this.stmts.insert.run(report)
302
+ }
303
+
304
+ findBySession(sessionId: string): AnalysisReport[] {
305
+ return this.stmts.findBySession.all(sessionId) as AnalysisReport[]
306
+ }
307
+
308
+ findById(id: string): AnalysisReport | undefined {
309
+ return this.stmts.findById.get(id) as AnalysisReport | undefined
310
+ }
311
+
312
+ deleteBySession(sessionId: string): void {
313
+ this.stmts.deleteBySession.run(sessionId)
314
+ }
315
+
316
+ deleteById(id: string): void {
317
+ this.stmts.deleteById.run(id)
318
+ }
319
+ }
320
+
321
+ // ============================================================
322
+ // Fingerprint Profiles Repository
323
+ // ============================================================
324
+
325
+ export class FingerprintProfilesRepo {
326
+ private stmts: {
327
+ upsert: Database.Statement
328
+ findBySessionId: Database.Statement
329
+ delete: Database.Statement
330
+ }
331
+
332
+ constructor(private db: Database.Database) {
333
+ this.stmts = {
334
+ upsert: db.prepare(
335
+ `INSERT OR REPLACE INTO fingerprint_profiles (session_id, profile_json)
336
+ VALUES (@session_id, @profile_json)`
337
+ ),
338
+ findBySessionId: db.prepare(
339
+ 'SELECT profile_json FROM fingerprint_profiles WHERE session_id = ?'
340
+ ),
341
+ delete: db.prepare('DELETE FROM fingerprint_profiles WHERE session_id = ?')
342
+ }
343
+ }
344
+
345
+ upsert(sessionId: string, profile: FingerprintProfile): void {
346
+ this.stmts.upsert.run({
347
+ session_id: sessionId,
348
+ profile_json: JSON.stringify(profile),
349
+ })
350
+ }
351
+
352
+ findBySessionId(sessionId: string): FingerprintProfile | null {
353
+ const row = this.stmts.findBySessionId.get(sessionId) as { profile_json: string } | undefined
354
+ if (!row) return null
355
+ return JSON.parse(row.profile_json)
356
+ }
357
+
358
+ delete(sessionId: string): void {
359
+ this.stmts.delete.run(sessionId)
360
+ }
361
+ }
362
+
363
+ // ============================================================
364
+ // Chat Messages Repository
365
+ // ============================================================
366
+
367
+ export class ChatMessagesRepo {
368
+ private stmts: {
369
+ insert: Database.Statement
370
+ findByReport: Database.Statement
371
+ deleteByReport: Database.Statement
372
+ }
373
+
374
+ constructor(private db: Database.Database) {
375
+ this.stmts = {
376
+ insert: db.prepare(
377
+ `INSERT INTO chat_messages (report_id, role, content, created_at)
378
+ VALUES (@report_id, @role, @content, @created_at)`
379
+ ),
380
+ findByReport: db.prepare(
381
+ 'SELECT role, content FROM chat_messages WHERE report_id = ? ORDER BY id ASC'
382
+ ),
383
+ deleteByReport: db.prepare('DELETE FROM chat_messages WHERE report_id = ?')
384
+ }
385
+ }
386
+
387
+ append(reportId: string, role: string, content: string): void {
388
+ this.stmts.insert.run({
389
+ report_id: reportId,
390
+ role,
391
+ content,
392
+ created_at: Date.now(),
393
+ })
394
+ }
395
+
396
+ insertMany(reportId: string, messages: Array<{ role: string; content: string }>): void {
397
+ const now = Date.now()
398
+ const insertMany = this.db.transaction((msgs: Array<{ role: string; content: string }>) => {
399
+ for (const msg of msgs) {
400
+ this.stmts.insert.run({
401
+ report_id: reportId,
402
+ role: msg.role,
403
+ content: msg.content,
404
+ created_at: now,
405
+ })
406
+ }
407
+ })
408
+ insertMany(messages)
409
+ }
410
+
411
+ findByReport(reportId: string): Array<{ role: string; content: string }> {
412
+ return this.stmts.findByReport.all(reportId) as Array<{ role: string; content: string }>
413
+ }
414
+
415
+ deleteByReport(reportId: string): void {
416
+ this.stmts.deleteByReport.run(reportId)
417
+ }
418
+ }
419
+
420
+ // ============================================================
421
+ // AI Request Log Repository
422
+ // ============================================================
423
+
424
+ /** Columns returned in list queries (excludes large body fields) */
425
+ const AI_LOG_LIST_COLUMNS = `
426
+ id, session_id, report_id, type, provider, model,
427
+ request_url, request_method, status_code,
428
+ prompt_tokens, completion_tokens, duration_ms, error, created_at
429
+ `.trim();
430
+
431
+ export class AiRequestLogRepo {
432
+ private stmts: {
433
+ insert: Database.Statement;
434
+ findBySession: Database.Statement;
435
+ findAll: Database.Statement;
436
+ findById: Database.Statement;
437
+ deleteBySession: Database.Statement;
438
+ updateTokens: Database.Statement;
439
+ };
440
+
441
+ constructor(private db: Database.Database) {
442
+ this.stmts = {
443
+ insert: db.prepare(
444
+ `INSERT INTO ai_request_logs
445
+ (session_id, report_id, type, provider, model,
446
+ request_url, request_method, request_headers, request_body,
447
+ status_code, response_headers, response_body,
448
+ prompt_tokens, completion_tokens, duration_ms, error, created_at)
449
+ VALUES
450
+ (@session_id, @report_id, @type, @provider, @model,
451
+ @request_url, @request_method, @request_headers, @request_body,
452
+ @status_code, @response_headers, @response_body,
453
+ @prompt_tokens, @completion_tokens, @duration_ms, @error, @created_at)`
454
+ ),
455
+ findBySession: db.prepare(
456
+ `SELECT ${AI_LOG_LIST_COLUMNS} FROM ai_request_logs
457
+ WHERE session_id = ? ORDER BY created_at DESC`
458
+ ),
459
+ findAll: db.prepare(
460
+ `SELECT ${AI_LOG_LIST_COLUMNS} FROM ai_request_logs
461
+ ORDER BY created_at DESC LIMIT ? OFFSET ?`
462
+ ),
463
+ findById: db.prepare(
464
+ 'SELECT * FROM ai_request_logs WHERE id = ?'
465
+ ),
466
+ deleteBySession: db.prepare(
467
+ 'DELETE FROM ai_request_logs WHERE session_id = ?'
468
+ ),
469
+ updateTokens: db.prepare(
470
+ `UPDATE ai_request_logs SET prompt_tokens = ?, completion_tokens = ?
471
+ WHERE id = (SELECT MAX(id) FROM ai_request_logs WHERE session_id = ? AND type = ?)`
472
+ ),
473
+ };
474
+ }
475
+
476
+ insert(log: Omit<AiRequestLog, 'id'>): void {
477
+ this.stmts.insert.run(log);
478
+ }
479
+
480
+ findBySession(sessionId: string): AiRequestLog[] {
481
+ return this.stmts.findBySession.all(sessionId) as AiRequestLog[];
482
+ }
483
+
484
+ findAll(limit: number, offset: number): AiRequestLog[] {
485
+ return this.stmts.findAll.all(limit, offset) as AiRequestLog[];
486
+ }
487
+
488
+ findById(id: number): AiRequestLog | null {
489
+ return (this.stmts.findById.get(id) as AiRequestLog) ?? null;
490
+ }
491
+
492
+ deleteBySession(sessionId: string): void {
493
+ this.stmts.deleteBySession.run(sessionId);
494
+ }
495
+
496
+ updateLatestTokens(sessionId: string, type: string, promptTokens: number, completionTokens: number): void {
497
+ this.stmts.updateTokens.run(promptTokens, completionTokens, sessionId, type);
498
+ }
499
+ }
500
+
501
+ // ============================================================
502
+ // Interaction Events Repository
503
+ // ============================================================
504
+
505
+ export class InteractionEventsRepo {
506
+ private stmts: {
507
+ insert: Database.Statement;
508
+ getNextSequence: Database.Statement;
509
+ findBySession: Database.Statement;
510
+ findBySessionAndType: Database.Statement;
511
+ findById: Database.Statement;
512
+ deleteBySession: Database.Statement;
513
+ count: Database.Statement;
514
+ };
515
+
516
+ constructor(private db: Database.Database) {
517
+ this.stmts = {
518
+ insert: db.prepare(
519
+ `INSERT INTO interaction_events
520
+ (session_id, sequence, type, timestamp, x, y, viewport_x, viewport_y,
521
+ selector, xpath, tag_name, element_text, attributes, bounding_rect,
522
+ input_value, key, scroll_x, scroll_y, scroll_dx, scroll_dy,
523
+ url, page_title, path, created_at)
524
+ VALUES
525
+ (@session_id, @sequence, @type, @timestamp, @x, @y, @viewport_x, @viewport_y,
526
+ @selector, @xpath, @tag_name, @element_text, @attributes, @bounding_rect,
527
+ @input_value, @key, @scroll_x, @scroll_y, @scroll_dx, @scroll_dy,
528
+ @url, @page_title, @path, @created_at)`
529
+ ),
530
+ getNextSequence: db.prepare(
531
+ 'SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM interaction_events WHERE session_id = ?'
532
+ ),
533
+ findBySession: db.prepare(
534
+ 'SELECT * FROM interaction_events WHERE session_id = ? ORDER BY sequence ASC LIMIT ?'
535
+ ),
536
+ findBySessionAndType: db.prepare(
537
+ 'SELECT * FROM interaction_events WHERE session_id = ? AND type = ? ORDER BY sequence ASC'
538
+ ),
539
+ findById: db.prepare('SELECT * FROM interaction_events WHERE id = ?'),
540
+ deleteBySession: db.prepare('DELETE FROM interaction_events WHERE session_id = ?'),
541
+ count: db.prepare('SELECT COUNT(*) AS cnt FROM interaction_events WHERE session_id = ?'),
542
+ };
543
+ }
544
+
545
+ insert(event: Omit<InteractionEvent, 'id'>): void {
546
+ this.stmts.insert.run(event);
547
+ }
548
+
549
+ getNextSequence(sessionId: string): number {
550
+ const row = this.stmts.getNextSequence.get(sessionId) as { next_seq: number };
551
+ return row.next_seq;
552
+ }
553
+
554
+ findBySession(sessionId: string, limit: number = 1000): InteractionEvent[] {
555
+ return this.stmts.findBySession.all(sessionId, limit) as InteractionEvent[];
556
+ }
557
+
558
+ findBySessionAndType(sessionId: string, type: InteractionType): InteractionEvent[] {
559
+ return this.stmts.findBySessionAndType.all(sessionId, type) as InteractionEvent[];
560
+ }
561
+
562
+ findById(id: number): InteractionEvent | null {
563
+ return (this.stmts.findById.get(id) as InteractionEvent) ?? null;
564
+ }
565
+
566
+ deleteBySession(sessionId: string): void {
567
+ this.stmts.deleteBySession.run(sessionId);
568
+ }
569
+
570
+ count(sessionId: string): number {
571
+ const row = this.stmts.count.get(sessionId) as { cnt: number };
572
+ return row.cnt;
573
+ }
574
+ }
@@ -0,0 +1,48 @@
1
+ import type { Session as ElectronSession } from 'electron';
2
+ import type { FingerprintProfile } from '@shared/types';
3
+
4
+ /**
5
+ * Apply HTTP-level fingerprint spoofing to an Electron session.
6
+ * - Sets the User-Agent globally
7
+ * - Intercepts outgoing requests to rewrite Client Hints and Accept-Language headers
8
+ */
9
+ export function applyHttpSpoofing(
10
+ electronSession: ElectronSession,
11
+ profile: FingerprintProfile,
12
+ ): void {
13
+ // Set global User-Agent
14
+ electronSession.setUserAgent(profile.userAgent);
15
+
16
+ // Build Client Hints values from profile
17
+ const majorVersion = profile.userAgent.match(/Chrome\/(\d+)/)?.[1] ?? '131';
18
+ const brandList = `"Chromium";v="${majorVersion}", "Google Chrome";v="${majorVersion}", "Not-A.Brand";v="8"`;
19
+ const platformMap: Record<string, string> = {
20
+ 'Win32': '"Windows"',
21
+ 'MacIntel': '"macOS"',
22
+ 'Linux x86_64': '"Linux"',
23
+ };
24
+ const secPlatform = platformMap[profile.platform] ?? '"Windows"';
25
+ const acceptLanguage = profile.languages
26
+ .map((lang, i) => i === 0 ? lang : `${lang};q=${(1 - i * 0.1).toFixed(1)}`)
27
+ .join(',');
28
+
29
+ // Intercept and rewrite headers
30
+ electronSession.webRequest.onBeforeSendHeaders((details, callback) => {
31
+ const headers = { ...details.requestHeaders };
32
+
33
+ headers['Accept-Language'] = acceptLanguage;
34
+ headers['Sec-CH-UA'] = brandList;
35
+ headers['Sec-CH-UA-Platform'] = secPlatform;
36
+ headers['Sec-CH-UA-Mobile'] = '?0';
37
+
38
+ callback({ requestHeaders: headers });
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Remove HTTP spoofing from an Electron session.
44
+ * Restores default behavior by passing through all headers unchanged.
45
+ */
46
+ export function removeHttpSpoofing(electronSession: ElectronSession): void {
47
+ electronSession.webRequest.onBeforeSendHeaders(null);
48
+ }