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.
- package/CHANGELOG.md +59 -1
- package/README.md +4 -2
- package/dist/{analytics-repository-YOY5KMOD.js → analytics-repository-GA6P5S5J.js} +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.js +61 -46
- package/dist/bin/mcp-scraper-core.js +7 -7
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.js +7 -7
- package/dist/bin/paa-harvest.js +2 -2
- package/dist/{chunk-PMONKQGJ.js → chunk-4NZ6JFK5.js} +2 -2
- package/dist/{chunk-VEQPA65D.js → chunk-ACDNW7TG.js} +1 -1
- package/dist/{chunk-ID7J23WP.js → chunk-AIVNZZO3.js} +4 -3
- package/dist/{chunk-VXLU74YZ.js → chunk-BGD7U3AE.js} +207 -1
- package/dist/{chunk-HEUMDX4I.js → chunk-CNAJWRJT.js} +131 -1
- package/dist/{chunk-YPOOARYN.js → chunk-FUG2IRKP.js} +1 -1
- package/dist/{chunk-XBEUZJHS.js → chunk-HGJJQY24.js} +49 -5
- package/dist/{chunk-KNGTQD6U.js → chunk-NH4QAH3X.js} +3 -1
- package/dist/{chunk-5UAFBKJR.js → chunk-OPQIGAFB.js} +24 -5
- package/dist/{chunk-OY25W46Q.js → chunk-QSZXAA5F.js} +3121 -2222
- package/dist/{chunk-P4447RNF.js → chunk-UXVT7FWW.js} +2 -2
- package/dist/{chunk-62W6UER7.js → chunk-ZHPGPVJJ.js} +2 -2
- package/dist/{extract-bundle-232SA7MI.js → extract-bundle-HTWWJD6N.js} +2 -2
- package/dist/{gmail-service-IMUHMRH4.js → gmail-service-VXE7OYNA.js} +3 -3
- package/dist/index.cjs +23 -5
- package/dist/index.js +2 -2
- package/dist/{server-XBRLYGTR.js → server-ICMRDYI4.js} +4789 -2842
- package/dist/{site-extract-repository-VDONDDMR.js → site-extract-repository-Y5WQFSQW.js} +6 -2
- package/dist/{worker-ICCZR443.js → worker-FM3ZRDY4.js} +3 -3
- package/package.json +6 -2
- package/public/.well-known/xray-install.json +178 -0
- 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) {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./chunk-DNM65UCK.js";
|
|
8
8
|
import {
|
|
9
9
|
LedgerOperation
|
|
10
|
-
} from "./chunk-
|
|
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: [
|
|
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(
|
|
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-
|
|
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",
|
|
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')) =
|
|
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
|
};
|