mcp-scraper 0.80.0 → 0.83.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/README.md +4 -2
  3. package/dist/{analytics-repository-YOY5KMOD.js → analytics-repository-GA6P5S5J.js} +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.js +61 -46
  6. package/dist/bin/mcp-scraper-core.js +7 -7
  7. package/dist/bin/mcp-scraper-install.js +2 -2
  8. package/dist/bin/mcp-stdio-server.js +7 -7
  9. package/dist/bin/paa-harvest.js +2 -2
  10. package/dist/{chunk-PMONKQGJ.js → chunk-4NZ6JFK5.js} +2 -2
  11. package/dist/{chunk-VEQPA65D.js → chunk-ACDNW7TG.js} +1 -1
  12. package/dist/{chunk-ID7J23WP.js → chunk-AIVNZZO3.js} +4 -3
  13. package/dist/{chunk-VXLU74YZ.js → chunk-BGD7U3AE.js} +207 -1
  14. package/dist/{chunk-HEUMDX4I.js → chunk-CNAJWRJT.js} +131 -1
  15. package/dist/{chunk-YPOOARYN.js → chunk-FUG2IRKP.js} +1 -1
  16. package/dist/{chunk-XBEUZJHS.js → chunk-HGJJQY24.js} +49 -5
  17. package/dist/{chunk-KNGTQD6U.js → chunk-NH4QAH3X.js} +3 -1
  18. package/dist/{chunk-5UAFBKJR.js → chunk-OPQIGAFB.js} +24 -5
  19. package/dist/{chunk-OY25W46Q.js → chunk-QSZXAA5F.js} +3121 -2222
  20. package/dist/{chunk-P4447RNF.js → chunk-UXVT7FWW.js} +2 -2
  21. package/dist/{chunk-62W6UER7.js → chunk-ZHPGPVJJ.js} +2 -2
  22. package/dist/{extract-bundle-232SA7MI.js → extract-bundle-HTWWJD6N.js} +2 -2
  23. package/dist/{gmail-service-IMUHMRH4.js → gmail-service-VXE7OYNA.js} +3 -3
  24. package/dist/index.cjs +23 -5
  25. package/dist/index.js +2 -2
  26. package/dist/{server-XBRLYGTR.js → server-ICMRDYI4.js} +4789 -2842
  27. package/dist/{site-extract-repository-VDONDDMR.js → site-extract-repository-Y5WQFSQW.js} +6 -2
  28. package/dist/{worker-ICCZR443.js → worker-FM3ZRDY4.js} +3 -3
  29. package/package.json +6 -2
  30. package/public/.well-known/xray-install.json +178 -0
  31. package/public/agent-install.md +194 -0
@@ -145,6 +145,204 @@ function extractContactEvidence(html, pageUrl) {
145
145
  };
146
146
  }
147
147
 
148
+ // src/mcp/xray-install-contract.ts
149
+ import { createHash } from "crypto";
150
+ var XRAY_INSTALL_PROTOCOL_VERSION = "2.0.0";
151
+ var XRAY_SETUP_TOOL = "analytics_setup_site";
152
+ var XRAY_TRIGGER_PHRASE = "Install X-Ray";
153
+ var XRAY_CANONICAL_GOAL = "Install X-Ray and tell me what drives customers";
154
+ var CONTINUATION = `Call ${XRAY_SETUP_TOOL} with { "startUrl": "<site URL>", "goal": "${XRAY_CANONICAL_GOAL}" }; on every later call, reuse the returned setupId and revision.`;
155
+ var LOCAL_JSON = JSON.stringify({
156
+ mcpServers: {
157
+ "mcp-scraper": {
158
+ command: "npx",
159
+ args: ["-y", "--package", "mcp-scraper@latest", "mcp-scraper"],
160
+ env: { MCP_SCRAPER_API_KEY: "${env:MCP_SCRAPER_API_KEY}" }
161
+ }
162
+ }
163
+ }, null, 2);
164
+ var HOSTED_JSON = JSON.stringify({
165
+ mcpServers: {
166
+ "mcp-scraper": { url: "https://mcpscraper.dev/mcp" }
167
+ }
168
+ }, null, 2);
169
+ var XRAY_INSTALL_CONTRACT_PAYLOAD = {
170
+ schema: "https://mcpscraper.dev/schemas/xray-install-contract-v1.json",
171
+ protocolVersion: XRAY_INSTALL_PROTOCOL_VERSION,
172
+ product: "Thorbit X-Ray",
173
+ triggerPhrases: [XRAY_TRIGGER_PHRASE, "Install X-Ray on this site and tell me what drives customers"],
174
+ canonicalUrls: {
175
+ markdown: "https://mcpscraper.dev/agent-install.md",
176
+ json: "https://mcpscraper.dev/.well-known/xray-install.json",
177
+ hostedMcp: "https://mcpscraper.dev/mcp"
178
+ },
179
+ facade: {
180
+ tool: XRAY_SETUP_TOOL,
181
+ initialCall: { startUrl: "<site URL>", goal: XRAY_CANONICAL_GOAL },
182
+ continuationRule: "Treat setupId plus revision as the durable continuation handle. Reuse both after approval, runtime publication, verification, retry, or return."
183
+ },
184
+ topologies: {
185
+ local_stdio: {
186
+ label: "Local npm MCP process",
187
+ authority: "The client starts MCP Scraper locally. After the one-time Pixel insertion, X-Ray setup publishes only owner-confirmed runtime configuration.",
188
+ secretHandling: "Set MCP_SCRAPER_API_KEY in the local environment or client secret field. Never paste it into chat or commit it."
189
+ },
190
+ hosted_remote: {
191
+ label: "Hosted authenticated MCP",
192
+ authority: "The hosted MCP inspects approved public pages and publishes owner-confirmed X-Ray runtime configuration. It cannot read or write a caller-local path.",
193
+ secretHandling: "Use the MCP OAuth/connection flow. Never put a credential in a URL, prompt, or config shown in chat."
194
+ }
195
+ },
196
+ hosts: [
197
+ {
198
+ id: "codex",
199
+ label: "Codex",
200
+ supportedTopologies: ["local_stdio", "hosted_remote"],
201
+ mcpConfigMode: "toml",
202
+ workspaceRead: true,
203
+ workspaceWrite: true,
204
+ restartRequired: true,
205
+ exactConfig: '[mcp_servers.mcp-scraper]\ncommand = "npx"\nargs = ["-y", "--package", "mcp-scraper@latest", "mcp-scraper"]\nenv_vars = ["MCP_SCRAPER_API_KEY"]',
206
+ continuation: CONTINUATION,
207
+ rollback: "Remove the mcp_servers.mcp-scraper table, then restart Codex. Pixel removal is a separate owner-controlled site action."
208
+ },
209
+ {
210
+ id: "claude-code",
211
+ label: "Claude Code",
212
+ supportedTopologies: ["local_stdio", "hosted_remote"],
213
+ mcpConfigMode: "command",
214
+ workspaceRead: true,
215
+ workspaceWrite: true,
216
+ restartRequired: true,
217
+ exactConfig: 'claude mcp add mcp-scraper --scope user --env MCP_SCRAPER_API_KEY="$MCP_SCRAPER_API_KEY" -- npx -y --package mcp-scraper@latest mcp-scraper',
218
+ continuation: CONTINUATION,
219
+ rollback: "Run: claude mcp remove mcp-scraper -s user. Pixel removal is a separate owner-controlled site action."
220
+ },
221
+ {
222
+ id: "claude-desktop",
223
+ label: "Claude Desktop",
224
+ supportedTopologies: ["local_stdio"],
225
+ mcpConfigMode: "desktop_extension",
226
+ workspaceRead: false,
227
+ workspaceWrite: false,
228
+ restartRequired: true,
229
+ exactConfig: "Install https://mcpscraper.dev/downloads/mcp-scraper.mcpb and enter the API key only in Claude Desktop's protected configuration field.",
230
+ continuation: CONTINUATION,
231
+ rollback: "Remove the MCP Scraper extension in Claude Desktop. Pixel removal is a separate owner-controlled site action."
232
+ },
233
+ {
234
+ id: "cursor",
235
+ label: "Cursor",
236
+ supportedTopologies: ["local_stdio", "hosted_remote"],
237
+ mcpConfigMode: "json",
238
+ workspaceRead: true,
239
+ workspaceWrite: true,
240
+ restartRequired: true,
241
+ exactConfig: LOCAL_JSON,
242
+ continuation: CONTINUATION,
243
+ rollback: "Remove mcp-scraper from .cursor/mcp.json (or the user MCP settings), then restart Cursor."
244
+ },
245
+ {
246
+ id: "windsurf",
247
+ label: "Windsurf",
248
+ supportedTopologies: ["local_stdio"],
249
+ mcpConfigMode: "json",
250
+ workspaceRead: true,
251
+ workspaceWrite: true,
252
+ restartRequired: true,
253
+ exactConfig: LOCAL_JSON,
254
+ continuation: CONTINUATION,
255
+ rollback: "Remove mcp-scraper from the Windsurf MCP configuration, then restart Windsurf."
256
+ },
257
+ {
258
+ id: "cline",
259
+ label: "Cline",
260
+ supportedTopologies: ["local_stdio"],
261
+ mcpConfigMode: "json",
262
+ workspaceRead: true,
263
+ workspaceWrite: true,
264
+ restartRequired: true,
265
+ exactConfig: LOCAL_JSON,
266
+ continuation: CONTINUATION,
267
+ rollback: "Remove mcp-scraper from Cline MCP Servers, then restart the extension host."
268
+ },
269
+ {
270
+ id: "user-action-only",
271
+ label: "User-action-only MCP host",
272
+ supportedTopologies: ["hosted_remote"],
273
+ mcpConfigMode: "handoff_only",
274
+ workspaceRead: false,
275
+ workspaceWrite: false,
276
+ restartRequired: false,
277
+ exactConfig: HOSTED_JSON,
278
+ continuation: CONTINUATION,
279
+ rollback: "Disconnect MCP Scraper from the host. Pixel removal remains an owner-controlled site action."
280
+ }
281
+ ],
282
+ tagBootstrap: {
283
+ requirement: "one_global_pixel_once",
284
+ ongoingConfiguration: "xray_runtime_manifest",
285
+ sourceAccessRequiredAfterInstall: false,
286
+ supportedInsertionPaths: ["manual", "tag_manager", "cms_plugin", "coding_host"]
287
+ },
288
+ workflow: [
289
+ "Resolve Thorbit X-Ray entitlement and the target site without consuming MCP Scraper Credits.",
290
+ "Inspect the whole approved public site; enumerate distinct renderers, routes, and Pixel coverage.",
291
+ "Infer the business model and customer outcome, then propose one privacy-safe measurement plan.",
292
+ "If the Pixel is absent, return the canonical one-time tag and exact uncovered surfaces; do not claim ongoing control there.",
293
+ "Publish an approved measurement revision as a digest-bound runtime manifest; source or repository access is not required.",
294
+ "Verify route coverage, consent, PageViews, manifest convergence, bounded rule matches, and persisted definition/version evidence.",
295
+ "Require a separate exact-revision approval before any canary or real-visitor experiment activation.",
296
+ "Return first customer-journey answers when sufficient, or exact evidence-readiness thresholds when not.",
297
+ "On \u201Cimprove\u201D, propose one highest-value uncovered outcome, require approval when needed, and reverify."
298
+ ],
299
+ safetyRules: [
300
+ "Never invent or hand-author tracking code; use the canonical product-authored Pixel snippet returned by analytics_setup_site.",
301
+ "Never ask for or print API keys, OAuth tokens, signed URLs, cookies, or other credentials.",
302
+ "Never call ad platforms directly or silently activate events, definitions, funnels, or delivery destinations.",
303
+ "Never treat a CTA click as a customer, sale, or revenue event without verified outcome evidence.",
304
+ "Never claim full-domain installation while an in-scope route or renderer is missing, excluded without approval, or unverifiable.",
305
+ "Never treat event-name presence as verification; require persisted definition ID and version evidence."
306
+ ],
307
+ terminalStates: ["complete", "tag_required", "input_required", "approval_required", "runtime_convergence_required", "outcome_connection_required", "not_ready", "error"]
308
+ };
309
+ function canonicalJson(value) {
310
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
311
+ if (value && typeof value === "object") {
312
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
313
+ }
314
+ return JSON.stringify(value);
315
+ }
316
+ function xrayInstallDigestPayload() {
317
+ return canonicalJson(XRAY_INSTALL_CONTRACT_PAYLOAD);
318
+ }
319
+ var XRAY_INSTALL_PROTOCOL_DIGEST = `sha256:${createHash("sha256").update(xrayInstallDigestPayload(), "utf8").digest("hex")}`;
320
+ var XRAY_INSTALL_CONTRACT = {
321
+ ...XRAY_INSTALL_CONTRACT_PAYLOAD,
322
+ protocolDigest: XRAY_INSTALL_PROTOCOL_DIGEST
323
+ };
324
+ function xrayHostRecipe(host) {
325
+ const recipe = XRAY_INSTALL_CONTRACT.hosts.find((candidate) => candidate.id === host);
326
+ if (!recipe) throw new Error(`Unknown X-Ray host: ${host}`);
327
+ return recipe;
328
+ }
329
+ function renderXraySetupPrompt() {
330
+ return [
331
+ "# Install X-Ray",
332
+ "",
333
+ `Protocol: ${XRAY_INSTALL_PROTOCOL_VERSION} (${XRAY_INSTALL_PROTOCOL_DIGEST})`,
334
+ "",
335
+ `Use ${XRAY_SETUP_TOOL} as the single conversational setup facade. Ask for the target site URL if it is missing, then call it with the goal \u201C${XRAY_CANONICAL_GOAL}\u201D.`,
336
+ "",
337
+ "Continue the same setup with its setupId and revision. Confirm one global Pixel across all public rendering surfaces; present one measurement plan; publish it as a digest-bound runtime manifest after exact-revision approval; verify manifest and rule versions; then return first customer-journey answers or explicit readiness thresholds. GitHub or repository access is not required after the tag is installed.",
338
+ "",
339
+ "Do not request pasted credentials, invent tracking code, silently activate measurement, call ad platforms directly, or claim customers/revenue from CTA clicks."
340
+ ].join("\n");
341
+ }
342
+ function xrayInstallJson(contract = XRAY_INSTALL_CONTRACT) {
343
+ return JSON.parse(JSON.stringify(contract));
344
+ }
345
+
148
346
  // src/leads/contact-enrichment.ts
149
347
  var GENERIC_EMAIL_PREFIXES = /* @__PURE__ */ new Set([
150
348
  "admin",
@@ -289,5 +487,13 @@ export {
289
487
  selectPrimaryEmail,
290
488
  contactAttemptUrls,
291
489
  buildSerpEmailQuery,
292
- extractSerpEmailEvidence
490
+ extractSerpEmailEvidence,
491
+ XRAY_INSTALL_PROTOCOL_VERSION,
492
+ XRAY_SETUP_TOOL,
493
+ XRAY_CANONICAL_GOAL,
494
+ XRAY_INSTALL_PROTOCOL_DIGEST,
495
+ XRAY_INSTALL_CONTRACT,
496
+ xrayHostRecipe,
497
+ renderXraySetupPrompt,
498
+ xrayInstallJson
293
499
  };
@@ -3263,9 +3263,139 @@ async function migrateAnalytics() {
3263
3263
  await client.query(
3264
3264
  "CREATE INDEX IF NOT EXISTS analytics_spend_facts_hierarchy ON analytics_ad_spend_facts(site_id, platform, account_id, campaign_id, ad_set_id, ad_id, bucket DESC)"
3265
3265
  );
3266
+ await client.query(`
3267
+ CREATE TABLE IF NOT EXISTS analytics_runtime_manifests (
3268
+ manifest_id uuid PRIMARY KEY,
3269
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3270
+ revision integer NOT NULL CHECK (revision > 0),
3271
+ digest text NOT NULL CHECK (digest ~ '^[a-f0-9]{64}$'),
3272
+ manifest jsonb NOT NULL,
3273
+ is_current boolean NOT NULL DEFAULT false,
3274
+ created_by_user_id bigint NOT NULL,
3275
+ expires_at timestamptz NOT NULL,
3276
+ created_at timestamptz NOT NULL DEFAULT now(),
3277
+ UNIQUE(site_id, revision)
3278
+ )
3279
+ `);
3280
+ await client.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_runtime_manifests_one_current ON analytics_runtime_manifests(site_id) WHERE is_current");
3281
+ await client.query("CREATE INDEX IF NOT EXISTS analytics_runtime_manifests_site_created ON analytics_runtime_manifests(site_id, created_at DESC)");
3282
+ await client.query(`
3283
+ CREATE TABLE IF NOT EXISTS analytics_runtime_manifest_publications (
3284
+ publication_id uuid PRIMARY KEY,
3285
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3286
+ manifest_id uuid NOT NULL REFERENCES analytics_runtime_manifests(manifest_id) ON DELETE CASCADE,
3287
+ setup_id uuid,
3288
+ setup_revision integer,
3289
+ idempotency_key text NOT NULL,
3290
+ request_fingerprint text NOT NULL CHECK (request_fingerprint ~ '^[a-f0-9]{64}$'),
3291
+ manifest jsonb NOT NULL,
3292
+ published_at timestamptz NOT NULL DEFAULT now(),
3293
+ UNIQUE(site_id, idempotency_key)
3294
+ )
3295
+ `);
3296
+ await client.query(`
3297
+ CREATE TABLE IF NOT EXISTS analytics_runtime_rule_health (
3298
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3299
+ manifest_id uuid NOT NULL REFERENCES analytics_runtime_manifests(manifest_id) ON DELETE CASCADE,
3300
+ rule_id uuid NOT NULL,
3301
+ rule_version integer NOT NULL,
3302
+ route_path text NOT NULL,
3303
+ selector_quality text NOT NULL CHECK (selector_quality IN ('portable','stable','fragile')),
3304
+ match_count integer NOT NULL CHECK (match_count BETWEEN 0 AND 100),
3305
+ structural_fingerprint text NOT NULL CHECK (structural_fingerprint ~ '^[a-f0-9]{64}$'),
3306
+ observed_at timestamptz NOT NULL,
3307
+ PRIMARY KEY(manifest_id, rule_id, route_path)
3308
+ )
3309
+ `);
3310
+ await client.query(`
3311
+ CREATE TABLE IF NOT EXISTS analytics_experiments (
3312
+ experiment_id uuid PRIMARY KEY,
3313
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3314
+ current_revision integer NOT NULL CHECK (current_revision > 0),
3315
+ state text NOT NULL CHECK (state IN ('draft','approved','canary','active','paused','killed','archived')),
3316
+ created_by_user_id bigint NOT NULL,
3317
+ create_idempotency_key text,
3318
+ create_request_fingerprint text,
3319
+ started_at timestamptz,
3320
+ created_at timestamptz NOT NULL DEFAULT now(),
3321
+ updated_at timestamptz NOT NULL DEFAULT now()
3322
+ )
3323
+ `);
3324
+ await client.query("ALTER TABLE analytics_experiments ADD COLUMN IF NOT EXISTS create_idempotency_key text");
3325
+ await client.query("ALTER TABLE analytics_experiments ADD COLUMN IF NOT EXISTS create_request_fingerprint text");
3326
+ await client.query("ALTER TABLE analytics_experiments ADD COLUMN IF NOT EXISTS started_at timestamptz");
3327
+ await client.query("CREATE UNIQUE INDEX IF NOT EXISTS analytics_experiments_site_create_idempotency ON analytics_experiments(site_id, create_idempotency_key) WHERE create_idempotency_key IS NOT NULL");
3328
+ await client.query(`
3329
+ CREATE TABLE IF NOT EXISTS analytics_experiment_revisions (
3330
+ experiment_id uuid NOT NULL REFERENCES analytics_experiments(experiment_id) ON DELETE CASCADE,
3331
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3332
+ revision integer NOT NULL CHECK (revision > 0),
3333
+ plan jsonb NOT NULL,
3334
+ approved_by_user_id bigint,
3335
+ approved_at timestamptz,
3336
+ created_at timestamptz NOT NULL DEFAULT now(),
3337
+ PRIMARY KEY(experiment_id, revision)
3338
+ )
3339
+ `);
3340
+ await client.query(`
3341
+ CREATE TABLE IF NOT EXISTS analytics_experiment_receipts (
3342
+ receipt_id uuid PRIMARY KEY,
3343
+ experiment_id uuid NOT NULL REFERENCES analytics_experiments(experiment_id) ON DELETE CASCADE,
3344
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3345
+ revision integer NOT NULL,
3346
+ operation text NOT NULL,
3347
+ idempotency_key text,
3348
+ receipt jsonb NOT NULL,
3349
+ occurred_at timestamptz NOT NULL DEFAULT now(),
3350
+ UNIQUE(experiment_id, idempotency_key)
3351
+ )
3352
+ `);
3353
+ await client.query("CREATE INDEX IF NOT EXISTS analytics_experiments_site_updated ON analytics_experiments(site_id, updated_at DESC)");
3354
+ await client.query(`
3355
+ CREATE TABLE IF NOT EXISTS analytics_site_setups (
3356
+ setup_id uuid PRIMARY KEY,
3357
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3358
+ owner_user_id bigint NOT NULL,
3359
+ revision integer NOT NULL CHECK (revision > 0),
3360
+ status text NOT NULL,
3361
+ start_url text NOT NULL,
3362
+ state jsonb NOT NULL,
3363
+ expires_at timestamptz NOT NULL,
3364
+ created_at timestamptz NOT NULL,
3365
+ updated_at timestamptz NOT NULL
3366
+ )
3367
+ `);
3368
+ await client.query(`
3369
+ CREATE TABLE IF NOT EXISTS analytics_site_setup_revisions (
3370
+ setup_id uuid NOT NULL REFERENCES analytics_site_setups(setup_id) ON DELETE CASCADE,
3371
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3372
+ owner_user_id bigint NOT NULL,
3373
+ revision integer NOT NULL CHECK (revision > 0),
3374
+ status text NOT NULL,
3375
+ state jsonb NOT NULL,
3376
+ created_at timestamptz NOT NULL,
3377
+ PRIMARY KEY(setup_id, revision)
3378
+ )
3379
+ `);
3380
+ await client.query(`
3381
+ CREATE TABLE IF NOT EXISTS analytics_site_setup_receipts (
3382
+ receipt_id uuid PRIMARY KEY,
3383
+ setup_id uuid NOT NULL REFERENCES analytics_site_setups(setup_id) ON DELETE CASCADE,
3384
+ site_id uuid NOT NULL REFERENCES analytics_sites(id) ON DELETE CASCADE,
3385
+ owner_user_id bigint NOT NULL,
3386
+ revision integer NOT NULL,
3387
+ operation text NOT NULL,
3388
+ status text NOT NULL,
3389
+ receipt jsonb NOT NULL,
3390
+ occurred_at timestamptz NOT NULL
3391
+ )
3392
+ `);
3393
+ await client.query("CREATE INDEX IF NOT EXISTS analytics_site_setups_owner_updated ON analytics_site_setups(owner_user_id, updated_at DESC)");
3394
+ await client.query("CREATE INDEX IF NOT EXISTS analytics_site_setups_site_updated ON analytics_site_setups(site_id, updated_at DESC)");
3395
+ await client.query("CREATE INDEX IF NOT EXISTS analytics_site_setup_receipts_setup_revision ON analytics_site_setup_receipts(setup_id, revision, occurred_at)");
3266
3396
  await client.query(`
3267
3397
  INSERT INTO analytics_schema_migrations(version)
3268
- VALUES ('2026-08-05.1'), ('2026-08-05.2'), ('2026-08-05.3'), ('2026-08-05.4'), ('2026-08-05.5'), ('2026-08-05.6'), ('2026-08-05.7'), ('2026-08-05.8'), ('2026-08-05.9'), ('2026-08-05.10'), ('2026-08-05.11'), ('2026-08-05.12'), ('2026-08-05.13'), ('2026-08-05.14'), ('2026-08-26.1'), ('2026-08-26.2'), ('2026-08-26.3'), ('2026-08-26.4'), ('2026-08-27.1'), ('2026-08-28.1'), ('2026-08-28.2'), ('2026-08-28.3'), ('2026-08-28.4')
3398
+ VALUES ('2026-08-05.1'), ('2026-08-05.2'), ('2026-08-05.3'), ('2026-08-05.4'), ('2026-08-05.5'), ('2026-08-05.6'), ('2026-08-05.7'), ('2026-08-05.8'), ('2026-08-05.9'), ('2026-08-05.10'), ('2026-08-05.11'), ('2026-08-05.12'), ('2026-08-05.13'), ('2026-08-05.14'), ('2026-08-26.1'), ('2026-08-26.2'), ('2026-08-26.3'), ('2026-08-26.4'), ('2026-08-27.1'), ('2026-08-28.1'), ('2026-08-28.2'), ('2026-08-28.3'), ('2026-08-28.4'), ('2026-08-31.1'), ('2026-09-01.1')
3269
3399
  ON CONFLICT (version) DO NOTHING
3270
3400
  `);
3271
3401
  } catch (error) {
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var PACKAGE_VERSION = "0.80.0";
2
+ var PACKAGE_VERSION = "0.83.0";
3
3
 
4
4
  export {
5
5
  PACKAGE_VERSION
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-DNM65UCK.js";
8
8
  import {
9
9
  LedgerOperation
10
- } from "./chunk-PMONKQGJ.js";
10
+ } from "./chunk-4NZ6JFK5.js";
11
11
  import {
12
12
  CREDIT_LOT_TTL,
13
13
  getDb,
@@ -255,6 +255,7 @@ function createSiteExtractContentReader(ownerId) {
255
255
  }
256
256
 
257
257
  // src/api/site-extract-repository.ts
258
+ var XRAY_ENTITLED_EXTRACT_BILLING_CLASS = "xray_entitlement";
258
259
  function extractJobLimitInfo(job) {
259
260
  const effectiveMaxPages = Math.max(1, Number(job.options.effectiveMaxPages ?? job.options.maxPages ?? 1));
260
261
  const requestedMaxPages = Math.max(effectiveMaxPages, Number(job.options.requestedMaxPages ?? effectiveMaxPages));
@@ -366,16 +367,31 @@ async function listFundedPendingExtractJobs(limit = 25, staleMinutes = 2) {
366
367
  const safeStaleMinutes = Math.max(1, Math.min(60, Math.round(staleMinutes)));
367
368
  const res = await db.execute({
368
369
  sql: `SELECT j.* FROM site_extract_jobs j
369
- JOIN billing_debits d
370
+ LEFT JOIN billing_debits d
370
371
  ON d.idempotency_key = json_extract(j.options, '$.debitKey')
371
372
  AND d.user_id = j.user_id
372
373
  AND d.status = 'applied'
373
374
  WHERE j.status = 'pending'
374
375
  AND j.billed_mc IS NULL
376
+ AND (
377
+ d.idempotency_key IS NOT NULL
378
+ OR (
379
+ json_extract(j.options, '$.billingClass') = ?
380
+ AND json_extract(j.options, '$.heldMc') = 0
381
+ AND json_extract(j.options, '$.debitKey') IS NULL
382
+ AND json_extract(j.options, '$.xraySetupReceipt.billingClass') = ?
383
+ AND json_extract(j.options, '$.xraySetupReceipt.consumesMcpScraperCredits') = 0
384
+ )
385
+ )
375
386
  AND (j.next_dispatch_at IS NULL OR j.next_dispatch_at <= datetime('now'))
376
387
  AND j.updated_at <= datetime('now', ?)
377
388
  ORDER BY j.updated_at ASC LIMIT ?`,
378
- args: [`-${safeStaleMinutes} minutes`, Math.max(1, Math.min(100, Math.round(limit)))]
389
+ args: [
390
+ XRAY_ENTITLED_EXTRACT_BILLING_CLASS,
391
+ XRAY_ENTITLED_EXTRACT_BILLING_CLASS,
392
+ `-${safeStaleMinutes} minutes`,
393
+ Math.max(1, Math.min(100, Math.round(limit)))
394
+ ]
379
395
  });
380
396
  return res.rows.map((row) => rowToJob(row));
381
397
  }
@@ -687,16 +703,24 @@ async function failUnfundedExtractJob(jobId, error, publicError) {
687
703
  });
688
704
  }
689
705
  async function setExtractJobPublicError(jobId, publicError) {
706
+ const job = await getExtractJob(jobId);
707
+ const normalized = job?.options.billingClass === XRAY_ENTITLED_EXTRACT_BILLING_CLASS && publicError ? { ...publicError, charge_status: "not_charged" } : publicError;
690
708
  await getDb().execute({
691
709
  sql: `UPDATE site_extract_jobs SET public_error_json = ?, updated_at = datetime('now')
692
710
  WHERE id = ? AND status IN ('complete', 'partial', 'failed')`,
693
- args: [serializePublicErrorEnvelope(publicError), jobId]
711
+ args: [serializePublicErrorEnvelope(normalized), jobId]
694
712
  });
695
713
  }
696
714
  async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference) {
697
715
  const db = getDb();
698
716
  const job = await getExtractJob(jobId);
699
717
  if (!job || job.billedMc != null) return;
718
+ if (job.options.billingClass === XRAY_ENTITLED_EXTRACT_BILLING_CLASS) {
719
+ if (!await finalizeXRayEntitledExtractJobCharge(jobId)) {
720
+ throw new Error("xray_extract_zero_charge_finalization_rejected");
721
+ }
722
+ return;
723
+ }
700
724
  const debitKey = typeof job.options.debitKey === "string" ? job.options.debitKey : null;
701
725
  if (debitKey) {
702
726
  const settlement = await settleDebitMcIdempotent(
@@ -757,6 +781,24 @@ async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference)
757
781
  }
758
782
  ], "write");
759
783
  }
784
+ async function finalizeXRayEntitledExtractJobCharge(jobId) {
785
+ const db = getDb();
786
+ const result = await db.execute({
787
+ sql: `UPDATE site_extract_jobs
788
+ SET billed_mc = 0, updated_at = datetime('now')
789
+ WHERE id = ?
790
+ AND billed_mc IS NULL
791
+ AND json_extract(options, '$.billingClass') = ?
792
+ AND json_extract(options, '$.heldMc') = 0
793
+ AND json_extract(options, '$.debitKey') IS NULL
794
+ AND json_extract(options, '$.xraySetupReceipt.billingClass') = ?
795
+ AND json_extract(options, '$.xraySetupReceipt.consumesMcpScraperCredits') = 0
796
+ AND json_extract(options, '$.xraySetupReceipt.heldMc') = 0
797
+ AND json_extract(options, '$.xraySetupReceipt.billedMc') = 0`,
798
+ args: [jobId, XRAY_ENTITLED_EXTRACT_BILLING_CLASS, XRAY_ENTITLED_EXTRACT_BILLING_CLASS]
799
+ });
800
+ return result.rowsAffected === 1;
801
+ }
760
802
 
761
803
  export {
762
804
  SITE_EXTRACT_ARTIFACT_PREFIX,
@@ -767,6 +809,7 @@ export {
767
809
  readOwnedSiteExtractImageArtifact,
768
810
  cleanupExpiredSiteExtractArtifacts,
769
811
  createSiteExtractContentReader,
812
+ XRAY_ENTITLED_EXTRACT_BILLING_CLASS,
770
813
  extractJobLimitInfo,
771
814
  terminalExtractJobStatus,
772
815
  createExtractJob,
@@ -795,5 +838,6 @@ export {
795
838
  failExtractJob,
796
839
  failUnfundedExtractJob,
797
840
  setExtractJobPublicError,
798
- settleExtractJob
841
+ settleExtractJob,
842
+ finalizeXRayEntitledExtractJobCharge
799
843
  };
@@ -8,7 +8,7 @@ import {
8
8
  currentCostContext,
9
9
  recordKernelSession,
10
10
  runWithCostContext
11
- } from "./chunk-5UAFBKJR.js";
11
+ } from "./chunk-OPQIGAFB.js";
12
12
  import {
13
13
  DEFAULT_PROXY_MODE,
14
14
  HarvestOptionsSchema,
@@ -5120,7 +5120,9 @@ export {
5120
5120
  serpArrivalUrl,
5121
5121
  buildYouTubeChannelVideosUrl,
5122
5122
  BrowserDriver,
5123
+ sanitizeProviderLocalMessage,
5123
5124
  validateProviderSessionId,
5125
+ brightDataBrowserEndpoint,
5124
5126
  brightDataSerpEnabled,
5125
5127
  questionIdFor,
5126
5128
  classifyGoogleOutboundLink,
@@ -33,7 +33,7 @@ var NANGO_USD_PER_CONNECTION_MONTH = envRate("NANGO_USD_PER_CONNECTION_MONTH", 1
33
33
  var NANGO_USD_PER_FUNCTION_RUN = envRate("NANGO_USD_PER_FUNCTION_RUN", 1e-4);
34
34
  var NANGO_USD_PER_PROXY_REQUEST = envRate("NANGO_USD_PER_PROXY_REQUEST", 1e-4);
35
35
  var NANGO_USD_PER_COMPUTE_SEC = envRate("NANGO_USD_PER_COMPUTE_SEC", 2e-4);
36
- var BRIGHTDATA_BROWSER_USD_PER_GB = envRate("BRIGHTDATA_BROWSER_USD_PER_GB", 0);
36
+ var BRIGHTDATA_BROWSER_USD_PER_GB = envRate("BRIGHTDATA_BROWSER_USD_PER_GB", 8);
37
37
  function kernelCostUsd(ms, headful) {
38
38
  const sec = Math.max(0, ms) / 1e3;
39
39
  return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
@@ -74,7 +74,7 @@ async function runCostTelemetryMigration() {
74
74
  SELECT
75
75
  (SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN ('kernel_session_log', 'vendor_usage_log', 'cost_probe_runs')) = 3
76
76
  AND (SELECT COUNT(*) FROM pragma_table_info('kernel_session_log') WHERE name IN ('proxy_source', 'proxy_type', 'method')) = 3
77
- AND (SELECT COUNT(*) FROM pragma_table_info('vendor_usage_log') WHERE name IN ('method', 'source_key')) = 2
77
+ AND (SELECT COUNT(*) FROM pragma_table_info('vendor_usage_log') WHERE name IN ('method', 'source_key', 'provider_duration_ms', 'provider_captcha', 'provider_status')) = 5
78
78
  AND (SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name = 'vendor_usage_log_vendor_source_key') = 1
79
79
  AND (SELECT COUNT(*) FROM pragma_table_info('cost_probe_runs') WHERE name IN ('units', 'unit_type', 'mode')) = 3
80
80
  AS ready
@@ -132,6 +132,9 @@ async function runCostTelemetryMigration() {
132
132
  est_cost_usd REAL NOT NULL DEFAULT 0,
133
133
  error TEXT,
134
134
  source_key TEXT,
135
+ provider_duration_ms INTEGER,
136
+ provider_captcha INTEGER,
137
+ provider_status TEXT,
135
138
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
136
139
  )
137
140
  `);
@@ -146,6 +149,18 @@ async function runCostTelemetryMigration() {
146
149
  await db.execute(`ALTER TABLE vendor_usage_log ADD COLUMN source_key TEXT`);
147
150
  } catch {
148
151
  }
152
+ try {
153
+ await db.execute(`ALTER TABLE vendor_usage_log ADD COLUMN provider_duration_ms INTEGER`);
154
+ } catch {
155
+ }
156
+ try {
157
+ await db.execute(`ALTER TABLE vendor_usage_log ADD COLUMN provider_captcha INTEGER`);
158
+ } catch {
159
+ }
160
+ try {
161
+ await db.execute(`ALTER TABLE vendor_usage_log ADD COLUMN provider_status TEXT`);
162
+ } catch {
163
+ }
149
164
  await db.execute(`CREATE UNIQUE INDEX IF NOT EXISTS vendor_usage_log_vendor_source_key ON vendor_usage_log(vendor, source_key) WHERE source_key IS NOT NULL`);
150
165
  await db.execute(`
151
166
  CREATE TABLE IF NOT EXISTS cost_probe_runs (
@@ -228,8 +243,8 @@ async function recordVendorUsage(r) {
228
243
  const db = getDb();
229
244
  const result = await db.execute({
230
245
  sql: `INSERT OR IGNORE INTO vendor_usage_log
231
- (id, op, probe_run_id, user_id, vendor, model, units, unit_type, est_cost_usd, error, method, source_key)
232
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
246
+ (id, op, probe_run_id, user_id, vendor, model, units, unit_type, est_cost_usd, error, method, source_key, provider_duration_ms, provider_captcha, provider_status)
247
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
233
248
  args: [
234
249
  randomUUID(),
235
250
  r.op ?? ctx?.op ?? null,
@@ -242,7 +257,10 @@ async function recordVendorUsage(r) {
242
257
  vendorCostUsd(r.vendor, r.units),
243
258
  r.error ?? null,
244
259
  r.method ?? ctx?.subOp ?? null,
245
- r.sourceKey ?? null
260
+ r.sourceKey ?? null,
261
+ r.providerDurationMs ?? null,
262
+ boolToInt(r.providerCaptcha),
263
+ r.providerStatus ?? null
246
264
  ]
247
265
  });
248
266
  return result.rowsAffected === 1;
@@ -262,6 +280,7 @@ export {
262
280
  currentCostContext,
263
281
  HEADLESS_OPS,
264
282
  vendorCostUsd,
283
+ migrateCostTelemetry,
265
284
  recordKernelSession,
266
285
  recordVendorUsage
267
286
  };