mcp-scraper 0.33.4 → 0.33.6

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +1113 -306
  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 +12 -6
  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.map +1 -1
  15. package/dist/bin/paa-harvest.js +3 -3
  16. package/dist/{chunk-ACBNIIBU.js → chunk-6ZUK4H66.js} +10 -8
  17. package/dist/chunk-6ZUK4H66.js.map +1 -0
  18. package/dist/{chunk-ERORC7AP.js → chunk-A54ELKJK.js} +2 -2
  19. package/dist/{chunk-ICT7DDHL.js → chunk-AIK7WQD5.js} +29 -15
  20. package/dist/chunk-AIK7WQD5.js.map +1 -0
  21. package/dist/{chunk-SDHPQTFM.js → chunk-C2OD4ELT.js} +2 -2
  22. package/dist/chunk-F65IIUHI.js +7 -0
  23. package/dist/chunk-F65IIUHI.js.map +1 -0
  24. package/dist/{chunk-4767BA2O.js → chunk-LKIP7VDN.js} +17 -12
  25. package/dist/chunk-LKIP7VDN.js.map +1 -0
  26. package/dist/{chunk-MTSBI7ZH.js → chunk-WJJTGRK4.js} +2 -2
  27. package/dist/{chunk-3ZZOLQ6A.js → chunk-XL6YIWJ3.js} +3 -3
  28. package/dist/{db-WRILVOFY.js → db-5SZQUI7A.js} +2 -2
  29. package/dist/{extract-bundle-NFFVMUMT.js → extract-bundle-YNXTFCC3.js} +2 -2
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/{server-LNCSY6ME.js → server-SHOYO52X.js} +1036 -269
  33. package/dist/server-SHOYO52X.js.map +1 -0
  34. package/dist/{site-extract-repository-O36SIFI2.js → site-extract-repository-ZDTO7ODZ.js} +4 -4
  35. package/dist/{worker-JU5FX3NE.js → worker-BJ64GCDE.js} +6 -6
  36. package/docs/mcp-tool-manifest.generated.json +3 -3
  37. package/package.json +1 -1
  38. package/dist/chunk-4767BA2O.js.map +0 -1
  39. package/dist/chunk-ACBNIIBU.js.map +0 -1
  40. package/dist/chunk-ICT7DDHL.js.map +0 -1
  41. package/dist/chunk-IQ742H22.js +0 -7
  42. package/dist/chunk-IQ742H22.js.map +0 -1
  43. package/dist/server-LNCSY6ME.js.map +0 -1
  44. /package/dist/{chunk-ERORC7AP.js.map → chunk-A54ELKJK.js.map} +0 -0
  45. /package/dist/{chunk-SDHPQTFM.js.map → chunk-C2OD4ELT.js.map} +0 -0
  46. /package/dist/{chunk-MTSBI7ZH.js.map → chunk-WJJTGRK4.js.map} +0 -0
  47. /package/dist/{chunk-3ZZOLQ6A.js.map → chunk-XL6YIWJ3.js.map} +0 -0
  48. /package/dist/{db-WRILVOFY.js.map → db-5SZQUI7A.js.map} +0 -0
  49. /package/dist/{extract-bundle-NFFVMUMT.js.map → extract-bundle-YNXTFCC3.js.map} +0 -0
  50. /package/dist/{site-extract-repository-O36SIFI2.js.map → site-extract-repository-ZDTO7ODZ.js.map} +0 -0
  51. /package/dist/{worker-JU5FX3NE.js.map → worker-BJ64GCDE.js.map} +0 -0
@@ -3718,7 +3718,7 @@ async function migrate() {
3718
3718
  `);
3719
3719
  applied = { rows: [] };
3720
3720
  }
3721
- if (applied.rows.length > 0) return;
3721
+ if (applied.rows.length > 0 && await hasCurrentCoreSchema(db)) return;
3722
3722
  if (await hasCurrentCoreSchema(db)) {
3723
3723
  await db.execute({
3724
3724
  sql: "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)",
@@ -3907,9 +3907,14 @@ async function migrate() {
3907
3907
  source TEXT NOT NULL,
3908
3908
  stripe_pi TEXT,
3909
3909
  granted_at TEXT NOT NULL DEFAULT (datetime('now')),
3910
- expires_at TEXT NOT NULL
3910
+ expires_at TEXT NOT NULL,
3911
+ never_expires INTEGER NOT NULL DEFAULT 0
3911
3912
  )
3912
3913
  `);
3914
+ try {
3915
+ await db.execute(`ALTER TABLE credit_lots ADD COLUMN never_expires INTEGER NOT NULL DEFAULT 0`);
3916
+ } catch {
3917
+ }
3913
3918
  await db.execute(`CREATE INDEX IF NOT EXISTS credit_lots_user_active ON credit_lots(user_id, expires_at, remaining_mc)`);
3914
3919
  await db.execute(`
3915
3920
  CREATE TABLE IF NOT EXISTS free_credit_refreshes (
@@ -5240,7 +5245,7 @@ async function ensureMigrated(userId) {
5240
5245
  async function drainLots(userId, mc) {
5241
5246
  const db = getDb();
5242
5247
  const lots = await db.execute({
5243
- sql: `SELECT id, remaining_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now') ORDER BY expires_at ASC, granted_at ASC, id ASC`,
5248
+ sql: `SELECT id, remaining_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND (never_expires = 1 OR expires_at > datetime('now')) ORDER BY never_expires ASC, expires_at ASC, granted_at ASC, id ASC`,
5244
5249
  args: [userId]
5245
5250
  });
5246
5251
  let need = mc;
@@ -5256,7 +5261,7 @@ async function drainLots(userId, mc) {
5256
5261
  async function expireOldLots() {
5257
5262
  const db = getDb();
5258
5263
  const lots = await db.execute({
5259
- sql: `SELECT id, user_id, remaining_mc FROM credit_lots WHERE remaining_mc > 0 AND expires_at <= datetime('now') LIMIT 500`
5264
+ sql: `SELECT id, user_id, remaining_mc FROM credit_lots WHERE remaining_mc > 0 AND never_expires = 0 AND expires_at <= datetime('now') LIMIT 500`
5260
5265
  });
5261
5266
  if (lots.rows.length === 0) return { expired_lots: 0, expired_mc: 0 };
5262
5267
  let totalMc = 0;
@@ -5297,9 +5302,15 @@ async function migrateExistingBalancesToLots() {
5297
5302
  }
5298
5303
  return { migrated, total_mc: totalMc };
5299
5304
  }
5300
- async function creditMc(userId, mc, operation, description, stripePaymentIntent) {
5305
+ async function creditMc(userId, mc, operation, description, stripePaymentIntent, options) {
5301
5306
  const db = getDb();
5302
5307
  await ensureMigrated(userId);
5308
+ let neverExpires = options?.neverExpires;
5309
+ if (neverExpires === void 0 && mc > 0) {
5310
+ const tier = await db.execute({ sql: "SELECT subscription_tier FROM users WHERE id = ? LIMIT 1", args: [userId] });
5311
+ neverExpires = String(tier.rows[0]?.subscription_tier ?? "").startsWith("sonic");
5312
+ }
5313
+ const expiry = neverExpires ? "9999-12-31 23:59:59" : null;
5303
5314
  if (stripePaymentIntent) {
5304
5315
  const ledgerInsert = await db.execute({
5305
5316
  sql: "INSERT OR IGNORE INTO ledger (user_id, amount_mc, operation, description, stripe_pi) VALUES (?, ?, ?, ?, ?)",
@@ -5311,7 +5322,7 @@ async function creditMc(userId, mc, operation, description, stripePaymentIntent)
5311
5322
  }
5312
5323
  const stmts2 = [];
5313
5324
  if (mc > 0) {
5314
- stmts2.push({ sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, stripe_pi, expires_at) VALUES (?, ?, ?, ?, ?, datetime('now', ?))`, args: [userId, mc, mc, operation, stripePaymentIntent, CREDIT_LOT_TTL] });
5325
+ stmts2.push({ sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, stripe_pi, expires_at, never_expires) VALUES (?, ?, ?, ?, ?, COALESCE(?, datetime('now', ?)), ?)`, args: [userId, mc, mc, operation, stripePaymentIntent, expiry, CREDIT_LOT_TTL, neverExpires ? 1 : 0] });
5315
5326
  }
5316
5327
  stmts2.push({ sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [mc, userId] });
5317
5328
  await db.batch(stmts2, "write");
@@ -5320,7 +5331,7 @@ async function creditMc(userId, mc, operation, description, stripePaymentIntent)
5320
5331
  }
5321
5332
  const stmts = [];
5322
5333
  if (mc > 0) {
5323
- stmts.push({ sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, stripe_pi, expires_at) VALUES (?, ?, ?, ?, ?, datetime('now', ?))`, args: [userId, mc, mc, operation, null, CREDIT_LOT_TTL] });
5334
+ stmts.push({ sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, stripe_pi, expires_at, never_expires) VALUES (?, ?, ?, ?, ?, COALESCE(?, datetime('now', ?)), ?)`, args: [userId, mc, mc, operation, null, expiry, CREDIT_LOT_TTL, neverExpires ? 1 : 0] });
5324
5335
  }
5325
5336
  stmts.push({ sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [mc, userId] });
5326
5337
  stmts.push({ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description, stripe_pi) VALUES (?, ?, ?, ?, ?)", args: [userId, mc, operation, description ?? null, null] });
@@ -5390,11 +5401,11 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
5390
5401
  SELECT id,
5391
5402
  remaining_mc,
5392
5403
  COALESCE(SUM(remaining_mc) OVER (
5393
- ORDER BY expires_at ASC, granted_at ASC, id ASC
5404
+ ORDER BY never_expires ASC, expires_at ASC, granted_at ASC, id ASC
5394
5405
  ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
5395
5406
  ), 0) AS prior_mc
5396
5407
  FROM credit_lots
5397
- WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')
5408
+ WHERE user_id = ? AND remaining_mc > 0 AND (never_expires = 1 OR expires_at > datetime('now'))
5398
5409
  ), allocation AS (
5399
5410
  SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
5400
5411
  FROM ordered
@@ -5550,14 +5561,16 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
5550
5561
  },
5551
5562
  {
5552
5563
  sql: `INSERT INTO credit_lots
5553
- (user_id, amount_mc, remaining_mc, source, expires_at)
5564
+ (user_id, amount_mc, remaining_mc, source, expires_at, never_expires)
5554
5565
  SELECT e.user_id,
5555
5566
  d.amount_mc - e.amount_mc,
5556
5567
  d.amount_mc - e.amount_mc,
5557
5568
  ?,
5558
- datetime('now', ?)
5569
+ CASE WHEN u.subscription_tier LIKE 'sonic%' THEN '9999-12-31 23:59:59' ELSE datetime('now', ?) END,
5570
+ CASE WHEN u.subscription_tier LIKE 'sonic%' THEN 1 ELSE 0 END
5559
5571
  FROM billing_events e
5560
5572
  JOIN billing_debits d ON d.idempotency_key = ?
5573
+ JOIN users u ON u.id = e.user_id
5561
5574
  WHERE e.idempotency_key = ?
5562
5575
  AND e.status = 'applying'
5563
5576
  AND d.amount_mc > e.amount_mc`,
@@ -5623,7 +5636,7 @@ async function reconcileBalanceMc(userId, knownBalanceMc) {
5623
5636
  const res = await db.execute({
5624
5637
  sql: `SELECT
5625
5638
  COUNT(*) AS lot_count,
5626
- COALESCE(SUM(CASE WHEN remaining_mc > 0 AND expires_at > datetime('now') THEN remaining_mc ELSE 0 END), 0) AS balance_mc
5639
+ COALESCE(SUM(CASE WHEN remaining_mc > 0 AND (never_expires = 1 OR expires_at > datetime('now')) THEN remaining_mc ELSE 0 END), 0) AS balance_mc
5627
5640
  FROM credit_lots WHERE user_id = ?`,
5628
5641
  args: [userId]
5629
5642
  });
@@ -5631,7 +5644,7 @@ async function reconcileBalanceMc(userId, knownBalanceMc) {
5631
5644
  if (Number(res.rows[0]?.lot_count ?? 0) === 0) {
5632
5645
  await ensureMigrated(Number(userId));
5633
5646
  const migrated = await db.execute({
5634
- sql: `SELECT COALESCE(SUM(remaining_mc), 0) AS balance_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')`,
5647
+ sql: `SELECT COALESCE(SUM(remaining_mc), 0) AS balance_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND (never_expires = 1 OR expires_at > datetime('now'))`,
5635
5648
  args: [userId]
5636
5649
  });
5637
5650
  balanceMc = Number(migrated.rows[0]?.balance_mc ?? 0);
@@ -5655,7 +5668,7 @@ var init_db = __esm({
5655
5668
  DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
5656
5669
  _db = null;
5657
5670
  _rateLimitSchemaReady = false;
5658
- CORE_SCHEMA_VERSION = "2026-07-14.1";
5671
+ CORE_SCHEMA_VERSION = "2026-07-22.1";
5659
5672
  CORE_SCHEMA_TABLES = [
5660
5673
  "billing_authorizations",
5661
5674
  "billing_debits",
@@ -5722,6 +5735,7 @@ var init_db = __esm({
5722
5735
  "workflow_webhook_deliveries_run"
5723
5736
  ];
5724
5737
  CORE_SCHEMA_COLUMNS = {
5738
+ credit_lots: ["never_expires"],
5725
5739
  harvest_attempts: ["debug_json"],
5726
5740
  oauth_tokens: ["replaced_by", "rotated_at"],
5727
5741
  users: [
@@ -8984,7 +8998,7 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
8984
8998
  topup_url: topupUrl
8985
8999
  };
8986
9000
  }
8987
- 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, CONNECTED_USAGE_RATE_POLICY_VERSION, CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD, CONNECTED_FUNCTION_RUN_CREDITS, CONNECTED_PROXY_REQUEST_CREDITS, CONNECTED_COMPUTE_CREDITS_PER_SECOND, CONNECTED_FUNCTION_RUN_MC, CONNECTED_PROXY_REQUEST_MC, CONNECTED_COMPUTE_MC_PER_SECOND, CONNECTED_USAGE_RATE_POLICY, 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, 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;
9001
+ 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, CONNECTED_USAGE_RATE_POLICY_VERSION, CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS, CONNECTED_ACTIVE_CONNECTION_MONTHLY_MC, CONNECTED_FUNCTION_RUN_CREDITS, CONNECTED_PROXY_REQUEST_CREDITS, CONNECTED_COMPUTE_CREDITS_PER_SECOND, CONNECTED_FUNCTION_RUN_MC, CONNECTED_PROXY_REQUEST_MC, CONNECTED_COMPUTE_MC_PER_SECOND, CONNECTED_USAGE_RATE_POLICY, 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, 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;
8988
9002
  var init_rates = __esm({
8989
9003
  "src/api/rates.ts"() {
8990
9004
  "use strict";
@@ -9032,8 +9046,9 @@ var init_rates = __esm({
9032
9046
  SCHEDULED_AGENT_LLM_MARKUP_BPS = 15e3;
9033
9047
  SCHEDULED_AGENT_LLM_MARKUP = SCHEDULED_AGENT_LLM_MARKUP_BPS / BILLING_BASIS_POINTS;
9034
9048
  SCHEDULED_RUN_DEFAULT_MAX_CREDITS = 5e3;
9035
- CONNECTED_USAGE_RATE_POLICY_VERSION = "2026-07-15.connected-usage-v1";
9036
- CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD = 3;
9049
+ CONNECTED_USAGE_RATE_POLICY_VERSION = "2026-07-23.connected-usage-v2";
9050
+ CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS = 15e3;
9051
+ CONNECTED_ACTIVE_CONNECTION_MONTHLY_MC = CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS * MC_PER_CREDIT;
9037
9052
  CONNECTED_FUNCTION_RUN_CREDITS = 2;
9038
9053
  CONNECTED_PROXY_REQUEST_CREDITS = 2;
9039
9054
  CONNECTED_COMPUTE_CREDITS_PER_SECOND = 5;
@@ -9041,18 +9056,17 @@ var init_rates = __esm({
9041
9056
  CONNECTED_PROXY_REQUEST_MC = CONNECTED_PROXY_REQUEST_CREDITS * MC_PER_CREDIT;
9042
9057
  CONNECTED_COMPUTE_MC_PER_SECOND = CONNECTED_COMPUTE_CREDITS_PER_SECOND * MC_PER_CREDIT;
9043
9058
  CONNECTED_USAGE_RATE_POLICY = {
9044
- schemaVersion: 1,
9059
+ schemaVersion: 2,
9045
9060
  ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION,
9046
- effectiveAt: "2026-07-15T00:00:00.000Z",
9061
+ effectiveAt: "2026-07-23T00:00:00.000Z",
9047
9062
  provider: "nango",
9048
9063
  entitlement: "paid_plan_add_on",
9049
9064
  connection: {
9050
- billingMode: "flat_recurring_usd",
9051
- amountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
9052
- currency: "usd",
9065
+ billingMode: "flat_recurring_credits",
9066
+ amountCredits: CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS,
9053
9067
  interval: "month",
9054
9068
  unit: "active_connection",
9055
- proration: "subscription_period"
9069
+ proration: "none"
9056
9070
  },
9057
9071
  usage: {
9058
9072
  billingMode: "credits",
@@ -9321,7 +9335,9 @@ var init_rates = __esm({
9321
9335
  SUBSCRIPTION_TIERS = {
9322
9336
  "price_1TmiHRS8aAcsk3TGwmSNfNIa": { tier: "starter", label: "Starter", price_id: "price_1TmiHRS8aAcsk3TGwmSNfNIa", monthly_usd: 12, credits_mc: 8e6, concurrency: 3, intro_coupon: "mcp-starter-1dollar-intro-12" },
9323
9337
  "price_1TrgihS8aAcsk3TG5cglrq4D": { tier: "growth", label: "Growth", price_id: "price_1TrgihS8aAcsk3TG5cglrq4D", monthly_usd: 40, credits_mc: 26666700, concurrency: 10, intro_coupon: null },
9324
- "price_1TrgihS8aAcsk3TG4HnG4gbY": { tier: "scale", label: "Scale", price_id: "price_1TrgihS8aAcsk3TG4HnG4gbY", monthly_usd: 100, credits_mc: 8e7, concurrency: 20, intro_coupon: null }
9338
+ "price_1TrgihS8aAcsk3TG4HnG4gbY": { tier: "scale", label: "Scale", price_id: "price_1TrgihS8aAcsk3TG4HnG4gbY", monthly_usd: 100, credits_mc: 8e7, concurrency: 20, intro_coupon: null },
9339
+ "price_1TwAl2S8aAcsk3TGC8xvpM3S": { tier: "sonic", label: "Sonic Annual", price_id: "price_1TwAl2S8aAcsk3TGC8xvpM3S", monthly_usd: 25, credits_mc: 25e7, concurrency: 3, intro_coupon: null, billing_interval: "year", credits_never_expire: true, private_offer: true },
9340
+ "price_1TwAl3S8aAcsk3TGnORwFd87": { tier: "sonic-memory", label: "Sonic Annual + Memory Pro", price_id: "price_1TwAl3S8aAcsk3TGnORwFd87", monthly_usd: 34.5, credits_mc: 25e7, concurrency: 3, intro_coupon: null, billing_interval: "year", credits_never_expire: true, includes_memory: true, private_offer: true }
9325
9341
  };
9326
9342
  SUBSCRIPTION_TIER_BY_KEY = Object.fromEntries(
9327
9343
  Object.values(SUBSCRIPTION_TIERS).map((t) => [t.tier, t])
@@ -9405,7 +9421,8 @@ var init_rates = __esm({
9405
9421
  DIFF_PAGE_REFUND: "diff_page_refund",
9406
9422
  SITE_AUDIT_LLM: "site_audit_llm",
9407
9423
  SCHEDULED_RUN: "scheduled_run",
9408
- CONNECTED_USAGE: "connected_usage"
9424
+ CONNECTED_USAGE: "connected_usage",
9425
+ CONNECTED_ACCOUNT_FEE: "connected_account_fee"
9409
9426
  };
9410
9427
  MEMORY_AI_MARGIN_MULTIPLE = 3;
9411
9428
  SITE_AUDIT_LLM_MARGIN_MULTIPLE = 1.5;
@@ -19249,12 +19266,117 @@ var init_video_routes = __esm({
19249
19266
  }
19250
19267
  });
19251
19268
 
19252
- // src/extractor/MapsSearchExtractor.ts
19253
- function buildGoogleLocalResultsUrl(input) {
19254
- const query = [input.query, input.location].filter(Boolean).join(" ");
19255
- const params = new URLSearchParams({ q: query, udm: "1", gl: input.gl, hl: input.hl, pws: "0" });
19256
- return `https://www.google.com/search?${params.toString()}`;
19269
+ // src/extractor/GoogleBusinessPanel.ts
19270
+ function clickNamedBusinessCardInBrowser(businessName) {
19271
+ const normalize4 = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
19272
+ const expected = normalize4(businessName);
19273
+ const headings = Array.from(document.querySelectorAll('div[role="heading"], h3')).filter((heading) => normalize4(heading.textContent) === expected);
19274
+ for (const heading of headings) {
19275
+ if (heading.closest('[role="dialog"], dialog')) continue;
19276
+ const directlyClickable = heading.closest(
19277
+ 'button, a, [role="button"], [role="link"], [jsaction]'
19278
+ );
19279
+ if (directlyClickable) {
19280
+ directlyClickable.click();
19281
+ return true;
19282
+ }
19283
+ let target = heading;
19284
+ for (let hop = 0; hop < 8; hop++) {
19285
+ const parent = target.parentElement;
19286
+ if (!parent || parent.querySelectorAll('div[role="heading"], h3').length > 1) break;
19287
+ target = parent;
19288
+ }
19289
+ target.click();
19290
+ return true;
19291
+ }
19292
+ return false;
19293
+ }
19294
+ function clickBusinessPanelRowInBrowser(input) {
19295
+ const normalize4 = (value) => (value ?? "").replace(/\s+/g, " ").trim();
19296
+ const expectedBusiness = normalize4(input.businessName).toLowerCase();
19297
+ const visible = (element) => element.getAttribute("aria-hidden") !== "true" && (element.offsetParent !== null || element.getClientRects().length > 0);
19298
+ const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => visible(dialog) && normalize4(dialog.innerText).toLowerCase().includes(expectedBusiness)).reverse();
19299
+ for (const dialog of dialogs) {
19300
+ const row = Array.from(
19301
+ dialog.querySelectorAll('button, a, [role="button"]')
19302
+ ).find((element) => visible(element) && normalize4(element.innerText).startsWith(input.prefix));
19303
+ if (row) {
19304
+ row.click();
19305
+ return true;
19306
+ }
19307
+ }
19308
+ return false;
19309
+ }
19310
+ function extractOpenBusinessPanelDialogItemsInBrowser(expectedHeading) {
19311
+ const normalize4 = (value) => (value ?? "").trim();
19312
+ const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => dialog.getAttribute("aria-hidden") !== "true").reverse();
19313
+ const matchingDialog = dialogs.find((dialog) => {
19314
+ const heading2 = dialog.querySelector('h1, h2, [role="heading"]');
19315
+ return normalize4(heading2?.innerText || heading2?.textContent).startsWith(expectedHeading);
19316
+ });
19317
+ if (!matchingDialog) return [];
19318
+ const heading = matchingDialog.querySelector('h1, h2, [role="heading"]');
19319
+ const headingLines = new Set(
19320
+ normalize4(heading?.innerText || heading?.textContent).split("\n").map((value) => value.trim()).filter(Boolean)
19321
+ );
19322
+ const seen = /* @__PURE__ */ new Set();
19323
+ const items = [];
19324
+ matchingDialog.querySelectorAll("*").forEach((element) => {
19325
+ if (heading?.contains(element)) return;
19326
+ if (element.children.length > 0) return;
19327
+ const value = normalize4(element.innerText || element.textContent);
19328
+ if (!value || headingLines.has(value) || seen.has(value)) return;
19329
+ if (/^(back|close|done|thanks)$/i.test(value)) return;
19330
+ if (/^serves\s.*following/i.test(value)) return;
19331
+ seen.add(value);
19332
+ items.push(value);
19333
+ });
19334
+ return items;
19335
+ }
19336
+ function closeOpenBusinessPanelDialogInBrowser(expectedHeading) {
19337
+ const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => dialog.getAttribute("aria-hidden") !== "true").reverse();
19338
+ const matchingDialog = dialogs.find((dialog) => {
19339
+ const heading = dialog.querySelector('h1, h2, [role="heading"]');
19340
+ return (heading?.innerText || heading?.textContent || "").trim().startsWith(expectedHeading);
19341
+ });
19342
+ const close = matchingDialog?.querySelector(
19343
+ '[aria-label*="Close" i], button[aria-label*="close" i]'
19344
+ );
19345
+ close?.click();
19346
+ return Boolean(close);
19347
+ }
19348
+ async function clickPanelRowWithRetry(page, businessName, prefix) {
19349
+ const input = { businessName, prefix };
19350
+ if (await page.evaluate(clickBusinessPanelRowInBrowser, input)) return true;
19351
+ await page.waitForTimeout(1500);
19352
+ return page.evaluate(clickBusinessPanelRowInBrowser, input);
19353
+ }
19354
+ async function extractExpandedList(page, businessName, rowPrefix, dialogHeading) {
19355
+ if (!await clickPanelRowWithRetry(page, businessName, rowPrefix)) return [];
19356
+ await page.waitForTimeout(500);
19357
+ const items = await page.evaluate(
19358
+ extractOpenBusinessPanelDialogItemsInBrowser,
19359
+ dialogHeading
19360
+ );
19361
+ await page.evaluate(closeOpenBusinessPanelDialogInBrowser, dialogHeading);
19362
+ await page.waitForTimeout(250);
19363
+ return items;
19364
+ }
19365
+ async function extractExpandedGoogleBusinessPanelServices(page, businessName) {
19366
+ const services = await extractExpandedList(page, businessName, "Services:", "Services");
19367
+ const areasServed = await extractExpandedList(page, businessName, "Serves ", "Areas Served");
19368
+ return { services, areasServed };
19369
+ }
19370
+ function completeCollapsedPanelValues(values) {
19371
+ return values.filter((value) => !/(?:\.{3}|…)\s*$/.test(value.trim()));
19257
19372
  }
19373
+ var init_GoogleBusinessPanel = __esm({
19374
+ "src/extractor/GoogleBusinessPanel.ts"() {
19375
+ "use strict";
19376
+ }
19377
+ });
19378
+
19379
+ // src/extractor/MapsSearchExtractor.ts
19258
19380
  function buildGoogleOrganicSearchUrl(input) {
19259
19381
  const query = [input.query, input.location].filter(Boolean).join(" ");
19260
19382
  const params = new URLSearchParams({ q: query, gl: input.gl, hl: input.hl, pws: "0" });
@@ -19282,6 +19404,7 @@ var init_MapsSearchExtractor = __esm({
19282
19404
  "use strict";
19283
19405
  init_errors();
19284
19406
  init_BrowserDriver();
19407
+ init_GoogleBusinessPanel();
19285
19408
  LOCAL_RESULTS_PAGE_LIMIT = 8;
19286
19409
  LOCAL_RESULTS_WAIT_MS = 1e3;
19287
19410
  MAPS_REDIRECT_BASE = "https://serp-redirector.vercel.app";
@@ -19530,6 +19653,11 @@ var init_MapsSearchExtractor = __esm({
19530
19653
  areasServed
19531
19654
  };
19532
19655
  }, includeServices);
19656
+ if (includeServices) {
19657
+ const expanded = await extractExpandedGoogleBusinessPanelServices(page, card.name);
19658
+ details.services = expanded.services.length > 0 ? expanded.services : completeCollapsedPanelValues(details.services);
19659
+ details.areasServed = expanded.areasServed.length > 0 ? expanded.areasServed : completeCollapsedPanelValues(details.areasServed);
19660
+ }
19533
19661
  await this.closeDialog(page);
19534
19662
  return details;
19535
19663
  }
@@ -19576,12 +19704,15 @@ var init_MapsSearchExtractor = __esm({
19576
19704
  });
19577
19705
 
19578
19706
  // src/extractor/MapsNavigator.ts
19579
- var MapsNavigator;
19707
+ var BUSINESS_PANEL_REDIRECT_BASE, MapsNavigator;
19580
19708
  var init_MapsNavigator = __esm({
19581
19709
  "src/extractor/MapsNavigator.ts"() {
19582
19710
  "use strict";
19583
19711
  init_selectors();
19712
+ init_BrowserDriver();
19713
+ init_GoogleBusinessPanel();
19584
19714
  init_MapsSearchExtractor();
19715
+ BUSINESS_PANEL_REDIRECT_BASE = "https://serp-redirector.vercel.app";
19585
19716
  MapsNavigator = class {
19586
19717
  constructor(page) {
19587
19718
  this.page = page;
@@ -19639,26 +19770,67 @@ var init_MapsNavigator = __esm({
19639
19770
  await this.page.waitForSelector('[role="main"] h2', { timeout: 5e3 }).catch(() => {
19640
19771
  });
19641
19772
  }
19642
- async navigateToSerpBusinessPanel(businessName, location2, gl, hl) {
19643
- const searchUrl = buildGoogleLocalResultsUrl({ query: businessName, location: location2, gl, hl });
19644
- await this.page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch(() => {
19773
+ async navigateToSerpBusinessPanel(businessName, location2, gl, hl, category) {
19774
+ const searchUrl = buildGoogleOrganicSearchUrl({
19775
+ query: category?.trim() || businessName,
19776
+ location: location2,
19777
+ gl,
19778
+ hl
19645
19779
  });
19646
- await this.page.waitForTimeout(1e3);
19647
- const clicked = await this.page.evaluate((name) => {
19648
- const normalized = name.replace(/\s+/g, " ").trim().toLowerCase();
19649
- const card = Array.from(document.querySelectorAll("button")).find(
19650
- (button) => button.querySelector("h3")?.textContent?.replace(/\s+/g, " ").trim().toLowerCase() === normalized
19651
- );
19652
- if (card) {
19653
- card.click();
19654
- return true;
19655
- }
19656
- return false;
19657
- }, businessName);
19658
- if (!clicked) return false;
19659
- await this.page.waitForSelector('dialog, [role="dialog"]', { timeout: 5e3 }).catch(() => {
19780
+ const arrivalUrl = serpArrivalUrl(
19781
+ searchUrl,
19782
+ process.env.SERP_REDIRECT_BASE?.trim() || BUSINESS_PANEL_REDIRECT_BASE
19783
+ );
19784
+ await this.page.goto(arrivalUrl, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch(() => {
19785
+ });
19786
+ await this.page.waitForTimeout(2e3);
19787
+ if (await this.isGoogleChallenge()) return false;
19788
+ await this.waitForNamedBusinessCard(businessName, 5e3);
19789
+ if (await this.page.evaluate(clickNamedBusinessCardInBrowser, businessName)) {
19790
+ return this.waitForBusinessDialog(businessName);
19791
+ }
19792
+ const moreBusinesses = this.page.getByRole("link", { name: /more (businesses|places|results)/i }).or(this.page.getByRole("button", { name: /more (businesses|places|results)/i })).first();
19793
+ if (!await moreBusinesses.isVisible({ timeout: 5e3 }).catch(() => false)) return false;
19794
+ await moreBusinesses.click({ timeout: 1e4 }).catch(() => {
19660
19795
  });
19661
- return this.page.locator('dialog, [role="dialog"]').count().then((count) => count > 0);
19796
+ await this.page.waitForTimeout(3e3);
19797
+ if (await this.isGoogleChallenge()) return false;
19798
+ await this.waitForNamedBusinessCard(businessName, 3e3);
19799
+ if (!await this.page.evaluate(clickNamedBusinessCardInBrowser, businessName)) {
19800
+ const searchBox = this.page.locator('textarea[name="q"]:visible, input[name="q"]:visible').first();
19801
+ if (!await searchBox.isVisible({ timeout: 5e3 }).catch(() => false)) return false;
19802
+ await searchBox.fill(`${businessName} ${location2}`);
19803
+ await searchBox.press("Enter");
19804
+ await this.page.waitForTimeout(3e3);
19805
+ if (await this.isGoogleChallenge()) return false;
19806
+ await this.waitForNamedBusinessCard(businessName, 1e4);
19807
+ if (!await this.page.evaluate(clickNamedBusinessCardInBrowser, businessName)) return false;
19808
+ }
19809
+ return this.waitForBusinessDialog(businessName);
19810
+ }
19811
+ async isGoogleChallenge() {
19812
+ return this.page.evaluate(() => {
19813
+ const text2 = (document.body?.innerText ?? "").slice(0, 2e3);
19814
+ return /unusual traffic|captcha|recaptcha|about this page/i.test(text2) || /\/sorry\//.test(location.href);
19815
+ }).catch(() => true);
19816
+ }
19817
+ async waitForNamedBusinessCard(businessName, timeout) {
19818
+ if (typeof this.page.waitForFunction !== "function") return;
19819
+ await this.page.waitForFunction(
19820
+ (expectedName) => {
19821
+ const normalize4 = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
19822
+ const expected = normalize4(expectedName);
19823
+ return Array.from(document.querySelectorAll('div[role="heading"], h3')).some((element) => normalize4(element.textContent ?? "") === expected);
19824
+ },
19825
+ businessName,
19826
+ { timeout }
19827
+ ).catch(() => void 0);
19828
+ }
19829
+ async waitForBusinessDialog(businessName) {
19830
+ const dialog = this.page.locator('dialog:visible, [role="dialog"]:visible').filter({ hasText: businessName }).first();
19831
+ await dialog.waitFor({ state: "visible", timeout: 1e4 }).catch(() => {
19832
+ });
19833
+ return dialog.isVisible().catch(() => false);
19662
19834
  }
19663
19835
  buildReviewsUrl(placeUrl) {
19664
19836
  const fid = placeUrl.match(/!1s(0x[0-9a-f]+:0x[0-9a-f]+)/i);
@@ -19705,6 +19877,79 @@ var init_MapsNavigator = __esm({
19705
19877
  }
19706
19878
  });
19707
19879
 
19880
+ // src/extractor/GoogleBusinessPanelRotation.ts
19881
+ async function extractGoogleBusinessPanelServicesWithRotation(options) {
19882
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 5);
19883
+ for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
19884
+ const driver = new BrowserDriver();
19885
+ const attemptNumber = attemptIndex + 1;
19886
+ const useDefaultProxy = attemptIndex < 2;
19887
+ const useHeadful = attemptIndex === maxAttempts - 1;
19888
+ try {
19889
+ await driver.launch({
19890
+ headless: useHeadful ? false : options.headless,
19891
+ headlessMode: useHeadful ? "headful" : options.headless ? "headless" : "headful",
19892
+ kernelApiKey: options.kernelApiKey,
19893
+ keepDefaultProxy: useDefaultProxy,
19894
+ proxyMode: "none",
19895
+ viewport: { width: 1280, height: 900 },
19896
+ locale: `${options.hl}-${options.gl.toUpperCase()}`
19897
+ });
19898
+ const page = driver.getPage();
19899
+ const opened = await new MapsNavigator(page).navigateToSerpBusinessPanel(
19900
+ options.businessName,
19901
+ options.location,
19902
+ options.gl,
19903
+ options.hl,
19904
+ options.category
19905
+ );
19906
+ if (!opened) {
19907
+ console.warn(JSON.stringify({
19908
+ event: "maps_place_services_panel_not_opened",
19909
+ attempt_number: attemptNumber,
19910
+ max_attempts: maxAttempts,
19911
+ keep_default_proxy: useDefaultProxy,
19912
+ headless_mode: useHeadful ? "headful" : options.headless ? "headless" : "headful",
19913
+ final_url: page.url()
19914
+ }));
19915
+ continue;
19916
+ }
19917
+ const result = await extractExpandedGoogleBusinessPanelServices(
19918
+ page,
19919
+ options.businessName
19920
+ );
19921
+ return {
19922
+ ...result,
19923
+ opened: true,
19924
+ attempts: attemptNumber
19925
+ };
19926
+ } catch (error) {
19927
+ console.warn(JSON.stringify({
19928
+ event: "maps_place_services_attempt_failed",
19929
+ attempt_number: attemptNumber,
19930
+ max_attempts: maxAttempts,
19931
+ message: error instanceof Error ? error.message : String(error)
19932
+ }));
19933
+ } finally {
19934
+ await driver.close().catch(() => void 0);
19935
+ }
19936
+ }
19937
+ return {
19938
+ services: [],
19939
+ areasServed: [],
19940
+ opened: false,
19941
+ attempts: maxAttempts
19942
+ };
19943
+ }
19944
+ var init_GoogleBusinessPanelRotation = __esm({
19945
+ "src/extractor/GoogleBusinessPanelRotation.ts"() {
19946
+ "use strict";
19947
+ init_BrowserDriver();
19948
+ init_GoogleBusinessPanel();
19949
+ init_MapsNavigator();
19950
+ }
19951
+ });
19952
+
19708
19953
  // src/extractor/MapsReviewCollector.ts
19709
19954
  var REVIEW_SCROLL_BUDGET_MS, REVIEW_SCROLL_STEP_MS, MapsReviewCollector;
19710
19955
  var init_MapsReviewCollector = __esm({
@@ -19790,6 +20035,8 @@ var init_MapsExtractor = __esm({
19790
20035
  "use strict";
19791
20036
  init_schemas3();
19792
20037
  init_selectors();
20038
+ init_GoogleBusinessPanel();
20039
+ init_GoogleBusinessPanelRotation();
19793
20040
  init_MapsNavigator();
19794
20041
  init_MapsReviewCollector();
19795
20042
  import_zod23 = require("zod");
@@ -19851,14 +20098,33 @@ var init_MapsExtractor = __esm({
19851
20098
  let servicesStatus = "not_requested";
19852
20099
  if (options.includeServices) {
19853
20100
  try {
19854
- const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location, options.gl, options.hl);
20101
+ let opened = await navigator.navigateToSerpBusinessPanel(
20102
+ options.businessName,
20103
+ options.location,
20104
+ options.gl,
20105
+ options.hl,
20106
+ overview.category ?? void 0
20107
+ );
19855
20108
  if (opened) {
19856
- const result = await this.extractServicesAndAreasServed(page);
20109
+ const result = await extractExpandedGoogleBusinessPanelServices(page, options.businessName);
19857
20110
  services = result.services;
19858
20111
  areasServed = result.areasServed;
19859
20112
  servicesStatus = services.length > 0 || areasServed.length > 0 ? "collected" : "none_exist";
19860
20113
  } else {
19861
- servicesStatus = "unavailable";
20114
+ await this.driver.close().catch(() => void 0);
20115
+ const result = await extractGoogleBusinessPanelServicesWithRotation({
20116
+ businessName: options.businessName,
20117
+ location: options.location,
20118
+ category: overview.category ?? void 0,
20119
+ gl: options.gl,
20120
+ hl: options.hl,
20121
+ headless: options.headless,
20122
+ kernelApiKey: options.kernelApiKey
20123
+ });
20124
+ opened = result.opened;
20125
+ services = result.services;
20126
+ areasServed = result.areasServed;
20127
+ servicesStatus = opened ? services.length > 0 || areasServed.length > 0 ? "collected" : "none_exist" : "unavailable";
19862
20128
  }
19863
20129
  } catch (err) {
19864
20130
  console.warn("[MapsExtractor] services/areas-served extraction failed:", err instanceof Error ? err.message : String(err));
@@ -20071,18 +20337,6 @@ var init_MapsExtractor = __esm({
20071
20337
  }
20072
20338
  return result.data;
20073
20339
  }
20074
- async extractServicesAndAreasServed(page) {
20075
- return page.evaluate(() => {
20076
- const visible = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).find((dialog) => dialog.offsetParent !== null);
20077
- if (!visible) return { services: [], areasServed: [] };
20078
- const values = Array.from(visible.querySelectorAll("button")).map((button) => button.innerText.replace(/\s+/g, " ").trim()).filter(Boolean);
20079
- const servicesText = values.find((value) => /^services\s*:/i.test(value));
20080
- const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
20081
- const served = values.find((value) => /^serves\b/i.test(value));
20082
- const areasServed = served ? [served.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
20083
- return { services, areasServed };
20084
- });
20085
- }
20086
20340
  };
20087
20341
  }
20088
20342
  });
@@ -29815,7 +30069,7 @@ var PACKAGE_VERSION;
29815
30069
  var init_version = __esm({
29816
30070
  "src/version.ts"() {
29817
30071
  "use strict";
29818
- PACKAGE_VERSION = "0.33.4";
30072
+ PACKAGE_VERSION = "0.33.6";
29819
30073
  }
29820
30074
  });
29821
30075
 
@@ -29845,9 +30099,11 @@ seam is noted so you can chain them.
29845
30099
 
29846
30100
  ## Google Maps
29847
30101
  - Find multiple places/competitors/prospects -> **maps_search** (returns \`results[]\` with name,
29848
- placeUrl, cid).
29849
- - One business deep-dive + reviews -> **maps_place_intel** (takes businessName + location, NOT an id \u2014
29850
- call it directly with a name from a \`maps_search\` result or from the user).
30102
+ placeUrl, cid; set \`includeServices: true\` to enrich each result where available).
30103
+ - One business deep-dive -> **maps_place_intel** (takes businessName + location, NOT an id; returns
30104
+ reviews, full hours, About attributes, entity IDs/CID, and with \`includeServices: true\`, the full
30105
+ configured services and areas-served lists. Call it directly with a name from a \`maps_search\` result
30106
+ or from the user).
29851
30107
 
29852
30108
  ## YouTube
29853
30109
  - Find or list videos -> **youtube_harvest** (returns \`videos[].videoId\`).
@@ -32307,7 +32563,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
32307
32563
  }, async (input) => formatInstagramMediaDownload(await executor.instagramMediaDownload(input), input));
32308
32564
  server.registerTool("maps_place_intel", {
32309
32565
  title: "Google Maps Business Profile Details",
32310
- description: "Extract Google Maps business intelligence for one known/named business: rating, reviews, category, address, phone, hours, entity IDs. Not for category searches or multi-business prospect lists \u2014 use maps_search for those. Split business name from location.",
32566
+ description: "Deep-dive one known/named Google Business Profile: rating, reviews, category, address, phone, full hours, About attributes, entity IDs/CID, and \u2014 with includeServices: true \u2014 the full configured services and areas-served lists. Not for category searches or multi-business prospect lists; use maps_search for those. Split business name from location.",
32311
32567
  inputSchema: MapsPlaceIntelInputSchema,
32312
32568
  outputSchema: recordOutputSchema("maps_place_intel", MapsPlaceIntelOutputSchema),
32313
32569
  annotations: liveWebToolAnnotations("Google Maps Business Profile Details")
@@ -40192,12 +40448,12 @@ function connectedAccountBillingView(actualQuantity, state) {
40192
40448
  const quantity = Math.max(0, Math.round(actualQuantity));
40193
40449
  return {
40194
40450
  provider: "nango",
40195
- billingMode: "flat_recurring_usd_plus_credits",
40451
+ billingMode: "flat_recurring_credits",
40196
40452
  priceId: connectedAccountPriceId(),
40197
- unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
40453
+ unitAmountCredits: CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS,
40198
40454
  interval: "month",
40199
40455
  quantity,
40200
- projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
40456
+ projectedMonthlyCredits: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS,
40201
40457
  status: state?.status ?? "pending",
40202
40458
  lastErrorCode: state?.last_error_code ?? null,
40203
40459
  syncedAt: state?.synced_at ?? null,
@@ -40207,29 +40463,23 @@ function connectedAccountBillingView(actualQuantity, state) {
40207
40463
  function findBasePlanItem(subscription) {
40208
40464
  return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
40209
40465
  }
40210
- function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
40211
- return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
40212
- }
40213
- function liveSubscription(subscription) {
40214
- return subscription.status === "active" || subscription.status === "trialing";
40215
- }
40216
- function safeStripeErrorCode(error) {
40466
+ function safeErrorCode(error) {
40217
40467
  if (error instanceof ConnectedAccountBillingError) return error.code;
40218
40468
  return "stripe_connection_billing_failed";
40219
40469
  }
40220
- async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
40470
+ async function persistFailure(user, quantityToPersist, error) {
40221
40471
  await setConnectedAccountBillingState({
40222
40472
  userId: user.id,
40223
40473
  stripeSubscriptionId: user.subscription_id,
40224
- stripeSubscriptionItemId: subscriptionItemId,
40474
+ stripeSubscriptionItemId: null,
40225
40475
  priceId: connectedAccountPriceId(),
40226
- quantity: actualQuantity,
40476
+ quantity: quantityToPersist,
40227
40477
  status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
40228
- lastErrorCode: safeStripeErrorCode(error),
40478
+ lastErrorCode: safeErrorCode(error),
40229
40479
  synced: false
40230
40480
  });
40231
40481
  }
40232
- async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
40482
+ async function reconcileConnectedAccountBilling(user, actualQuantity) {
40233
40483
  if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
40234
40484
  throw new Error("actualQuantity must be a non-negative safe integer");
40235
40485
  }
@@ -40243,7 +40493,7 @@ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClie
40243
40493
  await persistFailure(user, actualQuantity, error);
40244
40494
  throw error;
40245
40495
  }
40246
- if (!user.subscription_id) {
40496
+ if (!user.subscription_id || !(user.subscription_tier ?? "") || !Object.values(SUBSCRIPTION_TIERS).some((t) => t.tier === user.subscription_tier)) {
40247
40497
  if (actualQuantity === 0) {
40248
40498
  const state = await setConnectedAccountBillingState({
40249
40499
  userId: user.id,
@@ -40265,48 +40515,38 @@ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClie
40265
40515
  await persistFailure(user, actualQuantity, error);
40266
40516
  throw error;
40267
40517
  }
40268
- const client2 = stripeClient ?? (() => {
40269
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
40270
- if (!secret2) {
40271
- throw new ConnectedAccountBillingError(
40272
- "Stripe is not configured.",
40273
- "connected_account_price_not_configured",
40274
- 503
40275
- );
40276
- }
40277
- return new import_stripe.default(secret2, { apiVersion: STRIPE_API_VERSION });
40278
- })();
40279
- let currentItem;
40518
+ const existingState = await getConnectedAccountBillingState(user.id);
40519
+ const currentQuantity = Math.max(0, existingState?.quantity ?? 0);
40280
40520
  try {
40281
- const subscription = await client2.subscriptions.retrieve(user.subscription_id);
40282
- currentItem = findConnectedAccountItem(subscription, priceId);
40283
- if (!liveSubscription(subscription)) {
40284
- throw new ConnectedAccountBillingError(
40285
- "The plan subscription is not active.",
40286
- "subscription_not_active",
40287
- 402
40288
- );
40289
- }
40290
- if (!findBasePlanItem(subscription)) {
40291
- throw new ConnectedAccountBillingError(
40292
- "The base plan item could not be found on this subscription.",
40293
- "subscription_plan_item_missing",
40294
- 409
40521
+ if (actualQuantity > currentQuantity) {
40522
+ const newConnections = actualQuantity - currentQuantity;
40523
+ const feeMc = newConnections * CONNECTED_ACTIVE_CONNECTION_MONTHLY_MC;
40524
+ const debit = await debitMc(
40525
+ user.id,
40526
+ feeMc,
40527
+ LedgerOperation.CONNECTED_ACCOUNT_FEE,
40528
+ `${newConnections} connected account${newConnections === 1 ? "" : "s"} at ${CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS} credits/month`
40295
40529
  );
40296
- }
40297
- const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
40298
- let resultingItem = currentItem;
40299
- if (actualQuantity !== currentQuantity) {
40300
- const updated = await client2.subscriptions.update(subscription.id, {
40301
- items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
40302
- proration_behavior: "create_prorations"
40303
- });
40304
- resultingItem = findConnectedAccountItem(updated, priceId);
40530
+ if (!debit.ok) {
40531
+ const requiredMc = feeMc;
40532
+ const error = new ConnectedAccountBillingError(
40533
+ `Insufficient credits to connect. Balance: ${mcToCredits(debit.balance_mc)} credits. Connecting requires ${mcToCredits(requiredMc)} credits (${mcToCredits(requiredMc - debit.balance_mc)} short). Upgrade your plan or wait for your credits to refresh.`,
40534
+ "insufficient_credits",
40535
+ 402,
40536
+ {
40537
+ balanceCredits: mcToCredits(debit.balance_mc),
40538
+ requiredCredits: mcToCredits(requiredMc),
40539
+ shortByCredits: mcToCredits(Math.max(0, requiredMc - debit.balance_mc))
40540
+ }
40541
+ );
40542
+ await persistFailure(user, currentQuantity, error);
40543
+ throw error;
40544
+ }
40305
40545
  }
40306
40546
  const state = await setConnectedAccountBillingState({
40307
40547
  userId: user.id,
40308
- stripeSubscriptionId: subscription.id,
40309
- stripeSubscriptionItemId: resultingItem?.id ?? null,
40548
+ stripeSubscriptionId: user.subscription_id,
40549
+ stripeSubscriptionItemId: null,
40310
40550
  priceId,
40311
40551
  quantity: actualQuantity,
40312
40552
  status: "synced",
@@ -40315,10 +40555,10 @@ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClie
40315
40555
  });
40316
40556
  return connectedAccountBillingView(actualQuantity, state);
40317
40557
  } catch (error) {
40318
- await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
40319
40558
  if (error instanceof ConnectedAccountBillingError) throw error;
40559
+ await persistFailure(user, currentQuantity, error);
40320
40560
  throw new ConnectedAccountBillingError(
40321
- "Unable to update connected-account billing in Stripe.",
40561
+ "Unable to update connected-account billing.",
40322
40562
  "stripe_connection_billing_failed",
40323
40563
  503
40324
40564
  );
@@ -40327,24 +40567,24 @@ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClie
40327
40567
  async function currentConnectedAccountBillingView(userId, actualQuantity) {
40328
40568
  return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
40329
40569
  }
40330
- var import_stripe, DEFAULT_CONNECTED_ACCOUNT_PRICE_ID, STRIPE_API_VERSION, ConnectedAccountBillingError;
40570
+ var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID, ConnectedAccountBillingError;
40331
40571
  var init_connected_account_billing = __esm({
40332
40572
  "src/api/connected-account-billing.ts"() {
40333
40573
  "use strict";
40334
- import_stripe = __toESM(require("stripe"), 1);
40335
40574
  init_db();
40336
40575
  init_rates();
40337
40576
  DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
40338
- STRIPE_API_VERSION = "2026-02-25.clover";
40339
40577
  ConnectedAccountBillingError = class extends Error {
40340
- constructor(message, code, status = 409) {
40578
+ constructor(message, code, status = 409, details) {
40341
40579
  super(message);
40342
40580
  this.code = code;
40343
40581
  this.status = status;
40582
+ this.details = details;
40344
40583
  this.name = "ConnectedAccountBillingError";
40345
40584
  }
40346
40585
  code;
40347
40586
  status;
40587
+ details;
40348
40588
  };
40349
40589
  }
40350
40590
  });
@@ -40362,17 +40602,17 @@ async function resolveUser2(customerId, emailFallback) {
40362
40602
  if (user) await setStripeCustomerId(user.id, customerId);
40363
40603
  return user;
40364
40604
  }
40365
- var import_stripe2, import_hono19, stripe, stripeApp;
40605
+ var import_stripe, import_hono19, stripe, stripeApp;
40366
40606
  var init_stripe_routes = __esm({
40367
40607
  "src/api/stripe-routes.ts"() {
40368
40608
  "use strict";
40369
- import_stripe2 = __toESM(require("stripe"), 1);
40609
+ import_stripe = __toESM(require("stripe"), 1);
40370
40610
  import_hono19 = require("hono");
40371
40611
  init_db();
40372
40612
  init_rates();
40373
40613
  init_memory();
40374
40614
  init_connected_account_billing();
40375
- stripe = new import_stripe2.default(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
40615
+ stripe = new import_stripe.default(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
40376
40616
  stripeApp = new import_hono19.Hono();
40377
40617
  stripeApp.post("/webhooks", async (c) => {
40378
40618
  const sig = c.req.header("stripe-signature");
@@ -40401,17 +40641,41 @@ var init_stripe_routes = __esm({
40401
40641
  if (tier) {
40402
40642
  const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
40403
40643
  if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
40404
- await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
40644
+ await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id, { neverExpires: tier.credits_never_expire });
40405
40645
  await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
40646
+ if (tier.includes_memory) {
40647
+ const subscriptionId = subId ?? user.subscription_id;
40648
+ await setMemoryPlan(user.id, "pro", subscriptionId ?? null);
40649
+ await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "pro" }));
40650
+ }
40406
40651
  const credentials = await syncScheduledActionCredentials(user);
40407
40652
  if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
40653
+ const connectionState = await getConnectedAccountBillingState(user.id);
40654
+ const activeConnections = Math.max(0, connectionState?.quantity ?? 0);
40655
+ if (activeConnections > 0) {
40656
+ const feeMc = activeConnections * CONNECTED_ACTIVE_CONNECTION_MONTHLY_MC;
40657
+ const feeDescription = `${activeConnections} connected account${activeConnections === 1 ? "" : "s"} renewal at ${CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS} credits/month`;
40658
+ const feeDebit = await debitMcIdempotent(user.id, feeMc, LedgerOperation.CONNECTED_ACCOUNT_FEE, feeDescription, `connection-fee:${invoice.id}`);
40659
+ if (!feeDebit.ok) {
40660
+ await setConnectedAccountBillingState({
40661
+ userId: user.id,
40662
+ stripeSubscriptionId: user.subscription_id,
40663
+ stripeSubscriptionItemId: null,
40664
+ priceId: connectionState?.price_id ?? null,
40665
+ quantity: activeConnections,
40666
+ status: "blocked",
40667
+ lastErrorCode: "insufficient_credits",
40668
+ synced: false
40669
+ });
40670
+ }
40671
+ }
40408
40672
  }
40409
40673
  }
40410
40674
  }
40411
40675
  }
40412
40676
  if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") {
40413
40677
  const sub = event.data.object;
40414
- const priceId = sub.items.data[0]?.price?.id;
40678
+ const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
40415
40679
  if (priceId && priceId in MEMORY_PLANS) {
40416
40680
  const mem = MEMORY_PLANS[priceId];
40417
40681
  const user2 = await resolveUser2(sub.customer, sub.customer_email);
@@ -40430,6 +40694,14 @@ var init_stripe_routes = __esm({
40430
40694
  const live = sub.status === "active" || sub.status === "trialing";
40431
40695
  const newTier = live ? tier.tier : null;
40432
40696
  await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
40697
+ if (tier.includes_memory) {
40698
+ const newMemoryPlan = live ? "pro" : "free";
40699
+ await setMemoryPlan(user.id, newMemoryPlan, live ? sub.id : null);
40700
+ await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: newMemoryPlan }));
40701
+ } else if (user.memory_subscription_id === sub.id) {
40702
+ await setMemoryPlan(user.id, "free", null);
40703
+ await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "free" }));
40704
+ }
40433
40705
  if (live) {
40434
40706
  const credentials = await syncScheduledActionCredentials(user);
40435
40707
  if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
@@ -40441,7 +40713,19 @@ var init_stripe_routes = __esm({
40441
40713
  }
40442
40714
  if (event.type === "customer.subscription.deleted") {
40443
40715
  const sub = event.data.object;
40444
- const priceId = sub.items.data[0]?.price?.id;
40716
+ const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
40717
+ if (priceId && priceId in SUBSCRIPTION_TIERS) {
40718
+ const tier = SUBSCRIPTION_TIERS[priceId];
40719
+ const user2 = await getUserByStripeCustomerId(sub.customer);
40720
+ if (user2) {
40721
+ await setSubscriptionTier(user2.id, null, 0, null);
40722
+ if (tier.includes_memory || user2.memory_subscription_id === sub.id) {
40723
+ await setMemoryPlan(user2.id, "free", null);
40724
+ await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
40725
+ }
40726
+ }
40727
+ return c.json({ received: true });
40728
+ }
40445
40729
  if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
40446
40730
  const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
40447
40731
  if (user2) {
@@ -40452,9 +40736,7 @@ var init_stripe_routes = __esm({
40452
40736
  }
40453
40737
  const user = await getUserByStripeCustomerId(sub.customer);
40454
40738
  if (!user) return c.json({ received: true });
40455
- if (priceId && priceId in SUBSCRIPTION_TIERS) {
40456
- await setSubscriptionTier(user.id, null, 0, null);
40457
- } else if (priceId === CONCURRENCY_PRICE_ID) {
40739
+ if (priceId === CONCURRENCY_PRICE_ID) {
40458
40740
  await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
40459
40741
  await setConcurrencySubId(user.id, null);
40460
40742
  }
@@ -40514,9 +40796,9 @@ function negotiateScope(requested, user, resource) {
40514
40796
  const filtered = wanted.filter((s) => MEMORY_SCOPES.includes(s));
40515
40797
  const base = filtered.length > 0 ? filtered : ["memory.read", "memory.write"];
40516
40798
  const allowed = base.filter((s) => s === "memory.admin" ? operator : true);
40517
- const unique = [...new Set(allowed)];
40518
- if (operator && !unique.includes("memory.admin")) unique.push("memory.admin");
40519
- return unique.join(" ");
40799
+ const unique2 = [...new Set(allowed)];
40800
+ if (operator && !unique2.includes("memory.admin")) unique2.push("memory.admin");
40801
+ return unique2.join(" ");
40520
40802
  }
40521
40803
  async function sessionUser(token) {
40522
40804
  if (!token) return null;
@@ -42289,48 +42571,231 @@ var init_schedule_routes = __esm({
42289
42571
  }
42290
42572
  });
42291
42573
 
42292
- // src/api/resend-inbound-routes.ts
42293
- function classifyInboundRecipients(recipients, ...receivingDomains) {
42294
- const normalized = recipients.map((address) => address.trim().toLowerCase());
42295
- const domains = receivingDomains.map((domain) => domain?.trim().toLowerCase()).filter((domain) => Boolean(domain));
42296
- const supportAddresses = [SUPPORT_ADDRESS, ...domains.map((domain) => `support@${domain}`)];
42297
- const dmarcAddresses = [DMARC_ADDRESS, ...domains.map((domain) => `dmarc@${domain}`)];
42298
- if (supportAddresses.some((address) => normalized.includes(address))) return "support";
42299
- if (dmarcAddresses.some((address) => normalized.includes(address))) return "dmarc";
42300
- return "ignore";
42301
- }
42302
- function cleanLine(value, fallback) {
42303
- const clean2 = value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
42304
- return (clean2 || fallback).slice(0, 240);
42574
+ // src/api/resend-support-thread.ts
42575
+ function cleanLine(value, fallback, max = 240) {
42576
+ const clean2 = String(value ?? "").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
42577
+ return (clean2 || fallback).slice(0, max);
42305
42578
  }
42306
42579
  function quoteUntrusted(value) {
42307
- const clean2 = value.replace(/\0/g, "").trim().slice(0, 16e3);
42580
+ const clean2 = String(value ?? "").replace(/\0/g, "").trim().slice(0, 16e3);
42308
42581
  if (!clean2) return "> _(No plain-text body was available. Review the forwarded email.)_";
42309
42582
  return clean2.split("\n").map((line) => `> ${line}`).join("\n");
42310
42583
  }
42311
- function escapeHtml3(value) {
42312
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
42584
+ function shortHash(value, length = 24) {
42585
+ return (0, import_node_crypto19.createHash)("sha256").update(value).digest("hex").slice(0, length);
42313
42586
  }
42314
- function safeEmailId(value) {
42315
- return value.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100) || "unknown-email";
42587
+ function safePathPart(value) {
42588
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "contact";
42589
+ }
42590
+ function parseMailbox(value) {
42591
+ const raw = cleanLine(value, "", 500);
42592
+ if (!raw) return null;
42593
+ const angle = raw.match(/^(.*?)<([^<>]+)>$/);
42594
+ const candidate = (angle?.[2] ?? raw).trim().toLowerCase();
42595
+ const emailMatch = candidate.match(/[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?/i);
42596
+ if (!emailMatch) return null;
42597
+ const email = emailMatch[0].toLowerCase();
42598
+ const suppliedName = angle?.[1]?.trim().replace(/^["']|["']$/g, "");
42599
+ return { email, name: cleanLine(suppliedName, email) };
42600
+ }
42601
+ function supportSender(email) {
42602
+ return parseMailbox(email.reply_to?.[0]) ?? parseMailbox(email.from);
42603
+ }
42604
+ function normalizeSupportSubject(value) {
42605
+ let subject = cleanLine(value, "(no subject)", 240);
42606
+ for (let i = 0; i < 8; i += 1) {
42607
+ const next = subject.replace(/^\[(?:support|external)\]\s*/i, "").replace(/^(?:re|fw|fwd)\s*:\s*/i, "").trim();
42608
+ if (next === subject) break;
42609
+ subject = next;
42610
+ }
42611
+ return subject || "(no subject)";
42612
+ }
42613
+ function supportThreadToken(customerEmail, normalizedSubject) {
42614
+ return shortHash(`${customerEmail.trim().toLowerCase()}
42615
+ ${normalizeSupportSubject(normalizedSubject).toLowerCase()}`);
42616
+ }
42617
+ function supportThreadAddress(token, receivingDomain) {
42618
+ const safeToken = token.toLowerCase().replace(/[^a-f0-9]/g, "").slice(0, 24);
42619
+ const domain = receivingDomain.trim().toLowerCase();
42620
+ if (!safeToken || !/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(domain)) throw new Error("invalid support thread address");
42621
+ return `support+${safeToken}@${domain}`;
42622
+ }
42623
+ function isAutomatedMailbox(email) {
42624
+ const local = email.toLowerCase().split("@", 1)[0];
42625
+ return /^(?:no-?reply|do-?not-?reply|mailer-daemon|postmaster|dmarc|bounce|notifications?)$/.test(local);
42626
+ }
42627
+ function extractReplyText(value) {
42628
+ const text2 = String(value ?? "").replace(/\0/g, "").trim();
42629
+ if (!text2) return "";
42630
+ const lines = text2.split(/\r?\n/);
42631
+ const kept = [];
42632
+ for (const line of lines) {
42633
+ if (/^On .+ wrote:\s*$/i.test(line.trim())) break;
42634
+ if (/^-{2,}\s*Forwarded message\s*-{2,}$/i.test(line.trim())) break;
42635
+ if (/^From:\s+MCP Scraper Support\b/i.test(line.trim())) break;
42636
+ if (/^>\s?/.test(line)) break;
42637
+ kept.push(line);
42638
+ }
42639
+ return kept.join("\n").trim().slice(0, 16e3);
42640
+ }
42641
+ function personPath(identity) {
42642
+ const local = identity.email.split("@")[0];
42643
+ return `contacts/${safePathPart(identity.name === identity.email ? local : identity.name)}-${shortHash(identity.email, 12)}`;
42644
+ }
42645
+ function issuePath(token) {
42646
+ return `support/threads/${token}`;
42647
+ }
42648
+ function communicationPath(email) {
42649
+ const date = cleanLine(email.created_at, (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10).replace(/[^0-9-]/g, "");
42650
+ const id = email.id.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100) || shortHash(email.message_id || email.id);
42651
+ return `Email/${date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}-${id}`;
42652
+ }
42653
+ function noteRef(vault, path6) {
42654
+ return `${vault}::${path6.replace(/\.md$/i, "")}.md`;
42655
+ }
42656
+ function unique(values) {
42657
+ return [...new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))];
42658
+ }
42659
+ function searchPath(source) {
42660
+ if (!source?.startsWith("note:")) return null;
42661
+ const path6 = source.slice("note:".length).trim();
42662
+ return path6 || null;
42663
+ }
42664
+ async function getNote(memoryKey, vault, path6) {
42665
+ const result = await memoryCall("getTool", { vault, path: path6 }, memoryKey);
42666
+ return result.ok && result.note ? result.note : null;
42667
+ }
42668
+ async function inspectTagVocabulary(memoryKey) {
42669
+ const result = await memoryCall("listTagsTool", {}, memoryKey);
42670
+ if (!result.ok) throw new Error(result.error ?? "tag vocabulary could not be inspected");
42316
42671
  }
42317
- function buildSupportIssueNote(email) {
42318
- const sender = cleanLine(email.from, "unknown sender");
42319
- const subject = cleanLine(email.subject, "(no subject)");
42320
- const receivedAt = cleanLine(email.created_at, (/* @__PURE__ */ new Date()).toISOString());
42321
- const attachments = email.attachments?.length ? email.attachments.map((item) => cleanLine(item.filename ?? "unnamed attachment", "unnamed attachment")).join(", ") : "None";
42322
- const datePrefix = receivedAt.slice(0, 10).replace(/[^0-9-]/g, "") || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
42323
- const path6 = `support/inbound/${datePrefix}-${safeEmailId(email.id)}`;
42324
- const title = `Support email \u2014 ${subject} \u2014 ${sender}`.slice(0, 240);
42325
- const summary = `Inbound MCP Scraper support email from ${sender} about \u201C${subject}\u201D; awaiting review and response.`.slice(0, 500);
42672
+ async function captureNewNote(memoryKey, input) {
42673
+ const existing = await getNote(memoryKey, input.vault, input.path);
42674
+ if (existing) return existing;
42675
+ const prepared = await memoryCall("prepareMemoryWriteTool", {
42676
+ title: input.title,
42677
+ content: input.content,
42678
+ source: input.source,
42679
+ type: input.type,
42680
+ vault: input.vault,
42681
+ tagCandidates: input.tagCandidates,
42682
+ maxLinks: 4
42683
+ }, memoryKey);
42684
+ if (!prepared.ok) throw new Error(prepared.error ?? `${input.vault} memory preparation failed`);
42685
+ const resolvedTags = unique(
42686
+ (prepared.tagResolutions ?? []).filter((item) => item.action !== "omit").map((item) => item.tag ?? item.candidate)
42687
+ );
42688
+ const capture = await memoryCall("memoryCaptureTool", {
42689
+ vault: input.vault,
42690
+ path: input.path,
42691
+ title: input.title,
42692
+ content: input.content,
42693
+ props: { ...input.props, tags: resolvedTags },
42694
+ tagDecisions: input.tagCandidates
42695
+ }, memoryKey);
42696
+ if (!capture.ok && !capture.conflict) {
42697
+ const detail = JSON.stringify(capture);
42698
+ throw new Error(`${capture.error ?? `${input.vault} memory capture failed`}: ${detail.slice(0, 2e3)}`);
42699
+ }
42700
+ const stored = await getNote(memoryKey, input.vault, input.path);
42701
+ if (!stored) throw new Error(`${input.vault} memory capture could not be read back`);
42702
+ return stored;
42703
+ }
42704
+ async function findOrCreatePerson(memoryKey, customer, occurredAt) {
42705
+ const result = await memoryCall("searchTool", {
42706
+ vault: "People",
42707
+ query: customer.email,
42708
+ queries: [
42709
+ customer.email,
42710
+ `contact ${customer.email}`,
42711
+ `support relationship ${customer.email}`
42712
+ ],
42713
+ metadata: { email: customer.email },
42714
+ rerankTopN: 10
42715
+ }, memoryKey);
42716
+ if (!result.ok) throw new Error(result.error ?? "People lookup failed");
42717
+ for (const candidate of result.results ?? []) {
42718
+ const path7 = searchPath(candidate.source);
42719
+ if (!path7 || candidate.vault !== "People") continue;
42720
+ const note = await getNote(memoryKey, "People", path7);
42721
+ if (note && String(note.props?.email ?? "").toLowerCase() === customer.email) return note;
42722
+ }
42723
+ const path6 = personPath(customer);
42724
+ const existing = await getNote(memoryKey, "People", path6);
42725
+ if (existing) return existing;
42726
+ const title = customer.name === customer.email ? customer.email : `${customer.name} \u2014 ${customer.email}`;
42727
+ const summary = `MCP Scraper support contact identified by the verified sender email ${customer.email}.`;
42326
42728
  const content = [
42729
+ "## Identity",
42730
+ "",
42731
+ `- Name: ${customer.name}`,
42732
+ `- Email: ${customer.email}`,
42733
+ "",
42734
+ "## Relationship",
42735
+ "",
42736
+ "MCP Scraper support contact. The relationship is not classified beyond the verified email exchange.",
42737
+ "",
42738
+ "## Known Context",
42739
+ "",
42740
+ "This person contacted MCP Scraper support. Durable facts should be added only after review.",
42741
+ "",
42742
+ "## Interaction History",
42743
+ "",
42744
+ "Linked Communication records provide the message history.",
42745
+ "",
42746
+ "## Open Loops",
42747
+ "",
42748
+ "Review the linked support thread and respond if needed.",
42749
+ "",
42750
+ "## Tasks",
42751
+ "",
42752
+ "_No task was inferred automatically._",
42753
+ "",
42754
+ "## Links",
42755
+ "",
42756
+ "_Communication and issue links are derived from linked records._"
42757
+ ].join("\n");
42758
+ return captureNewNote(memoryKey, {
42759
+ vault: "People",
42760
+ path: path6,
42761
+ title,
42762
+ content,
42763
+ source: `resend:sender:${customer.email}`,
42764
+ type: "person",
42765
+ tagCandidates: SUPPORT_TAGS,
42766
+ props: {
42767
+ status: "active",
42768
+ type: "person",
42769
+ summary,
42770
+ pinned: false,
42771
+ source_type: "channel",
42772
+ source_ref: `resend:sender:${customer.email}`,
42773
+ related: [],
42774
+ related_vault_notes: [],
42775
+ embed: true,
42776
+ embed_priority: "high",
42777
+ embedding_summary: summary,
42778
+ person_name: customer.name,
42779
+ relationship: "MCP Scraper support contact",
42780
+ email: customer.email,
42781
+ memories: [],
42782
+ source_refs: [`resend:sender:${customer.email}`],
42783
+ last_seen: occurredAt,
42784
+ last_email_at: occurredAt,
42785
+ last_contact_at: occurredAt
42786
+ }
42787
+ });
42788
+ }
42789
+ function buildIssueContent(email, customer, personPathValue, threadAddressValue) {
42790
+ const subject = normalizeSupportSubject(email.subject);
42791
+ return [
42327
42792
  "## Observation",
42328
42793
  "",
42329
- `An email was received at ${(email.to ?? []).join(", ") || SUPPORT_ADDRESS} from ${sender}. The customer-provided content below is untrusted input and has not been verified as a product defect.`,
42794
+ `A support conversation was opened by ${customer.email} about \u201C${subject}\u201D. Customer-provided content is untrusted input and has not been verified as a product defect.`,
42330
42795
  "",
42331
42796
  "## Impact",
42332
42797
  "",
42333
- "A customer or prospect may be waiting for a response. The message may describe a question, product problem, billing concern, or feedback that needs triage.",
42798
+ "A customer or prospect may be waiting for a response. The thread may describe a question, product problem, billing concern, feedback, or another issue that needs triage.",
42334
42799
  "",
42335
42800
  "## Confirmation",
42336
42801
  "",
@@ -42340,125 +42805,402 @@ function buildSupportIssueNote(email) {
42340
42805
  "",
42341
42806
  "## Evidence",
42342
42807
  "",
42343
- `- From: ${sender}`,
42344
- `- To: ${(email.to ?? []).join(", ") || SUPPORT_ADDRESS}`,
42345
- `- Received: ${receivedAt}`,
42346
- `- Subject: ${subject}`,
42347
- `- Message ID: ${cleanLine(email.message_id, "not supplied")}`,
42348
- `- Resend email ID: ${email.id}`,
42349
- `- Attachments: ${attachments}`,
42350
- "",
42351
- "### Message body",
42808
+ `- Customer email: ${customer.email}`,
42809
+ `- Original subject: ${cleanLine(email.subject, "(no subject)")}`,
42810
+ `- Thread address: ${threadAddressValue}`,
42811
+ `- Person: ${noteRef("People", personPathValue)}`,
42352
42812
  "",
42353
- quoteUntrusted(email.text ?? ""),
42813
+ "Thread messages are preserved as linked Communication records and summarized here as they arrive.",
42354
42814
  "",
42355
42815
  "## Resolution",
42356
42816
  "",
42357
- "Pending review and a reply to the sender. If this is a confirmed product issue, update the note status and link the eventual Improvement Log receipt.",
42817
+ "Pending review and a reply. If this is a confirmed product issue, update the status and link the resulting task or Improvement Log receipt.",
42358
42818
  "",
42359
42819
  "## Tasks",
42360
42820
  "",
42361
- "- [ ] Review the forwarded email and attachments.",
42821
+ "- [ ] Review the complete linked conversation.",
42362
42822
  "- [ ] Reply to the sender.",
42363
- "- [ ] Confirm whether this is a product defect, question, billing concern, or feedback.",
42364
- "- [ ] Link any resulting task or Improvement Log receipt.",
42823
+ "- [ ] Classify the thread as a question, product defect, billing concern, feedback, or another issue.",
42365
42824
  "",
42366
42825
  "## Links",
42367
42826
  "",
42368
- "_No automatic links were added; support messages should be linked during human triage to avoid false relationships._"
42827
+ `- Person: ${noteRef("People", personPathValue)}`
42369
42828
  ].join("\n");
42370
- return { path: path6, title, content, summary };
42371
- }
42372
- async function captureSupportIssue(email, memoryKey) {
42373
- const note = buildSupportIssueNote(email);
42374
- const tagCandidates = [
42375
- { tag: "customer-support", central: true, reusable: true, description: "Inbound and in-product MCP Scraper customer support requests." },
42376
- { tag: "inbound-email", central: true, reusable: true, description: "Messages received through an inbound email integration." },
42377
- { tag: "product-feedback", central: true, reusable: true, description: "Customer feedback and reported product problems." }
42378
- ];
42379
- const prepared = await memoryCall("prepareMemoryWriteTool", {
42380
- title: note.title,
42381
- content: note.content,
42382
- source: `resend:email:${email.id}`,
42383
- type: "user_friction",
42384
- vault: "Issues",
42385
- tagCandidates,
42386
- maxLinks: 4
42387
- }, memoryKey);
42388
- if (!prepared.ok) throw new Error(prepared.error ?? "memory preparation failed");
42389
- const resolvedTags = (prepared.tagResolutions ?? []).filter((item) => item.action !== "omit").map((item) => item.tag ?? item.candidate).filter((tag) => Boolean(tag));
42390
- const capture = await memoryCall("memoryCaptureTool", {
42829
+ }
42830
+ async function findOrCreateIssue(memoryKey, email, customer, personPathValue, token, threadAddressValue) {
42831
+ const path6 = issuePath(token);
42832
+ const existing = await getNote(memoryKey, "Issues", path6);
42833
+ if (existing) return existing;
42834
+ const subject = normalizeSupportSubject(email.subject);
42835
+ const summary = `Support conversation with ${customer.email} about \u201C${subject}\u201D; full history is linked through Communication records.`;
42836
+ return captureNewNote(memoryKey, {
42391
42837
  vault: "Issues",
42392
- path: note.path,
42393
- title: note.title,
42394
- content: note.content,
42838
+ path: path6,
42839
+ title: `Support thread \u2014 ${subject} \u2014 ${customer.email}`.slice(0, 240),
42840
+ content: buildIssueContent(email, customer, personPathValue, threadAddressValue),
42841
+ source: `resend:thread:${token}`,
42842
+ type: "user_friction",
42843
+ tagCandidates: ISSUE_TAGS,
42395
42844
  props: {
42396
42845
  status: "ai_observed",
42397
- summary: note.summary,
42398
- tags: resolvedTags,
42846
+ type: "user_friction",
42847
+ summary,
42399
42848
  pinned: false,
42400
- source_type: "channel",
42401
- source_ref: `resend:email:${email.id}`,
42849
+ source_type: "thread",
42850
+ source_ref: `resend:thread:${token}`,
42402
42851
  related: [],
42403
- related_vault_notes: [],
42852
+ related_vault_notes: [noteRef("People", personPathValue)],
42404
42853
  embed: true,
42405
42854
  embed_priority: "high",
42406
- embedding_summary: note.summary,
42407
- type: "user_friction",
42855
+ embedding_summary: summary,
42408
42856
  severity: "untriaged",
42409
42857
  system: "MCP Scraper customer support",
42410
42858
  observed_at: email.created_at,
42411
42859
  detected_by: "resend-email-webhook",
42412
- reproducible: false
42860
+ reproducible: false,
42861
+ person_refs: [personPathValue],
42862
+ customer_email: customer.email,
42863
+ customer_name: customer.name,
42864
+ normalized_subject: subject,
42865
+ thread_token: token,
42866
+ thread_address: threadAddressValue,
42867
+ communication_refs: [],
42868
+ message_count: 0,
42869
+ attention_state: "needs_reply"
42870
+ }
42871
+ });
42872
+ }
42873
+ function buildCommunicationContent(email, customer, direction, personPathValue, issuePathValue, parentPath) {
42874
+ const sender = supportSender(email);
42875
+ const body = direction === "outbound" ? extractReplyText(email.text) : email.text;
42876
+ const participants = direction === "inbound" ? `- From: ${sender?.email ?? email.from}
42877
+ - To: MCP Scraper support` : `- From: MCP Scraper support
42878
+ - To: ${customer.email}`;
42879
+ return [
42880
+ "## Summary",
42881
+ "",
42882
+ `${direction === "inbound" ? "Inbound" : "Outbound"} support email in the \u201C${normalizeSupportSubject(email.subject)}\u201D thread.`,
42883
+ "",
42884
+ "## Participants",
42885
+ "",
42886
+ participants,
42887
+ `- Person: ${noteRef("People", personPathValue)}`,
42888
+ "",
42889
+ "## Message",
42890
+ "",
42891
+ quoteUntrusted(body),
42892
+ "",
42893
+ "## Decision",
42894
+ "",
42895
+ "_No decision was inferred automatically._",
42896
+ "",
42897
+ "## Follow Up Tasks",
42898
+ "",
42899
+ direction === "inbound" ? "- [ ] Review and reply." : "- [ ] Wait for the customer response or close the thread after review.",
42900
+ "",
42901
+ "## Links",
42902
+ "",
42903
+ `- Issue: ${noteRef("Issues", issuePathValue)}`,
42904
+ `- Person: ${noteRef("People", personPathValue)}`,
42905
+ ...parentPath ? [`- Previous message: [[${parentPath}]]`] : []
42906
+ ].join("\n");
42907
+ }
42908
+ async function captureCommunication(memoryKey, email, customer, direction, personPathValue, issuePathValue, token, parentPath) {
42909
+ const path6 = communicationPath(email);
42910
+ const existing = await getNote(memoryKey, "Communication", path6);
42911
+ if (existing) return existing;
42912
+ const subject = normalizeSupportSubject(email.subject);
42913
+ const summary = `${direction === "inbound" ? "Inbound message from" : "Outbound reply to"} ${customer.email} in the support thread about \u201C${subject}\u201D.`;
42914
+ const providerMessageRef = cleanLine(email.message_id, email.id, 500);
42915
+ return captureNewNote(memoryKey, {
42916
+ vault: "Communication",
42917
+ path: path6,
42918
+ title: `${direction === "inbound" ? "Inbound" : "Outbound"} email \u2014 ${subject} \u2014 ${customer.email}`.slice(0, 240),
42919
+ content: buildCommunicationContent(email, customer, direction, personPathValue, issuePathValue, parentPath),
42920
+ source: `resend:email:${email.id}`,
42921
+ type: "email",
42922
+ tagCandidates: SUPPORT_TAGS,
42923
+ props: {
42924
+ status: direction === "inbound" ? "inbox" : "responded",
42925
+ type: "email",
42926
+ summary,
42927
+ pinned: false,
42928
+ source_type: "channel",
42929
+ source_ref: `resend:email:${email.id}`,
42930
+ related: parentPath ? [`[[${parentPath}]]`] : [],
42931
+ related_vault_notes: [
42932
+ noteRef("People", personPathValue),
42933
+ noteRef("Issues", issuePathValue)
42934
+ ],
42935
+ embed: true,
42936
+ embed_priority: "high",
42937
+ embedding_summary: summary,
42938
+ channel: "email",
42939
+ participants: [customer.email, "support@updates.mcpscraper.dev"],
42940
+ person_refs: [personPathValue],
42941
+ occurred_at: email.created_at,
42942
+ direction,
42943
+ attention_state: direction === "inbound" ? "needs_reply" : "waiting",
42944
+ provider: "resend",
42945
+ provider_connection_ref: "resend:updates.mcpscraper.dev",
42946
+ provider_message_ref: providerMessageRef,
42947
+ provider_thread_ref: token,
42948
+ thread_ref: token,
42949
+ issue_ref: issuePathValue,
42950
+ sender_email: supportSender(email)?.email ?? email.from,
42951
+ recipient_emails: email.to ?? [],
42952
+ parentMessageId: parentPath,
42953
+ parent_communication_ref: parentPath
42954
+ }
42955
+ });
42956
+ }
42957
+ function appendIssueEvidence(content, email, communicationPathValue, direction) {
42958
+ const marker = `- Resend email ID: ${email.id}`;
42959
+ if (content.includes(marker)) return content;
42960
+ const block = [
42961
+ `### ${direction === "inbound" ? "Inbound" : "Outbound"} email \u2014 ${cleanLine(email.created_at, (/* @__PURE__ */ new Date()).toISOString())}`,
42962
+ "",
42963
+ `- Communication: ${noteRef("Communication", communicationPathValue)}`,
42964
+ `- Subject: ${cleanLine(email.subject, "(no subject)")}`,
42965
+ marker,
42966
+ `- Message ID: ${cleanLine(email.message_id, "not supplied", 500)}`,
42967
+ "",
42968
+ quoteUntrusted(direction === "outbound" ? extractReplyText(email.text) : email.text),
42969
+ ""
42970
+ ].join("\n");
42971
+ const anchor = "\n## Resolution";
42972
+ return content.includes(anchor) ? content.replace(anchor, `
42973
+ ${block}${anchor}`) : `${content}
42974
+
42975
+ ${block}`;
42976
+ }
42977
+ async function updateIssueThread(memoryKey, issuePathValue, email, communicationPathValue, direction, personPathValue) {
42978
+ for (let attempt = 0; attempt < 2; attempt += 1) {
42979
+ const current = await getNote(memoryKey, "Issues", issuePathValue);
42980
+ if (!current) throw new Error("support thread issue disappeared before update");
42981
+ const currentRefs = unique(Array.isArray(current.props?.communication_refs) ? current.props.communication_refs : []);
42982
+ const alreadyLinked = currentRefs.includes(communicationPathValue);
42983
+ const communicationRefs = alreadyLinked ? currentRefs : [...currentRefs, communicationPathValue];
42984
+ const relatedVaultNotes = unique([
42985
+ ...Array.isArray(current.props?.related_vault_notes) ? current.props.related_vault_notes : [],
42986
+ noteRef("People", personPathValue),
42987
+ ...communicationRefs.map((path6) => noteRef("Communication", path6))
42988
+ ]);
42989
+ const result = await memoryCall("putTool", {
42990
+ vault: "Issues",
42991
+ path: issuePathValue,
42992
+ title: current.title,
42993
+ content: appendIssueEvidence(current.content, email, communicationPathValue, direction),
42994
+ baseRevision: current.revision,
42995
+ props: {
42996
+ related_vault_notes: relatedVaultNotes,
42997
+ communication_refs: communicationRefs,
42998
+ last_communication_ref: communicationPathValue,
42999
+ last_message_at: email.created_at,
43000
+ message_count: communicationRefs.length,
43001
+ attention_state: direction === "inbound" ? "needs_reply" : "waiting"
43002
+ }
43003
+ }, memoryKey);
43004
+ if (result.ok) return;
43005
+ if (!result.conflict || attempt === 1) throw new Error(result.error ?? "support thread issue update failed");
43006
+ }
43007
+ }
43008
+ async function getSupportThreadByToken(memoryKey, token) {
43009
+ const safeToken = token.toLowerCase().replace(/[^a-f0-9]/g, "").slice(0, 24);
43010
+ if (safeToken.length !== 24) return null;
43011
+ const path6 = issuePath(safeToken);
43012
+ const issue = await getNote(memoryKey, "Issues", path6);
43013
+ if (!issue) return null;
43014
+ const customerEmail = parseMailbox(String(issue.props?.customer_email ?? ""));
43015
+ const personRefs = Array.isArray(issue.props?.person_refs) ? issue.props.person_refs : [];
43016
+ const personPathValue = personRefs.find((value) => typeof value === "string");
43017
+ const threadAddressValue = String(issue.props?.thread_address ?? "");
43018
+ if (!customerEmail || !personPathValue || !threadAddressValue) return null;
43019
+ return {
43020
+ issue,
43021
+ issuePath: path6,
43022
+ threadToken: safeToken,
43023
+ threadAddress: threadAddressValue,
43024
+ customer: {
43025
+ email: customerEmail.email,
43026
+ name: cleanLine(String(issue.props?.customer_name ?? ""), customerEmail.email)
42413
43027
  },
42414
- tagDecisions: tagCandidates
42415
- }, memoryKey);
42416
- if (capture.ok) return capture.note?.path ?? note.path;
42417
- if (capture.conflict) {
42418
- const existing = await memoryCall("getTool", { vault: "Issues", path: note.path }, memoryKey);
42419
- if (existing.ok) return note.path;
43028
+ personPath: personPathValue,
43029
+ normalizedSubject: cleanLine(String(issue.props?.normalized_subject ?? ""), "(no subject)")
43030
+ };
43031
+ }
43032
+ async function captureSupportMessage(memoryKey, email, options) {
43033
+ await inspectTagVocabulary(memoryKey);
43034
+ const customer = options.customer ?? supportSender(email);
43035
+ if (!customer) throw new Error("support sender does not contain a usable email address");
43036
+ const normalizedSubject = options.existingThread?.normalizedSubject ?? normalizeSupportSubject(email.subject);
43037
+ const token = options.existingThread?.threadToken ?? supportThreadToken(customer.email, normalizedSubject);
43038
+ const threadAddressValue = options.existingThread?.threadAddress ?? supportThreadAddress(token, options.receivingDomain);
43039
+ const person = options.existingThread ? await getNote(memoryKey, "People", options.existingThread.personPath) : await findOrCreatePerson(memoryKey, customer, email.created_at);
43040
+ if (!person) throw new Error("support person could not be resolved");
43041
+ const issue = options.existingThread?.issue ?? await findOrCreateIssue(
43042
+ memoryKey,
43043
+ email,
43044
+ customer,
43045
+ person.path,
43046
+ token,
43047
+ threadAddressValue
43048
+ );
43049
+ const parentPath = typeof issue.props?.last_communication_ref === "string" ? issue.props.last_communication_ref : void 0;
43050
+ const communication = await captureCommunication(
43051
+ memoryKey,
43052
+ email,
43053
+ customer,
43054
+ options.direction,
43055
+ person.path,
43056
+ issue.path,
43057
+ token,
43058
+ parentPath
43059
+ );
43060
+ await updateIssueThread(memoryKey, issue.path, email, communication.path, options.direction, person.path);
43061
+ return {
43062
+ issuePath: issue.path,
43063
+ communicationPath: communication.path,
43064
+ personPath: person.path,
43065
+ parentCommunicationPath: parentPath,
43066
+ threadToken: token,
43067
+ threadAddress: threadAddressValue,
43068
+ customerEmail: customer.email,
43069
+ normalizedSubject,
43070
+ direction: options.direction
43071
+ };
43072
+ }
43073
+ var import_node_crypto19, SUPPORT_TAGS, ISSUE_TAGS;
43074
+ var init_resend_support_thread = __esm({
43075
+ "src/api/resend-support-thread.ts"() {
43076
+ "use strict";
43077
+ import_node_crypto19 = require("crypto");
43078
+ init_memory();
43079
+ SUPPORT_TAGS = [
43080
+ {
43081
+ tag: "support",
43082
+ central: true,
43083
+ reusable: true,
43084
+ description: "Customer support work, context, and follow-up."
43085
+ },
43086
+ {
43087
+ tag: "customer-support",
43088
+ central: true,
43089
+ reusable: true,
43090
+ description: "Inbound and in-product MCP Scraper customer support requests."
43091
+ },
43092
+ {
43093
+ tag: "inbound-email",
43094
+ central: true,
43095
+ reusable: true,
43096
+ description: "Messages received through an inbound email integration."
43097
+ }
43098
+ ];
43099
+ ISSUE_TAGS = [
43100
+ ...SUPPORT_TAGS,
43101
+ {
43102
+ tag: "product-feedback",
43103
+ central: true,
43104
+ reusable: true,
43105
+ description: "Customer feedback and reported product problems."
43106
+ }
43107
+ ];
43108
+ }
43109
+ });
43110
+
43111
+ // src/api/resend-inbound-routes.ts
43112
+ function receivingDomains(values) {
43113
+ return values.map((domain) => domain?.trim().toLowerCase()).filter((domain) => Boolean(domain));
43114
+ }
43115
+ function extractSupportThreadToken(recipients, ...domainsInput) {
43116
+ const domains = receivingDomains(domainsInput);
43117
+ for (const recipient of recipients) {
43118
+ const address = parseMailbox(recipient)?.email ?? recipient.trim().toLowerCase();
43119
+ const match = address.match(/^support\+([a-f0-9]{24})@(.+)$/i);
43120
+ if (match && domains.includes(match[2].toLowerCase())) return match[1].toLowerCase();
42420
43121
  }
42421
- throw new Error(capture.error ?? "memory capture failed");
43122
+ return null;
43123
+ }
43124
+ function classifyInboundRecipients(recipients, ...domainsInput) {
43125
+ const normalized = recipients.map((address) => parseMailbox(address)?.email ?? address.trim().toLowerCase());
43126
+ const domains = receivingDomains(domainsInput);
43127
+ const supportAddresses = [SUPPORT_ADDRESS, ...domains.map((domain) => `support@${domain}`)];
43128
+ const dmarcAddresses = [DMARC_ADDRESS, ...domains.map((domain) => `dmarc@${domain}`)];
43129
+ if (dmarcAddresses.some((address) => normalized.includes(address))) return "dmarc";
43130
+ if (extractSupportThreadToken(recipients, ...domains)) return "support-thread";
43131
+ if (supportAddresses.some((address) => normalized.includes(address))) return "support";
43132
+ return "ignore";
43133
+ }
43134
+ function escapeHtml3(value) {
43135
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
43136
+ }
43137
+ function safeEmailId(value) {
43138
+ return value.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100) || "unknown-email";
42422
43139
  }
42423
- async function forwardReceivedEmail(resend, email, destination, label) {
43140
+ async function receivedAttachments(resend, email) {
42424
43141
  const attachmentResult = await resend.emails.receiving.attachments.list({ emailId: email.id });
42425
43142
  if (attachmentResult.error) throw new Error(`attachment retrieval failed: ${attachmentResult.error.message}`);
42426
- const attachments = (attachmentResult.data?.data ?? []).filter((item) => item.content_disposition !== "inline").map((item) => ({
43143
+ return (attachmentResult.data?.data ?? []).filter((item) => item.content_disposition !== "inline").map((item) => ({
42427
43144
  path: item.download_url,
42428
43145
  filename: item.filename ?? "attachment",
42429
43146
  contentId: item.content_id
42430
43147
  }));
42431
- const sender = email.reply_to?.[0] || email.from;
42432
- const subject = cleanLine(email.subject, "(no subject)");
43148
+ }
43149
+ async function forwardSupportEmail(resend, email, destination, threadAddress) {
43150
+ const attachments = await receivedAttachments(resend, email);
43151
+ const sender = supportSender(email);
43152
+ const subject = normalizeSupportSubject(email.subject);
42433
43153
  const headerText = [
42434
- `${label} message received at ${(email.to ?? []).join(", ")}`,
43154
+ `Support message received at ${(email.to ?? []).join(", ")}`,
42435
43155
  `From: ${email.from}`,
42436
- `Reply to: ${sender}`,
43156
+ `Customer email: ${sender?.email ?? email.from}`,
42437
43157
  `Received: ${email.created_at}`,
43158
+ `Thread reply address: ${threadAddress}`,
42438
43159
  ""
42439
43160
  ].join("\n");
42440
43161
  const headerHtml = [
42441
43162
  '<div style="font-family:Arial,sans-serif;font-size:13px;line-height:1.5;color:#5f5a54;border-bottom:1px solid #e9e4db;padding-bottom:12px;margin-bottom:18px">',
42442
- `<strong>${label} message received</strong><br>`,
43163
+ "<strong>Support message received</strong><br>",
42443
43164
  `From: ${escapeHtml3(email.from)}<br>`,
42444
- `Reply to: ${escapeHtml3(sender)}<br>`,
42445
- `Received: ${escapeHtml3(email.created_at)}`,
43165
+ `Customer email: ${escapeHtml3(sender?.email ?? email.from)}<br>`,
43166
+ `Received: ${escapeHtml3(email.created_at)}<br>`,
43167
+ `Thread reply address: ${escapeHtml3(threadAddress)}`,
42446
43168
  "</div>"
42447
43169
  ].join("");
42448
43170
  const sent = await resend.emails.send({
42449
43171
  from: FORWARD_FROM,
42450
43172
  to: destination,
42451
- replyTo: sender,
42452
- subject: `[${label}] ${subject}`,
43173
+ replyTo: threadAddress,
43174
+ subject: `[Support] ${subject}`,
42453
43175
  text: `${headerText}${email.text || "(HTML-only message; view the HTML version.)"}`,
42454
43176
  html: email.html ? `${headerHtml}${email.html}` : void 0,
42455
43177
  attachments: attachments.length ? attachments : void 0,
43178
+ headers: { "X-MCP-Support-Thread": threadAddress },
43179
+ tags: [
43180
+ { name: "category", value: "support_forward" },
43181
+ { name: "inbound_id", value: safeEmailId(email.id) }
43182
+ ]
43183
+ }, { idempotencyKey: `mcp-inbound-support-${safeEmailId(email.id)}` });
43184
+ if (sent.error) throw new Error(`support forward failed: ${sent.error.message}`);
43185
+ return sent.data.id;
43186
+ }
43187
+ async function relayOperatorReply(resend, email, customerEmail, threadAddress, normalizedSubject) {
43188
+ const attachments = await receivedAttachments(resend, email);
43189
+ const replyText = extractReplyText(email.text) || "(No plain-text reply was available.)";
43190
+ const sent = await resend.emails.send({
43191
+ from: FORWARD_FROM,
43192
+ to: customerEmail,
43193
+ replyTo: threadAddress,
43194
+ subject: `Re: ${normalizeSupportSubject(normalizedSubject)}`,
43195
+ text: replyText,
43196
+ attachments: attachments.length ? attachments : void 0,
43197
+ headers: { "X-MCP-Support-Thread": threadAddress },
42456
43198
  tags: [
42457
- { name: "category", value: label === "Support" ? "support_forward" : "dmarc_report" },
43199
+ { name: "category", value: "support_reply" },
42458
43200
  { name: "inbound_id", value: safeEmailId(email.id) }
42459
43201
  ]
42460
- }, { idempotencyKey: `mcp-inbound-${label.toLowerCase()}-${safeEmailId(email.id)}` });
42461
- if (sent.error) throw new Error(`forward failed: ${sent.error.message}`);
43202
+ }, { idempotencyKey: `mcp-support-reply-${safeEmailId(email.id)}` });
43203
+ if (sent.error) throw new Error(`support reply relay failed: ${sent.error.message}`);
42462
43204
  return sent.data.id;
42463
43205
  }
42464
43206
  var import_hono24, import_resend, SUPPORT_ADDRESS, DMARC_ADDRESS, FORWARD_FROM, resendInboundApp;
@@ -42467,7 +43209,7 @@ var init_resend_inbound_routes = __esm({
42467
43209
  "use strict";
42468
43210
  import_hono24 = require("hono");
42469
43211
  import_resend = require("resend");
42470
- init_memory();
43212
+ init_resend_support_thread();
42471
43213
  SUPPORT_ADDRESS = "support@mcpscraper.dev";
42472
43214
  DMARC_ADDRESS = "dmarc@mcpscraper.dev";
42473
43215
  FORWARD_FROM = "MCP Scraper Support <support@updates.mcpscraper.dev>";
@@ -42494,32 +43236,99 @@ var init_resend_inbound_routes = __esm({
42494
43236
  }
42495
43237
  if (event.type !== "email.received") return c.json({ ok: true, ignored: true });
42496
43238
  const receivedEvent = event;
42497
- const route = classifyInboundRecipients(
42498
- receivedEvent.data.to ?? [],
43239
+ const domains = [
42499
43240
  process.env.RESEND_RECEIVING_DOMAIN,
42500
43241
  process.env.RESEND_MANAGED_RECEIVING_DOMAIN
42501
- );
42502
- if (route === "ignore") return c.json({ ok: true, ignored: true });
43242
+ ];
43243
+ const route = classifyInboundRecipients(receivedEvent.data.to ?? [], ...domains);
43244
+ if (route === "ignore") return c.json({ ok: true, route, ignored: true });
43245
+ if (route === "dmarc") {
43246
+ return c.json({ ok: true, route, ignored: true, reason: "dmarc-report-suppressed" });
43247
+ }
42503
43248
  const received = await resend.emails.receiving.get(receivedEvent.data.email_id);
42504
43249
  if (received.error || !received.data) {
42505
43250
  console.error("[resend/inbound] could not retrieve received email", receivedEvent.data.email_id);
42506
43251
  return c.json({ error: "received email could not be retrieved" }, 502);
42507
43252
  }
43253
+ const destination = process.env.SUPPORT_FORWARD_TO?.trim();
43254
+ const memoryKey = process.env.SUPPORT_INBOX_MEMORY_KEY?.trim();
43255
+ const receivingDomain = process.env.RESEND_RECEIVING_DOMAIN?.trim();
43256
+ if (!destination || !memoryKey || !receivingDomain) {
43257
+ return c.json({ error: "support routing is not configured" }, 503);
43258
+ }
42508
43259
  try {
43260
+ const email = received.data;
43261
+ const sender = supportSender(email);
43262
+ if (!sender || isAutomatedMailbox(sender.email)) {
43263
+ return c.json({ ok: true, route, ignored: true, reason: "automated-or-unusable-sender" });
43264
+ }
42509
43265
  if (route === "support") {
42510
- const destination2 = process.env.SUPPORT_FORWARD_TO?.trim();
42511
- const memoryKey = process.env.SUPPORT_INBOX_MEMORY_KEY?.trim();
42512
- if (!destination2 || !memoryKey) return c.json({ error: "support routing is not configured" }, 503);
42513
- const [forwardedId2, memoryPath] = await Promise.all([
42514
- forwardReceivedEmail(resend, received.data, destination2, "Support"),
42515
- captureSupportIssue(received.data, memoryKey)
42516
- ]);
42517
- return c.json({ ok: true, route, forwardedId: forwardedId2, memoryPath });
43266
+ const captured = await captureSupportMessage(memoryKey, email, {
43267
+ receivingDomain,
43268
+ direction: "inbound",
43269
+ customer: sender
43270
+ });
43271
+ const forwardedId = await forwardSupportEmail(resend, received.data, destination, captured.threadAddress);
43272
+ return c.json({
43273
+ ok: true,
43274
+ route,
43275
+ direction: "inbound",
43276
+ forwardedId,
43277
+ issuePath: captured.issuePath,
43278
+ communicationPath: captured.communicationPath,
43279
+ personPath: captured.personPath,
43280
+ threadAddress: captured.threadAddress
43281
+ });
42518
43282
  }
42519
- const destination = process.env.DMARC_FORWARD_TO?.trim() || process.env.SUPPORT_FORWARD_TO?.trim();
42520
- if (!destination) return c.json({ error: "DMARC routing is not configured" }, 503);
42521
- const forwardedId = await forwardReceivedEmail(resend, received.data, destination, "DMARC");
42522
- return c.json({ ok: true, route, forwardedId });
43283
+ const token = extractSupportThreadToken(receivedEvent.data.to ?? [], ...domains);
43284
+ const thread = token ? await getSupportThreadByToken(memoryKey, token) : null;
43285
+ if (!thread) return c.json({ ok: true, route, ignored: true, reason: "unknown-support-thread" });
43286
+ const operator = parseMailbox(destination);
43287
+ if (operator && sender.email === operator.email) {
43288
+ const captured = await captureSupportMessage(memoryKey, email, {
43289
+ receivingDomain,
43290
+ direction: "outbound",
43291
+ customer: thread.customer,
43292
+ existingThread: thread
43293
+ });
43294
+ const relayedId = await relayOperatorReply(
43295
+ resend,
43296
+ received.data,
43297
+ thread.customer.email,
43298
+ captured.threadAddress,
43299
+ thread.normalizedSubject
43300
+ );
43301
+ return c.json({
43302
+ ok: true,
43303
+ route,
43304
+ direction: "outbound",
43305
+ relayedId,
43306
+ issuePath: captured.issuePath,
43307
+ communicationPath: captured.communicationPath,
43308
+ personPath: captured.personPath,
43309
+ threadAddress: captured.threadAddress
43310
+ });
43311
+ }
43312
+ if (sender.email === thread.customer.email) {
43313
+ const captured = await captureSupportMessage(memoryKey, email, {
43314
+ receivingDomain,
43315
+ direction: "inbound",
43316
+ customer: thread.customer,
43317
+ existingThread: thread
43318
+ });
43319
+ const forwardedId = await forwardSupportEmail(resend, received.data, destination, captured.threadAddress);
43320
+ return c.json({
43321
+ ok: true,
43322
+ route,
43323
+ direction: "inbound",
43324
+ forwardedId,
43325
+ issuePath: captured.issuePath,
43326
+ communicationPath: captured.communicationPath,
43327
+ personPath: captured.personPath,
43328
+ threadAddress: captured.threadAddress
43329
+ });
43330
+ }
43331
+ return c.json({ ok: true, route, ignored: true, reason: "unrecognized-thread-sender" });
42523
43332
  } catch (err) {
42524
43333
  console.error("[resend/inbound] processing failed", receivedEvent.data.email_id, err instanceof Error ? err.message : String(err));
42525
43334
  return c.json({ error: "inbound email processing failed" }, 502);
@@ -42572,7 +43381,7 @@ var init_site_audit_worker = __esm({
42572
43381
 
42573
43382
  // src/api/page-diff.ts
42574
43383
  function sha256Hex(value) {
42575
- return (0, import_node_crypto19.createHash)("sha256").update(value).digest("hex");
43384
+ return (0, import_node_crypto20.createHash)("sha256").update(value).digest("hex");
42576
43385
  }
42577
43386
  function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
42578
43387
  if (value.length <= maxChars) return { value, truncated: false };
@@ -42629,11 +43438,11 @@ function diffPageContent(oldContent, newContent) {
42629
43438
  totalChangedLineCount
42630
43439
  };
42631
43440
  }
42632
- var import_node_crypto19, import_diff, MAX_SNAPSHOT_CONTENT_CHARS, MAX_DIFF_HUNKS, MAX_DIFF_LINES_PER_RESPONSE;
43441
+ var import_node_crypto20, import_diff, MAX_SNAPSHOT_CONTENT_CHARS, MAX_DIFF_HUNKS, MAX_DIFF_LINES_PER_RESPONSE;
42633
43442
  var init_page_diff = __esm({
42634
43443
  "src/api/page-diff.ts"() {
42635
43444
  "use strict";
42636
- import_node_crypto19 = require("crypto");
43445
+ import_node_crypto20 = require("crypto");
42637
43446
  import_diff = require("diff");
42638
43447
  MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
42639
43448
  MAX_DIFF_HUNKS = 200;
@@ -42710,7 +43519,7 @@ function canonicalJson(value) {
42710
43519
  return JSON.stringify(value);
42711
43520
  }
42712
43521
  function sha2562(value) {
42713
- return (0, import_node_crypto20.createHash)("sha256").update(value).digest("hex");
43522
+ return (0, import_node_crypto21.createHash)("sha256").update(value).digest("hex");
42714
43523
  }
42715
43524
  function sensitiveKey(key) {
42716
43525
  const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -42925,11 +43734,11 @@ async function importServiceConnectionToMemory(identity, input, dependencies) {
42925
43734
  ...!searchReady ? { warning: "The snapshot was stored, but no search chunks were indexed yet." } : {}
42926
43735
  };
42927
43736
  }
42928
- var import_node_crypto20, CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES, CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES, CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS, CONNECTION_MEMORY_IMPORT_MAX_DEPTH, ConnectionMemoryImportError;
43737
+ var import_node_crypto21, CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES, CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES, CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS, CONNECTION_MEMORY_IMPORT_MAX_DEPTH, ConnectionMemoryImportError;
42929
43738
  var init_connection_memory_import = __esm({
42930
43739
  "src/api/connection-memory-import.ts"() {
42931
43740
  "use strict";
42932
- import_node_crypto20 = require("crypto");
43741
+ import_node_crypto21 = require("crypto");
42933
43742
  init_slugify();
42934
43743
  CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
42935
43744
  CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
@@ -43103,7 +43912,7 @@ function finitePositive(value, fallback) {
43103
43912
  return Number.isFinite(value) && value > 0 ? value : fallback;
43104
43913
  }
43105
43914
  async function collectConnectedDataExport(args) {
43106
- const exportId = (0, import_node_crypto21.randomUUID)();
43915
+ const exportId = (0, import_node_crypto22.randomUUID)();
43107
43916
  const now = args.now ?? Date.now;
43108
43917
  const startedAt = now();
43109
43918
  const budgetMs = finitePositive(CONNECTED_DATA_EXPORT_BUDGET_MS, 24e4);
@@ -43213,11 +44022,11 @@ ${lines.length ? `${lines.join("\n")}
43213
44022
  untrustedContent: true
43214
44023
  };
43215
44024
  }
43216
- var import_node_crypto21, CONNECTED_DATA_INLINE_BUDGET_BYTES, CONNECTED_DATA_MAX_EXPORT_BYTES, CONNECTED_DATA_EXPORT_BUDGET_MS, CONNECTED_DATA_PAGE_START_HEADROOM_MS, CONNECTED_DATA_DATASETS, ConnectedDataExportValidationError;
44025
+ var import_node_crypto22, CONNECTED_DATA_INLINE_BUDGET_BYTES, CONNECTED_DATA_MAX_EXPORT_BYTES, CONNECTED_DATA_EXPORT_BUDGET_MS, CONNECTED_DATA_PAGE_START_HEADROOM_MS, CONNECTED_DATA_DATASETS, ConnectedDataExportValidationError;
43217
44026
  var init_connected_data_export = __esm({
43218
44027
  "src/api/connected-data-export.ts"() {
43219
44028
  "use strict";
43220
- import_node_crypto21 = require("crypto");
44029
+ import_node_crypto22 = require("crypto");
43221
44030
  CONNECTED_DATA_INLINE_BUDGET_BYTES = Number(
43222
44031
  process.env.MCP_SCRAPER_CONNECTED_DATA_INLINE_BUDGET_BYTES ?? 5e4
43223
44032
  );
@@ -43330,7 +44139,7 @@ async function exportSearchConsoleTableData(args) {
43330
44139
  offset += rows.length;
43331
44140
  if (stoppedForBytes || rows.length < limit || offset >= matchedRows) break;
43332
44141
  }
43333
- const exportId = (0, import_node_crypto22.randomUUID)();
44142
+ const exportId = (0, import_node_crypto23.randomUUID)();
43334
44143
  const artifact = await args.writeArtifact({
43335
44144
  ownerId: args.ownerId,
43336
44145
  exportId,
@@ -43352,11 +44161,11 @@ async function exportSearchConsoleTableData(args) {
43352
44161
  warnings
43353
44162
  };
43354
44163
  }
43355
- var import_node_crypto22, SEARCH_CONSOLE_TABLE_EXPORT_MAX_ROWS, SEARCH_CONSOLE_TABLE_EXPORT_PAGE_SIZE, SEARCH_CONSOLE_TABLE_EXPORT_MAX_BYTES, SEARCH_CONSOLE_TABLE_COLUMNS, SearchConsoleTableExportValidationError;
44164
+ var import_node_crypto23, SEARCH_CONSOLE_TABLE_EXPORT_MAX_ROWS, SEARCH_CONSOLE_TABLE_EXPORT_PAGE_SIZE, SEARCH_CONSOLE_TABLE_EXPORT_MAX_BYTES, SEARCH_CONSOLE_TABLE_COLUMNS, SearchConsoleTableExportValidationError;
43356
44165
  var init_search_console_table_export = __esm({
43357
44166
  "src/api/search-console-table-export.ts"() {
43358
44167
  "use strict";
43359
- import_node_crypto22 = require("crypto");
44168
+ import_node_crypto23 = require("crypto");
43360
44169
  SEARCH_CONSOLE_TABLE_EXPORT_MAX_ROWS = 5e4;
43361
44170
  SEARCH_CONSOLE_TABLE_EXPORT_PAGE_SIZE = 2e3;
43362
44171
  SEARCH_CONSOLE_TABLE_EXPORT_MAX_BYTES = 50 * 1024 * 1024;
@@ -43784,7 +44593,7 @@ async function reconcileDiscoveredNangoConnections(identity, discovered) {
43784
44593
  updated_at = excluded.updated_at
43785
44594
  `,
43786
44595
  args: [
43787
- (0, import_node_crypto23.randomUUID)(),
44596
+ (0, import_node_crypto24.randomUUID)(),
43788
44597
  userId,
43789
44598
  connection.providerConfigKey,
43790
44599
  connection.provider,
@@ -43855,7 +44664,7 @@ async function recordServiceConnectionHealth(args) {
43855
44664
  });
43856
44665
  await getDb().execute({
43857
44666
  sql: `INSERT INTO service_connection_health_events (id, connection_id, operational_status, failure_code, retryable, evidence_source) VALUES (?, ?, ?, ?, ?, ?)`,
43858
- args: [(0, import_node_crypto23.randomUUID)(), args.connectionId, args.operationalStatus, args.failureCode ?? null, args.retryable == null ? null : args.retryable ? 1 : 0, args.evidenceSource]
44667
+ args: [(0, import_node_crypto24.randomUUID)(), args.connectionId, args.operationalStatus, args.failureCode ?? null, args.retryable == null ? null : args.retryable ? 1 : 0, args.evidenceSource]
43859
44668
  });
43860
44669
  }
43861
44670
  async function setServiceConnectionActions(identity, connectionId, enabled) {
@@ -43895,7 +44704,7 @@ async function claimServiceConnectionAction(args) {
43895
44704
  if (!connection) throw new Error("service_connection_not_found");
43896
44705
  const inserted = await getDb().execute({
43897
44706
  sql: `INSERT OR IGNORE INTO service_connection_action_audit (id, connection_id, user_id, tool, request_id, status) VALUES (?, ?, ?, ?, ?, 'started')`,
43898
- args: [(0, import_node_crypto23.randomUUID)(), connection.id, connection.userId, args.tool, args.requestId]
44707
+ args: [(0, import_node_crypto24.randomUUID)(), connection.id, connection.userId, args.tool, args.requestId]
43899
44708
  });
43900
44709
  if (Number(inserted.rowsAffected ?? 0) === 1) return { claimed: true };
43901
44710
  const existing = await getDb().execute({
@@ -43908,11 +44717,11 @@ async function claimServiceConnectionAction(args) {
43908
44717
  errorCode: typeof existing.rows[0]?.error_code === "string" ? existing.rows[0].error_code : null
43909
44718
  };
43910
44719
  }
43911
- var import_node_crypto23, schemaReady2, schemaDb;
44720
+ var import_node_crypto24, schemaReady2, schemaDb;
43912
44721
  var init_service_connections = __esm({
43913
44722
  "src/api/service-connections.ts"() {
43914
44723
  "use strict";
43915
- import_node_crypto23 = require("crypto");
44724
+ import_node_crypto24 = require("crypto");
43916
44725
  init_db();
43917
44726
  schemaReady2 = null;
43918
44727
  schemaDb = null;
@@ -44229,7 +45038,7 @@ async function listNangoToolsDirect(identity, connectionId) {
44229
45038
  });
44230
45039
  const readTools = tools.filter((tool) => classifyTool(tool) === "read").map((tool) => tool.name);
44231
45040
  const actionTools = tools.filter((tool) => classifyTool(tool) === "action").map((tool) => tool.name);
44232
- const revision = (0, import_node_crypto24.createHash)("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
45041
+ const revision = (0, import_node_crypto25.createHash)("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
44233
45042
  await updateServiceConnectionTools(connection.id, readTools, actionTools, revision);
44234
45043
  const refreshed = await getOwnedServiceConnection(identity, connection.id);
44235
45044
  return { connection: refreshed ?? { ...connection, readTools, actionTools, toolRevision: revision }, tools };
@@ -44249,8 +45058,8 @@ async function callNangoToolDirect(args) {
44249
45058
  identity: args.identity,
44250
45059
  ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION
44251
45060
  });
44252
- const requestId = args.requestId?.trim() || (0, import_node_crypto24.randomUUID)();
44253
- const idempotencyKey = `main-nango:${(0, import_node_crypto24.createHash)("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
45061
+ const requestId = args.requestId?.trim() || (0, import_node_crypto25.randomUUID)();
45062
+ const idempotencyKey = `main-nango:${(0, import_node_crypto25.createHash)("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
44254
45063
  const startedAt = /* @__PURE__ */ new Date();
44255
45064
  const started = performance.now();
44256
45065
  let result;
@@ -44275,7 +45084,7 @@ async function callNangoToolDirect(args) {
44275
45084
  toolName: args.tool,
44276
45085
  operationKind: args.operationKind ?? args.classification,
44277
45086
  outcome: providerError ? "error" : "partial",
44278
- requestId: requestId.length <= 200 ? requestId : (0, import_node_crypto24.createHash)("sha256").update(requestId).digest("hex"),
45087
+ requestId: requestId.length <= 200 ? requestId : (0, import_node_crypto25.createHash)("sha256").update(requestId).digest("hex"),
44279
45088
  startedAt: startedAt.toISOString(),
44280
45089
  completedAt: completedAt.toISOString()
44281
45090
  }
@@ -44312,15 +45121,15 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
44312
45121
  providerContractHash: MAIN_INTEGRATION_CONTRACT_HASH,
44313
45122
  protocolVersion: null,
44314
45123
  schemaSource: "live_tools_list",
44315
- schemaHash: (0, import_node_crypto24.createHash)("sha256").update(JSON.stringify(projected)).digest("hex"),
45124
+ schemaHash: (0, import_node_crypto25.createHash)("sha256").update(JSON.stringify(projected)).digest("hex"),
44316
45125
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
44317
45126
  };
44318
45127
  }
44319
- var import_node_crypto24, import_client6, import_streamableHttp, DEFAULT_NANGO_MCP_URL, NANGO_TIMEOUT_MS, NANGO_CONNECTION_PAGE_SIZE, NANGO_CONNECTION_MAX_PAGES, MAIN_INTEGRATION_CONTRACT_VERSION, MAIN_INTEGRATION_CONTRACT_HASH, MainNangoTransportError;
45128
+ var import_node_crypto25, import_client6, import_streamableHttp, DEFAULT_NANGO_MCP_URL, NANGO_TIMEOUT_MS, NANGO_CONNECTION_PAGE_SIZE, NANGO_CONNECTION_MAX_PAGES, MAIN_INTEGRATION_CONTRACT_VERSION, MAIN_INTEGRATION_CONTRACT_HASH, MainNangoTransportError;
44320
45129
  var init_main_nango_transport = __esm({
44321
45130
  "src/api/main-nango-transport.ts"() {
44322
45131
  "use strict";
44323
- import_node_crypto24 = require("crypto");
45132
+ import_node_crypto25 = require("crypto");
44324
45133
  import_client6 = require("@modelcontextprotocol/sdk/client/index.js");
44325
45134
  import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
44326
45135
  init_service_connections();
@@ -44942,7 +45751,7 @@ async function setScheduleConnectionActionsEnabled(identity, connectionId, enabl
44942
45751
  return data.connection.actionsEnabled === true;
44943
45752
  }
44944
45753
  async function callScheduleConnectionAction(identity, connectionId, input, tool, idempotencyKey) {
44945
- const requestId = `main-connected-action:${(0, import_node_crypto25.createHash)("sha256").update(identity).update("\0").update(idempotencyKey?.trim() || (0, import_node_crypto25.randomUUID)()).digest("hex")}`;
45754
+ const requestId = `main-connected-action:${(0, import_node_crypto26.createHash)("sha256").update(identity).update("\0").update(idempotencyKey?.trim() || (0, import_node_crypto26.randomUUID)()).digest("hex")}`;
44946
45755
  if (mainOwnsIntegrations()) {
44947
45756
  const selectedTool = tool?.trim();
44948
45757
  if (!selectedTool) throw new NangoControlError("An action tool is required.", 400, "invalid_request", false);
@@ -45084,7 +45893,7 @@ function canonicalJson2(value) {
45084
45893
  return JSON.stringify(value);
45085
45894
  }
45086
45895
  function projectedToolSchemaHash(tool) {
45087
- return (0, import_node_crypto25.createHash)("sha256").update(canonicalJson2(tool)).digest("hex");
45896
+ return (0, import_node_crypto26.createHash)("sha256").update(canonicalJson2(tool)).digest("hex");
45088
45897
  }
45089
45898
  async function describeNangoTool(identity, connectionId, tool, fresh) {
45090
45899
  if (mainOwnsIntegrations()) {
@@ -45345,11 +46154,11 @@ async function callMainOwnedExportPage(identity, input) {
45345
46154
  untrustedContent: true
45346
46155
  };
45347
46156
  }
45348
- var import_node_crypto25, DEFAULT_NANGO_CONTROL_URL, DISABLED_NANGO_TOOLS, CONNECTION_SYNC_REQUIRED_TOOLS, CONNECTION_SYNC_OPTIONAL_TOOLS, NangoControlError, ScheduleConnectionValidationError, SAFE_CONTROL_ERROR_CODES, FIXED_CONTROL_ERROR_MESSAGES, CONTROL_ERROR_CODE_ALIASES;
46157
+ var import_node_crypto26, DEFAULT_NANGO_CONTROL_URL, DISABLED_NANGO_TOOLS, CONNECTION_SYNC_REQUIRED_TOOLS, CONNECTION_SYNC_OPTIONAL_TOOLS, NangoControlError, ScheduleConnectionValidationError, SAFE_CONTROL_ERROR_CODES, FIXED_CONTROL_ERROR_MESSAGES, CONTROL_ERROR_CODE_ALIASES;
45349
46158
  var init_nango_control = __esm({
45350
46159
  "src/api/nango-control.ts"() {
45351
46160
  "use strict";
45352
- import_node_crypto25 = require("crypto");
46161
+ import_node_crypto26 = require("crypto");
45353
46162
  init_connected_data_export();
45354
46163
  init_main_nango_transport();
45355
46164
  init_service_connections();
@@ -45818,8 +46627,8 @@ function signingSecret() {
45818
46627
  return secret2;
45819
46628
  }
45820
46629
  function schedulerIntegrationSignature(args) {
45821
- const bodyHash = (0, import_node_crypto26.createHash)("sha256").update(args.body).digest("hex");
45822
- return (0, import_node_crypto26.createHmac)("sha256", args.secret).update(`${args.method.toUpperCase()}
46630
+ const bodyHash = (0, import_node_crypto27.createHash)("sha256").update(args.body).digest("hex");
46631
+ return (0, import_node_crypto27.createHmac)("sha256", args.secret).update(`${args.method.toUpperCase()}
45823
46632
  ${args.path}
45824
46633
  ${args.timestamp}
45825
46634
  ${args.nonce}
@@ -45866,17 +46675,17 @@ async function verifySchedulerIntegrationRequest(request, rawBody) {
45866
46675
  });
45867
46676
  const suppliedBytes = Buffer.from(signature, "hex");
45868
46677
  const expectedBytes = Buffer.from(expected, "hex");
45869
- if (suppliedBytes.length !== expectedBytes.length || !(0, import_node_crypto26.timingSafeEqual)(suppliedBytes, expectedBytes)) {
46678
+ if (suppliedBytes.length !== expectedBytes.length || !(0, import_node_crypto27.timingSafeEqual)(suppliedBytes, expectedBytes)) {
45870
46679
  throw new SchedulerIntegrationAuthError("invalid_signature");
45871
46680
  }
45872
46681
  await claimNonce(nonce, timestampMs);
45873
46682
  return { requestId };
45874
46683
  }
45875
- var import_node_crypto26, MAX_CLOCK_SKEW_MS, SchedulerIntegrationAuthError;
46684
+ var import_node_crypto27, MAX_CLOCK_SKEW_MS, SchedulerIntegrationAuthError;
45876
46685
  var init_scheduler_integration_auth = __esm({
45877
46686
  "src/api/scheduler-integration-auth.ts"() {
45878
46687
  "use strict";
45879
- import_node_crypto26 = require("crypto");
46688
+ import_node_crypto27 = require("crypto");
45880
46689
  init_db();
45881
46690
  init_service_connections();
45882
46691
  MAX_CLOCK_SKEW_MS = 5 * 60 * 1e3;
@@ -46070,7 +46879,7 @@ function requireStripeSecret() {
46070
46879
  async function createSignupStripeCustomer(email) {
46071
46880
  const secret2 = requireStripeSecret();
46072
46881
  if (!secret2) return null;
46073
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
46882
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
46074
46883
  const customer = await stripeClient.customers.create({
46075
46884
  email,
46076
46885
  description: "MCP Scraper free account",
@@ -46340,7 +47149,7 @@ async function chargeTierChangeNow(stripeClient, subscriptionId, customerId) {
46340
47149
  return { ok: false, amountDue: 0, error: err instanceof Error ? err.message : "Unable to charge the plan change immediately." };
46341
47150
  }
46342
47151
  }
46343
- var import_resend2, import_hono25, import_hono26, import_factory6, import_cookie2, import_stripe3, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth2, adminAuth, sessionAuth, requireIntegrationsTier, requirePaidSchedulingTier, app, STRIPE_API_VERSION2, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
47152
+ var import_resend2, import_hono25, import_hono26, import_factory6, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth2, adminAuth, sessionAuth, requireIntegrationsTier, requirePaidSchedulingTier, app, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
46344
47153
  var init_server = __esm({
46345
47154
  "src/api/server.ts"() {
46346
47155
  "use strict";
@@ -46397,7 +47206,7 @@ var init_server = __esm({
46397
47206
  import_factory6 = require("hono/factory");
46398
47207
  import_cookie2 = require("hono/cookie");
46399
47208
  init_db();
46400
- import_stripe3 = __toESM(require("stripe"), 1);
47209
+ import_stripe2 = __toESM(require("stripe"), 1);
46401
47210
  init_rates();
46402
47211
  init_server_schemas();
46403
47212
  init_page_diff();
@@ -46482,7 +47291,7 @@ var init_server = __esm({
46482
47291
  return next();
46483
47292
  });
46484
47293
  app = new import_hono25.Hono();
46485
- STRIPE_API_VERSION2 = "2026-02-25.clover";
47294
+ STRIPE_API_VERSION = "2026-02-25.clover";
46486
47295
  app.use("*", async (c, next) => {
46487
47296
  await next();
46488
47297
  c.header("Cache-Control", "no-store");
@@ -46514,7 +47323,7 @@ var init_server = __esm({
46514
47323
  const user = await createUser(normalizedEmail, void 0, password, stripeCustomerId ?? void 0);
46515
47324
  if (stripeCustomerId) {
46516
47325
  try {
46517
- const stripeClient = new import_stripe3.default(process.env.STRIPE_SECRET_KEY, { apiVersion: STRIPE_API_VERSION2 });
47326
+ const stripeClient = new import_stripe2.default(process.env.STRIPE_SECRET_KEY, { apiVersion: STRIPE_API_VERSION });
46518
47327
  await stripeClient.customers.update(stripeCustomerId, {
46519
47328
  metadata: {
46520
47329
  app: "mcp-scraper",
@@ -46582,7 +47391,7 @@ var init_server = __esm({
46582
47391
  }
46583
47392
  const secret2 = requireStripeSecret();
46584
47393
  if (secret2 && user.stripe_customer_id) {
46585
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
47394
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
46586
47395
  const subs = await stripeClient.subscriptions.list({ customer: user.stripe_customer_id, status: "all", limit: 100 });
46587
47396
  for (const sub of subs.data) {
46588
47397
  if (sub.status === "active" || sub.status === "trialing" || sub.status === "past_due") {
@@ -46896,7 +47705,7 @@ var init_server = __esm({
46896
47705
  if (!priceId || !MEMORY_PLANS[priceId]) return c.json({ error: "Invalid priceId \u2014 must be a memory plan price." }, 400);
46897
47706
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
46898
47707
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
46899
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
47708
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
46900
47709
  let customerId = user.stripe_customer_id;
46901
47710
  if (!customerId) {
46902
47711
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -46931,7 +47740,7 @@ var init_server = __esm({
46931
47740
  if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
46932
47741
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
46933
47742
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
46934
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
47743
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
46935
47744
  const session = await stripeClient.billingPortal.sessions.create({
46936
47745
  customer: user.stripe_customer_id,
46937
47746
  return_url: `${appOrigin()}/memory`
@@ -47014,10 +47823,8 @@ var init_server = __esm({
47014
47823
  if (!providerConfigKey) return c.json({ ok: false, error: "providerConfigKey is required." }, 400);
47015
47824
  try {
47016
47825
  if (providerConfigKey !== "resend") {
47017
- await reconcileConnectedAccountBilling(
47018
- user,
47019
- billableNangoConnectionCount(await getNangoConnections(user.email, { summaryOnly: true }))
47020
- );
47826
+ const existingCount = billableNangoConnectionCount(await getNangoConnections(user.email, { summaryOnly: true }));
47827
+ await reconcileConnectedAccountBilling(user, existingCount + 1);
47021
47828
  }
47022
47829
  const session = providerConfigKey === "resend" ? await createResendConnectSession(user.email, resendOAuthCallbackUrl()) : await createNangoConnectSession(user.email, providerConfigKey);
47023
47830
  return c.json({ ok: true, ...session });
@@ -48248,7 +49055,7 @@ var init_server = __esm({
48248
49055
  if (user.concurrency_stripe_sub_id) return c.json({ error: "Already subscribed \u2014 cancel existing slot first to change." }, 409);
48249
49056
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
48250
49057
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
48251
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
49058
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
48252
49059
  let customerId = user.stripe_customer_id;
48253
49060
  if (!customerId) {
48254
49061
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -48278,10 +49085,10 @@ var init_server = __esm({
48278
49085
  const user = c.get("sessionUser");
48279
49086
  const raw = await c.req.json().catch(() => ({}));
48280
49087
  const tier = SUBSCRIPTION_TIER_BY_KEY[String(raw.tier ?? "")];
48281
- if (!tier) return c.json({ error: "Invalid tier. Choose starter, growth, or scale." }, 400);
49088
+ if (!tier) return c.json({ error: "Invalid subscription tier." }, 400);
48282
49089
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
48283
49090
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
48284
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
49091
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
48285
49092
  let customerId = user.stripe_customer_id;
48286
49093
  if (!customerId) {
48287
49094
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -48333,7 +49140,7 @@ var init_server = __esm({
48333
49140
  if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
48334
49141
  const secret2 = requireStripeSecret();
48335
49142
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
48336
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
49143
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
48337
49144
  const session = await stripeClient.billingPortal.sessions.create({
48338
49145
  customer: user.stripe_customer_id,
48339
49146
  return_url: `${appOrigin()}/billing`
@@ -48363,7 +49170,7 @@ var init_server = __esm({
48363
49170
  }
48364
49171
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
48365
49172
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
48366
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
49173
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
48367
49174
  let customerId = user.stripe_customer_id;
48368
49175
  if (!customerId) {
48369
49176
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -48407,7 +49214,7 @@ var init_server = __esm({
48407
49214
  if (!tier) return c.json({ error: "Invalid tier. Choose starter, growth, or scale.", tiers: Object.keys(SUBSCRIPTION_TIER_BY_KEY) }, 400);
48408
49215
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
48409
49216
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
48410
- const stripeClient = new import_stripe3.default(secret2, { apiVersion: STRIPE_API_VERSION2 });
49217
+ const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
48411
49218
  let customerId = user.stripe_customer_id;
48412
49219
  if (!customerId) {
48413
49220
  const customer = await stripeClient.customers.create({ email: user.email });
@@ -48468,7 +49275,7 @@ var init_server = __esm({
48468
49275
  if (!user.concurrency_stripe_sub_id) return c.json({ error: "No active concurrency subscription." }, 404);
48469
49276
  const stripeSecret = process.env.STRIPE_SECRET_KEY?.trim();
48470
49277
  if (!stripeSecret) return c.json({ error: "Stripe is not configured." }, 503);
48471
- const stripeClient = new import_stripe3.default(stripeSecret, { apiVersion: STRIPE_API_VERSION2 });
49278
+ const stripeClient = new import_stripe2.default(stripeSecret, { apiVersion: STRIPE_API_VERSION });
48472
49279
  await stripeClient.subscriptions.cancel(user.concurrency_stripe_sub_id);
48473
49280
  const nextSlots = Math.max(0, user.extra_concurrency_slots - 1);
48474
49281
  await setExtraConcurrencySlots(user.id, nextSlots);