mcp-scraper 0.21.2 → 0.21.4

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 (57) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +1061 -308
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +101 -34
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +22 -0
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-ZJDANZTQ.js → chunk-E5J4HJBO.js} +3 -2
  18. package/dist/chunk-E5J4HJBO.js.map +1 -0
  19. package/dist/{chunk-DZBGULWW.js → chunk-ICT7DDHL.js} +399 -3
  20. package/dist/chunk-ICT7DDHL.js.map +1 -0
  21. package/dist/{chunk-2FF4GBQ4.js → chunk-JHI373VB.js} +49 -7
  22. package/dist/chunk-JHI373VB.js.map +1 -0
  23. package/dist/{chunk-GMZFQRH2.js → chunk-MTSBI7ZH.js} +23 -6
  24. package/dist/chunk-MTSBI7ZH.js.map +1 -0
  25. package/dist/{chunk-ZNNYPE2U.js → chunk-N4JHXPW6.js} +89 -27
  26. package/dist/chunk-N4JHXPW6.js.map +1 -0
  27. package/dist/{chunk-X3NIXHHG.js → chunk-PNBYYAEK.js} +3 -3
  28. package/dist/{chunk-JBME5WJR.js → chunk-SDHPQTFM.js} +2 -2
  29. package/dist/chunk-WQI7JIIN.js +7 -0
  30. package/dist/chunk-WQI7JIIN.js.map +1 -0
  31. package/dist/{db-EONGTNJM.js → db-WRILVOFY.js} +6 -2
  32. package/dist/{extract-bundle-HMCYUVBJ.js → extract-bundle-NFFVMUMT.js} +2 -2
  33. package/dist/index.cjs +22 -0
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.js +3 -3
  36. package/dist/{server-CHTX6YQA.js → server-OLOOFN5W.js} +499 -271
  37. package/dist/server-OLOOFN5W.js.map +1 -0
  38. package/dist/{site-extract-repository-5V5EEQ72.js → site-extract-repository-2QQSL2QC.js} +4 -4
  39. package/dist/{worker-GE5JMHE6.js → worker-RV2TYXE3.js} +15 -8
  40. package/dist/worker-RV2TYXE3.js.map +1 -0
  41. package/docs/mcp-tool-manifest.generated.json +90 -11
  42. package/docs/specs/oauth-mcp-spec.md +2 -2
  43. package/package.json +1 -1
  44. package/dist/chunk-2FF4GBQ4.js.map +0 -1
  45. package/dist/chunk-DZBGULWW.js.map +0 -1
  46. package/dist/chunk-GMZFQRH2.js.map +0 -1
  47. package/dist/chunk-NBF4F4UT.js +0 -7
  48. package/dist/chunk-NBF4F4UT.js.map +0 -1
  49. package/dist/chunk-ZJDANZTQ.js.map +0 -1
  50. package/dist/chunk-ZNNYPE2U.js.map +0 -1
  51. package/dist/server-CHTX6YQA.js.map +0 -1
  52. package/dist/worker-GE5JMHE6.js.map +0 -1
  53. /package/dist/{chunk-X3NIXHHG.js.map → chunk-PNBYYAEK.js.map} +0 -0
  54. /package/dist/{chunk-JBME5WJR.js.map → chunk-SDHPQTFM.js.map} +0 -0
  55. /package/dist/{db-EONGTNJM.js.map → db-WRILVOFY.js.map} +0 -0
  56. /package/dist/{extract-bundle-HMCYUVBJ.js.map → extract-bundle-NFFVMUMT.js.map} +0 -0
  57. /package/dist/{site-extract-repository-5V5EEQ72.js.map → site-extract-repository-2QQSL2QC.js.map} +0 -0
@@ -6,7 +6,7 @@ import {
6
6
  createHarvestAttemptRecorder,
7
7
  harvestProblemResponse,
8
8
  serializeHarvestProblem
9
- } from "./chunk-JBME5WJR.js";
9
+ } from "./chunk-SDHPQTFM.js";
10
10
  import {
11
11
  BrowserDriver,
12
12
  MapsSelectors,
@@ -14,7 +14,7 @@ import {
14
14
  deleteKernelProxyId,
15
15
  harvest,
16
16
  resolveKernelProxyId
17
- } from "./chunk-ZJDANZTQ.js";
17
+ } from "./chunk-E5J4HJBO.js";
18
18
  import {
19
19
  HttpMcpToolExecutor,
20
20
  MemoryMcpToolExecutor,
@@ -35,7 +35,7 @@ import {
35
35
  sanitizeAttempts,
36
36
  sanitizeHarvestResult,
37
37
  transcribeMediaUrl
38
- } from "./chunk-ZNNYPE2U.js";
38
+ } from "./chunk-N4JHXPW6.js";
39
39
  import {
40
40
  auditImages,
41
41
  buildLinkReport,
@@ -49,11 +49,10 @@ import {
49
49
  browserServiceProxyId,
50
50
  currentCostContext,
51
51
  kernelCostUsd,
52
- migrateCostTelemetry,
53
52
  recordKernelSession,
54
53
  recordVendorUsage,
55
54
  runWithCostContext
56
- } from "./chunk-GMZFQRH2.js";
55
+ } from "./chunk-MTSBI7ZH.js";
57
56
  import {
58
57
  csvRecords,
59
58
  listWorkflowDefinitions,
@@ -77,7 +76,7 @@ import {
77
76
  RawMapsOverviewSchema,
78
77
  RawMapsReviewStatsSchema
79
78
  } from "./chunk-XGIPATLV.js";
80
- import "./chunk-NBF4F4UT.js";
79
+ import "./chunk-WQI7JIIN.js";
81
80
  import {
82
81
  completeExtractJob,
83
82
  countSuccessfulPages,
@@ -88,12 +87,11 @@ import {
88
87
  saveExtractPages,
89
88
  setExtractJobTotal,
90
89
  settleExtractJob
91
- } from "./chunk-X3NIXHHG.js";
90
+ } from "./chunk-PNBYYAEK.js";
92
91
  import {
93
92
  BROWSER_OPEN_MIN_BALANCE_MC,
94
93
  CONCURRENCY_PRICE_ID,
95
94
  CREDIT_COST_CATALOG,
96
- FREE_SIGNUP_MC,
97
95
  LedgerOperation,
98
96
  MC_COSTS,
99
97
  MC_PER_CREDIT,
@@ -115,9 +113,11 @@ import {
115
113
  VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE,
116
114
  browserActiveCostMc,
117
115
  concurrencySlotBillingInfo,
116
+ getPublicRatesPayload,
118
117
  insufficientBalanceResponse,
118
+ serpActualCostMc,
119
119
  vendorCostUsdToMc
120
- } from "./chunk-2FF4GBQ4.js";
120
+ } from "./chunk-JHI373VB.js";
121
121
  import {
122
122
  CaptchaError,
123
123
  RECAPTCHA_INSTRUCTIONS,
@@ -143,6 +143,7 @@ import {
143
143
  creditMc,
144
144
  deactivateUser,
145
145
  debitMc,
146
+ debitMcIdempotent,
146
147
  deleteWorkflowSchedule,
147
148
  expireOldLots,
148
149
  failJob,
@@ -198,9 +199,10 @@ import {
198
199
  setStripeCustomerId,
199
200
  setSubscriptionTier,
200
201
  setUserMemoryCreds,
202
+ settleDebitMcIdempotent,
201
203
  upsertPageSnapshot,
202
204
  verifyPassword
203
- } from "./chunk-DZBGULWW.js";
205
+ } from "./chunk-ICT7DDHL.js";
204
206
 
205
207
  // src/blog/registry.ts
206
208
  var posts = [
@@ -7358,7 +7360,7 @@ var siteExtractFn = inngest.createFunction(
7358
7360
  return buildSeoAudit(pages, job.startUrl);
7359
7361
  }) : null;
7360
7362
  const artifacts = await step.run("finalize", async () => {
7361
- const { assembleExtractArtifacts } = await import("./extract-bundle-HMCYUVBJ.js");
7363
+ const { assembleExtractArtifacts } = await import("./extract-bundle-NFFVMUMT.js");
7362
7364
  const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
7363
7365
  await completeExtractJob(jobId, stored);
7364
7366
  return stored;
@@ -7988,8 +7990,17 @@ import { createHash as createHash2, createHmac, randomUUID, timingSafeEqual } fr
7988
7990
  // src/api/scheduling-access.ts
7989
7991
  var PAID_PLAN_REQUIRED_CODE = "paid_plan_required";
7990
7992
  var PAID_PLAN_REQUIRED_MESSAGE = "Scheduled Actions requires an active MCP Scraper Starter plan or higher.";
7993
+ function schedulingAccessOverrides() {
7994
+ return new Set(
7995
+ (process.env.SCHEDULING_ACCESS_OVERRIDE_IDENTITIES ?? "").split(",").map((identity) => identity.trim().toLowerCase()).filter(Boolean)
7996
+ );
7997
+ }
7991
7998
  function hasPaidSchedulingAccess(user) {
7992
- return typeof user.subscription_tier === "string" && user.subscription_tier.trim().length > 0;
7999
+ const tier = typeof user.subscription_tier === "string" ? user.subscription_tier.trim() : "";
8000
+ const subscriptionId = typeof user.subscription_id === "string" ? user.subscription_id.trim() : "";
8001
+ if (tier && subscriptionId) return true;
8002
+ const identity = typeof user.email === "string" ? user.email.trim().toLowerCase() : "";
8003
+ return identity.length > 0 && schedulingAccessOverrides().has(identity);
7993
8004
  }
7994
8005
  function paidPlanRequiredPayload() {
7995
8006
  return {
@@ -8026,7 +8037,7 @@ function memoryScheduleToolNeedsCredentials(upstreamName) {
8026
8037
  // src/api/unified-billing.ts
8027
8038
  var SCHEDULED_RUN_BILLING_CLASS = "scheduled_agent_run";
8028
8039
  var SCHEDULED_RUN_SOURCE_SURFACE = "mastra_scheduler";
8029
- var AUTHORIZATION_TTL_MS = 30 * 60 * 1e3;
8040
+ var AUTHORIZATION_TTL_MS = 24 * 60 * 60 * 1e3;
8030
8041
  var UnifiedBillingError = class extends Error {
8031
8042
  constructor(code, message, httpStatus, details = {}) {
8032
8043
  super(message);
@@ -8088,6 +8099,15 @@ function safeMetadata(value) {
8088
8099
  if (!value) return "{}";
8089
8100
  return JSON.stringify(value).slice(0, 4e3);
8090
8101
  }
8102
+ function metadataReason(row) {
8103
+ if (!row.metadata) return void 0;
8104
+ try {
8105
+ const metadata = JSON.parse(row.metadata);
8106
+ return typeof metadata.reason === "string" ? metadata.reason : void 0;
8107
+ } catch {
8108
+ return void 0;
8109
+ }
8110
+ }
8091
8111
  function mapAuthorization(row, balanceMc) {
8092
8112
  return {
8093
8113
  id: row.id,
@@ -8116,23 +8136,28 @@ async function eventByKey(idempotencyKey) {
8116
8136
  });
8117
8137
  return result.rows[0] ? result.rows[0] : null;
8118
8138
  }
8119
- function mapEvent(row, balanceMc, duplicate) {
8120
- let reason;
8121
- if (row.metadata) {
8122
- try {
8123
- const metadata = JSON.parse(row.metadata);
8124
- if (typeof metadata.reason === "string") reason = metadata.reason;
8125
- } catch {
8126
- }
8127
- }
8139
+ function mapStartEvent(row, balanceMc, duplicate) {
8140
+ return {
8141
+ id: row.id,
8142
+ authorizationId: row.authorization_id,
8143
+ status: "started",
8144
+ amountMc: SCHEDULED_RUN_BASE_MC,
8145
+ credits: SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT,
8146
+ balanceMc,
8147
+ duplicate
8148
+ };
8149
+ }
8150
+ function mapSettlementEvent(row, balanceMc, duplicate) {
8128
8151
  const rawNanos = row.raw_cost_usd_nanos == null ? null : Number(row.raw_cost_usd_nanos);
8129
- const modelMc = Math.max(0, Number(row.amount_mc) - SCHEDULED_RUN_BASE_MC);
8152
+ const modelMc = Math.max(0, Number(row.amount_mc));
8153
+ const amountMc = SCHEDULED_RUN_BASE_MC + modelMc;
8154
+ const reason = metadataReason(row);
8130
8155
  return {
8131
8156
  id: row.id,
8132
8157
  authorizationId: row.authorization_id,
8133
8158
  status: row.status === "settled" ? "settled" : "cost_pending",
8134
- amountMc: Number(row.amount_mc),
8135
- credits: Number(row.amount_mc) / MC_PER_CREDIT,
8159
+ amountMc,
8160
+ credits: amountMc / MC_PER_CREDIT,
8136
8161
  baseMc: SCHEDULED_RUN_BASE_MC,
8137
8162
  modelMc,
8138
8163
  rawCostUsd: rawNanos == null ? null : rawNanos / USD_NANOS,
@@ -8154,7 +8179,7 @@ async function authorizeScheduledRun(args) {
8154
8179
  args: [user.id, SCHEDULED_RUN_BILLING_CLASS]
8155
8180
  });
8156
8181
  if (pending.rows.length > 0) {
8157
- throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved cost", 409);
8182
+ throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved model cost", 409);
8158
8183
  }
8159
8184
  const existing = await getDb().execute({
8160
8185
  sql: "SELECT id, user_id, idempotency_key, status, minimum_amount_mc, maximum_amount_mc, token_hash, expires_at FROM billing_authorizations WHERE idempotency_key = ? LIMIT 1",
@@ -8163,14 +8188,9 @@ async function authorizeScheduledRun(args) {
8163
8188
  if (existing.rows[0]) {
8164
8189
  const row = existing.rows[0];
8165
8190
  if (Number(row.user_id) !== user.id) {
8166
- throw new UnifiedBillingError(
8167
- "invalid_request",
8168
- "the idempotency key is already in use by another billing identity",
8169
- 409
8170
- );
8191
+ throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
8171
8192
  }
8172
- const balanceMc2 = await reconcileBalanceMc(user.id, user.balance_mc);
8173
- return mapAuthorization(row, balanceMc2);
8193
+ return mapAuthorization(row, await reconcileBalanceMc(user.id, user.balance_mc));
8174
8194
  }
8175
8195
  const maximumCredits = args.maximumCredits ?? SCHEDULED_RUN_DEFAULT_MAX_CREDITS;
8176
8196
  if (!Number.isFinite(maximumCredits) || maximumCredits < SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT) {
@@ -8191,17 +8211,7 @@ async function authorizeScheduledRun(args) {
8191
8211
  const expiresAt = new Date(Date.now() + AUTHORIZATION_TTL_MS).toISOString();
8192
8212
  const inserted = await getDb().execute({
8193
8213
  sql: "INSERT OR IGNORE INTO billing_authorizations (id, user_id, idempotency_key, billing_class, status, minimum_amount_mc, maximum_amount_mc, token_hash, metadata, expires_at) VALUES (?, ?, ?, ?, 'authorized', ?, ?, ?, ?, ?)",
8194
- args: [
8195
- id,
8196
- user.id,
8197
- idempotencyKey,
8198
- SCHEDULED_RUN_BILLING_CLASS,
8199
- SCHEDULED_RUN_BASE_MC,
8200
- maximumMc,
8201
- tokenHash(token),
8202
- safeMetadata(args.metadata),
8203
- expiresAt
8204
- ]
8214
+ args: [id, user.id, idempotencyKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_BASE_MC, maximumMc, tokenHash(token), safeMetadata(args.metadata), expiresAt]
8205
8215
  });
8206
8216
  if (inserted.rowsAffected === 0) {
8207
8217
  const raced = await getDb().execute({
@@ -8211,11 +8221,7 @@ async function authorizeScheduledRun(args) {
8211
8221
  if (!raced.rows[0]) throw new Error("billing authorization insert raced without a readable winner");
8212
8222
  const row = raced.rows[0];
8213
8223
  if (Number(row.user_id) !== user.id) {
8214
- throw new UnifiedBillingError(
8215
- "invalid_request",
8216
- "the idempotency key is already in use by another billing identity",
8217
- 409
8218
- );
8224
+ throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
8219
8225
  }
8220
8226
  return mapAuthorization(row, balanceMc);
8221
8227
  }
@@ -8232,180 +8238,235 @@ async function authorizeScheduledRun(args) {
8232
8238
  reservationMode: "balance_check"
8233
8239
  };
8234
8240
  }
8235
- async function settleScheduledRun(args) {
8236
- const identity = normalizeIdentity(args.identity);
8237
- const user = await getUserByEmail(identity);
8238
- if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
8239
- const authorization = await authorizationById(args.authorizationId);
8241
+ async function startScheduledRun(args) {
8242
+ const user = await resolveSchedulingUser(args.identity);
8243
+ let authorization = await authorizationById(args.authorizationId);
8240
8244
  if (!authorization || Number(authorization.user_id) !== user.id) {
8241
8245
  throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
8242
8246
  }
8243
8247
  verifyToken(authorization, args.authorizationToken);
8244
- const settlementKey = authorization.id + ":settlement";
8245
- const existing = await eventByKey(settlementKey);
8246
- if (existing) {
8247
- const balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
8248
- if (existing.status === "settling") {
8249
- throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
8250
- }
8251
- return mapEvent(existing, balanceMc, true);
8248
+ const startKey = authorization.id + ":start";
8249
+ const existing = await eventByKey(startKey);
8250
+ if (existing?.status === "settled") {
8251
+ await getDb().execute({
8252
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting')",
8253
+ args: [authorization.id]
8254
+ });
8255
+ return mapStartEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
8252
8256
  }
8253
- if (authorization.status !== "authorized") {
8254
- throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer open", 409);
8257
+ if (!hasPaidSchedulingAccess(user)) {
8258
+ throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
8255
8259
  }
8256
- if (Date.parse(authorization.expires_at) <= Date.now()) {
8260
+ if (authorization.status === "authorized") {
8261
+ if (Date.parse(authorization.expires_at) <= Date.now()) {
8262
+ await getDb().execute({
8263
+ sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
8264
+ args: [authorization.id]
8265
+ });
8266
+ throw new UnifiedBillingError("authorization_expired", "billing authorization expired before execution started", 409);
8267
+ }
8257
8268
  await getDb().execute({
8258
- sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
8269
+ sql: "UPDATE billing_authorizations SET status = 'starting' WHERE id = ? AND status = 'authorized'",
8259
8270
  args: [authorization.id]
8260
8271
  });
8261
- throw new UnifiedBillingError("authorization_expired", "billing authorization expired before settlement", 409);
8272
+ authorization = await authorizationById(authorization.id);
8262
8273
  }
8263
- const rawCostUsd = args.rawCostUsd ?? null;
8264
- if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
8265
- throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
8274
+ if (!authorization || !["starting", "started"].includes(authorization.status)) {
8275
+ throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer startable", 409);
8266
8276
  }
8267
- if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
8268
- throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
8269
- }
8270
- const eventId = randomUUID();
8271
- const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
8272
- const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
8273
- const totalMc = SCHEDULED_RUN_BASE_MC + modelMc;
8274
- const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : totalMc > Number(authorization.maximum_amount_mc) ? "max_credits_per_run_exceeded" : null;
8275
- if (pendingReason) {
8277
+ const eventId = existing?.id ?? randomUUID();
8278
+ if (!existing) {
8276
8279
  await getDb().execute({
8277
- sql: "INSERT INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'cost_pending', ?, ?, ?, ?, ?, ?, ?, ?)",
8280
+ sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, multiplier_bps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'capturing', ?, ?, ?)",
8278
8281
  args: [
8279
8282
  eventId,
8280
8283
  user.id,
8281
8284
  authorization.id,
8282
- settlementKey,
8285
+ startKey,
8283
8286
  SCHEDULED_RUN_BILLING_CLASS,
8284
8287
  SCHEDULED_RUN_SOURCE_SURFACE,
8285
- totalMc,
8286
- rawCostNanos,
8288
+ SCHEDULED_RUN_BASE_MC,
8287
8289
  SCHEDULED_AGENT_LLM_MARKUP_BPS,
8288
- Math.max(0, Math.round(args.inputTokens ?? 0)),
8289
- Math.max(0, Math.round(args.outputTokens ?? 0)),
8290
- Math.max(0, Math.round(args.cachedTokens ?? 0)),
8291
- Math.max(0, Math.round(args.modelSteps ?? 0)),
8292
- safeMetadata({ ...args.metadata, reason: pendingReason })
8290
+ safeMetadata({ ...args.metadata, billingPhase: "start", chargeMode: "base_capture" })
8293
8291
  ]
8294
8292
  });
8295
- await getDb().execute({
8296
- sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
8297
- args: [authorization.id]
8298
- });
8299
- return {
8300
- id: eventId,
8301
- authorizationId: authorization.id,
8302
- status: "cost_pending",
8303
- amountMc: totalMc,
8304
- credits: totalMc / MC_PER_CREDIT,
8305
- baseMc: SCHEDULED_RUN_BASE_MC,
8306
- modelMc,
8307
- rawCostUsd,
8308
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
8309
- balanceMc: await reconcileBalanceMc(user.id, user.balance_mc),
8310
- reason: pendingReason,
8311
- duplicate: false
8312
- };
8313
8293
  }
8314
- const claimed = await getDb().execute({
8315
- sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, 'settling', ?, ?, ?, ?, ?, ?, ?, ?)",
8316
- args: [
8317
- eventId,
8318
- user.id,
8319
- authorization.id,
8320
- settlementKey,
8321
- SCHEDULED_RUN_BILLING_CLASS,
8322
- SCHEDULED_RUN_SOURCE_SURFACE,
8323
- totalMc,
8324
- rawCostNanos,
8325
- SCHEDULED_AGENT_LLM_MARKUP_BPS,
8326
- Math.max(0, Math.round(args.inputTokens ?? 0)),
8327
- Math.max(0, Math.round(args.outputTokens ?? 0)),
8328
- Math.max(0, Math.round(args.cachedTokens ?? 0)),
8329
- Math.max(0, Math.round(args.modelSteps ?? 0)),
8330
- safeMetadata(args.metadata)
8331
- ]
8332
- });
8333
- if (claimed.rowsAffected === 0) {
8334
- const raced = await eventByKey(settlementKey);
8335
- if (!raced) throw new Error("billing settlement raced without a readable winner");
8336
- if (raced.status === "settling") {
8337
- throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
8338
- }
8339
- return mapEvent(raced, await reconcileBalanceMc(user.id, user.balance_mc), true);
8340
- }
8341
- const debit = await debitMc(
8294
+ const debit = await debitMcIdempotent(
8342
8295
  user.id,
8343
- totalMc,
8296
+ SCHEDULED_RUN_BASE_MC,
8344
8297
  LedgerOperation.SCHEDULED_RUN,
8345
- "scheduled-run:" + authorization.id
8298
+ "scheduled-run:" + authorization.id + ":base",
8299
+ startKey
8346
8300
  );
8347
8301
  if (!debit.ok) {
8348
- const metadata = safeMetadata({ ...args.metadata, reason: "insufficient_credits_at_settlement" });
8349
8302
  await getDb().execute({
8350
- sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ?",
8351
- args: [metadata, eventId]
8352
- });
8353
- await getDb().execute({
8354
- sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
8303
+ sql: "UPDATE billing_authorizations SET status = 'authorized' WHERE id = ? AND status = 'starting'",
8355
8304
  args: [authorization.id]
8356
8305
  });
8357
- return {
8358
- id: eventId,
8359
- authorizationId: authorization.id,
8360
- status: "cost_pending",
8361
- amountMc: totalMc,
8362
- credits: totalMc / MC_PER_CREDIT,
8363
- baseMc: SCHEDULED_RUN_BASE_MC,
8306
+ throw new UnifiedBillingError(
8307
+ "insufficient_credits",
8308
+ "the wallet could not capture the scheduled-run base charge",
8309
+ 402,
8310
+ { balanceMc: debit.balance_mc, requiredMc: SCHEDULED_RUN_BASE_MC }
8311
+ );
8312
+ }
8313
+ await getDb().execute({
8314
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE idempotency_key = ? AND status = 'capturing'",
8315
+ args: [startKey]
8316
+ });
8317
+ await getDb().execute({
8318
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting', 'started')",
8319
+ args: [authorization.id]
8320
+ });
8321
+ const started = await eventByKey(startKey);
8322
+ if (!started) throw new Error("scheduled-run base capture completed without an event receipt");
8323
+ return mapStartEvent(started, debit.balance_mc, debit.duplicate);
8324
+ }
8325
+ function validateModelCost(args) {
8326
+ const rawCostUsd = args.rawCostUsd ?? null;
8327
+ if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
8328
+ throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
8329
+ }
8330
+ if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
8331
+ throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
8332
+ }
8333
+ return rawCostUsd;
8334
+ }
8335
+ async function recoverSettlement(row, user, authorization, duplicate) {
8336
+ const modelMc = Math.max(0, Number(row.amount_mc));
8337
+ let balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
8338
+ if (modelMc > 0) {
8339
+ const debit = await debitMcIdempotent(
8340
+ user.id,
8364
8341
  modelMc,
8365
- rawCostUsd,
8366
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
8367
- balanceMc: debit.balance_mc,
8368
- reason: "insufficient_credits_at_settlement",
8369
- duplicate: false
8370
- };
8342
+ LedgerOperation.SCHEDULED_RUN,
8343
+ "scheduled-run:" + authorization.id + ":model",
8344
+ authorization.id + ":model"
8345
+ );
8346
+ balanceMc = debit.balance_mc;
8347
+ if (!debit.ok) {
8348
+ await getDb().execute({
8349
+ sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ? AND status = 'settling'",
8350
+ args: [safeMetadata({ reason: "insufficient_credits_for_model_delta", billingPhase: "settlement", chargeMode: "model_delta" }), row.id]
8351
+ });
8352
+ await getDb().execute({
8353
+ sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
8354
+ args: [authorization.id]
8355
+ });
8356
+ const pending = await eventByKey(authorization.id + ":settlement");
8357
+ if (!pending) throw new Error("scheduled-run pending settlement lost its event");
8358
+ return mapSettlementEvent(pending, balanceMc, duplicate);
8359
+ }
8371
8360
  }
8372
8361
  await getDb().execute({
8373
- sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
8374
- args: [eventId]
8362
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status = 'settling'",
8363
+ args: [row.id]
8375
8364
  });
8376
8365
  await getDb().execute({
8377
- sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
8366
+ sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status IN ('started', 'cost_pending', 'settled')",
8378
8367
  args: [authorization.id]
8379
8368
  });
8380
- return {
8381
- id: eventId,
8382
- authorizationId: authorization.id,
8383
- status: "settled",
8384
- amountMc: totalMc,
8385
- credits: totalMc / MC_PER_CREDIT,
8386
- baseMc: SCHEDULED_RUN_BASE_MC,
8369
+ const settled = await eventByKey(authorization.id + ":settlement");
8370
+ if (!settled) throw new Error("scheduled-run settlement completed without an event receipt");
8371
+ return mapSettlementEvent(settled, balanceMc, duplicate);
8372
+ }
8373
+ async function settleScheduledRun(args) {
8374
+ const user = await resolveSchedulingUser(args.identity);
8375
+ let authorization = await authorizationById(args.authorizationId);
8376
+ if (!authorization || Number(authorization.user_id) !== user.id) {
8377
+ throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
8378
+ }
8379
+ verifyToken(authorization, args.authorizationToken);
8380
+ const rawCostUsd = validateModelCost(args);
8381
+ const startEvent = await eventByKey(authorization.id + ":start");
8382
+ if (!startEvent || startEvent.status !== "settled") {
8383
+ throw new UnifiedBillingError("authorization_closed", "scheduled-run settlement requires a captured start", 409);
8384
+ }
8385
+ if (authorization.status === "starting") {
8386
+ await getDb().execute({
8387
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status = 'starting'",
8388
+ args: [authorization.id]
8389
+ });
8390
+ authorization = await authorizationById(authorization.id);
8391
+ }
8392
+ if (!authorization || !["started", "cost_pending", "settled"].includes(authorization.status)) {
8393
+ throw new UnifiedBillingError("authorization_closed", "billing authorization is not open for settlement", 409);
8394
+ }
8395
+ const settlementKey = authorization.id + ":settlement";
8396
+ let existing = await eventByKey(settlementKey);
8397
+ if (existing?.status === "settled") {
8398
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
8399
+ }
8400
+ if (existing?.status === "settling") {
8401
+ return recoverSettlement(existing, user, authorization, true);
8402
+ }
8403
+ const hadExistingClaim = Boolean(existing);
8404
+ const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
8405
+ const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
8406
+ const maximumModelMc = Math.max(0, Number(authorization.maximum_amount_mc) - SCHEDULED_RUN_BASE_MC);
8407
+ const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : modelMc > maximumModelMc ? "max_credits_per_run_exceeded" : null;
8408
+ const metadata = safeMetadata({
8409
+ ...args.metadata,
8410
+ billingPhase: "settlement",
8411
+ chargeMode: "model_delta",
8412
+ ...pendingReason ? { reason: pendingReason } : {}
8413
+ });
8414
+ const eventId = existing?.id ?? randomUUID();
8415
+ const status = pendingReason ? "cost_pending" : "settling";
8416
+ const eventValues = [
8387
8417
  modelMc,
8388
- rawCostUsd,
8389
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
8390
- balanceMc: debit.balance_mc,
8391
- duplicate: false
8392
- };
8418
+ rawCostNanos,
8419
+ SCHEDULED_AGENT_LLM_MARKUP_BPS,
8420
+ Math.max(0, Math.round(args.inputTokens ?? 0)),
8421
+ Math.max(0, Math.round(args.outputTokens ?? 0)),
8422
+ Math.max(0, Math.round(args.cachedTokens ?? 0)),
8423
+ Math.max(0, Math.round(args.modelSteps ?? 0)),
8424
+ metadata
8425
+ ];
8426
+ if (existing) {
8427
+ await getDb().execute({
8428
+ sql: "UPDATE billing_events SET status = ?, amount_mc = ?, raw_cost_usd_nanos = ?, multiplier_bps = ?, input_tokens = ?, output_tokens = ?, cached_tokens = ?, model_steps = ?, metadata = ?, settled_at = NULL WHERE id = ? AND status = 'cost_pending'",
8429
+ args: [status, ...eventValues, existing.id]
8430
+ });
8431
+ } else {
8432
+ await getDb().execute({
8433
+ sql: "INSERT OR IGNORE INTO billing_events (id, user_id, authorization_id, idempotency_key, billing_class, source_surface, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, input_tokens, output_tokens, cached_tokens, model_steps, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
8434
+ args: [eventId, user.id, authorization.id, settlementKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_SOURCE_SURFACE, status, ...eventValues]
8435
+ });
8436
+ }
8437
+ existing = await eventByKey(settlementKey);
8438
+ if (!existing) throw new Error("scheduled-run settlement claim was not readable");
8439
+ const duplicateClaim = hadExistingClaim || eventId !== existing.id;
8440
+ if (existing.status === "settled") {
8441
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
8442
+ }
8443
+ if (existing.status === "cost_pending") {
8444
+ await getDb().execute({
8445
+ sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
8446
+ args: [authorization.id]
8447
+ });
8448
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), duplicateClaim);
8449
+ }
8450
+ if (existing.status !== "settling") {
8451
+ throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement could not be claimed", 409);
8452
+ }
8453
+ return recoverSettlement(existing, user, authorization, duplicateClaim);
8393
8454
  }
8394
8455
  async function voidScheduledRunAuthorization(args) {
8395
- const identity = normalizeIdentity(args.identity);
8396
- const user = await getUserByEmail(identity);
8397
- if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
8456
+ const user = await resolveSchedulingUser(args.identity);
8398
8457
  const authorization = await authorizationById(args.authorizationId);
8399
8458
  if (!authorization || Number(authorization.user_id) !== user.id) {
8400
8459
  throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
8401
8460
  }
8402
8461
  verifyToken(authorization, args.authorizationToken);
8403
8462
  if (authorization.status === "authorized") {
8404
- await getDb().execute({
8463
+ const result = await getDb().execute({
8405
8464
  sql: "UPDATE billing_authorizations SET status = 'void', metadata = ?, settled_at = datetime('now') WHERE id = ? AND status = 'authorized'",
8406
8465
  args: [safeMetadata({ reason: args.reason ?? "voided_before_execution" }), authorization.id]
8407
8466
  });
8408
- return { ok: true, status: "void" };
8467
+ if (result.rowsAffected > 0) return { ok: true, status: "void" };
8468
+ const raced = await authorizationById(authorization.id);
8469
+ return { ok: true, status: raced?.status ?? "closed" };
8409
8470
  }
8410
8471
  return { ok: true, status: authorization.status };
8411
8472
  }
@@ -8609,6 +8670,12 @@ var settleSchema = z10.object({
8609
8670
  modelSteps: z10.number().finite().nonnegative().optional(),
8610
8671
  metadata: metadataSchema
8611
8672
  });
8673
+ var startSchema = z10.object({
8674
+ identity: z10.string().min(1).max(320),
8675
+ authorizationId: z10.string().uuid(),
8676
+ authorizationToken: z10.string().min(20).max(500),
8677
+ metadata: metadataSchema
8678
+ });
8612
8679
  var voidSchema = z10.object({
8613
8680
  identity: z10.string().min(1).max(320),
8614
8681
  authorizationId: z10.string().uuid(),
@@ -8704,6 +8771,19 @@ internalBillingApp.post("/scheduled-run/settle", async (c) => {
8704
8771
  return c.json(response.body, response.status);
8705
8772
  }
8706
8773
  });
8774
+ internalBillingApp.post("/scheduled-run/start", async (c) => {
8775
+ const parsed = startSchema.safeParse(await c.req.json().catch(() => null));
8776
+ if (!parsed.success) {
8777
+ return c.json({ ok: false, error_code: "invalid_request", error: "invalid scheduled-run start request" }, 400);
8778
+ }
8779
+ try {
8780
+ const start = await startScheduledRun(parsed.data);
8781
+ return c.json({ ok: true, start });
8782
+ } catch (error) {
8783
+ const response = billingError(error);
8784
+ return c.json(response.body, response.status);
8785
+ }
8786
+ });
8707
8787
  internalBillingApp.post("/scheduled-run/void", async (c) => {
8708
8788
  const parsed = voidSchema.safeParse(await c.req.json().catch(() => null));
8709
8789
  if (!parsed.success) {
@@ -10924,6 +11004,15 @@ function buildPageIntelUrl(body, country) {
10924
11004
  if (body.pageId?.trim()) return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&is_targeted_country=false&media_type=all&search_type=page&view_all_page_id=${body.pageId.trim()}`;
10925
11005
  return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&q=${encodeURIComponent(body.query.trim())}&search_type=keyword_unordered`;
10926
11006
  }
11007
+ function pageIntelRequestContext(body) {
11008
+ const country = body.country?.trim().toUpperCase() ?? "US";
11009
+ return {
11010
+ country,
11011
+ maxAds: Math.min(200, Math.max(1, body.maxAds ?? 50)),
11012
+ listingUrl: buildPageIntelUrl(body, country),
11013
+ subject: body.pageId ?? body.query ?? body.libraryId ?? ""
11014
+ };
11015
+ }
10927
11016
  function normalizeAdvertiserName(value) {
10928
11017
  return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
10929
11018
  }
@@ -11037,16 +11126,14 @@ facebookAdApp.post("/page-intel", createApiKeyAuth(), async (c) => {
11037
11126
  const parsed = FacebookPageIntelBodySchema.safeParse(raw);
11038
11127
  if (!parsed.success) return c.json(invalidRequest(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11039
11128
  const body = parsed.data;
11040
- const maxAds = Math.min(200, Math.max(1, body.maxAds ?? 50));
11041
- const country = body.country?.trim().toUpperCase() ?? "US";
11042
- const listingUrl = buildPageIntelUrl(body, country);
11129
+ const { country, maxAds, listingUrl, subject } = pageIntelRequestContext(body);
11043
11130
  const fbUser = c.get("user");
11044
11131
  const gate = await acquireConcurrencyGate(fbUser, "facebook_page_intel", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { pageId: body.pageId, query: body.query, libraryId: body.libraryId, country } });
11045
11132
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11046
11133
  const driver = new BrowserDriver();
11047
11134
  let refunded = false, debited = false;
11048
11135
  try {
11049
- const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD, body.pageId ?? body.query ?? body.libraryId ?? "");
11136
+ const { ok: fbOk, balance_mc: fbBal } = await debitMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD, subject);
11050
11137
  if (!fbOk) return c.json(insufficientBalanceResponse(fbBal, MC_COSTS.fb_ad), 402);
11051
11138
  debited = true;
11052
11139
  await driver.launch(await kernelLaunchOptsResidential());
@@ -11056,17 +11143,17 @@ facebookAdApp.post("/page-intel", createApiKeyAuth(), async (c) => {
11056
11143
  if (result.ads.length === 0 && await detectSoftBlock(driver)) {
11057
11144
  await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "soft-block empty result");
11058
11145
  refunded = true;
11059
- await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: body.pageId ?? body.query ?? body.libraryId ?? "", error: "soft-block: empty result refunded" });
11146
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: "soft-block: empty result refunded" });
11060
11147
  return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
11061
11148
  }
11062
11149
  const match = assessPageIntelMatch(body, result);
11063
11150
  const output = { ...result, ...match };
11064
- await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: body.pageId ?? body.query ?? body.libraryId ?? "", resultCount: result.ads.length, result: output });
11151
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: subject, resultCount: result.ads.length, result: output });
11065
11152
  return c.json(output);
11066
11153
  } catch (err) {
11067
11154
  const msg = err instanceof Error ? err.message : String(err);
11068
11155
  if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "failed call");
11069
- await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: body.pageId ?? body.query ?? body.libraryId ?? "", error: msg });
11156
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: subject, error: msg });
11070
11157
  if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) {
11071
11158
  return c.json({ error: msg }, 503);
11072
11159
  }
@@ -15004,6 +15091,7 @@ workflowApp.post("/cron/dispatch", async (c) => {
15004
15091
 
15005
15092
  // src/api/serp-intelligence-routes.ts
15006
15093
  import { Hono as Hono16 } from "hono";
15094
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
15007
15095
 
15008
15096
  // src/serp-intelligence/page-snapshot-extractor.ts
15009
15097
  import { createHash as createHash3 } from "crypto";
@@ -15397,12 +15485,22 @@ var SerpIntelligencePageSnapshotRequestSchema = z24.object({
15397
15485
  sourcePosition: z24.number().int().min(1).optional()
15398
15486
  }).strict();
15399
15487
  var SerpIntelligencePageSnapshotsBodySchema = z24.object({
15400
- urls: z24.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
15488
+ urls: z24.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25).optional(),
15401
15489
  targets: z24.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
15402
15490
  maxConcurrency: z24.number().int().min(1).max(5).default(2),
15403
15491
  timeoutMs: z24.number().int().min(1e3).max(6e4).default(15e3),
15404
15492
  debug: z24.boolean().default(false)
15405
- }).strict();
15493
+ }).strict().superRefine((body, ctx) => {
15494
+ const hasUrls = Boolean(body.urls?.length);
15495
+ const hasTargets = Boolean(body.targets?.length);
15496
+ if (hasUrls === hasTargets) {
15497
+ ctx.addIssue({
15498
+ code: z24.ZodIssueCode.custom,
15499
+ message: "provide exactly one of urls or targets",
15500
+ path: hasUrls ? ["targets"] : ["urls"]
15501
+ });
15502
+ }
15503
+ });
15406
15504
  var SerpIntelligenceAICitationSchema = z24.object({
15407
15505
  text: z24.string(),
15408
15506
  href: z24.string()
@@ -15689,14 +15787,24 @@ async function captureSerpIntelligenceSnapshot(rawInput, runtimeOptions = {}) {
15689
15787
  });
15690
15788
  const pageSnapshotLimit = normalizePageSnapshotLimit(parsedInput);
15691
15789
  const pageSnapshotTargets = collectPageSnapshotTargets(harvestResult, pageSnapshotLimit);
15692
- const pageSnapshotArtifacts = pageSnapshotTargets.length > 0 ? (await capturePageSnapshotsFn(pageSnapshotTargets, {
15693
- kernelApiKey: runtimeOptions.kernelApiKey ?? browserServiceApiKey(),
15694
- timeoutMs: runtimeOptions.pageSnapshotTimeoutMs,
15695
- maxConcurrency: runtimeOptions.pageSnapshotMaxConcurrency,
15696
- debug,
15697
- includeContent: runtimeOptions.includePageSnapshotContent,
15698
- maxContentChars: runtimeOptions.maxPageSnapshotContentChars
15699
- })).pageSnapshotArtifacts.map(stripPageSnapshotArtifact) : [];
15790
+ const pageSnapshotArtifacts = pageSnapshotTargets.length > 0 ? (await runWithCostContext(
15791
+ {
15792
+ ...currentCostContext() ?? { op: "serp" },
15793
+ subOp: "page_snapshot",
15794
+ // Page snapshot launches have their own fixed product rate. They
15795
+ // must not overwrite the headless/headful mode used to settle the
15796
+ // preceding SERP harvest.
15797
+ headlessSentOut: void 0
15798
+ },
15799
+ () => capturePageSnapshotsFn(pageSnapshotTargets, {
15800
+ kernelApiKey: runtimeOptions.kernelApiKey ?? browserServiceApiKey(),
15801
+ timeoutMs: runtimeOptions.pageSnapshotTimeoutMs,
15802
+ maxConcurrency: runtimeOptions.pageSnapshotMaxConcurrency,
15803
+ debug,
15804
+ includeContent: runtimeOptions.includePageSnapshotContent,
15805
+ maxContentChars: runtimeOptions.maxPageSnapshotContentChars
15806
+ })
15807
+ )).pageSnapshotArtifacts.map(stripPageSnapshotArtifact) : [];
15700
15808
  return normalizeHarvestResultForSerpIntelligence({
15701
15809
  harvestResult,
15702
15810
  attempts,
@@ -15713,6 +15821,7 @@ var SERP_INTELLIGENCE_RATE_LIMIT = 60;
15713
15821
  var SERP_INTELLIGENCE_RATE_WINDOW_SECONDS = 60;
15714
15822
  var POST_CAPTURE_ROUTE_LABEL = "POST /capture";
15715
15823
  var POST_PAGE_SNAPSHOTS_ROUTE_LABEL = "POST /page-snapshots";
15824
+ var SERP_CAPTURE_BILLING_SOURCE = "serp_intelligence_capture";
15716
15825
  var serpIntelligenceApp = new Hono16();
15717
15826
  serpIntelligenceApp.use("*", createApiKeyAuth());
15718
15827
  function structuredError(input) {
@@ -15752,18 +15861,46 @@ async function enforceSerpIntelligenceRateLimit(c, userId) {
15752
15861
  );
15753
15862
  }
15754
15863
  function pageSnapshotTargetsFromBody(body) {
15755
- return body.targets?.length ? body.targets : body.urls.map((url) => ({
15864
+ return body.targets?.length ? body.targets : (body.urls ?? []).map((url) => ({
15756
15865
  url,
15757
15866
  sourceKind: "configured_target",
15758
15867
  sourcePosition: null
15759
15868
  }));
15760
15869
  }
15870
+ async function logRequestEventBestEffort(input) {
15871
+ try {
15872
+ await logRequestEvent(input);
15873
+ } catch (error) {
15874
+ console.warn("[serp-intelligence] request audit log failed:", error instanceof Error ? error.message : String(error));
15875
+ }
15876
+ }
15877
+ function captureBillingKeys(userId, suppliedKey, body) {
15878
+ const responseKey = suppliedKey?.trim() || randomUUID3();
15879
+ const requestFingerprint = createHash4("sha256").update(JSON.stringify(body)).digest("hex");
15880
+ const keyDigest = createHash4("sha256").update(String(userId)).update("\0").update(responseKey).digest("hex");
15881
+ return {
15882
+ responseKey,
15883
+ debitKey: `serp-capture:${userId}:${keyDigest}`,
15884
+ debitDescription: `${body.query} [request:${requestFingerprint}]`
15885
+ };
15886
+ }
15761
15887
  serpIntelligenceApp.post("/capture", async (c) => {
15762
15888
  void POST_CAPTURE_ROUTE_LABEL;
15763
15889
  const user = c.get("user");
15764
15890
  const raw = await c.req.json().catch(() => ({}));
15765
15891
  const parsed = SerpIntelligenceCaptureBodySchema.safeParse(raw);
15766
15892
  if (!parsed.success) return c.json(formatZodError(parsed.error), 400);
15893
+ const suppliedIdempotencyKey = c.req.header("idempotency-key") ?? c.req.header("x-idempotency-key");
15894
+ if (suppliedIdempotencyKey && suppliedIdempotencyKey.trim().length > 500) {
15895
+ return c.json(structuredError({
15896
+ error_code: "invalid_request",
15897
+ error_type: "validation_error",
15898
+ message: "Idempotency-Key must be 500 characters or fewer.",
15899
+ retryable: false
15900
+ }), 400);
15901
+ }
15902
+ const billingKeys = captureBillingKeys(user.id, suppliedIdempotencyKey, parsed.data);
15903
+ c.header("Idempotency-Key", billingKeys.responseKey);
15767
15904
  const limited = await enforceSerpIntelligenceRateLimit(c, user.id);
15768
15905
  if (limited) return limited;
15769
15906
  const gate = await acquireConcurrencyGate(user, "serp_intelligence_capture", {
@@ -15771,51 +15908,130 @@ serpIntelligenceApp.post("/capture", async (c) => {
15771
15908
  metadata: { query: parsed.data.query, location: parsed.data.location }
15772
15909
  });
15773
15910
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
15774
- const cost = MC_COSTS.serp;
15775
- let debited = false;
15911
+ const pageSnapshotHold = parsed.data.includePageSnapshots ? parsed.data.pageSnapshotLimit * MC_COSTS.page_scrape : 0;
15912
+ const cost = MC_COSTS.serp_headful + pageSnapshotHold;
15913
+ const headlessSentOut = { value: null };
15776
15914
  try {
15777
- const { ok, balance_mc } = await debitMc(
15915
+ const { ok, balance_mc } = await debitMcIdempotent(
15778
15916
  user.id,
15779
15917
  cost,
15780
15918
  LedgerOperation.SERP,
15781
- parsed.data.query
15919
+ billingKeys.debitDescription,
15920
+ billingKeys.debitKey
15782
15921
  );
15783
- if (!ok) return c.json(insufficientBalanceResponse(balance_mc, cost), 402);
15784
- debited = true;
15785
- const result = await captureSerpIntelligenceSnapshot(parsed.data, {
15786
- kernelApiKey: browserServiceApiKey(),
15787
- kernelProxyId: browserServiceProxyId(),
15788
- signal: c.req.raw.signal,
15789
- billing: { creditsUsed: cost / MC_PER_CREDIT }
15922
+ if (!ok) {
15923
+ await releaseConcurrencyGate(gate.lockId);
15924
+ return c.json(insufficientBalanceResponse(balance_mc, cost), 402);
15925
+ }
15926
+ } catch (error) {
15927
+ const idempotencyConflict = error instanceof Error && error.message === "idempotent debit key conflicts with another debit";
15928
+ const body = structuredError({
15929
+ error_code: idempotencyConflict ? "idempotency_conflict" : "capture_failed",
15930
+ error_type: idempotencyConflict ? "validation_error" : "capture_error",
15931
+ message: error instanceof Error ? error.message : String(error),
15932
+ retryable: !idempotencyConflict
15790
15933
  });
15791
- await logRequestEvent({
15934
+ await logRequestEventBestEffort({
15792
15935
  userId: user.id,
15793
- source: "serp_intelligence_capture",
15794
- status: "done",
15936
+ source: SERP_CAPTURE_BILLING_SOURCE,
15937
+ status: "failed",
15795
15938
  query: parsed.data.query,
15796
15939
  location: parsed.data.location,
15797
- resultCount: result.harvestResult.organicResults.length,
15798
- result
15940
+ error: body.message
15799
15941
  });
15800
- return c.json(result);
15942
+ await releaseConcurrencyGate(gate.lockId);
15943
+ return c.json(body, idempotencyConflict ? 409 : 500);
15944
+ }
15945
+ let result;
15946
+ try {
15947
+ const parentCostContext = currentCostContext();
15948
+ result = await runWithCostContext(
15949
+ {
15950
+ ...parentCostContext ?? {},
15951
+ op: "serp",
15952
+ userId: Number(user.id),
15953
+ headlessSentOut
15954
+ },
15955
+ () => captureSerpIntelligenceSnapshot(parsed.data, {
15956
+ kernelApiKey: browserServiceApiKey(),
15957
+ kernelProxyId: browserServiceProxyId(),
15958
+ signal: c.req.raw.signal
15959
+ })
15960
+ );
15801
15961
  } catch (error) {
15802
- if (debited) await creditMc(user.id, cost, LedgerOperation.REFUND, "failed serp intelligence capture");
15962
+ try {
15963
+ await settleDebitMcIdempotent(
15964
+ user.id,
15965
+ billingKeys.debitKey,
15966
+ 0,
15967
+ LedgerOperation.SERP_REFUND,
15968
+ "failed serp intelligence capture",
15969
+ SERP_CAPTURE_BILLING_SOURCE
15970
+ );
15971
+ } catch (settlementError) {
15972
+ console.warn(
15973
+ "[serp-intelligence] failed capture refund remains recoverable:",
15974
+ settlementError instanceof Error ? settlementError.message : String(settlementError)
15975
+ );
15976
+ }
15803
15977
  const body = error instanceof SerpIntelligenceCaptureError ? error.toJSON() : structuredError({
15804
15978
  error_code: "capture_failed",
15805
15979
  error_type: "capture_error",
15806
15980
  message: error instanceof Error ? error.message : String(error),
15807
15981
  retryable: true
15808
15982
  });
15809
- await logRequestEvent({
15983
+ await logRequestEventBestEffort({
15810
15984
  userId: user.id,
15811
- source: "serp_intelligence_capture",
15985
+ source: SERP_CAPTURE_BILLING_SOURCE,
15812
15986
  status: "failed",
15813
15987
  query: parsed.data.query,
15814
15988
  location: parsed.data.location,
15815
15989
  error: "message" in body && typeof body.message === "string" ? body.message : "SERP Intelligence capture failed"
15816
15990
  });
15817
15991
  const status = error instanceof SerpIntelligenceCaptureError ? error.httpStatus : 500;
15992
+ await releaseConcurrencyGate(gate.lockId);
15818
15993
  return c.json(body, status);
15994
+ }
15995
+ try {
15996
+ const requestedActualCost = serpActualCostMc(headlessSentOut.value) + result.pageSnapshotArtifacts.length * MC_COSTS.page_scrape;
15997
+ const settlement = await settleDebitMcIdempotent(
15998
+ user.id,
15999
+ billingKeys.debitKey,
16000
+ requestedActualCost,
16001
+ LedgerOperation.SERP_REFUND,
16002
+ "SERP capture pricing settle",
16003
+ SERP_CAPTURE_BILLING_SOURCE
16004
+ );
16005
+ const settledResult = {
16006
+ ...result,
16007
+ billing: { creditsUsed: settlement.final_amount_mc / MC_PER_CREDIT }
16008
+ };
16009
+ await logRequestEventBestEffort({
16010
+ userId: user.id,
16011
+ source: SERP_CAPTURE_BILLING_SOURCE,
16012
+ status: "done",
16013
+ query: parsed.data.query,
16014
+ location: parsed.data.location,
16015
+ resultCount: settledResult.harvestResult.organicResults.length,
16016
+ result: settledResult
16017
+ });
16018
+ return c.json(settledResult);
16019
+ } catch (error) {
16020
+ const body = structuredError({
16021
+ error_code: "capture_failed",
16022
+ error_type: "capture_error",
16023
+ message: error instanceof Error ? error.message : String(error),
16024
+ retryable: true
16025
+ });
16026
+ await logRequestEventBestEffort({
16027
+ userId: user.id,
16028
+ source: SERP_CAPTURE_BILLING_SOURCE,
16029
+ status: "failed",
16030
+ query: parsed.data.query,
16031
+ location: parsed.data.location,
16032
+ error: body.message
16033
+ });
16034
+ return c.json(body, 500);
15819
16035
  } finally {
15820
16036
  await releaseConcurrencyGate(gate.lockId);
15821
16037
  }
@@ -15851,7 +16067,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
15851
16067
  maxConcurrency: parsed.data.maxConcurrency,
15852
16068
  debug: parsed.data.debug
15853
16069
  });
15854
- await logRequestEvent({
16070
+ await logRequestEventBestEffort({
15855
16071
  userId: user.id,
15856
16072
  source: "serp_intelligence_page_snapshots",
15857
16073
  status: "done",
@@ -15868,7 +16084,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
15868
16084
  message: error instanceof Error ? error.message : String(error),
15869
16085
  retryable: true
15870
16086
  });
15871
- await logRequestEvent({
16087
+ await logRequestEventBestEffort({
15872
16088
  userId: user.id,
15873
16089
  source: "serp_intelligence_page_snapshots",
15874
16090
  status: "failed",
@@ -16513,15 +16729,42 @@ mcpApp.all("/", async (c) => {
16513
16729
  });
16514
16730
 
16515
16731
  // src/api/browser-agent-routes.ts
16516
- import { randomUUID as randomUUID4 } from "crypto";
16732
+ import { randomUUID as randomUUID5 } from "crypto";
16517
16733
  import { Hono as Hono18 } from "hono";
16518
16734
 
16519
16735
  // src/api/browser-agent-db.ts
16520
- import { randomUUID as randomUUID3 } from "crypto";
16736
+ import { randomUUID as randomUUID4 } from "crypto";
16521
16737
  var _ready = false;
16738
+ var _migrationPromise = null;
16522
16739
  async function migrateBrowserAgent() {
16523
16740
  if (_ready) return;
16741
+ if (_migrationPromise) return _migrationPromise;
16742
+ _migrationPromise = runBrowserAgentMigration().finally(() => {
16743
+ _migrationPromise = null;
16744
+ });
16745
+ return _migrationPromise;
16746
+ }
16747
+ async function runBrowserAgentMigration() {
16524
16748
  const db = getDb();
16749
+ const existing = await db.execute(`
16750
+ SELECT
16751
+ (SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN (
16752
+ 'browser_agent_sessions',
16753
+ 'browser_agent_actions',
16754
+ 'browser_agent_replays',
16755
+ 'browser_auth_connections',
16756
+ 'browser_agent_extensions'
16757
+ )) = 5
16758
+ AND (SELECT COUNT(*) FROM pragma_table_info('browser_agent_sessions') WHERE name = 'concurrency_lock_id') = 1
16759
+ AND (SELECT COUNT(*) FROM pragma_table_info('browser_auth_connections') WHERE name IN (
16760
+ 'account_email', 'note', 'updated_at', 'status', 'browser_agent_session_id'
16761
+ )) = 5
16762
+ AS ready
16763
+ `);
16764
+ if (Number(existing.rows[0]?.ready ?? 0) === 1) {
16765
+ _ready = true;
16766
+ return;
16767
+ }
16525
16768
  await db.execute(`
16526
16769
  CREATE TABLE IF NOT EXISTS browser_agent_sessions (
16527
16770
  id TEXT PRIMARY KEY,
@@ -16621,7 +16864,7 @@ async function migrateBrowserAgent() {
16621
16864
  }
16622
16865
  async function createExtensionRow(input) {
16623
16866
  const db = getDb();
16624
- const id = `bext_${randomUUID3().replace(/-/g, "").slice(0, 20)}`;
16867
+ const id = `bext_${randomUUID4().replace(/-/g, "").slice(0, 20)}`;
16625
16868
  await db.execute({
16626
16869
  sql: `INSERT INTO browser_agent_extensions (id, user_id, name, backend_id, backend_name, source, source_url, size_bytes)
16627
16870
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -16656,7 +16899,7 @@ async function deleteExtensionRow(userId, name) {
16656
16899
  }
16657
16900
  async function createAuthConnectionRow(input) {
16658
16901
  const db = getDb();
16659
- const connectionId = `authc_${randomUUID3().replace(/-/g, "").slice(0, 20)}`;
16902
+ const connectionId = `authc_${randomUUID4().replace(/-/g, "").slice(0, 20)}`;
16660
16903
  await db.execute({
16661
16904
  sql: `INSERT INTO browser_auth_connections (connection_id, domain, profile, account_email, note, status, browser_agent_session_id)
16662
16905
  VALUES (?, ?, ?, ?, ?, 'NEEDS_AUTH', ?)`,
@@ -16690,7 +16933,7 @@ async function listAuthConnectionsByProfile(profile) {
16690
16933
  }
16691
16934
  async function createSessionRow(input) {
16692
16935
  const db = getDb();
16693
- const id = `bas_${randomUUID3().replace(/-/g, "").slice(0, 20)}`;
16936
+ const id = `bas_${randomUUID4().replace(/-/g, "").slice(0, 20)}`;
16694
16937
  await db.execute({
16695
16938
  sql: `INSERT INTO browser_agent_sessions (id, runtime_session_id, live_view_url, cdp_ws_url, status, label, user_id, concurrency_lock_id, last_action_at)
16696
16939
  VALUES (?, ?, ?, ?, 'open', ?, ?, ?, datetime('now'))`,
@@ -16759,7 +17002,7 @@ async function recordAction(input) {
16759
17002
  sql: `INSERT INTO browser_agent_actions (id, session_id, type, params_json, ok, error)
16760
17003
  VALUES (?, ?, ?, ?, ?, ?)`,
16761
17004
  args: [
16762
- `baa_${randomUUID3().replace(/-/g, "").slice(0, 20)}`,
17005
+ `baa_${randomUUID4().replace(/-/g, "").slice(0, 20)}`,
16763
17006
  input.sessionId,
16764
17007
  input.type,
16765
17008
  input.params == null ? null : JSON.stringify(input.params),
@@ -18447,7 +18690,7 @@ function buildBrowserAgentRoutes() {
18447
18690
  }
18448
18691
  const existing = await getExtensionRow(user.id, name);
18449
18692
  if (existing) return c.json({ error: `an extension named "${name}" already exists \u2014 delete it first or pick another name` }, 409);
18450
- const backendName = `u${user.id}_${randomUUID4().replace(/-/g, "")}`;
18693
+ const backendName = `u${user.id}_${randomUUID5().replace(/-/g, "")}`;
18451
18694
  try {
18452
18695
  const imported = await importExtensionFromStore(storeUrl, backendName);
18453
18696
  const row = await createExtensionRow({
@@ -18877,7 +19120,7 @@ async function resolveUser(customerId, emailFallback) {
18877
19120
  // src/api/oauth-routes.ts
18878
19121
  import { Hono as Hono20 } from "hono";
18879
19122
  import { getCookie, setCookie } from "hono/cookie";
18880
- import { createHash as createHash4, randomBytes as randomBytes2, randomUUID as randomUUID5 } from "crypto";
19123
+ import { createHash as createHash5, randomBytes as randomBytes2, randomUUID as randomUUID6 } from "crypto";
18881
19124
  import { importPKCS8, exportJWK, SignJWT } from "jose";
18882
19125
  var ISSUER = "https://mcpscraper.dev";
18883
19126
  var RESOURCE = () => process.env.MCP_MEMORY_RESOURCE_URL ?? "https://memory.mcpscraper.dev/mcp";
@@ -18910,7 +19153,7 @@ async function getKeys() {
18910
19153
  const privateKey = await importPKCS8(pem, "RS256", { extractable: true });
18911
19154
  const full = await exportJWK(privateKey);
18912
19155
  const publicJwk = { kty: full.kty, n: full.n, e: full.e };
18913
- const kid = createHash4("sha256").update(JSON.stringify({ e: publicJwk.e, kty: publicJwk.kty, n: publicJwk.n })).digest("base64url").slice(0, 16);
19156
+ const kid = createHash5("sha256").update(JSON.stringify({ e: publicJwk.e, kty: publicJwk.kty, n: publicJwk.n })).digest("base64url").slice(0, 16);
18914
19157
  publicJwk.kid = kid;
18915
19158
  publicJwk.alg = "RS256";
18916
19159
  publicJwk.use = "sig";
@@ -19220,12 +19463,12 @@ oauthApp.post("/oauth/authorize", async (c) => {
19220
19463
  });
19221
19464
  function pkceMatches(verifier, challenge) {
19222
19465
  if (!verifier) return false;
19223
- const computed = createHash4("sha256").update(verifier).digest("base64url");
19466
+ const computed = createHash5("sha256").update(verifier).digest("base64url");
19224
19467
  return computed === challenge;
19225
19468
  }
19226
19469
  async function mintAccessToken(identity, scope, plan, audience) {
19227
19470
  const { privateKey, kid } = await getKeys();
19228
- return new SignJWT({ scope, plan }).setProtectedHeader({ alg: "RS256", kid }).setIssuer(ISSUER).setSubject(identity).setAudience(audience).setIssuedAt().setJti(randomUUID5()).setExpirationTime(`${ACCESS_TTL_SECONDS}s`).sign(privateKey);
19471
+ return new SignJWT({ scope, plan }).setProtectedHeader({ alg: "RS256", kid }).setIssuer(ISSUER).setSubject(identity).setAudience(audience).setIssuedAt().setJti(randomUUID6()).setExpirationTime(`${ACCESS_TTL_SECONDS}s`).sign(privateKey);
19229
19472
  }
19230
19473
  function tokenErrorResponse(c, error, description, status) {
19231
19474
  return c.json({ error, error_description: description }, status);
@@ -20571,13 +20814,13 @@ import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "
20571
20814
  import Stripe2 from "stripe";
20572
20815
 
20573
20816
  // src/api/page-diff.ts
20574
- import { createHash as createHash5 } from "crypto";
20817
+ import { createHash as createHash6 } from "crypto";
20575
20818
  import { diffLines } from "diff";
20576
20819
  var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
20577
20820
  var MAX_DIFF_HUNKS = 200;
20578
20821
  var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
20579
20822
  function sha256Hex(value) {
20580
- return createHash5("sha256").update(value).digest("hex");
20823
+ return createHash6("sha256").update(value).digest("hex");
20581
20824
  }
20582
20825
  function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
20583
20826
  if (value.length <= maxChars) return { value, truncated: false };
@@ -20684,7 +20927,7 @@ async function persistScrapeBody(user, opts) {
20684
20927
  }
20685
20928
 
20686
20929
  // src/api/connection-memory-import.ts
20687
- import { createHash as createHash6 } from "crypto";
20930
+ import { createHash as createHash7 } from "crypto";
20688
20931
  var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
20689
20932
  var CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
20690
20933
  var CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS = 256e3;
@@ -20714,7 +20957,7 @@ function canonicalJson(value) {
20714
20957
  return JSON.stringify(value);
20715
20958
  }
20716
20959
  function sha2562(value) {
20717
- return createHash6("sha256").update(value).digest("hex");
20960
+ return createHash7("sha256").update(value).digest("hex");
20718
20961
  }
20719
20962
  function sensitiveKey(key) {
20720
20963
  const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -20983,7 +21226,7 @@ async function cleanupExpiredScrapeBlobs(maxAgeMs = SCRAPE_BLOB_TTL_MS) {
20983
21226
  }
20984
21227
 
20985
21228
  // src/api/connected-data-export.ts
20986
- import { randomUUID as randomUUID6 } from "crypto";
21229
+ import { randomUUID as randomUUID7 } from "crypto";
20987
21230
  var CONNECTED_DATA_INLINE_BUDGET_BYTES = Number(
20988
21231
  process.env.MCP_SCRAPER_CONNECTED_DATA_INLINE_BUDGET_BYTES ?? 5e4
20989
21232
  );
@@ -21108,7 +21351,7 @@ function finitePositive(value, fallback) {
21108
21351
  return Number.isFinite(value) && value > 0 ? value : fallback;
21109
21352
  }
21110
21353
  async function collectConnectedDataExport(args) {
21111
- const exportId = randomUUID6();
21354
+ const exportId = randomUUID7();
21112
21355
  const now = args.now ?? Date.now;
21113
21356
  const startedAt = now();
21114
21357
  const budgetMs = finitePositive(CONNECTED_DATA_EXPORT_BUDGET_MS, 24e4);
@@ -21220,12 +21463,6 @@ ${lines.length ? `${lines.join("\n")}
21220
21463
  }
21221
21464
 
21222
21465
  // src/api/credit-operations.ts
21223
- async function grantSignupCredit(userId) {
21224
- if (!FREE_SIGNUP_MC || FREE_SIGNUP_MC <= 0) return;
21225
- const already = await ledgerExistsForOperation(userId, LedgerOperation.SIGNUP_GRANT);
21226
- if (already) return;
21227
- await creditMc(userId, FREE_SIGNUP_MC, LedgerOperation.SIGNUP_GRANT, "Welcome credits");
21228
- }
21229
21466
  async function applyMonthlyFreeRefresh(user) {
21230
21467
  return user;
21231
21468
  }
@@ -21437,7 +21674,7 @@ async function dbUsage(identity, plan) {
21437
21674
  }
21438
21675
 
21439
21676
  // src/api/nango-control.ts
21440
- import { createHash as createHash7 } from "crypto";
21677
+ import { createHash as createHash8 } from "crypto";
21441
21678
  var DEFAULT_NANGO_CONTROL_URL = "https://mcp-scraper-scheduler.vercel.app";
21442
21679
  var DISABLED_NANGO_TOOLS = {
21443
21680
  "google-mail": /* @__PURE__ */ new Set(["list-filters"]),
@@ -22036,7 +22273,7 @@ function canonicalJson2(value) {
22036
22273
  return JSON.stringify(value);
22037
22274
  }
22038
22275
  function projectedToolSchemaHash(tool) {
22039
- return createHash7("sha256").update(canonicalJson2(tool)).digest("hex");
22276
+ return createHash8("sha256").update(canonicalJson2(tool)).digest("hex");
22040
22277
  }
22041
22278
  async function describeNangoTool(identity, connectionId, tool, fresh) {
22042
22279
  const body = await controlRequest("/api/internal/nango/connections/actions/describe", {
@@ -22637,7 +22874,6 @@ app.use("*", (c, next) => {
22637
22874
  const forceHeadless = c.req.header("x-kernel-headless") === "true";
22638
22875
  return runWithCostContext({ op, probeRunId, forceHeadless }, () => next());
22639
22876
  });
22640
- void migrateCostTelemetry();
22641
22877
  app.post("/auth/register", requireAllowedOrigin, async (c) => {
22642
22878
  const { email, password } = await c.req.json();
22643
22879
  const normalizedEmail = email?.trim().toLowerCase();
@@ -22668,7 +22904,6 @@ app.post("/auth/register", requireAllowedOrigin, async (c) => {
22668
22904
  }
22669
22905
  }
22670
22906
  const newKey = await rotateApiKey(user.id);
22671
- await grantSignupCredit(user.id);
22672
22907
  void (async () => {
22673
22908
  try {
22674
22909
  const full = await getUserById(user.id);
@@ -22776,11 +23011,7 @@ app.get("/catalog", (c) => {
22776
23011
  return c.json(CATALOG);
22777
23012
  });
22778
23013
  app.get("/rates", (c) => {
22779
- const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
22780
- ...cost,
22781
- ...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
22782
- }));
22783
- return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
23014
+ return c.json(getPublicRatesPayload());
22784
23015
  });
22785
23016
  app.get("/me", async (c) => {
22786
23017
  const token = getCookie2(c, "session");
@@ -23851,7 +24082,7 @@ app.post("/harvest", auth2, async (c) => {
23851
24082
  serpOnly: body.serpOnly ?? false,
23852
24083
  pages: Math.min(2, Math.max(1, body.pages ?? 1))
23853
24084
  };
23854
- const harvestCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount(options.maxQuestions);
24085
+ const harvestCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount(options.maxQuestions);
23855
24086
  const { ok: harvestOk, balance_mc: harvestBal } = await debitMc(user.id, harvestCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
23856
24087
  if (!harvestOk) return c.json(insufficientBalanceResponse(harvestBal, harvestCost), 402);
23857
24088
  const jobId = await createJob(user.id, options.query, { ...options, billingHoldMc: harvestCost }, body.callback_url);
@@ -23887,7 +24118,7 @@ app.post("/harvest/sync", auth2, async (c) => {
23887
24118
  serpOnly: body.serpOnly ?? false,
23888
24119
  pages: Math.min(2, Math.max(1, body.pages ?? 1))
23889
24120
  };
23890
- const syncCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount(options.maxQuestions);
24121
+ const syncCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount(options.maxQuestions);
23891
24122
  const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
23892
24123
  if (!syncOk) return c.json(insufficientBalanceResponse(syncBal, syncCost), 402);
23893
24124
  const jobId = await createRunningJob(user.id, options.query, options);
@@ -23897,10 +24128,11 @@ app.post("/harvest/sync", auth2, async (c) => {
23897
24128
  c.req.raw.signal,
23898
24129
  AbortSignal.timeout(syncTimeoutMs)
23899
24130
  ]);
24131
+ const headlessSentOut = { value: null };
23900
24132
  try {
23901
24133
  const harvestCtx = currentCostContext();
23902
24134
  const result = await runWithCostContext(
23903
- { ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
24135
+ { ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id, headlessSentOut },
23904
24136
  () => harvest({
23905
24137
  ...options,
23906
24138
  kernelApiKey: browserServiceApiKey(),
@@ -23919,6 +24151,11 @@ app.post("/harvest/sync", auth2, async (c) => {
23919
24151
  const diff = syncCost - actualCost;
23920
24152
  if (diff > 0) await creditMc(user.id, diff, LedgerOperation.PAA_REFUND, "overestimate refund");
23921
24153
  else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.PAA, options.query);
24154
+ } else {
24155
+ const actualCost = serpActualCostMc(headlessSentOut.value);
24156
+ const diff = syncCost - actualCost;
24157
+ if (diff > 0) await creditMc(user.id, diff, LedgerOperation.SERP_REFUND, "headless-mode pricing settle");
24158
+ else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.SERP, options.query);
23922
24159
  }
23923
24160
  return c.json({ job_id: jobId, status: "done", result: sanitizeHarvestResult(result), attempts: sanitizeAttempts(attempts) });
23924
24161
  } catch (err) {
@@ -23994,22 +24231,13 @@ app.delete("/admin/users/:id", adminAuth, async (c) => {
23994
24231
  });
23995
24232
  app.post("/admin/backfill-signup-credits", adminAuth, async (c) => {
23996
24233
  const users = await listUsers();
23997
- let processed = 0;
23998
- let credited = 0;
23999
- let skipped = 0;
24000
- const users_credited = [];
24001
- for (const user of users) {
24002
- processed++;
24003
- const alreadyHasGrant = await ledgerExistsForOperation(user.id, LedgerOperation.SIGNUP_GRANT);
24004
- if (alreadyHasGrant) {
24005
- skipped++;
24006
- } else {
24007
- await grantSignupCredit(user.id);
24008
- credited++;
24009
- users_credited.push({ id: user.id, email: user.email });
24010
- }
24011
- }
24012
- return c.json({ processed, credited, skipped, users_credited });
24234
+ return c.json({
24235
+ processed: users.length,
24236
+ credited: 0,
24237
+ skipped: users.length,
24238
+ users_credited: [],
24239
+ retired: true
24240
+ });
24013
24241
  });
24014
24242
  app.post("/extract-url", auth2, async (c) => {
24015
24243
  const raw = await c.req.json().catch(() => ({}));
@@ -24590,7 +24818,7 @@ app.get("/cron/tick", async (c) => {
24590
24818
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
24591
24819
  return c.json({ error: "Unauthorized" }, 401);
24592
24820
  }
24593
- const { drainQueue } = await import("./worker-GE5JMHE6.js");
24821
+ const { drainQueue } = await import("./worker-RV2TYXE3.js");
24594
24822
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
24595
24823
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
24596
24824
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
@@ -24618,15 +24846,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
24618
24846
  return c.json({ error: "Unauthorized" }, 401);
24619
24847
  }
24620
24848
  const jobId = c.req.param("id");
24621
- const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-5V5EEQ72.js");
24622
- const { assembleExtractArtifacts } = await import("./extract-bundle-HMCYUVBJ.js");
24849
+ const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-2QQSL2QC.js");
24850
+ const { assembleExtractArtifacts } = await import("./extract-bundle-NFFVMUMT.js");
24623
24851
  const job = await getExtractJob2(jobId);
24624
24852
  if (!job) return c.json({ error: "job not found" }, 404);
24625
24853
  const stored = await assembleExtractArtifacts(job, {});
24626
24854
  await completeExtractJob2(jobId, stored);
24627
24855
  let settlement = "already_settled_or_refunded";
24628
24856
  if (job.billedMc == null && job.userId != null) {
24629
- const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-5V5EEQ72.js");
24857
+ const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-2QQSL2QC.js");
24630
24858
  const heldMc = Number(job.options.heldMc ?? 0);
24631
24859
  const successful = await countSuccessfulPages2(jobId);
24632
24860
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -24792,4 +25020,4 @@ app.get("/blog/:slug/", (c) => {
24792
25020
  export {
24793
25021
  app
24794
25022
  };
24795
- //# sourceMappingURL=server-CHTX6YQA.js.map
25023
+ //# sourceMappingURL=server-OLOOFN5W.js.map