mcp-scraper 0.69.1 → 0.72.1

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 (45) hide show
  1. package/CHANGELOG.md +63 -2
  2. package/README.md +6 -2
  3. package/dist/{analytics-repository-62CAZAIP.js → analytics-repository-6YLRSWYO.js} +3 -2
  4. package/dist/bin/api-server.cjs +18989 -13102
  5. package/dist/bin/api-server.js +4 -4
  6. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +3 -3
  9. package/dist/bin/mcp-scraper-install.js +2 -2
  10. package/dist/bin/mcp-stdio-server.cjs +10493 -9135
  11. package/dist/bin/mcp-stdio-server.js +9 -7
  12. package/dist/bin/paa-harvest.cjs +308 -41
  13. package/dist/bin/paa-harvest.js +4 -3
  14. package/dist/{chunk-RODSHBN7.js → chunk-57F5PKS5.js} +1 -1
  15. package/dist/chunk-7RQULQF3.js +263 -0
  16. package/dist/{chunk-UL4ZKAWZ.js → chunk-A66DGFOU.js} +1 -1
  17. package/dist/{chunk-ES25GP6C.js → chunk-CXY5WV45.js} +24 -34
  18. package/dist/{chunk-Y46YNQMM.js → chunk-F5TK4KMT.js} +1 -1
  19. package/dist/{chunk-62LEXS5O.js → chunk-F6MUGMRN.js} +306 -44
  20. package/dist/{chunk-5RFQGIYC.js → chunk-FVL4GUTP.js} +2 -2
  21. package/dist/{chunk-OZIUFN6B.js → chunk-I45JV4EU.js} +2 -2
  22. package/dist/{chunk-2LXTOAKS.js → chunk-KIPIBPUB.js} +9 -1
  23. package/dist/chunk-MB72PA6S.js +2003 -0
  24. package/dist/chunk-OM7HVEJ3.js +26 -0
  25. package/dist/chunk-PJEEKOUM.js +404 -0
  26. package/dist/{chunk-RX2QAHML.js → chunk-RAFQEPJ4.js} +2 -2
  27. package/dist/{chunk-KO5CK5ZT.js → chunk-SR7GSLEA.js} +202 -441
  28. package/dist/chunk-T3MZISOF.js +240 -0
  29. package/dist/{chunk-65FTMB7G.js → chunk-WEFPBAAG.js} +180 -9
  30. package/dist/{chunk-LN6N3YLK.js → chunk-WFQ2E4WE.js} +1 -1
  31. package/dist/{chunk-2LMMZUU5.js → chunk-XYRD4KLT.js} +3173 -2019
  32. package/dist/{db-ZGZAKYBW.js → db-566MOHHD.js} +7 -1
  33. package/dist/{extract-bundle-SXSJCPQI.js → extract-bundle-337ZDIIQ.js} +4 -4
  34. package/dist/gmail-service-E276SBJU.js +25 -0
  35. package/dist/index.cjs +322 -49
  36. package/dist/index.d.cts +15 -2
  37. package/dist/index.d.ts +15 -2
  38. package/dist/index.js +4 -3
  39. package/dist/{lead-list-enrichment-repository-CXLVIV3R.js → lead-list-enrichment-repository-WWEMJ7EN.js} +2 -2
  40. package/dist/{location-data-repository-G72M7DTN.js → location-data-repository-H4B2BWGY.js} +2 -2
  41. package/dist/{server-VDJBV4V4.js → server-QY2AZ2OW.js} +8377 -6637
  42. package/dist/{site-extract-repository-RLB3TFJ3.js → site-extract-repository-ACNAVNDN.js} +3 -3
  43. package/dist/{worker-KHSJG365.js → worker-JQORPCFQ.js} +9 -7
  44. package/package.json +1 -1
  45. package/dist/chunk-2L4C4DAZ.js +0 -103
@@ -0,0 +1,240 @@
1
+ // src/api/connected-data-artifacts.ts
2
+ import { createHash, randomUUID } from "crypto";
3
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
4
+ import { homedir } from "os";
5
+ import { dirname, join } from "path";
6
+ var CONNECTED_DATA_ARTIFACT_PREFIX = "connected-data-exports/";
7
+ var CONNECTED_DATA_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
8
+ var CONNECTED_DATA_DOWNLOAD_TTL_MS = 15 * 60 * 1e3;
9
+ function privateBlobToken() {
10
+ return process.env.CONNECTED_DATA_READ_WRITE_TOKEN?.trim() || process.env.CONNECTED_DATA_BLOB_READ_WRITE_TOKEN?.trim() || null;
11
+ }
12
+ function localBaseDir() {
13
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join(homedir(), "Downloads", "mcp-scraper");
14
+ }
15
+ function isHosted() {
16
+ return process.env.VERCEL === "1" || process.env.NODE_ENV === "production";
17
+ }
18
+ function safeFilename(value) {
19
+ const safe = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
20
+ return (safe || "connected-data-export").slice(0, 120);
21
+ }
22
+ function artifactTimestamp(artifactId) {
23
+ if (!artifactId.startsWith(CONNECTED_DATA_ARTIFACT_PREFIX)) return null;
24
+ const filename = artifactId.split("/").at(-1) ?? "";
25
+ const match = filename.match(/^(\d{13})-/);
26
+ if (!match) return null;
27
+ const timestamp = Number(match[1]);
28
+ return Number.isFinite(timestamp) ? timestamp : null;
29
+ }
30
+ function connectedDataArtifactOwnerId(artifactId) {
31
+ if (!artifactId.startsWith(CONNECTED_DATA_ARTIFACT_PREFIX)) return null;
32
+ const rest = artifactId.slice(CONNECTED_DATA_ARTIFACT_PREFIX.length);
33
+ const owner = rest.split("/")[0];
34
+ return owner || null;
35
+ }
36
+ function connectedDataArtifactExpiresAt(artifactId) {
37
+ const timestamp = artifactTimestamp(artifactId);
38
+ return timestamp === null ? null : new Date(timestamp + CONNECTED_DATA_ARTIFACT_TTL_MS);
39
+ }
40
+ function connectedDataArtifactIsExpired(artifactId, now = Date.now()) {
41
+ const timestamp = artifactTimestamp(artifactId);
42
+ return timestamp === null || timestamp + CONNECTED_DATA_ARTIFACT_TTL_MS <= now;
43
+ }
44
+ async function signedDownloadUrl(pathname, expiresAt) {
45
+ const token = privateBlobToken();
46
+ if (!token) return null;
47
+ const validUntil = Math.min(Date.now() + CONNECTED_DATA_DOWNLOAD_TTL_MS, expiresAt.getTime());
48
+ if (validUntil <= Date.now()) return null;
49
+ const { issueSignedToken, presignUrl } = await import("@vercel/blob");
50
+ const signedToken = await issueSignedToken({
51
+ token,
52
+ pathname,
53
+ operations: ["get"],
54
+ validUntil
55
+ });
56
+ const { presignedUrl } = await presignUrl(signedToken, {
57
+ access: "private",
58
+ operation: "get",
59
+ pathname,
60
+ validUntil
61
+ });
62
+ return { url: presignedUrl, expiresAt: new Date(validUntil).toISOString() };
63
+ }
64
+ async function createConnectedDataArtifact(args) {
65
+ return createConnectedDataBinaryArtifact({
66
+ ...args,
67
+ contentType: "application/x-ndjson"
68
+ });
69
+ }
70
+ async function createConnectedDataBinaryArtifact(args) {
71
+ const requestedCreatedAt = args.stableCreatedAt ? Date.parse(args.stableCreatedAt) : Number.NaN;
72
+ const createdAt = Number.isFinite(requestedCreatedAt) ? requestedCreatedAt : Date.now();
73
+ const filename = safeFilename(args.filename);
74
+ const extension = filename.includes(".") ? `.${filename.split(".").at(-1)}` : ".bin";
75
+ const stableSuffix = args.stableKey ? safeFilename(args.stableKey) : randomUUID();
76
+ const requestedPathname = `${CONNECTED_DATA_ARTIFACT_PREFIX}${args.ownerId}/${createdAt}-${safeFilename(args.exportId)}-${stableSuffix}--${filename}`;
77
+ const content = typeof args.content === "string" ? Buffer.from(args.content) : Buffer.from(args.content);
78
+ const bytes = content.byteLength;
79
+ const sha256 = createHash("sha256").update(content).digest("hex");
80
+ const expiresAt = new Date(createdAt + CONNECTED_DATA_ARTIFACT_TTL_MS);
81
+ const token = privateBlobToken();
82
+ let artifactId = requestedPathname;
83
+ if (token) {
84
+ const { put } = await import("@vercel/blob");
85
+ const stored = await put(requestedPathname, content, {
86
+ access: "private",
87
+ token,
88
+ contentType: args.contentType,
89
+ addRandomSuffix: !args.stableKey,
90
+ ...args.stableKey ? { allowOverwrite: true } : {},
91
+ cacheControlMaxAge: 60,
92
+ multipart: bytes > 100 * 1024 * 1024
93
+ });
94
+ artifactId = stored.pathname;
95
+ } else {
96
+ if (isHosted()) throw new Error("connected_data_private_blob_not_configured");
97
+ const path = join(localBaseDir(), "blobs", requestedPathname);
98
+ await mkdir(dirname(path), { recursive: true });
99
+ await writeFile(path, content);
100
+ }
101
+ const download = await signedDownloadUrl(artifactId, expiresAt);
102
+ return {
103
+ artifactId,
104
+ filename,
105
+ contentType: args.contentType,
106
+ bytes,
107
+ sha256,
108
+ createdAt: new Date(createdAt).toISOString(),
109
+ expiresAt: expiresAt.toISOString(),
110
+ downloadUrl: download?.url ?? null,
111
+ downloadUrlExpiresAt: download?.expiresAt ?? null,
112
+ readback: {
113
+ tool: "report_artifact_read",
114
+ arguments: { artifactId, offset: 0, maxBytes: 2e4 },
115
+ continuation: "Repeat with offset set to the previous result nextOffset until nextOffset is null."
116
+ }
117
+ };
118
+ }
119
+ async function renewConnectedDataArtifactDownload(args) {
120
+ if (connectedDataArtifactOwnerId(args.artifactId) !== args.ownerId) return null;
121
+ const expiresAt = connectedDataArtifactExpiresAt(args.artifactId);
122
+ if (!expiresAt || expiresAt.getTime() <= Date.now()) return null;
123
+ const download = await signedDownloadUrl(args.artifactId, expiresAt);
124
+ if (!download) return null;
125
+ return {
126
+ downloadUrl: download.url,
127
+ downloadUrlExpiresAt: download.expiresAt,
128
+ expiresAt: expiresAt.toISOString()
129
+ };
130
+ }
131
+ async function streamToBuffer(stream, maxBytes = Number.POSITIVE_INFINITY) {
132
+ const reader = stream.getReader();
133
+ const chunks = [];
134
+ let totalBytes = 0;
135
+ try {
136
+ for (; ; ) {
137
+ const { done, value } = await reader.read();
138
+ if (done) break;
139
+ if (value) {
140
+ const chunk = Buffer.from(value);
141
+ totalBytes += chunk.byteLength;
142
+ if (totalBytes > maxBytes) {
143
+ await reader.cancel("artifact_too_large").catch(() => void 0);
144
+ return null;
145
+ }
146
+ chunks.push(chunk);
147
+ }
148
+ }
149
+ } finally {
150
+ reader.releaseLock();
151
+ }
152
+ return Buffer.concat(chunks);
153
+ }
154
+ async function readConnectedDataArtifactWindow(artifactId, offset, maxBytes) {
155
+ if (connectedDataArtifactIsExpired(artifactId)) return null;
156
+ const token = privateBlobToken();
157
+ let buffer;
158
+ if (token) {
159
+ const { get } = await import("@vercel/blob");
160
+ const result = await get(artifactId, { access: "private", token, useCache: false });
161
+ if (!result || result.statusCode !== 200) return null;
162
+ const streamed = await streamToBuffer(result.stream);
163
+ if (!streamed) return null;
164
+ buffer = streamed;
165
+ } else {
166
+ if (isHosted()) return null;
167
+ try {
168
+ buffer = await readFile(join(localBaseDir(), "blobs", artifactId));
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ const totalBytes = buffer.length;
174
+ const start = Math.max(0, offset);
175
+ const end = Math.min(totalBytes, start + maxBytes);
176
+ return {
177
+ text: buffer.subarray(start, end).toString("utf8"),
178
+ totalBytes,
179
+ nextOffset: end < totalBytes ? end : null
180
+ };
181
+ }
182
+ async function readConnectedDataArtifactBytes(args) {
183
+ if (connectedDataArtifactOwnerId(args.artifactId) !== args.ownerId || connectedDataArtifactIsExpired(args.artifactId)) return null;
184
+ const token = privateBlobToken();
185
+ let buffer;
186
+ if (token) {
187
+ const { get } = await import("@vercel/blob");
188
+ const result = await get(args.artifactId, { access: "private", token, useCache: false });
189
+ if (!result || result.statusCode !== 200) return null;
190
+ const streamed = await streamToBuffer(result.stream, args.maxBytes ?? 25 * 1024 * 1024);
191
+ if (!streamed) return null;
192
+ buffer = streamed;
193
+ } else {
194
+ if (isHosted()) return null;
195
+ try {
196
+ const path = join(localBaseDir(), "blobs", args.artifactId);
197
+ const info = await stat(path);
198
+ if (info.size > (args.maxBytes ?? 25 * 1024 * 1024)) return null;
199
+ buffer = await readFile(path);
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+ if (buffer.byteLength > (args.maxBytes ?? 25 * 1024 * 1024)) return null;
205
+ return buffer;
206
+ }
207
+ async function cleanupExpiredConnectedDataArtifacts(args = {}) {
208
+ const now = args.now ?? /* @__PURE__ */ new Date();
209
+ const token = privateBlobToken();
210
+ if (!token) return { deleted: 0, store: isHosted() ? "none" : "local" };
211
+ if (!args.force && !(now.getUTCHours() === 3 && now.getUTCMinutes() === 17)) {
212
+ return { deleted: 0, store: "private-vercel-blob", skipped: true };
213
+ }
214
+ const cutoff = now.getTime() - CONNECTED_DATA_ARTIFACT_TTL_MS;
215
+ const { list, del } = await import("@vercel/blob");
216
+ let cursor;
217
+ let deleted = 0;
218
+ for (let page = 0; page < 20; page++) {
219
+ const result = await list({ prefix: CONNECTED_DATA_ARTIFACT_PREFIX, token, limit: 1e3, cursor });
220
+ const expired = result.blobs.filter((blob) => new Date(blob.uploadedAt).getTime() <= cutoff);
221
+ if (expired.length > 0) {
222
+ await del(expired.map((blob) => blob.pathname), { token });
223
+ deleted += expired.length;
224
+ }
225
+ if (!result.hasMore || !result.cursor) break;
226
+ cursor = result.cursor;
227
+ }
228
+ return { deleted, store: "private-vercel-blob" };
229
+ }
230
+
231
+ export {
232
+ CONNECTED_DATA_ARTIFACT_PREFIX,
233
+ connectedDataArtifactOwnerId,
234
+ createConnectedDataArtifact,
235
+ createConnectedDataBinaryArtifact,
236
+ renewConnectedDataArtifactDownload,
237
+ readConnectedDataArtifactWindow,
238
+ readConnectedDataArtifactBytes,
239
+ cleanupExpiredConnectedDataArtifacts
240
+ };
@@ -379,7 +379,7 @@ var DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
379
379
  var DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
380
380
  var _db = null;
381
381
  var _rateLimitSchemaReady = false;
382
- var CORE_SCHEMA_VERSION = "2026-08-23.1";
382
+ var CORE_SCHEMA_VERSION = "2026-08-27.1";
383
383
  var SITE_EXTRACT_COUNTER_REPAIR_VERSION = "data-2026-07-28-site-extract-counters.1";
384
384
  var CORE_SCHEMA_TABLES = [
385
385
  "admin_credit_adjustment_events",
@@ -437,6 +437,7 @@ var CORE_SCHEMA_INDEXES = [
437
437
  "credit_lots_user_active",
438
438
  "credit_lot_repairs_user_applied",
439
439
  "harvest_attempts_job_id",
440
+ "harvest_attempts_provider_lookup",
440
441
  "harvest_attempts_user_created_at",
441
442
  "inbox_messages_user_sent_at",
442
443
  "jobs_created_at_status_user",
@@ -480,7 +481,23 @@ var CORE_SCHEMA_TRIGGERS = [
480
481
  ];
481
482
  var CORE_SCHEMA_COLUMNS = {
482
483
  credit_lots: ["never_expires"],
483
- harvest_attempts: ["debug_json"],
484
+ harvest_attempts: [
485
+ "debug_json",
486
+ "browser_provider",
487
+ "provider_session_id",
488
+ "provider_lookup_status",
489
+ "provider_status",
490
+ "provider_error_code",
491
+ "provider_error_message",
492
+ "provider_reason_source",
493
+ "provider_duration_ms",
494
+ "provider_bandwidth_bytes",
495
+ "provider_captcha",
496
+ "provider_checked_at",
497
+ "provider_detail_json",
498
+ "provider_disconnect_observed",
499
+ "provider_disconnect_message"
500
+ ],
484
501
  jobs: ["public_error_json"],
485
502
  oauth_tokens: ["replaced_by", "rotated_at"],
486
503
  site_extract_jobs: [
@@ -689,6 +706,20 @@ async function migrate(options = {}) {
689
706
  browser_close_succeeded INTEGER,
690
707
  browser_close_error TEXT,
691
708
  debug_json TEXT,
709
+ browser_provider TEXT,
710
+ provider_session_id TEXT,
711
+ provider_lookup_status TEXT,
712
+ provider_status TEXT,
713
+ provider_error_code TEXT,
714
+ provider_error_message TEXT,
715
+ provider_reason_source TEXT,
716
+ provider_duration_ms INTEGER,
717
+ provider_bandwidth_bytes INTEGER,
718
+ provider_captcha INTEGER,
719
+ provider_checked_at TEXT,
720
+ provider_detail_json TEXT,
721
+ provider_disconnect_observed INTEGER,
722
+ provider_disconnect_message TEXT,
692
723
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
693
724
  completed_at TEXT,
694
725
  UNIQUE(job_id, attempt_number)
@@ -700,6 +731,28 @@ async function migrate(options = {}) {
700
731
  await db.execute(`ALTER TABLE harvest_attempts ADD COLUMN debug_json TEXT`);
701
732
  } catch {
702
733
  }
734
+ for (const column of [
735
+ "browser_provider TEXT",
736
+ "provider_session_id TEXT",
737
+ "provider_lookup_status TEXT",
738
+ "provider_status TEXT",
739
+ "provider_error_code TEXT",
740
+ "provider_error_message TEXT",
741
+ "provider_reason_source TEXT",
742
+ "provider_duration_ms INTEGER",
743
+ "provider_bandwidth_bytes INTEGER",
744
+ "provider_captcha INTEGER",
745
+ "provider_checked_at TEXT",
746
+ "provider_detail_json TEXT",
747
+ "provider_disconnect_observed INTEGER",
748
+ "provider_disconnect_message TEXT"
749
+ ]) {
750
+ try {
751
+ await db.execute(`ALTER TABLE harvest_attempts ADD COLUMN ${column}`);
752
+ } catch {
753
+ }
754
+ }
755
+ await db.execute(`CREATE INDEX IF NOT EXISTS harvest_attempts_provider_lookup ON harvest_attempts(browser_provider, provider_lookup_status, provider_checked_at)`);
703
756
  try {
704
757
  await db.execute(`ALTER TABLE users ADD COLUMN password_hash TEXT`);
705
758
  } catch {
@@ -1854,6 +1907,20 @@ function rowToHarvestAttemptLog(row) {
1854
1907
  kernel_delete_error: row.kernel_delete_error != null ? String(row.kernel_delete_error) : null,
1855
1908
  browser_close_succeeded: nullableBoolean(row.browser_close_succeeded),
1856
1909
  browser_close_error: row.browser_close_error != null ? String(row.browser_close_error) : null,
1910
+ browser_provider: row.browser_provider != null ? String(row.browser_provider) : null,
1911
+ provider_session_id: row.provider_session_id != null ? String(row.provider_session_id) : null,
1912
+ provider_lookup_status: row.provider_lookup_status != null ? String(row.provider_lookup_status) : null,
1913
+ provider_status: row.provider_status != null ? String(row.provider_status) : null,
1914
+ provider_error_code: row.provider_error_code != null ? String(row.provider_error_code) : null,
1915
+ provider_error_message: row.provider_error_message != null ? String(row.provider_error_message) : null,
1916
+ provider_reason_source: row.provider_reason_source != null ? String(row.provider_reason_source) : null,
1917
+ provider_duration_ms: row.provider_duration_ms != null ? Number(row.provider_duration_ms) : null,
1918
+ provider_bandwidth_bytes: row.provider_bandwidth_bytes != null ? Number(row.provider_bandwidth_bytes) : null,
1919
+ provider_captcha: nullableBoolean(row.provider_captcha),
1920
+ provider_checked_at: row.provider_checked_at != null ? String(row.provider_checked_at) : null,
1921
+ provider_detail: parseNullableJson(row.provider_detail_json),
1922
+ provider_disconnect_observed: nullableBoolean(row.provider_disconnect_observed),
1923
+ provider_disconnect_message: row.provider_disconnect_message != null ? String(row.provider_disconnect_message) : null,
1857
1924
  debug: parseNullableJson(row.debug_json),
1858
1925
  created_at: String(row.created_at),
1859
1926
  completed_at: row.completed_at != null ? String(row.completed_at) : null
@@ -1906,13 +1973,7 @@ async function startHarvestAttempt(input) {
1906
1973
  id, job_id, user_id, attempt_number, max_attempts, query, location, max_questions, status, created_at
1907
1974
  )
1908
1975
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)
1909
- ON CONFLICT(job_id, attempt_number) DO UPDATE SET
1910
- status = 'running',
1911
- created_at = excluded.created_at,
1912
- completed_at = NULL,
1913
- outcome = NULL,
1914
- error = NULL,
1915
- debug_json = NULL
1976
+ ON CONFLICT(job_id, attempt_number) DO NOTHING
1916
1977
  `,
1917
1978
  args: [
1918
1979
  randomUUID(),
@@ -1943,6 +2004,32 @@ async function finishHarvestAttempt(input) {
1943
2004
  kernel_delete_error = ?,
1944
2005
  browser_close_succeeded = ?,
1945
2006
  browser_close_error = ?,
2007
+ browser_provider = COALESCE(browser_provider, ?),
2008
+ provider_session_id = COALESCE(provider_session_id, ?),
2009
+ provider_lookup_status = CASE
2010
+ WHEN provider_lookup_status = 'complete' THEN provider_lookup_status
2011
+ ELSE COALESCE(?, provider_lookup_status)
2012
+ END,
2013
+ provider_status = COALESCE(provider_status, ?),
2014
+ provider_error_code = CASE
2015
+ WHEN provider_reason_source = 'provider_session_api' THEN provider_error_code
2016
+ ELSE COALESCE(?, provider_error_code)
2017
+ END,
2018
+ provider_error_message = CASE
2019
+ WHEN provider_reason_source = 'provider_session_api' THEN provider_error_message
2020
+ ELSE COALESCE(?, provider_error_message)
2021
+ END,
2022
+ provider_reason_source = CASE
2023
+ WHEN provider_reason_source = 'provider_session_api' THEN provider_reason_source
2024
+ ELSE COALESCE(?, provider_reason_source)
2025
+ END,
2026
+ provider_duration_ms = COALESCE(provider_duration_ms, ?),
2027
+ provider_bandwidth_bytes = COALESCE(provider_bandwidth_bytes, ?),
2028
+ provider_captcha = COALESCE(provider_captcha, ?),
2029
+ provider_checked_at = COALESCE(provider_checked_at, ?),
2030
+ provider_detail_json = COALESCE(provider_detail_json, ?),
2031
+ provider_disconnect_observed = COALESCE(provider_disconnect_observed, ?),
2032
+ provider_disconnect_message = COALESCE(provider_disconnect_message, ?),
1946
2033
  debug_json = ?,
1947
2034
  completed_at = ?
1948
2035
  WHERE job_id = ? AND attempt_number = ?
@@ -1959,6 +2046,20 @@ async function finishHarvestAttempt(input) {
1959
2046
  input.kernelDeleteError,
1960
2047
  input.browserCloseSucceeded == null ? null : input.browserCloseSucceeded ? 1 : 0,
1961
2048
  input.browserCloseError,
2049
+ input.browserProvider ?? null,
2050
+ input.providerSessionId ?? null,
2051
+ input.providerLookupStatus ?? null,
2052
+ input.providerStatus ?? null,
2053
+ input.providerErrorCode ?? null,
2054
+ input.providerErrorMessage ?? null,
2055
+ input.providerReasonSource ?? null,
2056
+ input.providerDurationMs ?? null,
2057
+ input.providerBandwidthBytes ?? null,
2058
+ input.providerCaptcha == null ? null : input.providerCaptcha ? 1 : 0,
2059
+ input.providerCheckedAt ?? null,
2060
+ input.providerDetail == null ? null : boundedJson(input.providerDetail),
2061
+ input.providerDisconnectObserved == null ? null : input.providerDisconnectObserved ? 1 : 0,
2062
+ input.providerDisconnectMessage ?? null,
1962
2063
  input.debug == null ? null : JSON.stringify(input.debug),
1963
2064
  input.completedAt,
1964
2065
  input.jobId,
@@ -1966,6 +2067,65 @@ async function finishHarvestAttempt(input) {
1966
2067
  ]
1967
2068
  });
1968
2069
  }
2070
+ function boundedJson(value, maxBytes = 4096) {
2071
+ const serialized = JSON.stringify(value);
2072
+ return serialized.length <= maxBytes ? serialized : JSON.stringify({ truncated: true });
2073
+ }
2074
+ async function updateHarvestAttemptProviderTelemetry(input) {
2075
+ const result = await getDb().execute({
2076
+ sql: `
2077
+ UPDATE harvest_attempts SET
2078
+ provider_lookup_status = ?,
2079
+ provider_status = ?,
2080
+ provider_error_code = ?,
2081
+ provider_error_message = ?,
2082
+ provider_reason_source = ?,
2083
+ provider_duration_ms = ?,
2084
+ provider_bandwidth_bytes = ?,
2085
+ provider_captcha = ?,
2086
+ provider_checked_at = ?,
2087
+ provider_detail_json = ?
2088
+ WHERE id = ?
2089
+ AND job_id = ?
2090
+ AND user_id = ?
2091
+ AND browser_provider = 'bright_data_browser_api'
2092
+ AND provider_session_id = ?
2093
+ AND COALESCE(provider_lookup_status, '') <> 'complete'
2094
+ `,
2095
+ args: [
2096
+ input.lookupStatus,
2097
+ input.providerStatus ?? null,
2098
+ input.errorCode ?? null,
2099
+ input.errorMessage ?? null,
2100
+ input.reasonSource ?? null,
2101
+ input.durationMs ?? null,
2102
+ input.bandwidthBytes ?? null,
2103
+ input.captcha == null ? null : input.captcha ? 1 : 0,
2104
+ input.checkedAt,
2105
+ input.detail == null ? null : boundedJson(input.detail),
2106
+ input.attemptId,
2107
+ input.jobId,
2108
+ input.userId,
2109
+ input.providerSessionId
2110
+ ]
2111
+ });
2112
+ return result.rowsAffected === 1;
2113
+ }
2114
+ async function listHarvestAttemptsPendingProviderTelemetry(limit = 25) {
2115
+ const boundedLimit = Math.max(1, Math.min(100, Math.trunc(limit)));
2116
+ const result = await getDb().execute({
2117
+ sql: `
2118
+ SELECT * FROM harvest_attempts
2119
+ WHERE browser_provider = 'bright_data_browser_api'
2120
+ AND provider_session_id IS NOT NULL
2121
+ AND provider_lookup_status IN ('pending', 'temporary_error')
2122
+ ORDER BY COALESCE(provider_checked_at, created_at) ASC
2123
+ LIMIT ?
2124
+ `,
2125
+ args: [boundedLimit]
2126
+ });
2127
+ return result.rows.map((row) => rowToHarvestAttemptLog(row));
2128
+ }
1969
2129
  async function listHarvestAttempts(jobId, userId) {
1970
2130
  const res = userId == null ? await getDb().execute({
1971
2131
  sql: `SELECT * FROM harvest_attempts WHERE job_id = ? ORDER BY attempt_number ASC`,
@@ -2502,6 +2662,14 @@ async function completeJob(id, result) {
2502
2662
  args: [JSON.stringify(result), id]
2503
2663
  });
2504
2664
  }
2665
+ async function completeRunningJob(id, result) {
2666
+ const updated = await getDb().execute({
2667
+ sql: `UPDATE jobs SET status = 'done', result = ?, error = NULL, public_error_json = NULL, completed_at = datetime('now')
2668
+ WHERE id = ? AND status = 'running'`,
2669
+ args: [JSON.stringify(result), id]
2670
+ });
2671
+ return updated.rowsAffected === 1;
2672
+ }
2505
2673
  async function failJob(id, error, publicError) {
2506
2674
  await getDb().execute({
2507
2675
  sql: `UPDATE jobs SET status = 'failed', error = ?, public_error_json = ?, completed_at = datetime('now') WHERE id = ?`,
@@ -3574,6 +3742,8 @@ export {
3574
3742
  getOrCreateRunningJob,
3575
3743
  startHarvestAttempt,
3576
3744
  finishHarvestAttempt,
3745
+ updateHarvestAttemptProviderTelemetry,
3746
+ listHarvestAttemptsPendingProviderTelemetry,
3577
3747
  listHarvestAttempts,
3578
3748
  getJob,
3579
3749
  listJobs,
@@ -3611,6 +3781,7 @@ export {
3611
3781
  listStaleRunningDurablePaaJobs,
3612
3782
  markJobBillingSettled,
3613
3783
  completeJob,
3784
+ completeRunningJob,
3614
3785
  failJob,
3615
3786
  failRunningJob,
3616
3787
  cancelJob,
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  buildPublicErrorEnvelope,
10
10
  getDb
11
- } from "./chunk-65FTMB7G.js";
11
+ } from "./chunk-WEFPBAAG.js";
12
12
 
13
13
  // src/lib/media-extractor.ts
14
14
  import { createWriteStream, mkdirSync, rmSync } from "fs";