mcp-scraper 0.3.19 → 0.3.21

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 (33) hide show
  1. package/dist/bin/api-server.cjs +219 -164
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +2 -2
  13. package/dist/{chunk-5H22TOXQ.js → chunk-3KYRG7O7.js} +130 -7
  14. package/dist/chunk-3KYRG7O7.js.map +1 -0
  15. package/dist/{chunk-T337IGVE.js → chunk-7PQFU7TV.js} +13 -20
  16. package/dist/chunk-7PQFU7TV.js.map +1 -0
  17. package/dist/chunk-J4PSMJNZ.js +7 -0
  18. package/dist/chunk-J4PSMJNZ.js.map +1 -0
  19. package/dist/{chunk-A46NKAAM.js → chunk-UO252634.js} +2 -2
  20. package/dist/{db-P76GVIFN.js → db-EG5ETPTY.js} +12 -2
  21. package/dist/{server-EYNMXWUJ.js → server-K4UIE2R3.js} +88 -144
  22. package/dist/server-K4UIE2R3.js.map +1 -0
  23. package/dist/{worker-YZ2ZJE4F.js → worker-AITQTXHM.js} +3 -3
  24. package/docs/mcp-tool-manifest.generated.json +1 -1
  25. package/package.json +1 -1
  26. package/dist/chunk-5H22TOXQ.js.map +0 -1
  27. package/dist/chunk-AUKOY2IV.js +0 -7
  28. package/dist/chunk-AUKOY2IV.js.map +0 -1
  29. package/dist/chunk-T337IGVE.js.map +0 -1
  30. package/dist/server-EYNMXWUJ.js.map +0 -1
  31. /package/dist/{chunk-A46NKAAM.js.map → chunk-UO252634.js.map} +0 -0
  32. /package/dist/{db-P76GVIFN.js.map → db-EG5ETPTY.js.map} +0 -0
  33. /package/dist/{worker-YZ2ZJE4F.js.map → worker-AITQTXHM.js.map} +0 -0
@@ -7322,6 +7322,7 @@ var init_site_audit_service = __esm({
7322
7322
  // src/api/db.ts
7323
7323
  var db_exports = {};
7324
7324
  __export(db_exports, {
7325
+ CREDIT_LOT_TTL: () => CREDIT_LOT_TTL,
7325
7326
  SiteAuditJobRowSchema: () => SiteAuditJobRowSchema,
7326
7327
  SiteAuditPhaseLogRowSchema: () => SiteAuditPhaseLogRowSchema,
7327
7328
  advanceWorkflowRunStep: () => advanceWorkflowRunStep,
@@ -7348,6 +7349,7 @@ __export(db_exports, {
7348
7349
  deactivateUser: () => deactivateUser,
7349
7350
  debitMc: () => debitMc,
7350
7351
  deleteWorkflowSchedule: () => deleteWorkflowSchedule,
7352
+ expireOldLots: () => expireOldLots,
7351
7353
  failJob: () => failJob,
7352
7354
  failKpoJob: () => failKpoJob,
7353
7355
  failWorkflowRunRecord: () => failWorkflowRunRecord,
@@ -7385,6 +7387,7 @@ __export(db_exports, {
7385
7387
  markWorkflowRunRunning: () => markWorkflowRunRunning,
7386
7388
  markWorkflowScheduleRan: () => markWorkflowScheduleRan,
7387
7389
  migrate: () => migrate,
7390
+ migrateExistingBalancesToLots: () => migrateExistingBalancesToLots,
7388
7391
  patchWorkflowSchedule: () => patchWorkflowSchedule,
7389
7392
  reconcileBalanceMc: () => reconcileBalanceMc,
7390
7393
  recordStripeEvent: () => recordStripeEvent,
@@ -7393,9 +7396,11 @@ __export(db_exports, {
7393
7396
  revokeApiKey: () => revokeApiKey,
7394
7397
  rotateApiKey: () => rotateApiKey,
7395
7398
  setConcurrencySubId: () => setConcurrencySubId,
7399
+ setDbForTesting: () => setDbForTesting,
7396
7400
  setExtraConcurrencySlots: () => setExtraConcurrencySlots,
7397
7401
  setPassword: () => setPassword,
7398
7402
  setStripeCustomerId: () => setStripeCustomerId,
7403
+ setSubscriptionTier: () => setSubscriptionTier,
7399
7404
  startHarvestAttempt: () => startHarvestAttempt,
7400
7405
  stripeEventAlreadyProcessed: () => stripeEventAlreadyProcessed,
7401
7406
  updateKpoJobState: () => updateKpoJobState,
@@ -7405,6 +7410,9 @@ function getDb() {
7405
7410
  if (!_db) _db = (0, import_http.createClient)({ url: DB_URL, authToken: DB_TOKEN });
7406
7411
  return _db;
7407
7412
  }
7413
+ function setDbForTesting(client2) {
7414
+ _db = client2;
7415
+ }
7408
7416
  async function migrate() {
7409
7417
  const db = getDb();
7410
7418
  await db.execute(`
@@ -7520,6 +7528,18 @@ async function migrate() {
7520
7528
  await db.execute(`ALTER TABLE users ADD COLUMN concurrency_stripe_sub_id TEXT`);
7521
7529
  } catch {
7522
7530
  }
7531
+ try {
7532
+ await db.execute(`ALTER TABLE users ADD COLUMN subscription_concurrency INTEGER NOT NULL DEFAULT 0`);
7533
+ } catch {
7534
+ }
7535
+ try {
7536
+ await db.execute(`ALTER TABLE users ADD COLUMN subscription_tier TEXT`);
7537
+ } catch {
7538
+ }
7539
+ try {
7540
+ await db.execute(`ALTER TABLE users ADD COLUMN subscription_id TEXT`);
7541
+ } catch {
7542
+ }
7523
7543
  await db.execute(`
7524
7544
  CREATE TABLE IF NOT EXISTS concurrency_locks (
7525
7545
  id TEXT PRIMARY KEY,
@@ -7546,6 +7566,19 @@ async function migrate() {
7546
7566
  )
7547
7567
  `);
7548
7568
  await db.execute(`CREATE INDEX IF NOT EXISTS ledger_user_id ON ledger(user_id)`);
7569
+ await db.execute(`
7570
+ CREATE TABLE IF NOT EXISTS credit_lots (
7571
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7572
+ user_id INTEGER NOT NULL REFERENCES users(id),
7573
+ amount_mc INTEGER NOT NULL,
7574
+ remaining_mc INTEGER NOT NULL,
7575
+ source TEXT NOT NULL,
7576
+ stripe_pi TEXT,
7577
+ granted_at TEXT NOT NULL DEFAULT (datetime('now')),
7578
+ expires_at TEXT NOT NULL
7579
+ )
7580
+ `);
7581
+ await db.execute(`CREATE INDEX IF NOT EXISTS credit_lots_user_active ON credit_lots(user_id, expires_at, remaining_mc)`);
7549
7582
  await db.execute(`
7550
7583
  CREATE TABLE IF NOT EXISTS free_credit_refreshes (
7551
7584
  user_id INTEGER NOT NULL REFERENCES users(id),
@@ -7789,7 +7822,10 @@ function rowToUser(row) {
7789
7822
  stripe_customer_id: row.stripe_customer_id != null ? String(row.stripe_customer_id) : null,
7790
7823
  balance_mc: Number(row.balance_mc ?? 0),
7791
7824
  extra_concurrency_slots: Number(row.extra_concurrency_slots ?? 0),
7792
- concurrency_stripe_sub_id: row.concurrency_stripe_sub_id != null ? String(row.concurrency_stripe_sub_id) : null
7825
+ concurrency_stripe_sub_id: row.concurrency_stripe_sub_id != null ? String(row.concurrency_stripe_sub_id) : null,
7826
+ subscription_concurrency: Number(row.subscription_concurrency ?? 0),
7827
+ subscription_tier: row.subscription_tier != null ? String(row.subscription_tier) : null,
7828
+ subscription_id: row.subscription_id != null ? String(row.subscription_id) : null
7793
7829
  };
7794
7830
  }
7795
7831
  async function getUserByEmail(email) {
@@ -8574,12 +8610,95 @@ async function setConcurrencySubId(userId, subId) {
8574
8610
  args: [subId, userId]
8575
8611
  });
8576
8612
  }
8613
+ async function setSubscriptionTier(userId, tier, concurrency, subId) {
8614
+ await getDb().execute({
8615
+ sql: "UPDATE users SET subscription_tier = ?, subscription_concurrency = ?, subscription_id = ? WHERE id = ?",
8616
+ args: [tier, Math.max(0, Math.round(concurrency)), subId, userId]
8617
+ });
8618
+ }
8619
+ async function ensureMigrated(userId) {
8620
+ const db = getDb();
8621
+ const hasLots = await db.execute({ sql: "SELECT 1 FROM credit_lots WHERE user_id = ? LIMIT 1", args: [userId] });
8622
+ if (hasLots.rows.length) return;
8623
+ const legacy = await db.execute({ sql: "SELECT COALESCE(SUM(amount_mc), 0) AS bal FROM ledger WHERE user_id = ?", args: [userId] });
8624
+ const bal = Number(legacy.rows[0]?.bal ?? 0);
8625
+ if (bal > 0) {
8626
+ await db.execute({
8627
+ sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, expires_at) VALUES (?, ?, ?, 'migration', datetime('now', ?))`,
8628
+ args: [userId, bal, bal, CREDIT_LOT_TTL]
8629
+ });
8630
+ }
8631
+ }
8632
+ async function drainLots(userId, mc) {
8633
+ const db = getDb();
8634
+ const lots = await db.execute({
8635
+ 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`,
8636
+ args: [userId]
8637
+ });
8638
+ let need = mc;
8639
+ const updates = [];
8640
+ for (const lot of lots.rows) {
8641
+ if (need <= 0) break;
8642
+ const take = Math.min(Number(lot.remaining_mc), need);
8643
+ updates.push({ sql: "UPDATE credit_lots SET remaining_mc = remaining_mc - ? WHERE id = ?", args: [take, Number(lot.id)] });
8644
+ need -= take;
8645
+ }
8646
+ if (updates.length) await db.batch(updates, "write");
8647
+ }
8648
+ async function expireOldLots() {
8649
+ const db = getDb();
8650
+ const lots = await db.execute({
8651
+ sql: `SELECT id, user_id, remaining_mc FROM credit_lots WHERE remaining_mc > 0 AND expires_at <= datetime('now') LIMIT 500`
8652
+ });
8653
+ if (lots.rows.length === 0) return { expired_lots: 0, expired_mc: 0 };
8654
+ let totalMc = 0;
8655
+ const users = /* @__PURE__ */ new Set();
8656
+ const stmts = [];
8657
+ for (const lot of lots.rows) {
8658
+ const rem = Number(lot.remaining_mc);
8659
+ const uid = Number(lot.user_id);
8660
+ totalMc += rem;
8661
+ users.add(uid);
8662
+ stmts.push({ sql: "UPDATE credit_lots SET remaining_mc = 0 WHERE id = ?", args: [Number(lot.id)] });
8663
+ stmts.push({ sql: `INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, 'credit_expired', 'credits expired')`, args: [uid, -rem] });
8664
+ }
8665
+ await db.batch(stmts, "write");
8666
+ for (const uid of users) await reconcileBalanceMc(uid);
8667
+ return { expired_lots: lots.rows.length, expired_mc: totalMc };
8668
+ }
8669
+ async function migrateExistingBalancesToLots() {
8670
+ const db = getDb();
8671
+ const res = await db.execute({
8672
+ sql: `SELECT u.id AS user_id, COALESCE(SUM(l.amount_mc), 0) AS bal
8673
+ FROM users u LEFT JOIN ledger l ON l.user_id = u.id
8674
+ WHERE u.id NOT IN (SELECT DISTINCT user_id FROM credit_lots)
8675
+ GROUP BY u.id HAVING bal > 0`
8676
+ });
8677
+ let migrated = 0;
8678
+ let totalMc = 0;
8679
+ for (const row of res.rows) {
8680
+ const uid = Number(row.user_id);
8681
+ const bal = Number(row.bal);
8682
+ await db.execute({
8683
+ sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, expires_at) VALUES (?, ?, ?, 'migration', datetime('now', ?))`,
8684
+ args: [uid, bal, bal, CREDIT_LOT_TTL]
8685
+ });
8686
+ await reconcileBalanceMc(uid);
8687
+ migrated++;
8688
+ totalMc += bal;
8689
+ }
8690
+ return { migrated, total_mc: totalMc };
8691
+ }
8577
8692
  async function creditMc(userId, mc, operation, description, stripePaymentIntent) {
8578
8693
  const db = getDb();
8579
- await db.batch([
8580
- { sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [mc, userId] },
8581
- { sql: "INSERT INTO ledger (user_id, amount_mc, operation, description, stripe_pi) VALUES (?, ?, ?, ?, ?)", args: [userId, mc, operation, description ?? null, stripePaymentIntent ?? null] }
8582
- ], "write");
8694
+ await ensureMigrated(userId);
8695
+ const stmts = [];
8696
+ if (mc > 0) {
8697
+ 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, stripePaymentIntent ?? null, CREDIT_LOT_TTL] });
8698
+ }
8699
+ stmts.push({ sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [mc, userId] });
8700
+ stmts.push({ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description, stripe_pi) VALUES (?, ?, ?, ?, ?)", args: [userId, mc, operation, description ?? null, stripePaymentIntent ?? null] });
8701
+ await db.batch(stmts, "write");
8583
8702
  const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
8584
8703
  return Number(res.rows[0]?.balance_mc ?? 0);
8585
8704
  }
@@ -8599,6 +8718,7 @@ async function claimMonthlyFreeRefresh(userId, month) {
8599
8718
  }
8600
8719
  async function debitMc(userId, mc, operation, description) {
8601
8720
  const db = getDb();
8721
+ await ensureMigrated(userId);
8602
8722
  const upd = await db.execute({
8603
8723
  sql: "UPDATE users SET balance_mc = balance_mc - ? WHERE id = ? AND balance_mc >= ?",
8604
8724
  args: [mc, userId, mc]
@@ -8607,6 +8727,7 @@ async function debitMc(userId, mc, operation, description) {
8607
8727
  const res2 = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
8608
8728
  return { ok: false, balance_mc: Number(res2.rows[0]?.balance_mc ?? 0) };
8609
8729
  }
8730
+ await drainLots(userId, mc);
8610
8731
  await db.execute({
8611
8732
  sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)",
8612
8733
  args: [userId, -mc, operation, description ?? null]
@@ -8638,8 +8759,9 @@ async function ledgerExistsForStripePI(stripePI) {
8638
8759
  }
8639
8760
  async function reconcileBalanceMc(userId) {
8640
8761
  const db = getDb();
8762
+ await ensureMigrated(Number(userId));
8641
8763
  const res = await db.execute({
8642
- sql: "SELECT COALESCE(SUM(amount_mc), 0) AS balance_mc FROM ledger WHERE user_id = ?",
8764
+ 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')`,
8643
8765
  args: [userId]
8644
8766
  });
8645
8767
  const balanceMc = Number(res.rows[0]?.balance_mc ?? 0);
@@ -8649,7 +8771,7 @@ async function reconcileBalanceMc(userId) {
8649
8771
  });
8650
8772
  return balanceMc;
8651
8773
  }
8652
- var import_http, import_node_crypto2, import_zod10, DB_URL, DB_TOKEN, _db, _rateLimitSchemaReady, SiteAuditJobRowSchema, SiteAuditPhaseLogRowSchema;
8774
+ var import_http, import_node_crypto2, import_zod10, DB_URL, DB_TOKEN, _db, _rateLimitSchemaReady, CREDIT_LOT_TTL, SiteAuditJobRowSchema, SiteAuditPhaseLogRowSchema;
8653
8775
  var init_db = __esm({
8654
8776
  "src/api/db.ts"() {
8655
8777
  "use strict";
@@ -8660,6 +8782,7 @@ var init_db = __esm({
8660
8782
  DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
8661
8783
  _db = null;
8662
8784
  _rateLimitSchemaReady = false;
8785
+ CREDIT_LOT_TTL = "+3 months";
8663
8786
  SiteAuditJobRowSchema = import_zod10.z.object({
8664
8787
  id: import_zod10.z.string(),
8665
8788
  user_id: import_zod10.z.number(),
@@ -9512,7 +9635,7 @@ function insufficientBalanceResponse(balanceMc, requiredMc) {
9512
9635
  topup_url: topupUrl
9513
9636
  };
9514
9637
  }
9515
- var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, FREE_MONTHLY_REFRESH_MC, BALANCE_PRICE_IDS, BALANCE_PACK_LABELS, LedgerOperation;
9638
+ var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, 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, LedgerOperation;
9516
9639
  var init_rates = __esm({
9517
9640
  "src/api/rates.ts"() {
9518
9641
  "use strict";
@@ -9666,27 +9789,22 @@ var init_rates = __esm({
9666
9789
  }
9667
9790
  ];
9668
9791
  CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
9792
+ SUBSCRIPTION_TIERS = {
9793
+ "price_1TmiHRS8aAcsk3TGwmSNfNIa": { tier: "starter", label: "Starter", price_id: "price_1TmiHRS8aAcsk3TGwmSNfNIa", monthly_usd: 12, credits_mc: 10312500, concurrency: 3, intro_coupon: "mcp-starter-1dollar-intro-12" },
9794
+ "price_1Tmg1nS8aAcsk3TGe8zcnGTM": { tier: "growth", label: "Growth", price_id: "price_1Tmg1nS8aAcsk3TGe8zcnGTM", monthly_usd: 100, credits_mc: 9075e4, concurrency: 10, intro_coupon: null },
9795
+ "price_1Tmg1nS8aAcsk3TGsZw34iXS": { tier: "scale", label: "Scale", price_id: "price_1Tmg1nS8aAcsk3TGsZw34iXS", monthly_usd: 250, credits_mc: 226875e3, concurrency: 20, intro_coupon: null }
9796
+ };
9797
+ SUBSCRIPTION_TIER_BY_KEY = Object.fromEntries(
9798
+ Object.values(SUBSCRIPTION_TIERS).map((t) => [t.tier, t])
9799
+ );
9669
9800
  CONCURRENCY_SLOT_PRICE_USD = 5;
9670
9801
  CONCURRENCY_SLOT_PRICE_CURRENCY = "usd";
9671
9802
  CONCURRENCY_SLOT_PRICE_INTERVAL = "month";
9672
9803
  CONCURRENCY_SLOT_PRICE_LABEL = "$5/month";
9673
9804
  CONCURRENCY_SLOT_TERMINAL_COMMAND = "npx -y -p mcp-scraper@latest mcp-scraper-cli billing concurrency checkout";
9674
- FREE_SIGNUP_MC = 5e5;
9675
- FREE_MONTHLY_REFRESH_MC = 25e4;
9676
- BALANCE_PRICE_IDS = {
9677
- "price_1TZx6rS8aAcsk3TGNMc1Vgpo": 11e6,
9678
- "price_1TZx6sS8aAcsk3TGxgqB7khO": 275e5,
9679
- "price_1TZx6tS8aAcsk3TG8PnJqHlG": 605e5,
9680
- "price_1TZx6tS8aAcsk3TGNgRMpy0e": 121e6
9681
- };
9682
- BALANCE_PACK_LABELS = {
9683
- "price_1TZx6rS8aAcsk3TGNMc1Vgpo": "$10",
9684
- "price_1TZx6sS8aAcsk3TGxgqB7khO": "$25",
9685
- "price_1TZx6tS8aAcsk3TG8PnJqHlG": "$50",
9686
- "price_1TZx6tS8aAcsk3TGNgRMpy0e": "$100"
9687
- };
9688
9805
  LedgerOperation = {
9689
9806
  TOPUP: "topup",
9807
+ SUBSCRIPTION: "subscription",
9690
9808
  SIGNUP_GRANT: "signup_grant",
9691
9809
  MONTHLY_REFRESH: "monthly_free_refresh",
9692
9810
  PAA: "paa",
@@ -12111,7 +12229,8 @@ var init_server_schemas = __esm({
12111
12229
 
12112
12230
  // src/api/concurrency-gates.ts
12113
12231
  function concurrencyLimitForUser(user) {
12114
- return 1 + Math.max(0, Number(user.extra_concurrency_slots ?? 0));
12232
+ const tier = Math.max(0, Number(user.subscription_concurrency ?? 0));
12233
+ return Math.max(1, tier) + Math.max(0, Number(user.extra_concurrency_slots ?? 0));
12115
12234
  }
12116
12235
  function concurrencyLimitExceededResponse(gate) {
12117
12236
  const upgrade = concurrencySlotBillingInfo();
@@ -23762,7 +23881,7 @@ var PACKAGE_VERSION;
23762
23881
  var init_version = __esm({
23763
23882
  "src/version.ts"() {
23764
23883
  "use strict";
23765
- PACKAGE_VERSION = "0.3.19";
23884
+ PACKAGE_VERSION = "0.3.21";
23766
23885
  }
23767
23886
  });
23768
23887
 
@@ -27896,6 +28015,18 @@ var init_browser_agent_console = __esm({
27896
28015
  });
27897
28016
 
27898
28017
  // src/api/stripe-routes.ts
28018
+ function linePriceId(line) {
28019
+ const l = line;
28020
+ return l?.price?.id ?? l?.pricing?.price_details?.price ?? l?.plan?.id;
28021
+ }
28022
+ async function resolveUser(customerId, emailFallback) {
28023
+ let user = await getUserByStripeCustomerId(customerId);
28024
+ if (user) return user;
28025
+ if (!emailFallback) return void 0;
28026
+ user = await getUserByEmail(emailFallback);
28027
+ if (user) await setStripeCustomerId(user.id, customerId);
28028
+ return user;
28029
+ }
27899
28030
  var import_stripe, import_hono12, stripe, stripeApp;
27900
28031
  var init_stripe_routes = __esm({
27901
28032
  "src/api/stripe-routes.ts"() {
@@ -27917,46 +28048,43 @@ var init_stripe_routes = __esm({
27917
28048
  }
27918
28049
  const isNew = await recordStripeEvent(event.id);
27919
28050
  if (!isNew) return c.json({ received: true });
27920
- if (event.type === "checkout.session.completed") {
27921
- const session = event.data.object;
27922
- if (session.payment_status !== "paid") return c.json({ received: true });
27923
- const customerId = session.customer;
27924
- const lineItems = await stripe.checkout.sessions.listLineItems(session.id);
27925
- const priceId = lineItems.data[0]?.price?.id;
27926
- if (!priceId) return c.json({ received: true });
27927
- if (priceId in BALANCE_PRICE_IDS) {
27928
- const mc = BALANCE_PRICE_IDS[priceId];
27929
- const label = BALANCE_PACK_LABELS[priceId] ?? "unknown";
27930
- let user = await getUserByStripeCustomerId(customerId);
27931
- if (!user) {
27932
- const email = session.customer_details?.email;
27933
- if (!email) return c.json({ received: true });
27934
- user = await getUserByEmail(email);
27935
- if (!user) return c.json({ received: true });
27936
- await setStripeCustomerId(user.id, customerId);
28051
+ if (event.type === "invoice.paid" || event.type === "invoice.payment_succeeded") {
28052
+ const invoice = event.data.object;
28053
+ const lineTierId = invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
28054
+ if (lineTierId) {
28055
+ const tier = SUBSCRIPTION_TIERS[lineTierId];
28056
+ const user = await resolveUser(invoice.customer, invoice.customer_email ?? void 0);
28057
+ if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
28058
+ await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
28059
+ await setSubscriptionTier(user.id, tier.tier, tier.concurrency, invoice.subscription ?? user.subscription_id);
27937
28060
  }
27938
- const pi = session.payment_intent;
27939
- if (await ledgerExistsForStripePI(pi)) return c.json({ received: true });
27940
- await creditMc(user.id, mc, LedgerOperation.TOPUP, `${label} balance top-up`, pi);
27941
28061
  }
27942
28062
  }
27943
- if (event.type === "customer.subscription.created") {
28063
+ if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") {
27944
28064
  const sub = event.data.object;
27945
28065
  const priceId = sub.items.data[0]?.price?.id;
27946
- if (priceId !== CONCURRENCY_PRICE_ID) return c.json({ received: true });
27947
28066
  const user = await getUserByStripeCustomerId(sub.customer);
27948
28067
  if (!user) return c.json({ received: true });
27949
- await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
27950
- await setConcurrencySubId(user.id, sub.id);
28068
+ if (priceId && priceId in SUBSCRIPTION_TIERS) {
28069
+ const tier = SUBSCRIPTION_TIERS[priceId];
28070
+ const live = sub.status === "active" || sub.status === "trialing";
28071
+ await setSubscriptionTier(user.id, live ? tier.tier : null, live ? tier.concurrency : 0, live ? sub.id : null);
28072
+ } else if (priceId === CONCURRENCY_PRICE_ID && event.type === "customer.subscription.created") {
28073
+ await setExtraConcurrencySlots(user.id, user.extra_concurrency_slots + 1);
28074
+ await setConcurrencySubId(user.id, sub.id);
28075
+ }
27951
28076
  }
27952
28077
  if (event.type === "customer.subscription.deleted") {
27953
28078
  const sub = event.data.object;
27954
28079
  const priceId = sub.items.data[0]?.price?.id;
27955
- if (priceId !== CONCURRENCY_PRICE_ID) return c.json({ received: true });
27956
28080
  const user = await getUserByStripeCustomerId(sub.customer);
27957
28081
  if (!user) return c.json({ received: true });
27958
- await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
27959
- await setConcurrencySubId(user.id, null);
28082
+ if (priceId && priceId in SUBSCRIPTION_TIERS) {
28083
+ await setSubscriptionTier(user.id, null, 0, null);
28084
+ } else if (priceId === CONCURRENCY_PRICE_ID) {
28085
+ await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
28086
+ await setConcurrencySubId(user.id, null);
28087
+ }
27960
28088
  }
27961
28089
  return c.json({ received: true });
27962
28090
  });
@@ -28005,102 +28133,15 @@ var init_site_audit_worker = __esm({
28005
28133
  }
28006
28134
  });
28007
28135
 
28008
- // src/api/billing-schemas.ts
28009
- var import_zod28, BillingCheckoutBodySchema, FreeCreditBreakdownSchema, BillingBalanceResponseSchema, MonthlyRefreshSweepResultSchema;
28010
- var init_billing_schemas = __esm({
28011
- "src/api/billing-schemas.ts"() {
28012
- "use strict";
28013
- import_zod28 = require("zod");
28014
- BillingCheckoutBodySchema = import_zod28.z.object({
28015
- priceId: import_zod28.z.string().min(1)
28016
- });
28017
- FreeCreditBreakdownSchema = import_zod28.z.object({
28018
- signup_grant_mc: import_zod28.z.number().int().nonnegative(),
28019
- monthly_refresh_mc: import_zod28.z.number().int().nonnegative(),
28020
- total_free_mc: import_zod28.z.number().int().nonnegative(),
28021
- signup_grant_credits: import_zod28.z.number().nonnegative(),
28022
- monthly_refresh_credits: import_zod28.z.number().nonnegative(),
28023
- total_free_credits: import_zod28.z.number().nonnegative()
28024
- });
28025
- BillingBalanceResponseSchema = import_zod28.z.object({
28026
- balance_mc: import_zod28.z.number().int().nonnegative(),
28027
- balance_credits: import_zod28.z.number().nonnegative(),
28028
- free_credits: FreeCreditBreakdownSchema,
28029
- ledger: import_zod28.z.array(import_zod28.z.any())
28030
- });
28031
- MonthlyRefreshSweepResultSchema = import_zod28.z.object({
28032
- usersRefreshed: import_zod28.z.number().int().nonnegative(),
28033
- totalMcGranted: import_zod28.z.number().int().nonnegative()
28034
- });
28035
- }
28036
- });
28037
-
28038
28136
  // src/api/credit-operations.ts
28039
- function monthKey(value = /* @__PURE__ */ new Date()) {
28040
- return value.toISOString().slice(0, 7);
28041
- }
28042
- function userCreatedBeforeThisMonth(user) {
28043
- return user.created_at.slice(0, 7) < monthKey();
28044
- }
28045
- async function grantSignupCredit(userId) {
28046
- if (await ledgerExistsForOperation(userId, LedgerOperation.SIGNUP_GRANT)) return;
28047
- await creditMc(userId, FREE_SIGNUP_MC, LedgerOperation.SIGNUP_GRANT, "Welcome to MCP Scraper");
28137
+ async function grantSignupCredit(_userId) {
28138
+ return;
28048
28139
  }
28049
28140
  async function applyMonthlyFreeRefresh(user) {
28050
- if (!userCreatedBeforeThisMonth(user)) return user;
28051
- const claimed = await claimMonthlyFreeRefresh(user.id, monthKey());
28052
- if (!claimed) return user;
28053
- const balance = await creditMc(
28054
- user.id,
28055
- FREE_MONTHLY_REFRESH_MC,
28056
- LedgerOperation.MONTHLY_REFRESH,
28057
- "Monthly free credits"
28058
- );
28059
- return { ...user, balance_mc: balance };
28141
+ return user;
28060
28142
  }
28061
28143
  async function runMonthlyRefreshSweep() {
28062
- const db = getDb();
28063
- const monthStart = /* @__PURE__ */ new Date();
28064
- monthStart.setUTCDate(1);
28065
- monthStart.setUTCHours(0, 0, 0, 0);
28066
- const monthStartIso = monthStart.toISOString();
28067
- const currentMonth = monthKey();
28068
- const res = await db.execute({
28069
- sql: `
28070
- SELECT * FROM users
28071
- WHERE created_at < ?
28072
- AND active = 1
28073
- AND id NOT IN (
28074
- SELECT user_id FROM free_credit_refreshes WHERE month = ?
28075
- )
28076
- `,
28077
- args: [monthStartIso, currentMonth]
28078
- });
28079
- const users = res.rows.map((r) => ({
28080
- id: Number(r.id),
28081
- email: String(r.email),
28082
- name: r.name != null ? String(r.name) : null,
28083
- api_key: String(r.api_key ?? ""),
28084
- key_active: Number(r.key_active ?? 1),
28085
- password_hash: r.password_hash != null ? String(r.password_hash) : null,
28086
- created_at: String(r.created_at),
28087
- active: Number(r.active),
28088
- stripe_customer_id: r.stripe_customer_id != null ? String(r.stripe_customer_id) : null,
28089
- balance_mc: Number(r.balance_mc ?? 0),
28090
- extra_concurrency_slots: Number(r.extra_concurrency_slots ?? 0),
28091
- concurrency_stripe_sub_id: r.concurrency_stripe_sub_id != null ? String(r.concurrency_stripe_sub_id) : null
28092
- }));
28093
- let usersRefreshed = 0;
28094
- let totalMcGranted = 0;
28095
- for (const user of users) {
28096
- const before = user.balance_mc;
28097
- const after = await applyMonthlyFreeRefresh(user);
28098
- if (after.balance_mc !== before) {
28099
- usersRefreshed++;
28100
- totalMcGranted += FREE_MONTHLY_REFRESH_MC;
28101
- }
28102
- }
28103
- return { usersRefreshed, totalMcGranted };
28144
+ return { usersRefreshed: 0, totalMcGranted: 0 };
28104
28145
  }
28105
28146
  async function getFreeCreditBreakdown(userId) {
28106
28147
  const res = await getDb().execute({
@@ -28136,7 +28177,6 @@ var init_credit_operations = __esm({
28136
28177
  "use strict";
28137
28178
  init_db();
28138
28179
  init_rates();
28139
- init_db();
28140
28180
  }
28141
28181
  });
28142
28182
 
@@ -28486,7 +28526,6 @@ var init_server = __esm({
28486
28526
  init_db();
28487
28527
  import_stripe2 = __toESM(require("stripe"), 1);
28488
28528
  init_rates();
28489
- init_billing_schemas();
28490
28529
  init_server_schemas();
28491
28530
  init_schemas3();
28492
28531
  init_credit_operations();
@@ -28680,8 +28719,10 @@ var init_server = __esm({
28680
28719
  balance_mc: user.balance_mc,
28681
28720
  balance_credits: user.balance_mc / 1e3,
28682
28721
  extra_concurrency_slots: user.extra_concurrency_slots,
28683
- concurrency_limit: 1 + user.extra_concurrency_slots,
28722
+ concurrency_limit: concurrencyLimitForUser(user),
28684
28723
  has_concurrency_sub: !!user.concurrency_stripe_sub_id,
28724
+ subscription_tier: user.subscription_tier,
28725
+ subscription_concurrency: user.subscription_concurrency,
28685
28726
  ...stats
28686
28727
  });
28687
28728
  });
@@ -29107,13 +29148,12 @@ var init_server = __esm({
29107
29148
  });
29108
29149
  });
29109
29150
  app.post("/billing/checkout", requireAllowedOrigin, sessionAuth, async (c) => {
29151
+ return c.json({ error: "One-time credit packs are no longer available. Subscribe to a plan instead \u2014 use /billing/subscribe.", error_code: "packs_removed" }, 410);
29152
+ });
29153
+ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, async (c) => {
29110
29154
  try {
29111
29155
  const user = c.get("sessionUser");
29112
- const raw = await c.req.json();
29113
- const parsed = BillingCheckoutBodySchema.safeParse(raw);
29114
- if (!parsed.success) return c.json({ error: "Invalid request" }, 400);
29115
- const { priceId } = parsed.data;
29116
- if (!(priceId in BALANCE_PRICE_IDS)) return c.json({ error: "Invalid price" }, 400);
29156
+ if (user.concurrency_stripe_sub_id) return c.json({ error: "Already subscribed \u2014 cancel existing slot first to change." }, 409);
29117
29157
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
29118
29158
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
29119
29159
  const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
@@ -29125,26 +29165,28 @@ var init_server = __esm({
29125
29165
  }
29126
29166
  const session = await stripeClient.checkout.sessions.create({
29127
29167
  customer: customerId,
29128
- mode: "payment",
29129
- line_items: [{ price: priceId, quantity: 1 }],
29168
+ mode: "subscription",
29169
+ line_items: [{ price: CONCURRENCY_PRICE_ID, quantity: 1 }],
29130
29170
  ui_mode: "embedded",
29131
29171
  automatic_tax: { enabled: true },
29132
29172
  billing_address_collection: "required",
29133
29173
  customer_update: { address: "auto", name: "auto" },
29134
29174
  tax_id_collection: { enabled: true },
29135
- return_url: `${appOrigin()}/billing?session_id={CHECKOUT_SESSION_ID}`
29175
+ return_url: `${appOrigin()}/billing?slot_added=1`
29136
29176
  });
29137
29177
  return c.json({ clientSecret: session.client_secret });
29138
29178
  } catch (err) {
29139
29179
  const message = err instanceof Error ? err.message : "Unable to start checkout.";
29140
- console.error("[billing/checkout]", message);
29180
+ console.error("[billing/concurrency/checkout]", message);
29141
29181
  return c.json({ error: message }, 500);
29142
29182
  }
29143
29183
  });
29144
- app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, async (c) => {
29184
+ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
29145
29185
  try {
29146
29186
  const user = c.get("sessionUser");
29147
- if (user.concurrency_stripe_sub_id) return c.json({ error: "Already subscribed \u2014 cancel existing slot first to change." }, 409);
29187
+ const raw = await c.req.json().catch(() => ({}));
29188
+ const tier = SUBSCRIPTION_TIER_BY_KEY[String(raw.tier ?? "")];
29189
+ if (!tier) return c.json({ error: "Invalid tier. Choose starter, growth, or scale." }, 400);
29148
29190
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
29149
29191
  if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
29150
29192
  const stripeClient = new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
@@ -29154,21 +29196,33 @@ var init_server = __esm({
29154
29196
  customerId = customer.id;
29155
29197
  await setStripeCustomerId(user.id, customerId);
29156
29198
  }
29199
+ if (user.subscription_id) {
29200
+ const sub = await stripeClient.subscriptions.retrieve(user.subscription_id);
29201
+ const itemId = sub.items.data[0]?.id;
29202
+ if (itemId) {
29203
+ await stripeClient.subscriptions.update(user.subscription_id, {
29204
+ items: [{ id: itemId, price: tier.price_id }],
29205
+ proration_behavior: "create_prorations"
29206
+ });
29207
+ return c.json({ updated: true, tier: tier.tier });
29208
+ }
29209
+ }
29157
29210
  const session = await stripeClient.checkout.sessions.create({
29158
29211
  customer: customerId,
29159
29212
  mode: "subscription",
29160
- line_items: [{ price: CONCURRENCY_PRICE_ID, quantity: 1 }],
29213
+ line_items: [{ price: tier.price_id, quantity: 1 }],
29214
+ ...tier.intro_coupon ? { discounts: [{ coupon: tier.intro_coupon }] } : {},
29161
29215
  ui_mode: "embedded",
29162
29216
  automatic_tax: { enabled: true },
29163
29217
  billing_address_collection: "required",
29164
29218
  customer_update: { address: "auto", name: "auto" },
29165
29219
  tax_id_collection: { enabled: true },
29166
- return_url: `${appOrigin()}/billing?slot_added=1`
29220
+ return_url: `${appOrigin()}/billing?subscribed=${tier.tier}`
29167
29221
  });
29168
29222
  return c.json({ clientSecret: session.client_secret });
29169
29223
  } catch (err) {
29170
- const message = err instanceof Error ? err.message : "Unable to start checkout.";
29171
- console.error("[billing/concurrency/checkout]", message);
29224
+ const message = err instanceof Error ? err.message : "Unable to start subscription.";
29225
+ console.error("[billing/subscribe]", message);
29172
29226
  return c.json({ error: message }, 500);
29173
29227
  }
29174
29228
  });
@@ -29216,7 +29270,7 @@ var init_server = __esm({
29216
29270
  current_extra_slots: user.extra_concurrency_slots,
29217
29271
  current_limit: concurrencyLimitForUser(user),
29218
29272
  after_checkout_extra_slots: user.extra_concurrency_slots + 1,
29219
- after_checkout_limit: concurrencyLimitForUser({ extra_concurrency_slots: user.extra_concurrency_slots + 1 })
29273
+ after_checkout_limit: concurrencyLimitForUser({ extra_concurrency_slots: user.extra_concurrency_slots + 1, subscription_concurrency: user.subscription_concurrency })
29220
29274
  },
29221
29275
  next_step: "Open checkout_url in a browser, complete checkout, then restart or retry the MCP request."
29222
29276
  });
@@ -29288,12 +29342,13 @@ var init_server = __esm({
29288
29342
  const { drainQueue: drainQueue2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
29289
29343
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
29290
29344
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
29291
- const [results, sweepResult, reapResult] = await Promise.all([
29345
+ const [results, sweepResult, reapResult, expiredResult] = await Promise.all([
29292
29346
  drainQueue2(budget),
29293
29347
  runMonthlyRefreshSweep(),
29294
- reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 }))
29348
+ reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 })),
29349
+ expireOldLots().catch(() => ({ expired_lots: 0, expired_mc: 0 }))
29295
29350
  ]);
29296
- return c.json({ drained: results.length, results, sweepResult, reaped: reapResult, workflowDispatch: workflowDispatchResult });
29351
+ return c.json({ drained: results.length, results, sweepResult, reaped: reapResult, expired: expiredResult, workflowDispatch: workflowDispatchResult });
29297
29352
  });
29298
29353
  app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono14.serve)({ client: inngest, functions: [siteAuditFn, siteExtractFn] }));
29299
29354
  app.route("/api/internal/site-architecture-auditor", siteAuditApp);