mcp-scraper 0.21.1 → 0.21.3

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 (58) hide show
  1. package/README.md +4 -2
  2. package/dist/bin/api-server.cjs +749 -399
  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 +2 -2
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +2 -2
  11. package/dist/bin/mcp-stdio-server.cjs +17 -11
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +6 -6
  14. package/dist/bin/paa-harvest.cjs +1 -0
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-JBME5WJR.js → chunk-2P5VFIJM.js} +2 -2
  18. package/dist/{chunk-GMZFQRH2.js → chunk-5IU5ZFVO.js} +2 -2
  19. package/dist/chunk-5IU5ZFVO.js.map +1 -0
  20. package/dist/{chunk-7BHU4SJQ.js → chunk-7LO6KLKD.js} +14 -10
  21. package/dist/{chunk-7BHU4SJQ.js.map → chunk-7LO6KLKD.js.map} +1 -1
  22. package/dist/{chunk-DZBGULWW.js → chunk-H7APH5UG.js} +81 -1
  23. package/dist/chunk-H7APH5UG.js.map +1 -0
  24. package/dist/{chunk-ZJDANZTQ.js → chunk-JJ5RMEGX.js} +3 -2
  25. package/dist/chunk-JJ5RMEGX.js.map +1 -0
  26. package/dist/{chunk-HXGZMMW5.js → chunk-OBNKOYOE.js} +44 -8
  27. package/dist/chunk-OBNKOYOE.js.map +1 -0
  28. package/dist/chunk-VYUWWIVR.js +7 -0
  29. package/dist/chunk-VYUWWIVR.js.map +1 -0
  30. package/dist/{chunk-TUSLATZY.js → chunk-XDRHBGJR.js} +3 -3
  31. package/dist/{chunk-Q44DN6T2.js → chunk-YNHPZU6B.js} +2 -2
  32. package/dist/{chunk-Q44DN6T2.js.map → chunk-YNHPZU6B.js.map} +1 -1
  33. package/dist/{db-EONGTNJM.js → db-Z35S224E.js} +4 -2
  34. package/dist/{extract-bundle-HMCYUVBJ.js → extract-bundle-4RO3YU4Q.js} +2 -2
  35. package/dist/index.cjs +1 -0
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +3 -3
  38. package/dist/{server-FRR4B4Z5.js → server-PBBRSLBP.js} +595 -384
  39. package/dist/server-PBBRSLBP.js.map +1 -0
  40. package/dist/{site-extract-repository-YNPXEZCB.js → site-extract-repository-FDH6ZXKT.js} +4 -4
  41. package/dist/{worker-45WEWCMQ.js → worker-BXJQ7J4N.js} +13 -7
  42. package/dist/worker-BXJQ7J4N.js.map +1 -0
  43. package/docs/mcp-tool-manifest.generated.json +28 -10
  44. package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +16 -7
  45. package/package.json +2 -2
  46. package/dist/chunk-DZBGULWW.js.map +0 -1
  47. package/dist/chunk-GMZFQRH2.js.map +0 -1
  48. package/dist/chunk-HXGZMMW5.js.map +0 -1
  49. package/dist/chunk-QDUFW45J.js +0 -7
  50. package/dist/chunk-QDUFW45J.js.map +0 -1
  51. package/dist/chunk-ZJDANZTQ.js.map +0 -1
  52. package/dist/server-FRR4B4Z5.js.map +0 -1
  53. package/dist/worker-45WEWCMQ.js.map +0 -1
  54. /package/dist/{chunk-JBME5WJR.js.map → chunk-2P5VFIJM.js.map} +0 -0
  55. /package/dist/{chunk-TUSLATZY.js.map → chunk-XDRHBGJR.js.map} +0 -0
  56. /package/dist/{db-EONGTNJM.js.map → db-Z35S224E.js.map} +0 -0
  57. /package/dist/{extract-bundle-HMCYUVBJ.js.map → extract-bundle-4RO3YU4Q.js.map} +0 -0
  58. /package/dist/{site-extract-repository-YNPXEZCB.js.map → site-extract-repository-FDH6ZXKT.js.map} +0 -0
@@ -3602,6 +3602,7 @@ __export(db_exports, {
3602
3602
  creditMc: () => creditMc,
3603
3603
  deactivateUser: () => deactivateUser,
3604
3604
  debitMc: () => debitMc,
3605
+ debitMcIdempotent: () => debitMcIdempotent,
3605
3606
  deleteWorkflowSchedule: () => deleteWorkflowSchedule,
3606
3607
  expireOldLots: () => expireOldLots,
3607
3608
  failJob: () => failJob,
@@ -3918,6 +3919,19 @@ async function migrate() {
3918
3919
  `);
3919
3920
  await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_user_created_at ON billing_events(user_id, created_at DESC)`);
3920
3921
  await db.execute(`CREATE INDEX IF NOT EXISTS billing_events_authorization ON billing_events(authorization_id)`);
3922
+ await db.execute(`
3923
+ CREATE TABLE IF NOT EXISTS billing_debits (
3924
+ idempotency_key TEXT PRIMARY KEY,
3925
+ user_id INTEGER NOT NULL REFERENCES users(id),
3926
+ amount_mc INTEGER NOT NULL,
3927
+ operation TEXT NOT NULL,
3928
+ description TEXT,
3929
+ status TEXT NOT NULL DEFAULT 'claimed',
3930
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3931
+ applied_at TEXT
3932
+ )
3933
+ `);
3934
+ await db.execute(`CREATE INDEX IF NOT EXISTS billing_debits_user_status ON billing_debits(user_id, status)`);
3921
3935
  await db.execute(`
3922
3936
  CREATE TABLE IF NOT EXISTS site_audit_jobs (
3923
3937
  id TEXT PRIMARY KEY,
@@ -5289,6 +5303,72 @@ async function debitMc(userId, mc, operation, description) {
5289
5303
  const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
5290
5304
  return { ok: true, balance_mc: Number(res.rows[0]?.balance_mc ?? 0) };
5291
5305
  }
5306
+ async function debitMcIdempotent(userId, mc, operation, description, idempotencyKey) {
5307
+ if (!Number.isSafeInteger(mc) || mc <= 0) throw new Error("idempotent debit amount must be a positive integer");
5308
+ if (!idempotencyKey || idempotencyKey.length > 500) throw new Error("idempotent debit key is required and must be bounded");
5309
+ const db = getDb();
5310
+ await ensureMigrated(userId);
5311
+ const results = await db.batch([
5312
+ {
5313
+ sql: "UPDATE billing_debits SET amount_mc = ?, description = ? WHERE idempotency_key = ? AND user_id = ? AND operation = ? AND status = 'claimed'",
5314
+ args: [mc, description, idempotencyKey, userId, operation]
5315
+ },
5316
+ {
5317
+ sql: "INSERT OR IGNORE INTO billing_debits (idempotency_key, user_id, amount_mc, operation, description, status) VALUES (?, ?, ?, ?, ?, 'claimed')",
5318
+ args: [idempotencyKey, userId, mc, operation, description]
5319
+ },
5320
+ {
5321
+ sql: "UPDATE users SET balance_mc = balance_mc - ? WHERE id = ? AND balance_mc >= ? AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed')",
5322
+ args: [mc, userId, mc, idempotencyKey, userId, mc, operation]
5323
+ },
5324
+ {
5325
+ sql: "UPDATE billing_debits SET status = 'applying' WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND status = 'claimed' AND changes() = 1",
5326
+ args: [idempotencyKey, userId, mc, operation]
5327
+ },
5328
+ {
5329
+ sql: `WITH ordered AS MATERIALIZED (
5330
+ SELECT id,
5331
+ remaining_mc,
5332
+ COALESCE(SUM(remaining_mc) OVER (
5333
+ ORDER BY expires_at ASC, granted_at ASC, id ASC
5334
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
5335
+ ), 0) AS prior_mc
5336
+ FROM credit_lots
5337
+ WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')
5338
+ ), allocation AS (
5339
+ SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
5340
+ FROM ordered
5341
+ )
5342
+ UPDATE credit_lots
5343
+ SET remaining_mc = remaining_mc - COALESCE((SELECT take_mc FROM allocation WHERE allocation.id = credit_lots.id), 0)
5344
+ WHERE id IN (SELECT id FROM allocation WHERE take_mc > 0)
5345
+ AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
5346
+ args: [userId, mc, idempotencyKey]
5347
+ },
5348
+ {
5349
+ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) SELECT user_id, -amount_mc, operation, description FROM billing_debits WHERE idempotency_key = ? AND status = 'applying'",
5350
+ args: [idempotencyKey]
5351
+ },
5352
+ {
5353
+ sql: "UPDATE billing_debits SET status = 'applied', applied_at = datetime('now') WHERE idempotency_key = ? AND status = 'applying'",
5354
+ args: [idempotencyKey]
5355
+ }
5356
+ ], "write");
5357
+ const claim = await db.execute({
5358
+ sql: "SELECT user_id, amount_mc, operation, status FROM billing_debits WHERE idempotency_key = ? LIMIT 1",
5359
+ args: [idempotencyKey]
5360
+ });
5361
+ const row = claim.rows[0];
5362
+ if (!row || Number(row.user_id) !== userId || Number(row.amount_mc) !== mc || String(row.operation) !== operation) {
5363
+ throw new Error("idempotent debit key conflicts with another debit");
5364
+ }
5365
+ const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
5366
+ return {
5367
+ ok: String(row.status) === "applied",
5368
+ balance_mc: Number(balance.rows[0]?.balance_mc ?? 0),
5369
+ duplicate: results[2]?.rowsAffected === 0 && String(row.status) === "applied"
5370
+ };
5371
+ }
5292
5372
  async function getLedger(userId, limit = 50) {
5293
5373
  const res = await getDb().execute({
5294
5374
  sql: "SELECT * FROM ledger WHERE user_id = ? ORDER BY created_at DESC LIMIT ?",
@@ -8496,6 +8576,18 @@ function vendorCostUsdToMc(costUsd, markupBps) {
8496
8576
  if (mc > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("calculated mc exceeds the safe integer range");
8497
8577
  return Number(mc);
8498
8578
  }
8579
+ function getPublicRatesPayload() {
8580
+ const starter = SUBSCRIPTION_TIER_BY_KEY.starter;
8581
+ const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
8582
+ ...cost,
8583
+ ...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
8584
+ }));
8585
+ return {
8586
+ creditsPerDollarStarter: Math.round(mcToCredits(starter.credits_mc) / starter.monthly_usd),
8587
+ costs,
8588
+ scheduling: SCHEDULED_ACTIONS_RATE_POLICY
8589
+ };
8590
+ }
8499
8591
  function concurrencySlotBillingInfo() {
8500
8592
  const billingUrl = process.env.CONCURRENCY_BILLING_URL ?? "https://mcpscraper.dev/billing";
8501
8593
  return {
@@ -8525,12 +8617,14 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
8525
8617
  topup_url: topupUrl
8526
8618
  };
8527
8619
  }
8528
- var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, BILLING_MC_PER_USD_NUMERATOR, BILLING_MC_PER_USD_DENOMINATOR, BILLING_BASIS_POINTS, USD_NANOS, SCHEDULED_RUN_BASE_CREDITS, SCHEDULED_RUN_BASE_MC, SCHEDULED_AGENT_LLM_MARKUP_BPS, SCHEDULED_AGENT_LLM_MARKUP, SCHEDULED_RUN_DEFAULT_MAX_CREDITS, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, SCHEDULING_PLANS, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_HOLD_MC, MC_PER_USD;
8620
+ var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, BILLING_MC_PER_USD_NUMERATOR, BILLING_MC_PER_USD_DENOMINATOR, BILLING_BASIS_POINTS, USD_NANOS, SCHEDULED_RUN_BASE_CREDITS, SCHEDULED_RUN_BASE_MC, SCHEDULED_AGENT_LLM_MARKUP_BPS, SCHEDULED_AGENT_LLM_MARKUP, SCHEDULED_RUN_DEFAULT_MAX_CREDITS, SCHEDULED_ACTIONS_RATE_POLICY, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_MARGIN_MULTIPLE, MEDIA_TRANSCRIBE_HOLD_MC, MC_PER_USD;
8529
8621
  var init_rates = __esm({
8530
8622
  "src/api/rates.ts"() {
8531
8623
  "use strict";
8532
8624
  MC_COSTS = {
8533
8625
  serp: 100,
8626
+ serp_headless: 400,
8627
+ serp_headful: 1400,
8534
8628
  paa: 300,
8535
8629
  paa_base: 5500,
8536
8630
  page_scrape: 100,
@@ -8571,14 +8665,38 @@ var init_rates = __esm({
8571
8665
  SCHEDULED_AGENT_LLM_MARKUP_BPS = 15e3;
8572
8666
  SCHEDULED_AGENT_LLM_MARKUP = SCHEDULED_AGENT_LLM_MARKUP_BPS / BILLING_BASIS_POINTS;
8573
8667
  SCHEDULED_RUN_DEFAULT_MAX_CREDITS = 5e3;
8668
+ SCHEDULED_ACTIONS_RATE_POLICY = {
8669
+ billingMode: "credits",
8670
+ entitlement: "paid_plan_included",
8671
+ separateSubscriptionRequired: false,
8672
+ chargeEvent: "started_occurrence",
8673
+ runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
8674
+ runBaseMc: SCHEDULED_RUN_BASE_MC,
8675
+ llmCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP,
8676
+ llmCostMultiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
8677
+ modes: {
8678
+ connection_sync: {
8679
+ runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
8680
+ modelCostBilling: "none",
8681
+ modelCostMultiplier: 0
8682
+ },
8683
+ agent: {
8684
+ runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
8685
+ modelCostBilling: "actual_reported_cost",
8686
+ modelCostProvider: "openrouter",
8687
+ modelCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP,
8688
+ modelCostMultiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS
8689
+ }
8690
+ }
8691
+ };
8574
8692
  CREDIT_COST_CATALOG = [
8575
8693
  {
8576
8694
  key: "serp",
8577
8695
  label: "SERP search",
8578
8696
  aliases: ["search_serp", "serp", "google search", "organic results"],
8579
- credits: mcToCredits(MC_COSTS.serp),
8580
- unit: "per search",
8581
- notes: "Returns AI Overview, PAA snippet, videos, forums, and local pack."
8697
+ credits: mcToCredits(MC_COSTS.serp_headless),
8698
+ unit: "per search (headless); escalates to headful only when needed",
8699
+ notes: `Returns AI Overview, PAA snippet, videos, forums, and local pack. Billed ${mcToCredits(MC_COSTS.serp_headless)} credits when served headless (the common case), ${mcToCredits(MC_COSTS.serp_headful)} credits if an anti-bot escalation forces headful mode (overestimate refunded).`
8582
8700
  },
8583
8701
  {
8584
8702
  key: "paa",
@@ -8810,9 +8928,6 @@ var init_rates = __esm({
8810
8928
  MEMORY_PLAN_QUOTA = { free: MEMORY_FREE_QUOTA_BYTES, pro: 5e9, team: 5e10 };
8811
8929
  MEMORY_MARGIN_MULTIPLE = 3;
8812
8930
  MEMORY_PRO_COST_BUDGET_USD = 19 / MEMORY_MARGIN_MULTIPLE;
8813
- SCHEDULING_PLANS = {
8814
- "price_1ToXcHS8aAcsk3TGf3cW7zHx": { price_id: "price_1ToXcHS8aAcsk3TGf3cW7zHx", label: "Scheduled Actions", interval: "month", monthly_usd: 10, quota_per_period: 1e3 }
8815
- };
8816
8931
  LedgerOperation = {
8817
8932
  TOPUP: "topup",
8818
8933
  SUBSCRIPTION: "subscription",
@@ -8821,6 +8936,7 @@ var init_rates = __esm({
8821
8936
  PAA: "paa",
8822
8937
  PAA_REFUND: "paa_refund",
8823
8938
  SERP: "serp",
8939
+ SERP_REFUND: "serp_refund",
8824
8940
  REFUND: "refund",
8825
8941
  TRANSCRIPTION: "transcription",
8826
8942
  TRANSCRIPTION_HOLD: "transcription_hold",
@@ -11903,6 +12019,60 @@ var init_internal_memory_routes = __esm({
11903
12019
  }
11904
12020
  });
11905
12021
 
12022
+ // src/api/scheduling-access.ts
12023
+ function schedulingAccessOverrides() {
12024
+ return new Set(
12025
+ (process.env.SCHEDULING_ACCESS_OVERRIDE_IDENTITIES ?? "").split(",").map((identity) => identity.trim().toLowerCase()).filter(Boolean)
12026
+ );
12027
+ }
12028
+ function hasPaidSchedulingAccess(user) {
12029
+ const tier = typeof user.subscription_tier === "string" ? user.subscription_tier.trim() : "";
12030
+ const subscriptionId = typeof user.subscription_id === "string" ? user.subscription_id.trim() : "";
12031
+ if (tier && subscriptionId) return true;
12032
+ const identity = typeof user.email === "string" ? user.email.trim().toLowerCase() : "";
12033
+ return identity.length > 0 && schedulingAccessOverrides().has(identity);
12034
+ }
12035
+ function paidPlanRequiredPayload() {
12036
+ return {
12037
+ ok: false,
12038
+ enabled: false,
12039
+ code: PAID_PLAN_REQUIRED_CODE,
12040
+ error: PAID_PLAN_REQUIRED_MESSAGE,
12041
+ billingMode: "credits",
12042
+ runBaseCredits: SCHEDULED_RUN_BASE_CREDITS,
12043
+ llmCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP
12044
+ };
12045
+ }
12046
+ function memoryScheduleAccessPayload(upstreamName, user) {
12047
+ if (hasPaidSchedulingAccess(user)) return null;
12048
+ if (upstreamName === "getScheduleStatusTool") return paidPlanRequiredPayload();
12049
+ if (PLAN_GATED_MEMORY_SCHEDULE_TOOLS.has(upstreamName)) {
12050
+ return {
12051
+ ok: false,
12052
+ code: PAID_PLAN_REQUIRED_CODE,
12053
+ error: PAID_PLAN_REQUIRED_MESSAGE
12054
+ };
12055
+ }
12056
+ return null;
12057
+ }
12058
+ function memoryScheduleToolNeedsCredentials(upstreamName) {
12059
+ return upstreamName === "createScheduledActionTool" || upstreamName === "getScheduleLinkTool";
12060
+ }
12061
+ var PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, PLAN_GATED_MEMORY_SCHEDULE_TOOLS;
12062
+ var init_scheduling_access = __esm({
12063
+ "src/api/scheduling-access.ts"() {
12064
+ "use strict";
12065
+ init_rates();
12066
+ PAID_PLAN_REQUIRED_CODE = "paid_plan_required";
12067
+ PAID_PLAN_REQUIRED_MESSAGE = "Scheduled Actions requires an active MCP Scraper Starter plan or higher.";
12068
+ PLAN_GATED_MEMORY_SCHEDULE_TOOLS = /* @__PURE__ */ new Set([
12069
+ "createScheduledActionTool",
12070
+ "getScheduleLinkTool",
12071
+ "resumeScheduledActionTool"
12072
+ ]);
12073
+ }
12074
+ });
12075
+
11906
12076
  // src/api/unified-billing.ts
11907
12077
  function billingSecret() {
11908
12078
  const secret2 = process.env.INTERNAL_BILLING_SECRET ?? process.env.INTERNAL_MEMORY_DEBIT_SECRET;
@@ -11916,6 +12086,19 @@ function normalizeIdentity(identity) {
11916
12086
  }
11917
12087
  return normalized;
11918
12088
  }
12089
+ async function resolveSchedulingUser(identity) {
12090
+ const user = await getUserByEmail(normalizeIdentity(identity));
12091
+ if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
12092
+ return user;
12093
+ }
12094
+ async function getSchedulingAccess(identity) {
12095
+ const user = await getUserByEmail(normalizeIdentity(identity));
12096
+ if (!user) return { allowed: false, subscriptionTier: null };
12097
+ return {
12098
+ allowed: hasPaidSchedulingAccess(user),
12099
+ subscriptionTier: user.subscription_tier
12100
+ };
12101
+ }
11919
12102
  function normalizeIdempotencyKey(value) {
11920
12103
  const key = value.trim();
11921
12104
  if (!key || key.length > 500) {
@@ -11940,6 +12123,15 @@ function safeMetadata(value) {
11940
12123
  if (!value) return "{}";
11941
12124
  return JSON.stringify(value).slice(0, 4e3);
11942
12125
  }
12126
+ function metadataReason(row) {
12127
+ if (!row.metadata) return void 0;
12128
+ try {
12129
+ const metadata = JSON.parse(row.metadata);
12130
+ return typeof metadata.reason === "string" ? metadata.reason : void 0;
12131
+ } catch {
12132
+ return void 0;
12133
+ }
12134
+ }
11943
12135
  function mapAuthorization(row, balanceMc) {
11944
12136
  return {
11945
12137
  id: row.id,
@@ -11968,23 +12160,28 @@ async function eventByKey(idempotencyKey) {
11968
12160
  });
11969
12161
  return result.rows[0] ? result.rows[0] : null;
11970
12162
  }
11971
- function mapEvent(row, balanceMc, duplicate) {
11972
- let reason;
11973
- if (row.metadata) {
11974
- try {
11975
- const metadata = JSON.parse(row.metadata);
11976
- if (typeof metadata.reason === "string") reason = metadata.reason;
11977
- } catch {
11978
- }
11979
- }
12163
+ function mapStartEvent(row, balanceMc, duplicate) {
12164
+ return {
12165
+ id: row.id,
12166
+ authorizationId: row.authorization_id,
12167
+ status: "started",
12168
+ amountMc: SCHEDULED_RUN_BASE_MC,
12169
+ credits: SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT,
12170
+ balanceMc,
12171
+ duplicate
12172
+ };
12173
+ }
12174
+ function mapSettlementEvent(row, balanceMc, duplicate) {
11980
12175
  const rawNanos = row.raw_cost_usd_nanos == null ? null : Number(row.raw_cost_usd_nanos);
11981
- const modelMc = Math.max(0, Number(row.amount_mc) - SCHEDULED_RUN_BASE_MC);
12176
+ const modelMc = Math.max(0, Number(row.amount_mc));
12177
+ const amountMc = SCHEDULED_RUN_BASE_MC + modelMc;
12178
+ const reason = metadataReason(row);
11982
12179
  return {
11983
12180
  id: row.id,
11984
12181
  authorizationId: row.authorization_id,
11985
12182
  status: row.status === "settled" ? "settled" : "cost_pending",
11986
- amountMc: Number(row.amount_mc),
11987
- credits: Number(row.amount_mc) / MC_PER_CREDIT,
12183
+ amountMc,
12184
+ credits: amountMc / MC_PER_CREDIT,
11988
12185
  baseMc: SCHEDULED_RUN_BASE_MC,
11989
12186
  modelMc,
11990
12187
  rawCostUsd: rawNanos == null ? null : rawNanos / USD_NANOS,
@@ -11997,14 +12194,16 @@ function mapEvent(row, balanceMc, duplicate) {
11997
12194
  async function authorizeScheduledRun(args) {
11998
12195
  const identity = normalizeIdentity(args.identity);
11999
12196
  const idempotencyKey = normalizeIdempotencyKey(args.idempotencyKey);
12000
- const user = await getUserByEmail(identity);
12001
- if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
12197
+ const user = await resolveSchedulingUser(identity);
12198
+ if (!hasPaidSchedulingAccess(user)) {
12199
+ throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
12200
+ }
12002
12201
  const pending = await getDb().execute({
12003
12202
  sql: "SELECT id FROM billing_events WHERE user_id = ? AND billing_class = ? AND status = 'cost_pending' LIMIT 1",
12004
12203
  args: [user.id, SCHEDULED_RUN_BILLING_CLASS]
12005
12204
  });
12006
12205
  if (pending.rows.length > 0) {
12007
- throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved cost", 409);
12206
+ throw new UnifiedBillingError("pending_cost", "a previous scheduled run has an unresolved model cost", 409);
12008
12207
  }
12009
12208
  const existing = await getDb().execute({
12010
12209
  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",
@@ -12013,14 +12212,9 @@ async function authorizeScheduledRun(args) {
12013
12212
  if (existing.rows[0]) {
12014
12213
  const row = existing.rows[0];
12015
12214
  if (Number(row.user_id) !== user.id) {
12016
- throw new UnifiedBillingError(
12017
- "invalid_request",
12018
- "the idempotency key is already in use by another billing identity",
12019
- 409
12020
- );
12215
+ throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
12021
12216
  }
12022
- const balanceMc2 = await reconcileBalanceMc(user.id, user.balance_mc);
12023
- return mapAuthorization(row, balanceMc2);
12217
+ return mapAuthorization(row, await reconcileBalanceMc(user.id, user.balance_mc));
12024
12218
  }
12025
12219
  const maximumCredits = args.maximumCredits ?? SCHEDULED_RUN_DEFAULT_MAX_CREDITS;
12026
12220
  if (!Number.isFinite(maximumCredits) || maximumCredits < SCHEDULED_RUN_BASE_MC / MC_PER_CREDIT) {
@@ -12041,17 +12235,7 @@ async function authorizeScheduledRun(args) {
12041
12235
  const expiresAt = new Date(Date.now() + AUTHORIZATION_TTL_MS).toISOString();
12042
12236
  const inserted = await getDb().execute({
12043
12237
  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', ?, ?, ?, ?, ?)",
12044
- args: [
12045
- id,
12046
- user.id,
12047
- idempotencyKey,
12048
- SCHEDULED_RUN_BILLING_CLASS,
12049
- SCHEDULED_RUN_BASE_MC,
12050
- maximumMc,
12051
- tokenHash(token),
12052
- safeMetadata(args.metadata),
12053
- expiresAt
12054
- ]
12238
+ args: [id, user.id, idempotencyKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_BASE_MC, maximumMc, tokenHash(token), safeMetadata(args.metadata), expiresAt]
12055
12239
  });
12056
12240
  if (inserted.rowsAffected === 0) {
12057
12241
  const raced = await getDb().execute({
@@ -12061,11 +12245,7 @@ async function authorizeScheduledRun(args) {
12061
12245
  if (!raced.rows[0]) throw new Error("billing authorization insert raced without a readable winner");
12062
12246
  const row = raced.rows[0];
12063
12247
  if (Number(row.user_id) !== user.id) {
12064
- throw new UnifiedBillingError(
12065
- "invalid_request",
12066
- "the idempotency key is already in use by another billing identity",
12067
- 409
12068
- );
12248
+ throw new UnifiedBillingError("invalid_request", "the idempotency key is already in use by another billing identity", 409);
12069
12249
  }
12070
12250
  return mapAuthorization(row, balanceMc);
12071
12251
  }
@@ -12082,180 +12262,235 @@ async function authorizeScheduledRun(args) {
12082
12262
  reservationMode: "balance_check"
12083
12263
  };
12084
12264
  }
12085
- async function settleScheduledRun(args) {
12086
- const identity = normalizeIdentity(args.identity);
12087
- const user = await getUserByEmail(identity);
12088
- if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
12089
- const authorization = await authorizationById(args.authorizationId);
12265
+ async function startScheduledRun(args) {
12266
+ const user = await resolveSchedulingUser(args.identity);
12267
+ let authorization = await authorizationById(args.authorizationId);
12090
12268
  if (!authorization || Number(authorization.user_id) !== user.id) {
12091
12269
  throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
12092
12270
  }
12093
12271
  verifyToken(authorization, args.authorizationToken);
12094
- const settlementKey = authorization.id + ":settlement";
12095
- const existing = await eventByKey(settlementKey);
12096
- if (existing) {
12097
- const balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
12098
- if (existing.status === "settling") {
12099
- throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
12100
- }
12101
- return mapEvent(existing, balanceMc, true);
12272
+ const startKey = authorization.id + ":start";
12273
+ const existing = await eventByKey(startKey);
12274
+ if (existing?.status === "settled") {
12275
+ await getDb().execute({
12276
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting')",
12277
+ args: [authorization.id]
12278
+ });
12279
+ return mapStartEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
12102
12280
  }
12103
- if (authorization.status !== "authorized") {
12104
- throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer open", 409);
12281
+ if (!hasPaidSchedulingAccess(user)) {
12282
+ throw new UnifiedBillingError(PAID_PLAN_REQUIRED_CODE, PAID_PLAN_REQUIRED_MESSAGE, 403);
12105
12283
  }
12106
- if (Date.parse(authorization.expires_at) <= Date.now()) {
12284
+ if (authorization.status === "authorized") {
12285
+ if (Date.parse(authorization.expires_at) <= Date.now()) {
12286
+ await getDb().execute({
12287
+ sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
12288
+ args: [authorization.id]
12289
+ });
12290
+ throw new UnifiedBillingError("authorization_expired", "billing authorization expired before execution started", 409);
12291
+ }
12107
12292
  await getDb().execute({
12108
- sql: "UPDATE billing_authorizations SET status = 'expired' WHERE id = ? AND status = 'authorized'",
12293
+ sql: "UPDATE billing_authorizations SET status = 'starting' WHERE id = ? AND status = 'authorized'",
12109
12294
  args: [authorization.id]
12110
12295
  });
12111
- throw new UnifiedBillingError("authorization_expired", "billing authorization expired before settlement", 409);
12296
+ authorization = await authorizationById(authorization.id);
12112
12297
  }
12113
- const rawCostUsd = args.rawCostUsd ?? null;
12114
- if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
12115
- throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
12116
- }
12117
- if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
12118
- throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
12298
+ if (!authorization || !["starting", "started"].includes(authorization.status)) {
12299
+ throw new UnifiedBillingError("authorization_closed", "billing authorization is no longer startable", 409);
12119
12300
  }
12120
- const eventId = (0, import_node_crypto4.randomUUID)();
12121
- const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
12122
- const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
12123
- const totalMc = SCHEDULED_RUN_BASE_MC + modelMc;
12124
- const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : totalMc > Number(authorization.maximum_amount_mc) ? "max_credits_per_run_exceeded" : null;
12125
- if (pendingReason) {
12301
+ const eventId = existing?.id ?? (0, import_node_crypto4.randomUUID)();
12302
+ if (!existing) {
12126
12303
  await getDb().execute({
12127
- 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', ?, ?, ?, ?, ?, ?, ?, ?)",
12304
+ 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', ?, ?, ?)",
12128
12305
  args: [
12129
12306
  eventId,
12130
12307
  user.id,
12131
12308
  authorization.id,
12132
- settlementKey,
12309
+ startKey,
12133
12310
  SCHEDULED_RUN_BILLING_CLASS,
12134
12311
  SCHEDULED_RUN_SOURCE_SURFACE,
12135
- totalMc,
12136
- rawCostNanos,
12312
+ SCHEDULED_RUN_BASE_MC,
12137
12313
  SCHEDULED_AGENT_LLM_MARKUP_BPS,
12138
- Math.max(0, Math.round(args.inputTokens ?? 0)),
12139
- Math.max(0, Math.round(args.outputTokens ?? 0)),
12140
- Math.max(0, Math.round(args.cachedTokens ?? 0)),
12141
- Math.max(0, Math.round(args.modelSteps ?? 0)),
12142
- safeMetadata({ ...args.metadata, reason: pendingReason })
12314
+ safeMetadata({ ...args.metadata, billingPhase: "start", chargeMode: "base_capture" })
12143
12315
  ]
12144
12316
  });
12145
- await getDb().execute({
12146
- sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
12147
- args: [authorization.id]
12148
- });
12149
- return {
12150
- id: eventId,
12151
- authorizationId: authorization.id,
12152
- status: "cost_pending",
12153
- amountMc: totalMc,
12154
- credits: totalMc / MC_PER_CREDIT,
12155
- baseMc: SCHEDULED_RUN_BASE_MC,
12156
- modelMc,
12157
- rawCostUsd,
12158
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
12159
- balanceMc: await reconcileBalanceMc(user.id, user.balance_mc),
12160
- reason: pendingReason,
12161
- duplicate: false
12162
- };
12163
- }
12164
- const claimed = await getDb().execute({
12165
- 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', ?, ?, ?, ?, ?, ?, ?, ?)",
12166
- args: [
12167
- eventId,
12168
- user.id,
12169
- authorization.id,
12170
- settlementKey,
12171
- SCHEDULED_RUN_BILLING_CLASS,
12172
- SCHEDULED_RUN_SOURCE_SURFACE,
12173
- totalMc,
12174
- rawCostNanos,
12175
- SCHEDULED_AGENT_LLM_MARKUP_BPS,
12176
- Math.max(0, Math.round(args.inputTokens ?? 0)),
12177
- Math.max(0, Math.round(args.outputTokens ?? 0)),
12178
- Math.max(0, Math.round(args.cachedTokens ?? 0)),
12179
- Math.max(0, Math.round(args.modelSteps ?? 0)),
12180
- safeMetadata(args.metadata)
12181
- ]
12182
- });
12183
- if (claimed.rowsAffected === 0) {
12184
- const raced = await eventByKey(settlementKey);
12185
- if (!raced) throw new Error("billing settlement raced without a readable winner");
12186
- if (raced.status === "settling") {
12187
- throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement is already in progress", 409);
12188
- }
12189
- return mapEvent(raced, await reconcileBalanceMc(user.id, user.balance_mc), true);
12190
12317
  }
12191
- const debit = await debitMc(
12318
+ const debit = await debitMcIdempotent(
12192
12319
  user.id,
12193
- totalMc,
12320
+ SCHEDULED_RUN_BASE_MC,
12194
12321
  LedgerOperation.SCHEDULED_RUN,
12195
- "scheduled-run:" + authorization.id
12322
+ "scheduled-run:" + authorization.id + ":base",
12323
+ startKey
12196
12324
  );
12197
12325
  if (!debit.ok) {
12198
- const metadata = safeMetadata({ ...args.metadata, reason: "insufficient_credits_at_settlement" });
12199
12326
  await getDb().execute({
12200
- sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ?",
12201
- args: [metadata, eventId]
12202
- });
12203
- await getDb().execute({
12204
- sql: "UPDATE billing_authorizations SET status = 'cost_pending', settled_at = datetime('now') WHERE id = ?",
12327
+ sql: "UPDATE billing_authorizations SET status = 'authorized' WHERE id = ? AND status = 'starting'",
12205
12328
  args: [authorization.id]
12206
12329
  });
12207
- return {
12208
- id: eventId,
12209
- authorizationId: authorization.id,
12210
- status: "cost_pending",
12211
- amountMc: totalMc,
12212
- credits: totalMc / MC_PER_CREDIT,
12213
- baseMc: SCHEDULED_RUN_BASE_MC,
12330
+ throw new UnifiedBillingError(
12331
+ "insufficient_credits",
12332
+ "the wallet could not capture the scheduled-run base charge",
12333
+ 402,
12334
+ { balanceMc: debit.balance_mc, requiredMc: SCHEDULED_RUN_BASE_MC }
12335
+ );
12336
+ }
12337
+ await getDb().execute({
12338
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE idempotency_key = ? AND status = 'capturing'",
12339
+ args: [startKey]
12340
+ });
12341
+ await getDb().execute({
12342
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status IN ('authorized', 'starting', 'started')",
12343
+ args: [authorization.id]
12344
+ });
12345
+ const started = await eventByKey(startKey);
12346
+ if (!started) throw new Error("scheduled-run base capture completed without an event receipt");
12347
+ return mapStartEvent(started, debit.balance_mc, debit.duplicate);
12348
+ }
12349
+ function validateModelCost(args) {
12350
+ const rawCostUsd = args.rawCostUsd ?? null;
12351
+ if (args.modelCostStatus === "reported" && (rawCostUsd == null || !Number.isFinite(rawCostUsd) || rawCostUsd < 0)) {
12352
+ throw new UnifiedBillingError("invalid_request", "reported model cost must be a finite non-negative number", 400);
12353
+ }
12354
+ if (args.modelCostStatus === "not_applicable" && rawCostUsd != null && rawCostUsd !== 0) {
12355
+ throw new UnifiedBillingError("invalid_request", "not_applicable model cost cannot include a non-zero cost", 400);
12356
+ }
12357
+ return rawCostUsd;
12358
+ }
12359
+ async function recoverSettlement(row, user, authorization, duplicate) {
12360
+ const modelMc = Math.max(0, Number(row.amount_mc));
12361
+ let balanceMc = await reconcileBalanceMc(user.id, user.balance_mc);
12362
+ if (modelMc > 0) {
12363
+ const debit = await debitMcIdempotent(
12364
+ user.id,
12214
12365
  modelMc,
12215
- rawCostUsd,
12216
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
12217
- balanceMc: debit.balance_mc,
12218
- reason: "insufficient_credits_at_settlement",
12219
- duplicate: false
12220
- };
12366
+ LedgerOperation.SCHEDULED_RUN,
12367
+ "scheduled-run:" + authorization.id + ":model",
12368
+ authorization.id + ":model"
12369
+ );
12370
+ balanceMc = debit.balance_mc;
12371
+ if (!debit.ok) {
12372
+ await getDb().execute({
12373
+ sql: "UPDATE billing_events SET status = 'cost_pending', metadata = ? WHERE id = ? AND status = 'settling'",
12374
+ args: [safeMetadata({ reason: "insufficient_credits_for_model_delta", billingPhase: "settlement", chargeMode: "model_delta" }), row.id]
12375
+ });
12376
+ await getDb().execute({
12377
+ sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
12378
+ args: [authorization.id]
12379
+ });
12380
+ const pending = await eventByKey(authorization.id + ":settlement");
12381
+ if (!pending) throw new Error("scheduled-run pending settlement lost its event");
12382
+ return mapSettlementEvent(pending, balanceMc, duplicate);
12383
+ }
12221
12384
  }
12222
12385
  await getDb().execute({
12223
- sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
12224
- args: [eventId]
12386
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status = 'settling'",
12387
+ args: [row.id]
12225
12388
  });
12226
12389
  await getDb().execute({
12227
- sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ?",
12390
+ sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status IN ('started', 'cost_pending', 'settled')",
12228
12391
  args: [authorization.id]
12229
12392
  });
12230
- return {
12231
- id: eventId,
12232
- authorizationId: authorization.id,
12233
- status: "settled",
12234
- amountMc: totalMc,
12235
- credits: totalMc / MC_PER_CREDIT,
12236
- baseMc: SCHEDULED_RUN_BASE_MC,
12393
+ const settled = await eventByKey(authorization.id + ":settlement");
12394
+ if (!settled) throw new Error("scheduled-run settlement completed without an event receipt");
12395
+ return mapSettlementEvent(settled, balanceMc, duplicate);
12396
+ }
12397
+ async function settleScheduledRun(args) {
12398
+ const user = await resolveSchedulingUser(args.identity);
12399
+ let authorization = await authorizationById(args.authorizationId);
12400
+ if (!authorization || Number(authorization.user_id) !== user.id) {
12401
+ throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
12402
+ }
12403
+ verifyToken(authorization, args.authorizationToken);
12404
+ const rawCostUsd = validateModelCost(args);
12405
+ const startEvent = await eventByKey(authorization.id + ":start");
12406
+ if (!startEvent || startEvent.status !== "settled") {
12407
+ throw new UnifiedBillingError("authorization_closed", "scheduled-run settlement requires a captured start", 409);
12408
+ }
12409
+ if (authorization.status === "starting") {
12410
+ await getDb().execute({
12411
+ sql: "UPDATE billing_authorizations SET status = 'started' WHERE id = ? AND status = 'starting'",
12412
+ args: [authorization.id]
12413
+ });
12414
+ authorization = await authorizationById(authorization.id);
12415
+ }
12416
+ if (!authorization || !["started", "cost_pending", "settled"].includes(authorization.status)) {
12417
+ throw new UnifiedBillingError("authorization_closed", "billing authorization is not open for settlement", 409);
12418
+ }
12419
+ const settlementKey = authorization.id + ":settlement";
12420
+ let existing = await eventByKey(settlementKey);
12421
+ if (existing?.status === "settled") {
12422
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
12423
+ }
12424
+ if (existing?.status === "settling") {
12425
+ return recoverSettlement(existing, user, authorization, true);
12426
+ }
12427
+ const hadExistingClaim = Boolean(existing);
12428
+ const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
12429
+ const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
12430
+ const maximumModelMc = Math.max(0, Number(authorization.maximum_amount_mc) - SCHEDULED_RUN_BASE_MC);
12431
+ const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : modelMc > maximumModelMc ? "max_credits_per_run_exceeded" : null;
12432
+ const metadata = safeMetadata({
12433
+ ...args.metadata,
12434
+ billingPhase: "settlement",
12435
+ chargeMode: "model_delta",
12436
+ ...pendingReason ? { reason: pendingReason } : {}
12437
+ });
12438
+ const eventId = existing?.id ?? (0, import_node_crypto4.randomUUID)();
12439
+ const status = pendingReason ? "cost_pending" : "settling";
12440
+ const eventValues = [
12237
12441
  modelMc,
12238
- rawCostUsd,
12239
- multiplierBps: SCHEDULED_AGENT_LLM_MARKUP_BPS,
12240
- balanceMc: debit.balance_mc,
12241
- duplicate: false
12242
- };
12442
+ rawCostNanos,
12443
+ SCHEDULED_AGENT_LLM_MARKUP_BPS,
12444
+ Math.max(0, Math.round(args.inputTokens ?? 0)),
12445
+ Math.max(0, Math.round(args.outputTokens ?? 0)),
12446
+ Math.max(0, Math.round(args.cachedTokens ?? 0)),
12447
+ Math.max(0, Math.round(args.modelSteps ?? 0)),
12448
+ metadata
12449
+ ];
12450
+ if (existing) {
12451
+ await getDb().execute({
12452
+ 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'",
12453
+ args: [status, ...eventValues, existing.id]
12454
+ });
12455
+ } else {
12456
+ await getDb().execute({
12457
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
12458
+ args: [eventId, user.id, authorization.id, settlementKey, SCHEDULED_RUN_BILLING_CLASS, SCHEDULED_RUN_SOURCE_SURFACE, status, ...eventValues]
12459
+ });
12460
+ }
12461
+ existing = await eventByKey(settlementKey);
12462
+ if (!existing) throw new Error("scheduled-run settlement claim was not readable");
12463
+ const duplicateClaim = hadExistingClaim || eventId !== existing.id;
12464
+ if (existing.status === "settled") {
12465
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), true);
12466
+ }
12467
+ if (existing.status === "cost_pending") {
12468
+ await getDb().execute({
12469
+ sql: "UPDATE billing_authorizations SET status = 'cost_pending' WHERE id = ? AND status IN ('started', 'cost_pending')",
12470
+ args: [authorization.id]
12471
+ });
12472
+ return mapSettlementEvent(existing, await reconcileBalanceMc(user.id, user.balance_mc), duplicateClaim);
12473
+ }
12474
+ if (existing.status !== "settling") {
12475
+ throw new UnifiedBillingError("settlement_in_progress", "scheduled-run settlement could not be claimed", 409);
12476
+ }
12477
+ return recoverSettlement(existing, user, authorization, duplicateClaim);
12243
12478
  }
12244
12479
  async function voidScheduledRunAuthorization(args) {
12245
- const identity = normalizeIdentity(args.identity);
12246
- const user = await getUserByEmail(identity);
12247
- if (!user) throw new UnifiedBillingError("user_not_found", "billing identity was not found", 404);
12480
+ const user = await resolveSchedulingUser(args.identity);
12248
12481
  const authorization = await authorizationById(args.authorizationId);
12249
12482
  if (!authorization || Number(authorization.user_id) !== user.id) {
12250
12483
  throw new UnifiedBillingError("authorization_not_found", "billing authorization was not found", 404);
12251
12484
  }
12252
12485
  verifyToken(authorization, args.authorizationToken);
12253
12486
  if (authorization.status === "authorized") {
12254
- await getDb().execute({
12487
+ const result = await getDb().execute({
12255
12488
  sql: "UPDATE billing_authorizations SET status = 'void', metadata = ?, settled_at = datetime('now') WHERE id = ? AND status = 'authorized'",
12256
12489
  args: [safeMetadata({ reason: args.reason ?? "voided_before_execution" }), authorization.id]
12257
12490
  });
12258
- return { ok: true, status: "void" };
12491
+ if (result.rowsAffected > 0) return { ok: true, status: "void" };
12492
+ const raced = await authorizationById(authorization.id);
12493
+ return { ok: true, status: raced?.status ?? "closed" };
12259
12494
  }
12260
12495
  return { ok: true, status: authorization.status };
12261
12496
  }
@@ -12266,9 +12501,10 @@ var init_unified_billing = __esm({
12266
12501
  import_node_crypto4 = require("crypto");
12267
12502
  init_db();
12268
12503
  init_rates();
12504
+ init_scheduling_access();
12269
12505
  SCHEDULED_RUN_BILLING_CLASS = "scheduled_agent_run";
12270
12506
  SCHEDULED_RUN_SOURCE_SURFACE = "mastra_scheduler";
12271
- AUTHORIZATION_TTL_MS = 30 * 60 * 1e3;
12507
+ AUTHORIZATION_TTL_MS = 24 * 60 * 60 * 1e3;
12272
12508
  UnifiedBillingError = class extends Error {
12273
12509
  constructor(code, message, httpStatus, details = {}) {
12274
12510
  super(message);
@@ -12284,6 +12520,192 @@ var init_unified_billing = __esm({
12284
12520
  }
12285
12521
  });
12286
12522
 
12523
+ // src/api/session.ts
12524
+ function getSessionSecret() {
12525
+ const configured = process.env.SESSION_SECRET?.trim();
12526
+ if (configured) return configured;
12527
+ if (isProduction()) throw new Error("SESSION_SECRET is not set \u2014 add it to your Vercel env vars (see src/api/env.ts)");
12528
+ return "dev-secret-change-me";
12529
+ }
12530
+ function safeEqualHex(a, b) {
12531
+ if (a.length !== b.length) return false;
12532
+ try {
12533
+ return (0, import_node_crypto5.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
12534
+ } catch {
12535
+ return false;
12536
+ }
12537
+ }
12538
+ function signSession(userId) {
12539
+ const payload = String(userId);
12540
+ const sig = (0, import_node_crypto5.createHmac)("sha256", secret()).update(payload).digest("hex");
12541
+ return `${payload}.${sig}`;
12542
+ }
12543
+ function verifySession(token) {
12544
+ const dot = token.lastIndexOf(".");
12545
+ if (dot === -1) return null;
12546
+ const payload = token.slice(0, dot);
12547
+ const sig = token.slice(dot + 1);
12548
+ const expected = (0, import_node_crypto5.createHmac)("sha256", secret()).update(payload).digest("hex");
12549
+ if (!safeEqualHex(sig, expected)) return null;
12550
+ const id = parseInt(payload);
12551
+ return isNaN(id) ? null : id;
12552
+ }
12553
+ var import_node_crypto5, isProduction, secret;
12554
+ var init_session = __esm({
12555
+ "src/api/session.ts"() {
12556
+ "use strict";
12557
+ import_node_crypto5 = require("crypto");
12558
+ isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
12559
+ secret = () => getSessionSecret();
12560
+ }
12561
+ });
12562
+
12563
+ // src/api/memory.ts
12564
+ function parseMemoryRpcBody(text) {
12565
+ const dataLine = text.split("\n").find((line) => line.startsWith("data:"));
12566
+ const payload = dataLine ? dataLine.slice(5).trim() : text;
12567
+ return JSON.parse(payload);
12568
+ }
12569
+ function isMemoryOperator(email) {
12570
+ if (!email) return false;
12571
+ const ops = (process.env.MEMORY_OPERATOR_IDENTITIES ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
12572
+ return ops.includes(email.trim().toLowerCase());
12573
+ }
12574
+ function encKey() {
12575
+ return (0, import_node_crypto6.scryptSync)(getSessionSecret(), "mcp-memory-key-v1", 32);
12576
+ }
12577
+ function encryptMemoryKey(secret2) {
12578
+ const iv = (0, import_node_crypto6.randomBytes)(12);
12579
+ const cipher = (0, import_node_crypto6.createCipheriv)("aes-256-gcm", encKey(), iv);
12580
+ const enc = Buffer.concat([cipher.update(secret2, "utf8"), cipher.final()]);
12581
+ const tag = cipher.getAuthTag();
12582
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
12583
+ }
12584
+ function decryptMemoryKey(stored) {
12585
+ try {
12586
+ const [ivB, tagB, dataB] = stored.split(":");
12587
+ const decipher = (0, import_node_crypto6.createDecipheriv)("aes-256-gcm", encKey(), Buffer.from(ivB, "base64"));
12588
+ decipher.setAuthTag(Buffer.from(tagB, "base64"));
12589
+ return Buffer.concat([decipher.update(Buffer.from(dataB, "base64")), decipher.final()]).toString("utf8");
12590
+ } catch {
12591
+ return null;
12592
+ }
12593
+ }
12594
+ async function memoryCall(toolName, args, userMemoryKey) {
12595
+ try {
12596
+ const res = await fetch(`${MEMORY_BASE_URL()}/mcp`, {
12597
+ method: "POST",
12598
+ headers: {
12599
+ "content-type": "application/json",
12600
+ accept: "application/json, text/event-stream",
12601
+ authorization: `Bearer ${userMemoryKey}`
12602
+ },
12603
+ body: JSON.stringify({
12604
+ jsonrpc: "2.0",
12605
+ id: 1,
12606
+ method: "tools/call",
12607
+ params: { name: toolName, arguments: args }
12608
+ })
12609
+ });
12610
+ const text = await res.text();
12611
+ if (!res.ok) return { ok: false, error: `memory ${toolName} failed (${res.status})` };
12612
+ const body = parseMemoryRpcBody(text);
12613
+ if (body.error) return { ok: false, error: body.error.message ?? `memory ${toolName} failed` };
12614
+ if (body.result?.structuredContent) return body.result.structuredContent;
12615
+ const textContent = body.result?.content?.find((c) => c.type === "text")?.text;
12616
+ if (textContent) {
12617
+ try {
12618
+ return JSON.parse(textContent);
12619
+ } catch {
12620
+ return { ok: false, error: textContent };
12621
+ }
12622
+ }
12623
+ return { ok: false, error: `memory ${toolName} returned no result` };
12624
+ } catch (err) {
12625
+ return { ok: false, error: err?.message ?? "memory call failed" };
12626
+ }
12627
+ }
12628
+ function memoryIdentity(user) {
12629
+ return user.email;
12630
+ }
12631
+ function resolveMemoryIdentity(user) {
12632
+ return memoryIdentity(user);
12633
+ }
12634
+ function memoryPlanForReads(user) {
12635
+ if (isMemoryOperator(user.email)) return "unlimited";
12636
+ if (user.memory_plan === "pro" || user.memory_plan === "team") return user.memory_plan;
12637
+ return "free";
12638
+ }
12639
+ async function provisionMemoryForUser(user) {
12640
+ try {
12641
+ const { key } = await getOrCreateUserMemoryKey(user);
12642
+ if (key) await setMemoryProvisioned(user.id, true);
12643
+ } catch {
12644
+ }
12645
+ }
12646
+ function resolveEffectiveMemoryPlan(user) {
12647
+ return user.memory_plan === "pro" || user.memory_plan === "team" ? user.memory_plan : "free";
12648
+ }
12649
+ async function getOrCreateUserMemoryKey(user) {
12650
+ const creds = await getUserMemoryCreds(user.id);
12651
+ if (creds.memory_key) {
12652
+ const decrypted = decryptMemoryKey(creds.memory_key);
12653
+ if (decrypted) return { key: decrypted };
12654
+ }
12655
+ const admin = ADMIN_KEY();
12656
+ if (!admin) return { key: null, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
12657
+ const identity = memoryIdentity(user);
12658
+ const plan = isMemoryOperator(user.email) ? "unlimited" : (await getMemoryPlan(user.id)).plan;
12659
+ const provisioned = await import_provision_defaults.provisionDefaultsTool.execute(
12660
+ { apiKey: admin, granteeIdentity: identity, issueKey: true, plan },
12661
+ {}
12662
+ );
12663
+ if (!provisioned.ok || !provisioned.secret) {
12664
+ return { key: null, error: provisioned.error ?? "failed to provision default vaults" };
12665
+ }
12666
+ await setUserMemoryCreds(user.id, encryptMemoryKey(provisioned.secret), identity);
12667
+ return { key: provisioned.secret };
12668
+ }
12669
+ async function syncMemoryKeyPlan(user, plan) {
12670
+ const effectivePlan = isMemoryOperator(user.email) ? "unlimited" : plan;
12671
+ const { key, error } = await getOrCreateUserMemoryKey(user);
12672
+ if (!key) return { ok: false, error: error ?? "memory unavailable" };
12673
+ const listed = await memoryCall(
12674
+ "listKeysTool",
12675
+ {},
12676
+ key
12677
+ );
12678
+ if (!listed.ok || !listed.keys?.length) return { ok: false, error: listed.error ?? "no memory keys found" };
12679
+ const target = listed.keys[0];
12680
+ if (target.plan === effectivePlan) return { ok: true };
12681
+ const res = await memoryCall("setScopeTool", { keyId: target.keyId, plan: effectivePlan }, key);
12682
+ return { ok: res.ok, error: res.error };
12683
+ }
12684
+ async function syncScheduledActionCredentials(user) {
12685
+ const admin = ADMIN_KEY();
12686
+ if (!admin) return { ok: false, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
12687
+ const identity = resolveMemoryIdentity(user);
12688
+ const res = await import_set_schedule_entitlement.setScheduleEntitlementTool.execute({
12689
+ apiKey: admin,
12690
+ granteeIdentity: identity,
12691
+ mcpScraperApiKey: user.api_key
12692
+ }, {});
12693
+ return { ok: res.ok, error: res.error };
12694
+ }
12695
+ var import_node_crypto6, import_provision_defaults, import_set_schedule_entitlement, MEMORY_BASE_URL, ADMIN_KEY;
12696
+ var init_memory = __esm({
12697
+ "src/api/memory.ts"() {
12698
+ "use strict";
12699
+ import_node_crypto6 = require("crypto");
12700
+ init_session();
12701
+ init_db();
12702
+ import_provision_defaults = require("mcpscraper-memory-tools/tools/vaults/provision-defaults");
12703
+ import_set_schedule_entitlement = require("mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement");
12704
+ MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
12705
+ ADMIN_KEY = () => process.env.MCP_MEMORY_ADMIN_KEY?.trim() ?? "";
12706
+ }
12707
+ });
12708
+
12287
12709
  // src/api/internal-billing-routes.ts
12288
12710
  function internalRequestAuthorized(header) {
12289
12711
  const expected = process.env.INTERNAL_BILLING_SECRET ?? process.env.INTERNAL_MEMORY_DEBIT_SECRET;
@@ -12307,15 +12729,22 @@ function billingError(error) {
12307
12729
  status: 500
12308
12730
  };
12309
12731
  }
12310
- var import_hono3, import_zod12, internalBillingApp, metadataSchema, authorizeSchema, settleSchema, voidSchema;
12732
+ var import_hono3, import_zod12, internalBillingApp, metadataSchema, schedulingAccessSchema, authorizeSchema, settleSchema, startSchema, voidSchema;
12311
12733
  var init_internal_billing_routes = __esm({
12312
12734
  "src/api/internal-billing-routes.ts"() {
12313
12735
  "use strict";
12314
12736
  import_hono3 = require("hono");
12315
12737
  import_zod12 = require("zod");
12316
12738
  init_unified_billing();
12739
+ init_scheduling_access();
12740
+ init_db();
12741
+ init_memory();
12317
12742
  internalBillingApp = new import_hono3.Hono();
12318
12743
  metadataSchema = import_zod12.z.record(import_zod12.z.unknown()).optional();
12744
+ schedulingAccessSchema = import_zod12.z.object({
12745
+ identity: import_zod12.z.string().min(1).max(320),
12746
+ provisionCredentials: import_zod12.z.boolean().optional()
12747
+ });
12319
12748
  authorizeSchema = import_zod12.z.object({
12320
12749
  identity: import_zod12.z.string().min(1).max(320),
12321
12750
  idempotencyKey: import_zod12.z.string().min(1).max(500),
@@ -12334,6 +12763,12 @@ var init_internal_billing_routes = __esm({
12334
12763
  modelSteps: import_zod12.z.number().finite().nonnegative().optional(),
12335
12764
  metadata: metadataSchema
12336
12765
  });
12766
+ startSchema = import_zod12.z.object({
12767
+ identity: import_zod12.z.string().min(1).max(320),
12768
+ authorizationId: import_zod12.z.string().uuid(),
12769
+ authorizationToken: import_zod12.z.string().min(20).max(500),
12770
+ metadata: metadataSchema
12771
+ });
12337
12772
  voidSchema = import_zod12.z.object({
12338
12773
  identity: import_zod12.z.string().min(1).max(320),
12339
12774
  authorizationId: import_zod12.z.string().uuid(),
@@ -12359,6 +12794,41 @@ var init_internal_billing_routes = __esm({
12359
12794
  return c.json(response.body, response.status);
12360
12795
  }
12361
12796
  });
12797
+ internalBillingApp.post("/scheduling-access", async (c) => {
12798
+ const parsed = schedulingAccessSchema.safeParse(await c.req.json().catch(() => null));
12799
+ if (!parsed.success) {
12800
+ return c.json({ ok: false, error_code: "invalid_request", error: "invalid scheduling access request" }, 400);
12801
+ }
12802
+ try {
12803
+ const access2 = await getSchedulingAccess(parsed.data.identity);
12804
+ if (access2.allowed && parsed.data.provisionCredentials) {
12805
+ const user = await getUserByEmail(parsed.data.identity.trim().toLowerCase());
12806
+ if (!user) {
12807
+ return c.json({ ok: false, error_code: "user_not_found", error: "billing identity was not found" }, 404);
12808
+ }
12809
+ const credentials = await syncScheduledActionCredentials(user);
12810
+ if (!credentials.ok) {
12811
+ return c.json({
12812
+ ok: false,
12813
+ error_code: "schedule_credentials_unavailable",
12814
+ error: credentials.error ?? "Scheduled Actions credentials are unavailable."
12815
+ }, 503);
12816
+ }
12817
+ }
12818
+ return c.json({
12819
+ ok: true,
12820
+ allowed: access2.allowed,
12821
+ subscriptionTier: access2.subscriptionTier,
12822
+ ...access2.allowed ? {} : {
12823
+ error_code: PAID_PLAN_REQUIRED_CODE,
12824
+ error: PAID_PLAN_REQUIRED_MESSAGE
12825
+ }
12826
+ });
12827
+ } catch (error) {
12828
+ const response = billingError(error);
12829
+ return c.json(response.body, response.status);
12830
+ }
12831
+ });
12362
12832
  internalBillingApp.post("/scheduled-run/settle", async (c) => {
12363
12833
  const parsed = settleSchema.safeParse(await c.req.json().catch(() => null));
12364
12834
  if (!parsed.success) {
@@ -12372,6 +12842,19 @@ var init_internal_billing_routes = __esm({
12372
12842
  return c.json(response.body, response.status);
12373
12843
  }
12374
12844
  });
12845
+ internalBillingApp.post("/scheduled-run/start", async (c) => {
12846
+ const parsed = startSchema.safeParse(await c.req.json().catch(() => null));
12847
+ if (!parsed.success) {
12848
+ return c.json({ ok: false, error_code: "invalid_request", error: "invalid scheduled-run start request" }, 400);
12849
+ }
12850
+ try {
12851
+ const start = await startScheduledRun(parsed.data);
12852
+ return c.json({ ok: true, start });
12853
+ } catch (error) {
12854
+ const response = billingError(error);
12855
+ return c.json(response.body, response.status);
12856
+ }
12857
+ });
12375
12858
  internalBillingApp.post("/scheduled-run/void", async (c) => {
12376
12859
  const parsed = voidSchema.safeParse(await c.req.json().catch(() => null));
12377
12860
  if (!parsed.success) {
@@ -12658,6 +13141,7 @@ var init_BrowserDriver = __esm({
12658
13141
  else if (config.headlessMode === "headless") useHeadless = true;
12659
13142
  else useHeadless = costCtx?.forceHeadless === true || process.env.KERNEL_HEADLESS_DISABLED !== "true" && !!costCtx?.op && HEADLESS_OPS.has(costCtx.op);
12660
13143
  this.kernelHeadlessSent = useHeadless ? true : null;
13144
+ if (costCtx?.headlessSentOut) costCtx.headlessSentOut.value = this.kernelHeadlessSent;
12661
13145
  const kernelBrowser = await this.kernelClient.browsers.create({
12662
13146
  stealth: true,
12663
13147
  timeout_seconds: timeoutSeconds,
@@ -14785,7 +15269,7 @@ async function acquireConcurrencyGate(user, operation, options = {}) {
14785
15269
  return { ok: true, lockId: null, active: await countActiveUsageForUser(user.id), limit, operation, reused: true };
14786
15270
  }
14787
15271
  await expireConcurrencyLocksForUser(user.id);
14788
- const lockId = `cl_${(0, import_node_crypto5.randomUUID)().replace(/-/g, "").slice(0, 24)}`;
15272
+ const lockId = `cl_${(0, import_node_crypto7.randomUUID)().replace(/-/g, "").slice(0, 24)}`;
14789
15273
  const res = await getDb().execute({
14790
15274
  sql: `INSERT INTO concurrency_locks (id, user_id, operation, status, expires_at, metadata)
14791
15275
  SELECT ?, ?, ?, 'active', datetime('now', ?), ?
@@ -14839,11 +15323,11 @@ async function extendConcurrencyGate(lockId, ttlSeconds = DEFAULT_LOCK_TTL_SECON
14839
15323
  args: [lockTtlModifier(ttlSeconds), lockId]
14840
15324
  });
14841
15325
  }
14842
- var import_node_crypto5, DEFAULT_LOCK_TTL_SECONDS, DEFAULT_RETRY_AFTER_SECONDS, MAX_LOCK_TTL_SECONDS, schemaReady;
15326
+ var import_node_crypto7, DEFAULT_LOCK_TTL_SECONDS, DEFAULT_RETRY_AFTER_SECONDS, MAX_LOCK_TTL_SECONDS, schemaReady;
14843
15327
  var init_concurrency_gates = __esm({
14844
15328
  "src/api/concurrency-gates.ts"() {
14845
15329
  "use strict";
14846
- import_node_crypto5 = require("crypto");
15330
+ import_node_crypto7 = require("crypto");
14847
15331
  init_db();
14848
15332
  init_rates();
14849
15333
  DEFAULT_LOCK_TTL_SECONDS = 15 * 60;
@@ -17786,194 +18270,6 @@ var init_reddit_routes = __esm({
17786
18270
  }
17787
18271
  });
17788
18272
 
17789
- // src/api/session.ts
17790
- function getSessionSecret() {
17791
- const configured = process.env.SESSION_SECRET?.trim();
17792
- if (configured) return configured;
17793
- if (isProduction()) throw new Error("SESSION_SECRET is not set \u2014 add it to your Vercel env vars (see src/api/env.ts)");
17794
- return "dev-secret-change-me";
17795
- }
17796
- function safeEqualHex(a, b) {
17797
- if (a.length !== b.length) return false;
17798
- try {
17799
- return (0, import_node_crypto6.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
17800
- } catch {
17801
- return false;
17802
- }
17803
- }
17804
- function signSession(userId) {
17805
- const payload = String(userId);
17806
- const sig = (0, import_node_crypto6.createHmac)("sha256", secret()).update(payload).digest("hex");
17807
- return `${payload}.${sig}`;
17808
- }
17809
- function verifySession(token) {
17810
- const dot = token.lastIndexOf(".");
17811
- if (dot === -1) return null;
17812
- const payload = token.slice(0, dot);
17813
- const sig = token.slice(dot + 1);
17814
- const expected = (0, import_node_crypto6.createHmac)("sha256", secret()).update(payload).digest("hex");
17815
- if (!safeEqualHex(sig, expected)) return null;
17816
- const id = parseInt(payload);
17817
- return isNaN(id) ? null : id;
17818
- }
17819
- var import_node_crypto6, isProduction, secret;
17820
- var init_session = __esm({
17821
- "src/api/session.ts"() {
17822
- "use strict";
17823
- import_node_crypto6 = require("crypto");
17824
- isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
17825
- secret = () => getSessionSecret();
17826
- }
17827
- });
17828
-
17829
- // src/api/memory.ts
17830
- function parseMemoryRpcBody(text) {
17831
- const dataLine = text.split("\n").find((line) => line.startsWith("data:"));
17832
- const payload = dataLine ? dataLine.slice(5).trim() : text;
17833
- return JSON.parse(payload);
17834
- }
17835
- function isMemoryOperator(email) {
17836
- if (!email) return false;
17837
- const ops = (process.env.MEMORY_OPERATOR_IDENTITIES ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
17838
- return ops.includes(email.trim().toLowerCase());
17839
- }
17840
- function encKey() {
17841
- return (0, import_node_crypto7.scryptSync)(getSessionSecret(), "mcp-memory-key-v1", 32);
17842
- }
17843
- function encryptMemoryKey(secret2) {
17844
- const iv = (0, import_node_crypto7.randomBytes)(12);
17845
- const cipher = (0, import_node_crypto7.createCipheriv)("aes-256-gcm", encKey(), iv);
17846
- const enc = Buffer.concat([cipher.update(secret2, "utf8"), cipher.final()]);
17847
- const tag = cipher.getAuthTag();
17848
- return `${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
17849
- }
17850
- function decryptMemoryKey(stored) {
17851
- try {
17852
- const [ivB, tagB, dataB] = stored.split(":");
17853
- const decipher = (0, import_node_crypto7.createDecipheriv)("aes-256-gcm", encKey(), Buffer.from(ivB, "base64"));
17854
- decipher.setAuthTag(Buffer.from(tagB, "base64"));
17855
- return Buffer.concat([decipher.update(Buffer.from(dataB, "base64")), decipher.final()]).toString("utf8");
17856
- } catch {
17857
- return null;
17858
- }
17859
- }
17860
- async function memoryCall(toolName, args, userMemoryKey) {
17861
- try {
17862
- const res = await fetch(`${MEMORY_BASE_URL()}/mcp`, {
17863
- method: "POST",
17864
- headers: {
17865
- "content-type": "application/json",
17866
- accept: "application/json, text/event-stream",
17867
- authorization: `Bearer ${userMemoryKey}`
17868
- },
17869
- body: JSON.stringify({
17870
- jsonrpc: "2.0",
17871
- id: 1,
17872
- method: "tools/call",
17873
- params: { name: toolName, arguments: args }
17874
- })
17875
- });
17876
- const text = await res.text();
17877
- if (!res.ok) return { ok: false, error: `memory ${toolName} failed (${res.status})` };
17878
- const body = parseMemoryRpcBody(text);
17879
- if (body.error) return { ok: false, error: body.error.message ?? `memory ${toolName} failed` };
17880
- if (body.result?.structuredContent) return body.result.structuredContent;
17881
- const textContent = body.result?.content?.find((c) => c.type === "text")?.text;
17882
- if (textContent) {
17883
- try {
17884
- return JSON.parse(textContent);
17885
- } catch {
17886
- return { ok: false, error: textContent };
17887
- }
17888
- }
17889
- return { ok: false, error: `memory ${toolName} returned no result` };
17890
- } catch (err) {
17891
- return { ok: false, error: err?.message ?? "memory call failed" };
17892
- }
17893
- }
17894
- function memoryIdentity(user) {
17895
- return user.email;
17896
- }
17897
- function resolveMemoryIdentity(user) {
17898
- return memoryIdentity(user);
17899
- }
17900
- function memoryPlanForReads(user) {
17901
- if (isMemoryOperator(user.email)) return "unlimited";
17902
- if (user.memory_plan === "pro" || user.memory_plan === "team") return user.memory_plan;
17903
- return "free";
17904
- }
17905
- async function provisionMemoryForUser(user) {
17906
- try {
17907
- const { key } = await getOrCreateUserMemoryKey(user);
17908
- if (key) await setMemoryProvisioned(user.id, true);
17909
- } catch {
17910
- }
17911
- }
17912
- function resolveEffectiveMemoryPlan(user) {
17913
- return user.memory_plan === "pro" || user.memory_plan === "team" ? user.memory_plan : "free";
17914
- }
17915
- async function getOrCreateUserMemoryKey(user) {
17916
- const creds = await getUserMemoryCreds(user.id);
17917
- if (creds.memory_key) {
17918
- const decrypted = decryptMemoryKey(creds.memory_key);
17919
- if (decrypted) return { key: decrypted };
17920
- }
17921
- const admin = ADMIN_KEY();
17922
- if (!admin) return { key: null, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
17923
- const identity = memoryIdentity(user);
17924
- const plan = isMemoryOperator(user.email) ? "unlimited" : (await getMemoryPlan(user.id)).plan;
17925
- const provisioned = await import_provision_defaults.provisionDefaultsTool.execute(
17926
- { apiKey: admin, granteeIdentity: identity, issueKey: true, plan },
17927
- {}
17928
- );
17929
- if (!provisioned.ok || !provisioned.secret) {
17930
- return { key: null, error: provisioned.error ?? "failed to provision default vaults" };
17931
- }
17932
- await setUserMemoryCreds(user.id, encryptMemoryKey(provisioned.secret), identity);
17933
- return { key: provisioned.secret };
17934
- }
17935
- async function syncMemoryKeyPlan(user, plan) {
17936
- const effectivePlan = isMemoryOperator(user.email) ? "unlimited" : plan;
17937
- const { key, error } = await getOrCreateUserMemoryKey(user);
17938
- if (!key) return { ok: false, error: error ?? "memory unavailable" };
17939
- const listed = await memoryCall(
17940
- "listKeysTool",
17941
- {},
17942
- key
17943
- );
17944
- if (!listed.ok || !listed.keys?.length) return { ok: false, error: listed.error ?? "no memory keys found" };
17945
- const target = listed.keys[0];
17946
- if (target.plan === effectivePlan) return { ok: true };
17947
- const res = await memoryCall("setScopeTool", { keyId: target.keyId, plan: effectivePlan }, key);
17948
- return { ok: res.ok, error: res.error };
17949
- }
17950
- async function syncScheduleEntitlement(user, enabled, quotaPerPeriod) {
17951
- const admin = ADMIN_KEY();
17952
- if (!admin) return { ok: false, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
17953
- const identity = resolveMemoryIdentity(user);
17954
- const res = await import_set_schedule_entitlement.setScheduleEntitlementTool.execute({
17955
- apiKey: admin,
17956
- granteeIdentity: identity,
17957
- enabled,
17958
- quotaPerPeriod,
17959
- mcpScraperApiKey: user.api_key
17960
- }, {});
17961
- return { ok: res.ok, error: res.error };
17962
- }
17963
- var import_node_crypto7, import_provision_defaults, import_set_schedule_entitlement, MEMORY_BASE_URL, ADMIN_KEY;
17964
- var init_memory = __esm({
17965
- "src/api/memory.ts"() {
17966
- "use strict";
17967
- import_node_crypto7 = require("crypto");
17968
- init_session();
17969
- init_db();
17970
- import_provision_defaults = require("mcpscraper-memory-tools/tools/vaults/provision-defaults");
17971
- import_set_schedule_entitlement = require("mcpscraper-memory-tools/tools/schedule/set-schedule-entitlement");
17972
- MEMORY_BASE_URL = () => (process.env.MCP_MEMORY_BASE_URL ?? "https://memory.mcpscraper.dev").replace(/\/$/, "");
17973
- ADMIN_KEY = () => process.env.MCP_MEMORY_ADMIN_KEY?.trim() ?? "";
17974
- }
17975
- });
17976
-
17977
18273
  // src/api/video-routes.ts
17978
18274
  function invalidRequest5(message) {
17979
18275
  return { error_code: "invalid_request", message };
@@ -28401,7 +28697,7 @@ var PACKAGE_VERSION;
28401
28697
  var init_version = __esm({
28402
28698
  "src/version.ts"() {
28403
28699
  "use strict";
28404
- PACKAGE_VERSION = "0.21.1";
28700
+ PACKAGE_VERSION = "0.21.3";
28405
28701
  }
28406
28702
  });
28407
28703
 
@@ -34324,7 +34620,7 @@ var init_memory_tool_schemas = __esm({
34324
34620
  CreateScheduledActionSchema = {
34325
34621
  id: "create-scheduled-action",
34326
34622
  upstreamName: "createScheduledActionTool",
34327
- description: "Create a Credit-metered scheduled action in agent mode (default) or connection_sync mode. Each execution has a 75-Credit base charge; agent model usage is added at 1.5 times OpenRouter's actual reported cost. Agent mode follows the description and writes a result into the target vault. connection_sync deterministically runs approved read-only tools on bound service connections and ingests their data. Cadence 'once' runs a single time then completes permanently. Requires write access to the target vault.",
34623
+ description: "Create a Credit-metered scheduled action for an active MCP Scraper Starter plan or higher, in agent mode (default) or connection_sync mode. Each execution has a 75-Credit base charge; agent model usage is added at 1.5 times OpenRouter's actual reported cost. Agent mode follows the description and writes a result into the target vault. connection_sync deterministically runs approved read-only tools on bound service connections and ingests their data. Cadence 'once' runs a single time then completes permanently. Requires write access to the target vault.",
34328
34624
  input: {
34329
34625
  description: import_zod36.z.string().min(1).describe("Free-text description of what this action should do each time it runs."),
34330
34626
  vault: import_zod36.z.string().min(1).describe("The vault this action writes its results into. You must already have write access to it."),
@@ -34373,12 +34669,13 @@ var init_memory_tool_schemas = __esm({
34373
34669
  GetScheduleLinkSchema = {
34374
34670
  id: "get-schedule-link",
34375
34671
  upstreamName: "getScheduleLinkTool",
34376
- description: "Get your durable, bookmarkable link to the hosted Scheduled Actions page \u2014 a login-free UI to create, view, edit, pause, resume, and delete scheduled actions. The embedded secret is shown only once, on first call; it cannot be re-shown, only revoked and reissued via revoke-schedule-link.",
34672
+ description: "Get your durable, bookmarkable link to the hosted Scheduled Actions page. Requires an active MCP Scraper Starter plan or higher. The embedded secret is shown only once, on first call; it cannot be re-shown, only revoked and reissued via revoke-schedule-link.",
34377
34673
  input: {},
34378
34674
  output: {
34379
34675
  ok: import_zod36.z.boolean().describe("True on success; false on auth error."),
34380
34676
  url: import_zod36.z.string().optional().describe("The schedule link. Present only the first time a link is minted for this identity."),
34381
34677
  alreadyExists: import_zod36.z.boolean().optional().describe("True when a link already exists and was NOT re-shown. Use revoke-schedule-link then call this again to get a fresh one."),
34678
+ code: import_zod36.z.string().optional().describe("Machine-readable denial code when the link cannot be issued."),
34382
34679
  error: import_zod36.z.string().optional().describe("Human-readable failure reason when ok is false.")
34383
34680
  },
34384
34681
  annotations: {
@@ -34392,7 +34689,7 @@ var init_memory_tool_schemas = __esm({
34392
34689
  GetScheduleStatusSchema = {
34393
34690
  id: "get-schedule-status",
34394
34691
  upstreamName: "getScheduleStatusTool",
34395
- description: "Get the Credit-metered Scheduled Actions policy. Scheduling is included rather than sold as a separate subscription: each execution has a 75-Credit base charge, and agent model usage is billed at 1.5 times OpenRouter's actual reported cost.",
34692
+ description: "Get the Credit-metered Scheduled Actions access and billing policy. Scheduling requires an active MCP Scraper Starter plan or higher but has no separate subscription: each execution has a 75-Credit base charge, and agent model usage is billed at 1.5 times OpenRouter's actual reported cost.",
34396
34693
  input: {},
34397
34694
  output: {
34398
34695
  ok: import_zod36.z.boolean(),
@@ -34403,6 +34700,8 @@ var init_memory_tool_schemas = __esm({
34403
34700
  quotaPerPeriod: import_zod36.z.number().optional(),
34404
34701
  usedThisPeriod: import_zod36.z.number().optional(),
34405
34702
  periodStart: import_zod36.z.string().optional(),
34703
+ defaultTimezone: import_zod36.z.string().nullable().optional().describe("IANA timezone applied to new schedules that name a time without a zone; null means UTC."),
34704
+ code: import_zod36.z.string().optional().describe("Machine-readable reason scheduling is unavailable."),
34406
34705
  error: import_zod36.z.string().optional()
34407
34706
  },
34408
34707
  annotations: {
@@ -34496,6 +34795,7 @@ var init_memory_tool_schemas = __esm({
34496
34795
  output: {
34497
34796
  ok: import_zod36.z.boolean(),
34498
34797
  nextRunAt: import_zod36.z.string().optional(),
34798
+ code: import_zod36.z.string().optional().describe("Machine-readable denial code when the action cannot be resumed."),
34499
34799
  error: import_zod36.z.string().optional()
34500
34800
  },
34501
34801
  annotations: {
@@ -34527,11 +34827,11 @@ var init_memory_tool_schemas = __esm({
34527
34827
  SetScheduleEntitlementSchema = {
34528
34828
  id: "set-schedule-entitlement",
34529
34829
  upstreamName: "setScheduleEntitlementTool",
34530
- description: "Legacy migration and account-recovery control for old Scheduled Actions entitlements. New scheduling is Credit-metered and does not require a separate subscription. This admin-only tool may preserve a legacy API-key delegation or recover historical entitlement rows during migration.",
34830
+ description: "Admin-only scheduled-action credential provisioning and historical-row recovery. Rotate the encrypted delegated MCP Scraper API key without changing legacy enabled/quota fields by omitting those fields. Legacy entitlement values are not runtime access controls; paid-plan access is checked against MCP Scraper directly.",
34531
34831
  input: {
34532
34832
  granteeIdentity: import_zod36.z.string().min(1).describe("Identity whose scheduling entitlement is being set (e.g. an email)."),
34533
- enabled: import_zod36.z.boolean().describe("True to enable scheduled actions for this identity, false on cancel/expire."),
34534
- quotaPerPeriod: import_zod36.z.number().optional().describe("Legacy monthly execution quota retained only for historical-row compatibility; it does not price new Credit-metered runs."),
34833
+ enabled: import_zod36.z.boolean().optional().describe("Historical entitlement value for migration/recovery only. Omit to preserve the stored value."),
34834
+ quotaPerPeriod: import_zod36.z.number().optional().describe("Historical monthly execution quota retained only for migration compatibility. Omit to preserve the stored value."),
34535
34835
  mcpScraperApiKey: import_zod36.z.string().optional().describe("The identity's mcp-scraper API key, stored encrypted, used to reach mcp-scraper tools during scheduled-action execution.")
34536
34836
  },
34537
34837
  output: {
@@ -35269,6 +35569,7 @@ var init_memory_inprocess_executor = __esm({
35269
35569
  "src/mcp/memory-inprocess-executor.ts"() {
35270
35570
  "use strict";
35271
35571
  init_memory();
35572
+ init_scheduling_access();
35272
35573
  CUTOVER = /* @__PURE__ */ new Map();
35273
35574
  HybridMemoryToolExecutor = class {
35274
35575
  constructor(httpExecutor, user) {
@@ -35285,6 +35586,29 @@ var init_memory_inprocess_executor = __esm({
35285
35586
  return this.mkKeyPromise;
35286
35587
  }
35287
35588
  async callMemoryTool(upstreamName, args) {
35589
+ const accessDenied = memoryScheduleAccessPayload(upstreamName, this.user);
35590
+ if (accessDenied) {
35591
+ return {
35592
+ content: [{ type: "text", text: JSON.stringify(accessDenied) }],
35593
+ structuredContent: accessDenied,
35594
+ isError: false
35595
+ };
35596
+ }
35597
+ if (memoryScheduleToolNeedsCredentials(upstreamName)) {
35598
+ const credentials = await syncScheduledActionCredentials(this.user);
35599
+ if (!credentials.ok) {
35600
+ const result = {
35601
+ ok: false,
35602
+ code: "schedule_credentials_unavailable",
35603
+ error: credentials.error ?? "Scheduled Actions credentials are unavailable."
35604
+ };
35605
+ return {
35606
+ content: [{ type: "text", text: JSON.stringify(result) }],
35607
+ structuredContent: result,
35608
+ isError: false
35609
+ };
35610
+ }
35611
+ }
35288
35612
  const handler = CUTOVER.get(upstreamName);
35289
35613
  if (!handler) return this.httpExecutor.callMemoryTool(upstreamName, args);
35290
35614
  const mkKey = await this.resolveMkKey();
@@ -38331,6 +38655,8 @@ var init_stripe_routes = __esm({
38331
38655
  if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
38332
38656
  await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
38333
38657
  await setSubscriptionTier(user.id, tier.tier, tier.concurrency, invoice.subscription ?? user.subscription_id);
38658
+ const credentials = await syncScheduledActionCredentials(user);
38659
+ if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
38334
38660
  }
38335
38661
  }
38336
38662
  }
@@ -38348,15 +38674,6 @@ var init_stripe_routes = __esm({
38348
38674
  }
38349
38675
  return c.json({ received: true });
38350
38676
  }
38351
- if (priceId && priceId in SCHEDULING_PLANS) {
38352
- const sched = SCHEDULING_PLANS[priceId];
38353
- const user2 = await resolveUser(sub.customer, sub.customer_email);
38354
- if (user2) {
38355
- const live = sub.status === "active" || sub.status === "trialing";
38356
- await syncScheduleEntitlement(user2, live, sched.quota_per_period);
38357
- }
38358
- return c.json({ received: true });
38359
- }
38360
38677
  const user = await getUserByStripeCustomerId(sub.customer);
38361
38678
  if (!user) return c.json({ received: true });
38362
38679
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
@@ -38364,6 +38681,10 @@ var init_stripe_routes = __esm({
38364
38681
  const live = sub.status === "active" || sub.status === "trialing";
38365
38682
  const newTier = live ? tier.tier : null;
38366
38683
  await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
38684
+ if (live) {
38685
+ const credentials = await syncScheduledActionCredentials(user);
38686
+ if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
38687
+ }
38367
38688
  } else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
38368
38689
  await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
38369
38690
  await setConcurrencySubId(user.id, sub.id);
@@ -38380,11 +38701,6 @@ var init_stripe_routes = __esm({
38380
38701
  }
38381
38702
  return c.json({ received: true });
38382
38703
  }
38383
- if (priceId && priceId in SCHEDULING_PLANS) {
38384
- const user2 = await getUserByStripeCustomerId(sub.customer);
38385
- if (user2) await syncScheduleEntitlement(user2, false);
38386
- return c.json({ received: true });
38387
- }
38388
38704
  const user = await getUserByStripeCustomerId(sub.customer);
38389
38705
  if (!user) return c.json({ received: true });
38390
38706
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
@@ -42189,8 +42505,9 @@ async function processJob(job) {
42189
42505
  running++;
42190
42506
  try {
42191
42507
  const opts = typeof job.options === "string" ? JSON.parse(job.options) : job.options;
42508
+ const headlessSentOut = { value: null };
42192
42509
  const result = await runWithCostContext(
42193
- { op: opts.serpOnly ? "serp" : "paa", userId: Number(job.user_id) },
42510
+ { op: opts.serpOnly ? "serp" : "paa", userId: Number(job.user_id), headlessSentOut },
42194
42511
  () => harvest({
42195
42512
  ...opts,
42196
42513
  kernelApiKey: browserServiceApiKey(),
@@ -42207,6 +42524,11 @@ async function processJob(job) {
42207
42524
  const diff = opts.billingHoldMc - actualCost;
42208
42525
  if (diff > 0) await creditMc(job.user_id, diff, "paa_refund", "overestimate refund");
42209
42526
  else if (diff < 0) await debitMc(job.user_id, -diff, "paa", opts.query ?? job.query);
42527
+ } else if (opts.serpOnly && typeof opts.billingHoldMc === "number") {
42528
+ const actualCost = headlessSentOut.value === true ? MC_COSTS.serp_headless : MC_COSTS.serp_headful;
42529
+ const diff = opts.billingHoldMc - actualCost;
42530
+ if (diff > 0) await creditMc(job.user_id, diff, "serp_refund", "headless-mode pricing settle");
42531
+ else if (diff < 0) await debitMc(job.user_id, -diff, "serp", opts.query ?? job.query);
42210
42532
  }
42211
42533
  if (job.callback_url) {
42212
42534
  await deliverWebhook(job.callback_url, { job_id: job.id, status: "done", result, attempts });
@@ -42514,7 +42836,7 @@ async function checkHarvestLimits(user, reuseLockId) {
42514
42836
  }
42515
42837
  return null;
42516
42838
  }
42517
- var import_resend2, import_hono24, import_hono25, import_factory6, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth2, adminAuth, sessionAuth, requireIntegrationsTier, app, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
42839
+ var import_resend2, import_hono24, import_hono25, import_factory6, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth2, adminAuth, sessionAuth, requireIntegrationsTier, requirePaidSchedulingTier, app, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
42518
42840
  var init_server = __esm({
42519
42841
  "src/api/server.ts"() {
42520
42842
  "use strict";
@@ -42586,6 +42908,7 @@ var init_server = __esm({
42586
42908
  init_harvest_attempt_events();
42587
42909
  init_session();
42588
42910
  init_memory();
42911
+ init_scheduling_access();
42589
42912
  init_memory_db();
42590
42913
  init_nango_control();
42591
42914
  init_resend_control();
@@ -42645,6 +42968,11 @@ var init_server = __esm({
42645
42968
  }
42646
42969
  return next();
42647
42970
  });
42971
+ requirePaidSchedulingTier = (0, import_factory6.createMiddleware)(async (c, next) => {
42972
+ const user = c.get("user");
42973
+ if (!hasPaidSchedulingAccess(user)) return c.json(paidPlanRequiredPayload(), 403);
42974
+ return next();
42975
+ });
42648
42976
  app = new import_hono24.Hono();
42649
42977
  STRIPE_API_VERSION = "2026-02-25.clover";
42650
42978
  app.use("*", async (c, next) => {
@@ -42799,11 +43127,7 @@ var init_server = __esm({
42799
43127
  return c.json(CATALOG);
42800
43128
  });
42801
43129
  app.get("/rates", (c) => {
42802
- const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
42803
- ...cost,
42804
- ...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
42805
- }));
42806
- return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
43130
+ return c.json(getPublicRatesPayload());
42807
43131
  });
42808
43132
  app.get("/me", async (c) => {
42809
43133
  const token = (0, import_cookie2.getCookie)(c, "session");
@@ -42894,6 +43218,18 @@ var init_server = __esm({
42894
43218
  const user = c.get("user");
42895
43219
  const body = await c.req.json().catch(() => ({}));
42896
43220
  if (!body.toolName) return c.json({ ok: false, error: "toolName is required" }, 400);
43221
+ const accessDenied = memoryScheduleAccessPayload(body.toolName, user);
43222
+ if (accessDenied) return c.json(accessDenied);
43223
+ if (memoryScheduleToolNeedsCredentials(body.toolName)) {
43224
+ const credentials = await syncScheduledActionCredentials(user);
43225
+ if (!credentials.ok) {
43226
+ return c.json({
43227
+ ok: false,
43228
+ code: "schedule_credentials_unavailable",
43229
+ error: credentials.error ?? "Scheduled Actions credentials are unavailable."
43230
+ }, 503);
43231
+ }
43232
+ }
42897
43233
  const { key, error } = await getOrCreateUserMemoryKey(user);
42898
43234
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
42899
43235
  const result = await memoryCall(body.toolName, body.args ?? {}, key);
@@ -43093,7 +43429,7 @@ var init_server = __esm({
43093
43429
  llmCostMultiplier: SCHEDULED_AGENT_LLM_MARKUP
43094
43430
  }, 410);
43095
43431
  });
43096
- app.get("/schedule-status", auth2, async (c) => {
43432
+ app.get("/schedule-status", auth2, requirePaidSchedulingTier, async (c) => {
43097
43433
  try {
43098
43434
  const user = c.get("user");
43099
43435
  const { key, error } = await getOrCreateUserMemoryKey(user);
@@ -43460,9 +43796,13 @@ var init_server = __esm({
43460
43796
  return scheduleConnectionError(c, err, "Unable to run this connection action.");
43461
43797
  }
43462
43798
  });
43463
- app.post("/schedule-link", auth2, async (c) => {
43799
+ app.post("/schedule-link", auth2, requirePaidSchedulingTier, async (c) => {
43464
43800
  try {
43465
43801
  const user = c.get("user");
43802
+ const credentials = await syncScheduledActionCredentials(user);
43803
+ if (!credentials.ok) {
43804
+ return c.json({ ok: false, code: "schedule_credentials_unavailable", error: credentials.error ?? "Scheduled Actions credentials are unavailable." }, 503);
43805
+ }
43466
43806
  const { key, error } = await getOrCreateUserMemoryKey(user);
43467
43807
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
43468
43808
  const r = await memoryCall("getScheduleLinkTool", {}, key);
@@ -43500,8 +43840,12 @@ var init_server = __esm({
43500
43840
  });
43501
43841
  }
43502
43842
  });
43503
- app.post("/schedule-actions", auth2, async (c) => {
43843
+ app.post("/schedule-actions", auth2, requirePaidSchedulingTier, async (c) => {
43504
43844
  const user = c.get("user");
43845
+ const credentials = await syncScheduledActionCredentials(user);
43846
+ if (!credentials.ok) {
43847
+ return c.json({ ok: false, code: "schedule_credentials_unavailable", error: credentials.error ?? "Scheduled Actions credentials are unavailable." }, 503);
43848
+ }
43505
43849
  const { key, error } = await getOrCreateUserMemoryKey(user);
43506
43850
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
43507
43851
  const body = await c.req.json().catch(() => ({}));
@@ -43593,7 +43937,7 @@ var init_server = __esm({
43593
43937
  const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, key);
43594
43938
  return c.json(r);
43595
43939
  });
43596
- app.post("/schedule-actions/:id/resume", auth2, async (c) => {
43940
+ app.post("/schedule-actions/:id/resume", auth2, requirePaidSchedulingTier, async (c) => {
43597
43941
  const user = c.get("user");
43598
43942
  const { key, error } = await getOrCreateUserMemoryKey(user);
43599
43943
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
@@ -43698,7 +44042,7 @@ var init_server = __esm({
43698
44042
  serpOnly: body.serpOnly ?? false,
43699
44043
  pages: Math.min(2, Math.max(1, body.pages ?? 1))
43700
44044
  };
43701
- const harvestCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount2(options.maxQuestions);
44045
+ const harvestCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount2(options.maxQuestions);
43702
44046
  const { ok: harvestOk, balance_mc: harvestBal } = await debitMc(user.id, harvestCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
43703
44047
  if (!harvestOk) return c.json(insufficientBalanceResponse(harvestBal, harvestCost), 402);
43704
44048
  const jobId = await createJob(user.id, options.query, { ...options, billingHoldMc: harvestCost }, body.callback_url);
@@ -43734,7 +44078,7 @@ var init_server = __esm({
43734
44078
  serpOnly: body.serpOnly ?? false,
43735
44079
  pages: Math.min(2, Math.max(1, body.pages ?? 1))
43736
44080
  };
43737
- const syncCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount2(options.maxQuestions);
44081
+ const syncCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount2(options.maxQuestions);
43738
44082
  const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
43739
44083
  if (!syncOk) return c.json(insufficientBalanceResponse(syncBal, syncCost), 402);
43740
44084
  const jobId = await createRunningJob(user.id, options.query, options);
@@ -43744,10 +44088,11 @@ var init_server = __esm({
43744
44088
  c.req.raw.signal,
43745
44089
  AbortSignal.timeout(syncTimeoutMs)
43746
44090
  ]);
44091
+ const headlessSentOut = { value: null };
43747
44092
  try {
43748
44093
  const harvestCtx = currentCostContext();
43749
44094
  const result = await runWithCostContext(
43750
- { ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
44095
+ { ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id, headlessSentOut },
43751
44096
  () => harvest({
43752
44097
  ...options,
43753
44098
  kernelApiKey: browserServiceApiKey(),
@@ -43766,6 +44111,11 @@ var init_server = __esm({
43766
44111
  const diff = syncCost - actualCost;
43767
44112
  if (diff > 0) await creditMc(user.id, diff, LedgerOperation.PAA_REFUND, "overestimate refund");
43768
44113
  else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.PAA, options.query);
44114
+ } else {
44115
+ const actualCost = headlessSentOut.value === true ? MC_COSTS.serp_headless : MC_COSTS.serp_headful;
44116
+ const diff = syncCost - actualCost;
44117
+ if (diff > 0) await creditMc(user.id, diff, LedgerOperation.SERP_REFUND, "headless-mode pricing settle");
44118
+ else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.SERP, options.query);
43769
44119
  }
43770
44120
  return c.json({ job_id: jobId, status: "done", result: sanitizeHarvestResult(result), attempts: sanitizeAttempts(attempts) });
43771
44121
  } catch (err) {