mcp-scraper 0.32.2 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +432 -260
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/mcp-scraper-cli.cjs +5 -5
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +2 -2
  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 +47 -6
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +2 -2
  14. package/dist/bin/paa-harvest.js +1 -1
  15. package/dist/{chunk-MKZIL4EA.js → chunk-AT2PFWHW.js} +5 -5
  16. package/dist/chunk-AT2PFWHW.js.map +1 -0
  17. package/dist/{chunk-AEMABLEL.js → chunk-ERORC7AP.js} +1 -3
  18. package/dist/chunk-IASKABFL.js +7 -0
  19. package/dist/chunk-IASKABFL.js.map +1 -0
  20. package/dist/{chunk-XEED325T.js → chunk-JURXY3DP.js} +48 -7
  21. package/dist/chunk-JURXY3DP.js.map +1 -0
  22. package/dist/index.js +1 -1
  23. package/dist/{server-LTZDJHFG.js → server-RKAKXRAR.js} +378 -247
  24. package/dist/server-RKAKXRAR.js.map +1 -0
  25. package/dist/{worker-ZZCZNY3R.js → worker-JU5FX3NE.js} +2 -2
  26. package/docs/adr/0003-waive-unrecoverable-scheduled-model-cost.md +22 -0
  27. package/docs/adr/README.md +1 -0
  28. package/docs/mcp-tool-manifest.generated.json +170 -13
  29. package/package.json +1 -1
  30. package/dist/chunk-MKZIL4EA.js.map +0 -1
  31. package/dist/chunk-XEED325T.js.map +0 -1
  32. package/dist/chunk-XLO5CZ6T.js +0 -7
  33. package/dist/chunk-XLO5CZ6T.js.map +0 -1
  34. package/dist/server-LTZDJHFG.js.map +0 -1
  35. /package/dist/{chunk-AEMABLEL.js.map → chunk-ERORC7AP.js.map} +0 -0
  36. /package/dist/{worker-ZZCZNY3R.js.map → worker-JU5FX3NE.js.map} +0 -0
@@ -12,12 +12,10 @@ import {
12
12
  MapsSelectors,
13
13
  buildYouTubeChannelVideosUrl,
14
14
  deleteKernelProxyId,
15
- encodeUule,
16
15
  harvest,
17
- normalizeLocation,
18
16
  resolveKernelProxyId,
19
17
  serpArrivalUrl
20
- } from "./chunk-AEMABLEL.js";
18
+ } from "./chunk-ERORC7AP.js";
21
19
  import {
22
20
  HttpMcpToolExecutor,
23
21
  assessTranscriptSignal,
@@ -37,7 +35,7 @@ import {
37
35
  sanitizeAttempts,
38
36
  sanitizeHarvestResult,
39
37
  transcribeMediaUrl
40
- } from "./chunk-XEED325T.js";
38
+ } from "./chunk-JURXY3DP.js";
41
39
  import {
42
40
  auditImages,
43
41
  buildLinkReport,
@@ -66,7 +64,7 @@ import {
66
64
  workflowOutputBaseDir,
67
65
  workflowStepCount,
68
66
  workflowSupportsSteps
69
- } from "./chunk-MKZIL4EA.js";
67
+ } from "./chunk-AT2PFWHW.js";
70
68
  import "./chunk-K443GQY5.js";
71
69
  import {
72
70
  DEFAULT_MAPS_PROXY_MODE,
@@ -78,7 +76,7 @@ import {
78
76
  RawMapsOverviewSchema,
79
77
  RawMapsReviewStatsSchema
80
78
  } from "./chunk-V73MPRU6.js";
81
- import "./chunk-XLO5CZ6T.js";
79
+ import "./chunk-IASKABFL.js";
82
80
  import {
83
81
  completeExtractJob,
84
82
  countSuccessfulPages,
@@ -10029,7 +10027,7 @@ var HarvestBodySchema = z13.object({
10029
10027
  gl: z13.string().optional(),
10030
10028
  hl: z13.string().optional(),
10031
10029
  device: z13.enum(["desktop", "mobile"]).optional(),
10032
- proxyMode: z13.enum(["location", "configured", "none"]).optional(),
10030
+ proxyMode: z13.enum(["configured", "none"]).optional(),
10033
10031
  proxyZip: z13.string().regex(/^\d{5}$/).optional(),
10034
10032
  debug: z13.boolean().optional(),
10035
10033
  serpOnly: z13.boolean().optional(),
@@ -13088,15 +13086,29 @@ var MAPS_REDIRECT_BASE = "https://serp-redirector.vercel.app";
13088
13086
  function buildGoogleLocalResultsUrl(input) {
13089
13087
  const query = [input.query, input.location].filter(Boolean).join(" ");
13090
13088
  const params = new URLSearchParams({ q: query, udm: "1", gl: input.gl, hl: input.hl, pws: "0" });
13091
- if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
13092
13089
  return `https://www.google.com/search?${params.toString()}`;
13093
13090
  }
13094
13091
  function buildGoogleOrganicSearchUrl(input) {
13095
13092
  const query = [input.query, input.location].filter(Boolean).join(" ");
13096
13093
  const params = new URLSearchParams({ q: query, gl: input.gl, hl: input.hl, pws: "0" });
13097
- if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
13098
13094
  return `https://www.google.com/search?${params.toString()}`;
13099
13095
  }
13096
+ function mapsIdentifiersFromUrl(url) {
13097
+ const fid = url.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i) ?? url.match(/(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
13098
+ if (fid) {
13099
+ try {
13100
+ return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: BigInt(fid[2]).toString() };
13101
+ } catch {
13102
+ return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: null };
13103
+ }
13104
+ }
13105
+ try {
13106
+ const decimal = new URL(url).searchParams.get("cid");
13107
+ return { cid: null, cidDecimal: decimal && /^\d+$/.test(decimal) ? decimal : null };
13108
+ } catch {
13109
+ return { cid: null, cidDecimal: null };
13110
+ }
13111
+ }
13100
13112
  var MapsSearchExtractor = class {
13101
13113
  constructor(driver) {
13102
13114
  this.driver = driver;
@@ -13172,13 +13184,15 @@ var MapsSearchExtractor = class {
13172
13184
  for (const card of cards) {
13173
13185
  if (results.length >= options.maxResults || seen.has(card.cardKey)) continue;
13174
13186
  seen.add(card.cardKey);
13187
+ const cardIds = mapsIdentifiersFromUrl(card.directionsUrl ?? "");
13188
+ const cardPlaceUrl = cardIds.cidDecimal ? `https://www.google.com/maps?cid=${cardIds.cidDecimal}` : card.placeUrl;
13175
13189
  const details = await this.openCardAndExtractDialog(page, card, options.includeServices);
13176
13190
  results.push({
13177
13191
  position: results.length + 1,
13178
13192
  name: card.name,
13179
- placeUrl: details?.placeUrl ?? card.placeUrl,
13180
- cid: details?.cid ?? card.cid,
13181
- cidDecimal: details?.cidDecimal ?? card.cidDecimal,
13193
+ placeUrl: details?.placeUrl ?? cardPlaceUrl,
13194
+ cid: details?.cid ?? cardIds.cid,
13195
+ cidDecimal: details?.cidDecimal ?? cardIds.cidDecimal,
13182
13196
  rating: details?.rating ?? card.rating,
13183
13197
  reviewCount: details?.reviewCount ?? card.reviewCount,
13184
13198
  category: details?.category ?? card.category,
@@ -13235,8 +13249,10 @@ var MapsSearchExtractor = class {
13235
13249
  const phone = lines.map((line) => line.match(phonePattern)?.[0]).find((value) => Boolean(value)) ?? null;
13236
13250
  const hoursStatus = normalize((lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? "").split("\xB7")[0] ?? "");
13237
13251
  const address = lines.find((line) => addressPattern.test(line)) ?? null;
13238
- const websiteUrl = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href).find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
13252
+ const anchorHrefs = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href);
13253
+ const websiteUrl = anchorHrefs.find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
13239
13254
  if (!rating && !(phone && websiteUrl)) continue;
13255
+ const fidHref = anchorHrefs.find((href) => /(0x[0-9a-f]+):(0x[0-9a-f]+)/i.test(href)) ?? null;
13240
13256
  const key = `${name.toLowerCase()}|${lines.join(" ").toLowerCase()}`;
13241
13257
  if (seen.has(key)) continue;
13242
13258
  seen.add(key);
@@ -13256,7 +13272,7 @@ var MapsSearchExtractor = class {
13256
13272
  phone,
13257
13273
  hoursStatus,
13258
13274
  websiteUrl,
13259
- directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
13275
+ directionsUrl: fidHref ?? `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
13260
13276
  metadata: lines.slice(0, 20)
13261
13277
  });
13262
13278
  }
@@ -13955,6 +13971,7 @@ async function cleanupDisposableProxy(kernelApiKey, proxyId, eventName) {
13955
13971
  }
13956
13972
  }
13957
13973
  async function runMapsSearchWithRotation(options) {
13974
+ if (options.proxyMode === "location") options = { ...options, proxyMode: "none" };
13958
13975
  const attempts = [];
13959
13976
  const maxAttempts = mapsSearchMaxAttemptsForProxyMode(options.proxyMode) + 1;
13960
13977
  const started = Date.now();
@@ -14746,7 +14763,7 @@ var DirectoryWorkflowOptionsSchema = z23.object({
14746
14763
  saveCsv: z23.boolean().default(true),
14747
14764
  gl: z23.string().length(2).default("us"),
14748
14765
  hl: z23.string().length(2).default("en"),
14749
- proxyMode: z23.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
14766
+ proxyMode: z23.enum(["configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
14750
14767
  proxyZip: z23.string().regex(/^\d{5}$/).optional(),
14751
14768
  debug: z23.boolean().default(false),
14752
14769
  headless: z23.boolean().default(true),
@@ -19554,9 +19571,222 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
19554
19571
  }
19555
19572
 
19556
19573
  // src/api/stripe-routes.ts
19557
- import Stripe from "stripe";
19574
+ import Stripe2 from "stripe";
19558
19575
  import { Hono as Hono19 } from "hono";
19559
- var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
19576
+
19577
+ // src/api/connected-account-billing.ts
19578
+ import Stripe from "stripe";
19579
+ var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
19580
+ var STRIPE_API_VERSION = "2026-02-25.clover";
19581
+ async function ensureConnectedAccountBillingSchema() {
19582
+ await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
19583
+ user_id INTEGER PRIMARY KEY,
19584
+ stripe_subscription_id TEXT,
19585
+ stripe_subscription_item_id TEXT,
19586
+ price_id TEXT,
19587
+ quantity INTEGER NOT NULL DEFAULT 0,
19588
+ status TEXT NOT NULL DEFAULT 'pending',
19589
+ last_error_code TEXT,
19590
+ synced_at TEXT,
19591
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
19592
+ )`);
19593
+ }
19594
+ async function getConnectedAccountBillingState(userId) {
19595
+ await ensureConnectedAccountBillingSchema();
19596
+ const result = await getDb().execute({
19597
+ sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
19598
+ args: [Number(userId)]
19599
+ });
19600
+ return result.rows[0] ? result.rows[0] : null;
19601
+ }
19602
+ async function setConnectedAccountBillingState(input) {
19603
+ await ensureConnectedAccountBillingSchema();
19604
+ await getDb().execute({
19605
+ sql: `INSERT INTO connected_account_billing
19606
+ (user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
19607
+ VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
19608
+ ON CONFLICT(user_id) DO UPDATE SET
19609
+ stripe_subscription_id = excluded.stripe_subscription_id,
19610
+ stripe_subscription_item_id = excluded.stripe_subscription_item_id,
19611
+ price_id = excluded.price_id,
19612
+ quantity = excluded.quantity,
19613
+ status = excluded.status,
19614
+ last_error_code = excluded.last_error_code,
19615
+ synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
19616
+ updated_at = datetime('now')`,
19617
+ args: [
19618
+ Number(input.userId),
19619
+ input.stripeSubscriptionId,
19620
+ input.stripeSubscriptionItemId,
19621
+ input.priceId,
19622
+ input.quantity,
19623
+ input.status,
19624
+ input.lastErrorCode,
19625
+ input.synced ? 1 : 0,
19626
+ input.synced ? 1 : 0
19627
+ ]
19628
+ });
19629
+ const state = await getConnectedAccountBillingState(input.userId);
19630
+ if (!state) throw new Error("connected account billing state was not persisted");
19631
+ return state;
19632
+ }
19633
+ var ConnectedAccountBillingError = class extends Error {
19634
+ constructor(message, code, status = 409) {
19635
+ super(message);
19636
+ this.code = code;
19637
+ this.status = status;
19638
+ this.name = "ConnectedAccountBillingError";
19639
+ }
19640
+ code;
19641
+ status;
19642
+ };
19643
+ function connectedAccountPriceId() {
19644
+ const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
19645
+ if (configured) return configured;
19646
+ return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
19647
+ }
19648
+ function connectedAccountBillingView(actualQuantity, state) {
19649
+ const quantity = Math.max(0, Math.round(actualQuantity));
19650
+ return {
19651
+ provider: "nango",
19652
+ billingMode: "flat_recurring_usd_plus_credits",
19653
+ priceId: connectedAccountPriceId(),
19654
+ unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
19655
+ interval: "month",
19656
+ quantity,
19657
+ projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
19658
+ status: state?.status ?? "pending",
19659
+ lastErrorCode: state?.last_error_code ?? null,
19660
+ syncedAt: state?.synced_at ?? null,
19661
+ usage: CONNECTED_USAGE_RATE_POLICY.usage
19662
+ };
19663
+ }
19664
+ function findBasePlanItem(subscription) {
19665
+ return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
19666
+ }
19667
+ function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
19668
+ return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
19669
+ }
19670
+ function liveSubscription(subscription) {
19671
+ return subscription.status === "active" || subscription.status === "trialing";
19672
+ }
19673
+ function safeStripeErrorCode(error) {
19674
+ if (error instanceof ConnectedAccountBillingError) return error.code;
19675
+ return "stripe_connection_billing_failed";
19676
+ }
19677
+ async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
19678
+ await setConnectedAccountBillingState({
19679
+ userId: user.id,
19680
+ stripeSubscriptionId: user.subscription_id,
19681
+ stripeSubscriptionItemId: subscriptionItemId,
19682
+ priceId: connectedAccountPriceId(),
19683
+ quantity: actualQuantity,
19684
+ status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
19685
+ lastErrorCode: safeStripeErrorCode(error),
19686
+ synced: false
19687
+ });
19688
+ }
19689
+ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
19690
+ if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
19691
+ throw new Error("actualQuantity must be a non-negative safe integer");
19692
+ }
19693
+ const priceId = connectedAccountPriceId();
19694
+ if (!priceId) {
19695
+ const error = new ConnectedAccountBillingError(
19696
+ "Connected-account billing is not configured.",
19697
+ "connected_account_price_not_configured",
19698
+ 503
19699
+ );
19700
+ await persistFailure(user, actualQuantity, error);
19701
+ throw error;
19702
+ }
19703
+ if (!user.subscription_id) {
19704
+ if (actualQuantity === 0) {
19705
+ const state = await setConnectedAccountBillingState({
19706
+ userId: user.id,
19707
+ stripeSubscriptionId: null,
19708
+ stripeSubscriptionItemId: null,
19709
+ priceId,
19710
+ quantity: 0,
19711
+ status: "synced",
19712
+ lastErrorCode: null,
19713
+ synced: true
19714
+ });
19715
+ return connectedAccountBillingView(0, state);
19716
+ }
19717
+ const error = new ConnectedAccountBillingError(
19718
+ "An active paid plan is required before connected accounts can be billed.",
19719
+ "paid_plan_required",
19720
+ 403
19721
+ );
19722
+ await persistFailure(user, actualQuantity, error);
19723
+ throw error;
19724
+ }
19725
+ const client2 = stripeClient ?? (() => {
19726
+ const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
19727
+ if (!secret2) {
19728
+ throw new ConnectedAccountBillingError(
19729
+ "Stripe is not configured.",
19730
+ "connected_account_price_not_configured",
19731
+ 503
19732
+ );
19733
+ }
19734
+ return new Stripe(secret2, { apiVersion: STRIPE_API_VERSION });
19735
+ })();
19736
+ let currentItem;
19737
+ try {
19738
+ const subscription = await client2.subscriptions.retrieve(user.subscription_id);
19739
+ currentItem = findConnectedAccountItem(subscription, priceId);
19740
+ if (!liveSubscription(subscription)) {
19741
+ throw new ConnectedAccountBillingError(
19742
+ "The plan subscription is not active.",
19743
+ "subscription_not_active",
19744
+ 402
19745
+ );
19746
+ }
19747
+ if (!findBasePlanItem(subscription)) {
19748
+ throw new ConnectedAccountBillingError(
19749
+ "The base plan item could not be found on this subscription.",
19750
+ "subscription_plan_item_missing",
19751
+ 409
19752
+ );
19753
+ }
19754
+ const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
19755
+ let resultingItem = currentItem;
19756
+ if (actualQuantity !== currentQuantity) {
19757
+ const updated = await client2.subscriptions.update(subscription.id, {
19758
+ items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
19759
+ proration_behavior: "create_prorations"
19760
+ });
19761
+ resultingItem = findConnectedAccountItem(updated, priceId);
19762
+ }
19763
+ const state = await setConnectedAccountBillingState({
19764
+ userId: user.id,
19765
+ stripeSubscriptionId: subscription.id,
19766
+ stripeSubscriptionItemId: resultingItem?.id ?? null,
19767
+ priceId,
19768
+ quantity: actualQuantity,
19769
+ status: "synced",
19770
+ lastErrorCode: null,
19771
+ synced: true
19772
+ });
19773
+ return connectedAccountBillingView(actualQuantity, state);
19774
+ } catch (error) {
19775
+ await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
19776
+ if (error instanceof ConnectedAccountBillingError) throw error;
19777
+ throw new ConnectedAccountBillingError(
19778
+ "Unable to update connected-account billing in Stripe.",
19779
+ "stripe_connection_billing_failed",
19780
+ 503
19781
+ );
19782
+ }
19783
+ }
19784
+ async function currentConnectedAccountBillingView(userId, actualQuantity) {
19785
+ return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
19786
+ }
19787
+
19788
+ // src/api/stripe-routes.ts
19789
+ var stripe = new Stripe2(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
19560
19790
  var stripeApp = new Hono19();
19561
19791
  stripeApp.post("/webhooks", async (c) => {
19562
19792
  const sig = c.req.header("stripe-signature");
@@ -19573,15 +19803,23 @@ stripeApp.post("/webhooks", async (c) => {
19573
19803
  const invoice = event.data.object;
19574
19804
  const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
19575
19805
  if (memLineId) return c.json({ received: true });
19576
- const lineTierId = invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
19577
- if (lineTierId) {
19578
- const tier = SUBSCRIPTION_TIERS[lineTierId];
19579
- const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
19580
- if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
19581
- await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
19582
- await setSubscriptionTier(user.id, tier.tier, tier.concurrency, invoice.subscription ?? user.subscription_id);
19583
- const credentials = await syncScheduledActionCredentials(user);
19584
- if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
19806
+ const invoiceHasTierLine = invoice.lines.data.some((l) => {
19807
+ const id = linePriceId(l);
19808
+ return id && id in SUBSCRIPTION_TIERS;
19809
+ });
19810
+ if (invoiceHasTierLine) {
19811
+ const subId = invoice.subscription;
19812
+ const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
19813
+ const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
19814
+ const tier = tierPriceId ? SUBSCRIPTION_TIERS[tierPriceId] : void 0;
19815
+ if (tier) {
19816
+ const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
19817
+ if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
19818
+ await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
19819
+ await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
19820
+ const credentials = await syncScheduledActionCredentials(user);
19821
+ if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
19822
+ }
19585
19823
  }
19586
19824
  }
19587
19825
  }
@@ -22832,8 +23070,8 @@ async function reconcileDiscoveredNangoConnections(identity, discovered) {
22832
23070
  sql: `
22833
23071
  INSERT INTO service_connections (
22834
23072
  id, user_id, provider_config_key, provider, transport, upstream_connection_id,
22835
- label, lifecycle_status, reconnect_required, legacy_owner_identity, created_at, updated_at
22836
- ) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
23073
+ label, lifecycle_status, reconnect_required, actions_enabled, legacy_owner_identity, created_at, updated_at
23074
+ ) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, 1, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
22837
23075
  ON CONFLICT(user_id, provider_config_key, upstream_connection_id) DO UPDATE SET
22838
23076
  provider = excluded.provider,
22839
23077
  label = COALESCE(excluded.label, service_connections.label),
@@ -24902,217 +25140,6 @@ async function verifySchedulerIntegrationRequest(request, rawBody) {
24902
25140
  return { requestId };
24903
25141
  }
24904
25142
 
24905
- // src/api/connected-account-billing.ts
24906
- import Stripe2 from "stripe";
24907
- var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
24908
- var STRIPE_API_VERSION = "2026-02-25.clover";
24909
- async function ensureConnectedAccountBillingSchema() {
24910
- await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
24911
- user_id INTEGER PRIMARY KEY,
24912
- stripe_subscription_id TEXT,
24913
- stripe_subscription_item_id TEXT,
24914
- price_id TEXT,
24915
- quantity INTEGER NOT NULL DEFAULT 0,
24916
- status TEXT NOT NULL DEFAULT 'pending',
24917
- last_error_code TEXT,
24918
- synced_at TEXT,
24919
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
24920
- )`);
24921
- }
24922
- async function getConnectedAccountBillingState(userId) {
24923
- await ensureConnectedAccountBillingSchema();
24924
- const result = await getDb().execute({
24925
- sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
24926
- args: [Number(userId)]
24927
- });
24928
- return result.rows[0] ? result.rows[0] : null;
24929
- }
24930
- async function setConnectedAccountBillingState(input) {
24931
- await ensureConnectedAccountBillingSchema();
24932
- await getDb().execute({
24933
- sql: `INSERT INTO connected_account_billing
24934
- (user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
24935
- VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
24936
- ON CONFLICT(user_id) DO UPDATE SET
24937
- stripe_subscription_id = excluded.stripe_subscription_id,
24938
- stripe_subscription_item_id = excluded.stripe_subscription_item_id,
24939
- price_id = excluded.price_id,
24940
- quantity = excluded.quantity,
24941
- status = excluded.status,
24942
- last_error_code = excluded.last_error_code,
24943
- synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
24944
- updated_at = datetime('now')`,
24945
- args: [
24946
- Number(input.userId),
24947
- input.stripeSubscriptionId,
24948
- input.stripeSubscriptionItemId,
24949
- input.priceId,
24950
- input.quantity,
24951
- input.status,
24952
- input.lastErrorCode,
24953
- input.synced ? 1 : 0,
24954
- input.synced ? 1 : 0
24955
- ]
24956
- });
24957
- const state = await getConnectedAccountBillingState(input.userId);
24958
- if (!state) throw new Error("connected account billing state was not persisted");
24959
- return state;
24960
- }
24961
- var ConnectedAccountBillingError = class extends Error {
24962
- constructor(message, code, status = 409) {
24963
- super(message);
24964
- this.code = code;
24965
- this.status = status;
24966
- this.name = "ConnectedAccountBillingError";
24967
- }
24968
- code;
24969
- status;
24970
- };
24971
- function connectedAccountPriceId() {
24972
- const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
24973
- if (configured) return configured;
24974
- return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
24975
- }
24976
- function connectedAccountBillingView(actualQuantity, state) {
24977
- const quantity = Math.max(0, Math.round(actualQuantity));
24978
- return {
24979
- provider: "nango",
24980
- billingMode: "flat_recurring_usd_plus_credits",
24981
- priceId: connectedAccountPriceId(),
24982
- unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
24983
- interval: "month",
24984
- quantity,
24985
- projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
24986
- status: state?.status ?? "pending",
24987
- lastErrorCode: state?.last_error_code ?? null,
24988
- syncedAt: state?.synced_at ?? null,
24989
- usage: CONNECTED_USAGE_RATE_POLICY.usage
24990
- };
24991
- }
24992
- function findBasePlanItem(subscription) {
24993
- return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
24994
- }
24995
- function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
24996
- return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
24997
- }
24998
- function liveSubscription(subscription) {
24999
- return subscription.status === "active" || subscription.status === "trialing";
25000
- }
25001
- function safeStripeErrorCode(error) {
25002
- if (error instanceof ConnectedAccountBillingError) return error.code;
25003
- return "stripe_connection_billing_failed";
25004
- }
25005
- async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
25006
- await setConnectedAccountBillingState({
25007
- userId: user.id,
25008
- stripeSubscriptionId: user.subscription_id,
25009
- stripeSubscriptionItemId: subscriptionItemId,
25010
- priceId: connectedAccountPriceId(),
25011
- quantity: actualQuantity,
25012
- status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
25013
- lastErrorCode: safeStripeErrorCode(error),
25014
- synced: false
25015
- });
25016
- }
25017
- async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
25018
- if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
25019
- throw new Error("actualQuantity must be a non-negative safe integer");
25020
- }
25021
- const priceId = connectedAccountPriceId();
25022
- if (!priceId) {
25023
- const error = new ConnectedAccountBillingError(
25024
- "Connected-account billing is not configured.",
25025
- "connected_account_price_not_configured",
25026
- 503
25027
- );
25028
- await persistFailure(user, actualQuantity, error);
25029
- throw error;
25030
- }
25031
- if (!user.subscription_id) {
25032
- if (actualQuantity === 0) {
25033
- const state = await setConnectedAccountBillingState({
25034
- userId: user.id,
25035
- stripeSubscriptionId: null,
25036
- stripeSubscriptionItemId: null,
25037
- priceId,
25038
- quantity: 0,
25039
- status: "synced",
25040
- lastErrorCode: null,
25041
- synced: true
25042
- });
25043
- return connectedAccountBillingView(0, state);
25044
- }
25045
- const error = new ConnectedAccountBillingError(
25046
- "An active paid plan is required before connected accounts can be billed.",
25047
- "paid_plan_required",
25048
- 403
25049
- );
25050
- await persistFailure(user, actualQuantity, error);
25051
- throw error;
25052
- }
25053
- const client2 = stripeClient ?? (() => {
25054
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
25055
- if (!secret2) {
25056
- throw new ConnectedAccountBillingError(
25057
- "Stripe is not configured.",
25058
- "connected_account_price_not_configured",
25059
- 503
25060
- );
25061
- }
25062
- return new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
25063
- })();
25064
- let currentItem;
25065
- try {
25066
- const subscription = await client2.subscriptions.retrieve(user.subscription_id);
25067
- currentItem = findConnectedAccountItem(subscription, priceId);
25068
- if (!liveSubscription(subscription)) {
25069
- throw new ConnectedAccountBillingError(
25070
- "The plan subscription is not active.",
25071
- "subscription_not_active",
25072
- 402
25073
- );
25074
- }
25075
- if (!findBasePlanItem(subscription)) {
25076
- throw new ConnectedAccountBillingError(
25077
- "The base plan item could not be found on this subscription.",
25078
- "subscription_plan_item_missing",
25079
- 409
25080
- );
25081
- }
25082
- const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
25083
- let resultingItem = currentItem;
25084
- if (actualQuantity !== currentQuantity) {
25085
- const updated = await client2.subscriptions.update(subscription.id, {
25086
- items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
25087
- proration_behavior: "create_prorations"
25088
- });
25089
- resultingItem = findConnectedAccountItem(updated, priceId);
25090
- }
25091
- const state = await setConnectedAccountBillingState({
25092
- userId: user.id,
25093
- stripeSubscriptionId: subscription.id,
25094
- stripeSubscriptionItemId: resultingItem?.id ?? null,
25095
- priceId,
25096
- quantity: actualQuantity,
25097
- status: "synced",
25098
- lastErrorCode: null,
25099
- synced: true
25100
- });
25101
- return connectedAccountBillingView(actualQuantity, state);
25102
- } catch (error) {
25103
- await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
25104
- if (error instanceof ConnectedAccountBillingError) throw error;
25105
- throw new ConnectedAccountBillingError(
25106
- "Unable to update connected-account billing in Stripe.",
25107
- "stripe_connection_billing_failed",
25108
- 503
25109
- );
25110
- }
25111
- }
25112
- async function currentConnectedAccountBillingView(userId, actualQuantity) {
25113
- return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
25114
- }
25115
-
25116
25143
  // src/api/server.ts
25117
25144
  var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
25118
25145
  var isProduction2 = secureCookies2;
@@ -26097,7 +26124,7 @@ app.post("/schedule-connections/actions/slack/send-message", auth2, requireInteg
26097
26124
  user.email,
26098
26125
  connectionId,
26099
26126
  { channel: body.channel, text: body.text },
26100
- void 0,
26127
+ "send-message",
26101
26128
  connectedActionIdempotencyKey(c)
26102
26129
  );
26103
26130
  return c.json({ ok: true, result });
@@ -26117,7 +26144,7 @@ app.post("/schedule-connections/actions/gmail/send-message", auth2, requireInteg
26117
26144
  user.email,
26118
26145
  connectionId,
26119
26146
  { to: body.to, subject: body.subject, body: body.body },
26120
- void 0,
26147
+ "send-message",
26121
26148
  connectedActionIdempotencyKey(c)
26122
26149
  );
26123
26150
  return c.json({ ok: true, result });
@@ -26125,6 +26152,76 @@ app.post("/schedule-connections/actions/gmail/send-message", auth2, requireInteg
26125
26152
  return scheduleConnectionError(c, err, "Unable to send the email.");
26126
26153
  }
26127
26154
  });
26155
+ function toolResultText(result) {
26156
+ if (!result || typeof result !== "object") return null;
26157
+ const content = result.content;
26158
+ if (!Array.isArray(content)) return null;
26159
+ const textItem = content.find((item) => item && typeof item === "object" && item.type === "text");
26160
+ const text2 = textItem?.text;
26161
+ return typeof text2 === "string" ? text2 : null;
26162
+ }
26163
+ function parseEmailAddress(headerValue) {
26164
+ const value = headerValue?.trim();
26165
+ if (!value) return null;
26166
+ const angled = value.match(/^"?([^"<]*?)"?\s*<([^>]+)>$/);
26167
+ if (angled) return { email: angled[2].trim().toLowerCase(), name: angled[1].trim() || null };
26168
+ const bare = value.match(/^([^\s<>]+@[^\s<>]+)$/);
26169
+ return bare ? { email: bare[1].trim().toLowerCase(), name: null } : null;
26170
+ }
26171
+ app.post("/schedule-connections/actions/gmail/search-contacts", auth2, requireIntegrationsTier, async (c) => {
26172
+ const user = c.get("user");
26173
+ const body = await c.req.json().catch(() => ({}));
26174
+ const connectionId = providerConfigKeyFrom(body.connectionId);
26175
+ const query = typeof body.query === "string" ? body.query.trim() : "";
26176
+ const maxMessages = Math.min(150, Math.max(1, Number.isFinite(body.maxMessages) ? Number(body.maxMessages) : 50));
26177
+ const pageToken = typeof body.pageToken === "string" ? body.pageToken : void 0;
26178
+ if (!connectionId || !query) return c.json({ ok: false, error: "connectionId and query are required." }, 400);
26179
+ try {
26180
+ const listRaw = await callScheduleConnectionRead(user.email, connectionId, "list-messages", { q: query, maxResults: maxMessages, pageToken });
26181
+ const listText = toolResultText(listRaw);
26182
+ const list = listText ? JSON.parse(listText) : {};
26183
+ const ids = (list.messages ?? []).map((m) => m.id);
26184
+ const messages = [];
26185
+ const contacts = /* @__PURE__ */ new Map();
26186
+ for (const id of ids) {
26187
+ const msgRaw = await callScheduleConnectionRead(user.email, connectionId, "get-message", { id, format: "metadata" });
26188
+ const msgText = toolResultText(msgRaw);
26189
+ if (!msgText) continue;
26190
+ const msg = JSON.parse(msgText);
26191
+ const headers = new Map((msg.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
26192
+ const date = headers.get("date") ?? null;
26193
+ const fromRaw = headers.get("from");
26194
+ const from = parseEmailAddress(fromRaw);
26195
+ const subject = headers.get("subject") ?? null;
26196
+ messages.push({ id, date, from: fromRaw ?? null, to: headers.get("to") ?? null, subject, snippet: msg.snippet ?? null });
26197
+ if (from) {
26198
+ const domain = from.email.split("@")[1] ?? null;
26199
+ const existing = contacts.get(from.email);
26200
+ if (existing) {
26201
+ existing.messageCount += 1;
26202
+ existing.name = existing.name ?? from.name;
26203
+ if (subject && existing.sampleSubjects.length < 5 && !existing.sampleSubjects.includes(subject)) existing.sampleSubjects.push(subject);
26204
+ if (date && (!existing.lastSeen || date > existing.lastSeen)) existing.lastSeen = date;
26205
+ if (date && (!existing.firstSeen || date < existing.firstSeen)) existing.firstSeen = date;
26206
+ } else {
26207
+ contacts.set(from.email, { email: from.email, name: from.name, domain, messageCount: 1, firstSeen: date, lastSeen: date, sampleSubjects: subject ? [subject] : [] });
26208
+ }
26209
+ }
26210
+ }
26211
+ const totalMatches = list.resultSizeEstimate;
26212
+ return c.json({
26213
+ ok: true,
26214
+ totalMatches,
26215
+ messagesFetched: messages.length,
26216
+ truncated: typeof totalMatches === "number" ? totalMatches > messages.length : Boolean(list.nextPageToken),
26217
+ nextPageToken: list.nextPageToken ?? null,
26218
+ messages,
26219
+ contacts: Array.from(contacts.values())
26220
+ });
26221
+ } catch (err) {
26222
+ return scheduleConnectionError(c, err, "Unable to search Gmail contacts.");
26223
+ }
26224
+ });
26128
26225
  app.post("/schedule-connections/actions/google-calendar/create-event", auth2, requireIntegrationsTier, async (c) => {
26129
26226
  const user = c.get("user");
26130
26227
  const body = await c.req.json().catch(() => ({}));
@@ -26143,7 +26240,7 @@ app.post("/schedule-connections/actions/google-calendar/create-event", auth2, re
26143
26240
  start: { dateTime: body.startDateTime, timeZone },
26144
26241
  end: { dateTime: body.endDateTime, timeZone },
26145
26242
  attendees
26146
- }, void 0, connectedActionIdempotencyKey(c));
26243
+ }, "create-event", connectedActionIdempotencyKey(c));
26147
26244
  return c.json({ ok: true, result });
26148
26245
  } catch (err) {
26149
26246
  return scheduleConnectionError(c, err, "Unable to create the calendar event.");
@@ -26163,7 +26260,7 @@ app.post("/schedule-connections/actions/zoom/create-meeting", auth2, requireInte
26163
26260
  durationMinutes: typeof body.durationMinutes === "number" ? body.durationMinutes : 30,
26164
26261
  timezone: typeof body.timezone === "string" ? body.timezone : void 0,
26165
26262
  agenda: body.agenda
26166
- }, void 0, connectedActionIdempotencyKey(c));
26263
+ }, "create-meeting", connectedActionIdempotencyKey(c));
26167
26264
  return c.json({ ok: true, result });
26168
26265
  } catch (err) {
26169
26266
  return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
@@ -27191,6 +27288,22 @@ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, asy
27191
27288
  return c.json({ error: message }, 500);
27192
27289
  }
27193
27290
  });
27291
+ async function chargeTierChangeNow(stripeClient, subscriptionId, customerId) {
27292
+ try {
27293
+ const draft = await stripeClient.invoices.create({
27294
+ customer: customerId,
27295
+ subscription: subscriptionId,
27296
+ auto_advance: true,
27297
+ collection_method: "charge_automatically"
27298
+ });
27299
+ const finalized = await stripeClient.invoices.finalizeInvoice(draft.id);
27300
+ if (finalized.amount_due <= 0 || finalized.status === "paid") return { ok: true, amountDue: finalized.amount_due };
27301
+ const paid = await stripeClient.invoices.pay(finalized.id);
27302
+ return { ok: paid.status === "paid", amountDue: paid.amount_due };
27303
+ } catch (err) {
27304
+ return { ok: false, amountDue: 0, error: err instanceof Error ? err.message : "Unable to charge the plan change immediately." };
27305
+ }
27306
+ }
27194
27307
  app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
27195
27308
  try {
27196
27309
  const user = c.get("sessionUser");
@@ -27214,7 +27327,16 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
27214
27327
  items: [{ id: itemId, price: tier.price_id }],
27215
27328
  proration_behavior: "create_prorations"
27216
27329
  });
27217
- return c.json({ updated: true, tier: tier.tier });
27330
+ const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
27331
+ if (!billed.ok) {
27332
+ return c.json({
27333
+ updated: true,
27334
+ tier: tier.tier,
27335
+ billed: false,
27336
+ error: billed.error ?? "Plan changed, but the prorated charge could not be collected. Update your payment method and it will retry automatically."
27337
+ });
27338
+ }
27339
+ return c.json({ updated: true, tier: tier.tier, billed: true });
27218
27340
  }
27219
27341
  }
27220
27342
  const session = await stripeClient.checkout.sessions.create({
@@ -27331,7 +27453,16 @@ app.post("/billing/subscribe/terminal-checkout", auth2, async (c) => {
27331
27453
  items: [{ id: itemId, price: tier.price_id }],
27332
27454
  proration_behavior: "create_prorations"
27333
27455
  });
27334
- return c.json({ updated: true, tier: tier.tier, message: `Switched to ${tier.label} (prorated). Credits and concurrency update on the next invoice.` });
27456
+ const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
27457
+ if (!billed.ok) {
27458
+ return c.json({
27459
+ updated: true,
27460
+ tier: tier.tier,
27461
+ billed: false,
27462
+ message: `Switched to ${tier.label} (prorated), but the prorated charge could not be collected: ${billed.error ?? "unknown error"}. Update your payment method and it will retry automatically.`
27463
+ });
27464
+ }
27465
+ return c.json({ updated: true, tier: tier.tier, billed: true, message: `Switched to ${tier.label} and charged the prorated difference immediately \u2014 credits and concurrency are live now.` });
27335
27466
  }
27336
27467
  }
27337
27468
  const session = await stripeClient.checkout.sessions.create({
@@ -27430,7 +27561,7 @@ app.get("/cron/tick", async (c) => {
27430
27561
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
27431
27562
  return c.json({ error: "Unauthorized" }, 401);
27432
27563
  }
27433
- const { drainQueue } = await import("./worker-ZZCZNY3R.js");
27564
+ const { drainQueue } = await import("./worker-JU5FX3NE.js");
27434
27565
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
27435
27566
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
27436
27567
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
@@ -27633,4 +27764,4 @@ app.get("/blog/:slug/", (c) => {
27633
27764
  export {
27634
27765
  app
27635
27766
  };
27636
- //# sourceMappingURL=server-LTZDJHFG.js.map
27767
+ //# sourceMappingURL=server-RKAKXRAR.js.map