mcp-scraper 0.32.2 → 0.32.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19262,6 +19262,22 @@ function buildGoogleOrganicSearchUrl(input) {
19262
19262
  if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
19263
19263
  return `https://www.google.com/search?${params.toString()}`;
19264
19264
  }
19265
+ function mapsIdentifiersFromUrl(url) {
19266
+ 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);
19267
+ if (fid) {
19268
+ try {
19269
+ return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: BigInt(fid[2]).toString() };
19270
+ } catch {
19271
+ return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: null };
19272
+ }
19273
+ }
19274
+ try {
19275
+ const decimal = new URL(url).searchParams.get("cid");
19276
+ return { cid: null, cidDecimal: decimal && /^\d+$/.test(decimal) ? decimal : null };
19277
+ } catch {
19278
+ return { cid: null, cidDecimal: null };
19279
+ }
19280
+ }
19265
19281
  var LOCAL_RESULTS_PAGE_LIMIT, LOCAL_RESULTS_WAIT_MS, MAPS_REDIRECT_BASE, MapsSearchExtractor;
19266
19282
  var init_MapsSearchExtractor = __esm({
19267
19283
  "src/extractor/MapsSearchExtractor.ts"() {
@@ -19347,13 +19363,15 @@ var init_MapsSearchExtractor = __esm({
19347
19363
  for (const card of cards) {
19348
19364
  if (results.length >= options.maxResults || seen.has(card.cardKey)) continue;
19349
19365
  seen.add(card.cardKey);
19366
+ const cardIds = mapsIdentifiersFromUrl(card.directionsUrl ?? "");
19367
+ const cardPlaceUrl = cardIds.cidDecimal ? `https://www.google.com/maps?cid=${cardIds.cidDecimal}` : card.placeUrl;
19350
19368
  const details = await this.openCardAndExtractDialog(page, card, options.includeServices);
19351
19369
  results.push({
19352
19370
  position: results.length + 1,
19353
19371
  name: card.name,
19354
- placeUrl: details?.placeUrl ?? card.placeUrl,
19355
- cid: details?.cid ?? card.cid,
19356
- cidDecimal: details?.cidDecimal ?? card.cidDecimal,
19372
+ placeUrl: details?.placeUrl ?? cardPlaceUrl,
19373
+ cid: details?.cid ?? cardIds.cid,
19374
+ cidDecimal: details?.cidDecimal ?? cardIds.cidDecimal,
19357
19375
  rating: details?.rating ?? card.rating,
19358
19376
  reviewCount: details?.reviewCount ?? card.reviewCount,
19359
19377
  category: details?.category ?? card.category,
@@ -19410,8 +19428,10 @@ var init_MapsSearchExtractor = __esm({
19410
19428
  const phone = lines.map((line) => line.match(phonePattern)?.[0]).find((value) => Boolean(value)) ?? null;
19411
19429
  const hoursStatus = normalize4((lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? "").split("\xB7")[0] ?? "");
19412
19430
  const address = lines.find((line) => addressPattern.test(line)) ?? null;
19413
- const websiteUrl = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href).find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
19431
+ const anchorHrefs = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href);
19432
+ const websiteUrl = anchorHrefs.find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
19414
19433
  if (!rating && !(phone && websiteUrl)) continue;
19434
+ const fidHref = anchorHrefs.find((href) => /(0x[0-9a-f]+):(0x[0-9a-f]+)/i.test(href)) ?? null;
19415
19435
  const key = `${name.toLowerCase()}|${lines.join(" ").toLowerCase()}`;
19416
19436
  if (seen.has(key)) continue;
19417
19437
  seen.add(key);
@@ -19431,7 +19451,7 @@ var init_MapsSearchExtractor = __esm({
19431
19451
  phone,
19432
19452
  hoursStatus,
19433
19453
  websiteUrl,
19434
- directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
19454
+ directionsUrl: fidHref ?? `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
19435
19455
  metadata: lines.slice(0, 20)
19436
19456
  });
19437
19457
  }
@@ -29794,7 +29814,7 @@ var PACKAGE_VERSION;
29794
29814
  var init_version = __esm({
29795
29815
  "src/version.ts"() {
29796
29816
  "use strict";
29797
- PACKAGE_VERSION = "0.32.2";
29817
+ PACKAGE_VERSION = "0.32.3";
29798
29818
  }
29799
29819
  });
29800
29820
 
@@ -30245,7 +30265,7 @@ var init_meta_ad_creative_media = __esm({
30245
30265
  });
30246
30266
 
30247
30267
  // src/mcp/mcp-tool-schemas.ts
30248
- var import_zod35, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, CheckSiteExportInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, CheckSiteExportOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, TranscriptSignalOutput, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema, ListServiceConnectionsInputSchema, ListServiceConnectionsOutputSchema, TestServiceConnectionInputSchema, TestServiceConnectionOutputSchema, ReadServiceConnectionInputSchema, ReadServiceConnectionOutputSchema, MetaAdCreativeMediaInputSchema, MetaAdCreativeMediaOutputSchema, ImportServiceConnectionToMemoryInputSchema, ImportServiceConnectionToMemoryOutputSchema, DescribeServiceConnectionToolInputSchema, DescribeServiceConnectionToolOutputSchema, ConnectedDataContinuationSchema, ExportConnectedServiceDataInputSchema, ConnectedDataArtifactSchema, ExportConnectedServiceDataOutputSchema, SearchConsoleTableColumnSchema, SearchConsoleTableFilterSchema, ExportSearchConsoleTableDataInputSchema, ExportSearchConsoleTableDataOutputSchema, RenewConnectedDataExportDownloadInputSchema, RenewConnectedDataExportDownloadOutputSchema, CallServiceConnectionActionInputSchema, CallServiceConnectionActionOutputSchema, SetScheduledActionConnectionsInputSchema, SetScheduledActionConnectionsOutputSchema, SlackSendMessageInputSchema, SlackSendMessageOutputSchema, GmailSendMessageInputSchema, GmailSendMessageOutputSchema, GoogleCalendarCreateEventInputSchema, GoogleCalendarCreateEventOutputSchema, ZoomCreateMeetingInputSchema, ZoomCreateMeetingOutputSchema;
30268
+ var import_zod35, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, CheckSiteExportInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, CheckSiteExportOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, TranscriptSignalOutput, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema, ListServiceConnectionsInputSchema, ListServiceConnectionsOutputSchema, TestServiceConnectionInputSchema, TestServiceConnectionOutputSchema, ReadServiceConnectionInputSchema, ReadServiceConnectionOutputSchema, MetaAdCreativeMediaInputSchema, MetaAdCreativeMediaOutputSchema, ImportServiceConnectionToMemoryInputSchema, ImportServiceConnectionToMemoryOutputSchema, DescribeServiceConnectionToolInputSchema, DescribeServiceConnectionToolOutputSchema, ConnectedDataContinuationSchema, ExportConnectedServiceDataInputSchema, ConnectedDataArtifactSchema, ExportConnectedServiceDataOutputSchema, SearchConsoleTableColumnSchema, SearchConsoleTableFilterSchema, ExportSearchConsoleTableDataInputSchema, ExportSearchConsoleTableDataOutputSchema, RenewConnectedDataExportDownloadInputSchema, RenewConnectedDataExportDownloadOutputSchema, CallServiceConnectionActionInputSchema, CallServiceConnectionActionOutputSchema, SetScheduledActionConnectionsInputSchema, SetScheduledActionConnectionsOutputSchema, SlackSendMessageInputSchema, SlackSendMessageOutputSchema, GmailSendMessageInputSchema, GmailSendMessageOutputSchema, GmailSearchContactsInputSchema, GmailSearchContactsOutputSchema, GoogleCalendarCreateEventInputSchema, GoogleCalendarCreateEventOutputSchema, ZoomCreateMeetingInputSchema, ZoomCreateMeetingOutputSchema;
30249
30269
  var init_mcp_tool_schemas = __esm({
30250
30270
  "src/mcp/mcp-tool-schemas.ts"() {
30251
30271
  "use strict";
@@ -31652,6 +31672,37 @@ var init_mcp_tool_schemas = __esm({
31652
31672
  result: import_zod35.z.unknown().optional(),
31653
31673
  error: NullableString
31654
31674
  };
31675
+ GmailSearchContactsInputSchema = {
31676
+ connectionId: import_zod35.z.string().min(1).describe("A Gmail connectionId from list_service_connections. Read-only \u2014 does not require actionsEnabled."),
31677
+ query: import_zod35.z.string().min(1).max(500).describe('Gmail search syntax, e.g. "from:brandnorth.com after:2026/03/22" or "brandnorth.com".'),
31678
+ maxMessages: import_zod35.z.number().int().min(1).max(150).default(50).describe("Max messages to fetch and aggregate in this call. Paginate with pageToken for more; the response reports totalMatches and truncated so undercoverage is never silent."),
31679
+ pageToken: import_zod35.z.string().optional().describe("Continuation token from a prior response to fetch the next page.")
31680
+ };
31681
+ GmailSearchContactsOutputSchema = {
31682
+ ok: import_zod35.z.boolean(),
31683
+ totalMatches: import_zod35.z.number().optional().describe("Gmail's estimated total matches for the query, independent of how many were fetched this call."),
31684
+ messagesFetched: import_zod35.z.number().optional(),
31685
+ truncated: import_zod35.z.boolean().optional().describe("True when totalMatches exceeds messagesFetched \u2014 more results exist, use nextPageToken."),
31686
+ nextPageToken: NullableString,
31687
+ messages: import_zod35.z.array(import_zod35.z.object({
31688
+ id: import_zod35.z.string(),
31689
+ date: NullableString,
31690
+ from: NullableString,
31691
+ to: NullableString,
31692
+ subject: NullableString,
31693
+ snippet: NullableString
31694
+ })).optional(),
31695
+ contacts: import_zod35.z.array(import_zod35.z.object({
31696
+ email: import_zod35.z.string(),
31697
+ name: NullableString,
31698
+ domain: NullableString,
31699
+ messageCount: import_zod35.z.number(),
31700
+ firstSeen: NullableString,
31701
+ lastSeen: NullableString,
31702
+ sampleSubjects: import_zod35.z.array(import_zod35.z.string())
31703
+ })).optional().describe("Messages deduped by sender email address, newest-to-oldest within this fetch only \u2014 not the full query result set when truncated."),
31704
+ error: NullableString
31705
+ };
31655
31706
  GoogleCalendarCreateEventInputSchema = {
31656
31707
  connectionId: import_zod35.z.string().min(1).describe("A Google Calendar connectionId from list_service_connections, with actionsEnabled true."),
31657
31708
  calendarId: import_zod35.z.string().min(1).default("primary").describe('Calendar to create the event in. Default "primary".'),
@@ -32398,6 +32449,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
32398
32449
  outputSchema: recordOutputSchema("gmail_send_message", GmailSendMessageOutputSchema),
32399
32450
  annotations: { title: "Send Gmail Message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
32400
32451
  }, async (input) => executor.gmailSendMessage(input));
32452
+ server.registerTool("gmail_search_contacts", {
32453
+ title: "Search Gmail Contacts",
32454
+ description: 'Search Gmail with standard Gmail query syntax (e.g. "from:acme.com after:2026/03/22") and get back deduplicated sender contacts (email, name, domain, message count, first/last seen, sample subjects) instead of raw messages. Read-only \u2014 works on any connected Gmail connection from list_service_connections, no actionsEnabled required. Use this instead of looping list-messages/get-message yourself: those return bare message IDs and full raw MIME per message, which does not scale past a handful of messages. Reports totalMatches and truncated so incomplete coverage from a large result set is never silent \u2014 pass the returned nextPageToken to continue.',
32455
+ inputSchema: GmailSearchContactsInputSchema,
32456
+ outputSchema: recordOutputSchema("gmail_search_contacts", GmailSearchContactsOutputSchema),
32457
+ annotations: { title: "Search Gmail Contacts", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
32458
+ }, async (input) => executor.gmailSearchContacts(input));
32401
32459
  server.registerTool("google_calendar_create_event", {
32402
32460
  title: "Create Calendar Event",
32403
32461
  description: "Create a complete event on a connected, action-enabled Google Calendar connection. Always preserve the supplied purpose in description, include the Zoom join link when available, and include every explicitly named invitee in attendees. Do not create a bare meeting event. Requires a connectionId from list_service_connections with actionsEnabled true.",
@@ -32807,6 +32865,9 @@ var init_http_mcp_tool_executor = __esm({
32807
32865
  gmailSendMessage(input) {
32808
32866
  return this.call("/schedule-connections/actions/gmail/send-message", input);
32809
32867
  }
32868
+ gmailSearchContacts(input) {
32869
+ return this.call("/schedule-connections/actions/gmail/search-contacts", input);
32870
+ }
32810
32871
  googleCalendarCreateEvent(input) {
32811
32872
  return this.call("/schedule-connections/actions/google-calendar/create-event", input);
32812
32873
  }
@@ -40068,6 +40129,225 @@ var init_browser_agent_console = __esm({
40068
40129
  }
40069
40130
  });
40070
40131
 
40132
+ // src/api/connected-account-billing.ts
40133
+ async function ensureConnectedAccountBillingSchema() {
40134
+ await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
40135
+ user_id INTEGER PRIMARY KEY,
40136
+ stripe_subscription_id TEXT,
40137
+ stripe_subscription_item_id TEXT,
40138
+ price_id TEXT,
40139
+ quantity INTEGER NOT NULL DEFAULT 0,
40140
+ status TEXT NOT NULL DEFAULT 'pending',
40141
+ last_error_code TEXT,
40142
+ synced_at TEXT,
40143
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
40144
+ )`);
40145
+ }
40146
+ async function getConnectedAccountBillingState(userId) {
40147
+ await ensureConnectedAccountBillingSchema();
40148
+ const result = await getDb().execute({
40149
+ sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
40150
+ args: [Number(userId)]
40151
+ });
40152
+ return result.rows[0] ? result.rows[0] : null;
40153
+ }
40154
+ async function setConnectedAccountBillingState(input) {
40155
+ await ensureConnectedAccountBillingSchema();
40156
+ await getDb().execute({
40157
+ sql: `INSERT INTO connected_account_billing
40158
+ (user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
40159
+ VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
40160
+ ON CONFLICT(user_id) DO UPDATE SET
40161
+ stripe_subscription_id = excluded.stripe_subscription_id,
40162
+ stripe_subscription_item_id = excluded.stripe_subscription_item_id,
40163
+ price_id = excluded.price_id,
40164
+ quantity = excluded.quantity,
40165
+ status = excluded.status,
40166
+ last_error_code = excluded.last_error_code,
40167
+ synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
40168
+ updated_at = datetime('now')`,
40169
+ args: [
40170
+ Number(input.userId),
40171
+ input.stripeSubscriptionId,
40172
+ input.stripeSubscriptionItemId,
40173
+ input.priceId,
40174
+ input.quantity,
40175
+ input.status,
40176
+ input.lastErrorCode,
40177
+ input.synced ? 1 : 0,
40178
+ input.synced ? 1 : 0
40179
+ ]
40180
+ });
40181
+ const state = await getConnectedAccountBillingState(input.userId);
40182
+ if (!state) throw new Error("connected account billing state was not persisted");
40183
+ return state;
40184
+ }
40185
+ function connectedAccountPriceId() {
40186
+ const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
40187
+ if (configured) return configured;
40188
+ return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
40189
+ }
40190
+ function connectedAccountBillingView(actualQuantity, state) {
40191
+ const quantity = Math.max(0, Math.round(actualQuantity));
40192
+ return {
40193
+ provider: "nango",
40194
+ billingMode: "flat_recurring_usd_plus_credits",
40195
+ priceId: connectedAccountPriceId(),
40196
+ unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
40197
+ interval: "month",
40198
+ quantity,
40199
+ projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
40200
+ status: state?.status ?? "pending",
40201
+ lastErrorCode: state?.last_error_code ?? null,
40202
+ syncedAt: state?.synced_at ?? null,
40203
+ usage: CONNECTED_USAGE_RATE_POLICY.usage
40204
+ };
40205
+ }
40206
+ function findBasePlanItem(subscription) {
40207
+ return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
40208
+ }
40209
+ function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
40210
+ return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
40211
+ }
40212
+ function liveSubscription(subscription) {
40213
+ return subscription.status === "active" || subscription.status === "trialing";
40214
+ }
40215
+ function safeStripeErrorCode(error) {
40216
+ if (error instanceof ConnectedAccountBillingError) return error.code;
40217
+ return "stripe_connection_billing_failed";
40218
+ }
40219
+ async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
40220
+ await setConnectedAccountBillingState({
40221
+ userId: user.id,
40222
+ stripeSubscriptionId: user.subscription_id,
40223
+ stripeSubscriptionItemId: subscriptionItemId,
40224
+ priceId: connectedAccountPriceId(),
40225
+ quantity: actualQuantity,
40226
+ status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
40227
+ lastErrorCode: safeStripeErrorCode(error),
40228
+ synced: false
40229
+ });
40230
+ }
40231
+ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
40232
+ if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
40233
+ throw new Error("actualQuantity must be a non-negative safe integer");
40234
+ }
40235
+ const priceId = connectedAccountPriceId();
40236
+ if (!priceId) {
40237
+ const error = new ConnectedAccountBillingError(
40238
+ "Connected-account billing is not configured.",
40239
+ "connected_account_price_not_configured",
40240
+ 503
40241
+ );
40242
+ await persistFailure(user, actualQuantity, error);
40243
+ throw error;
40244
+ }
40245
+ if (!user.subscription_id) {
40246
+ if (actualQuantity === 0) {
40247
+ const state = await setConnectedAccountBillingState({
40248
+ userId: user.id,
40249
+ stripeSubscriptionId: null,
40250
+ stripeSubscriptionItemId: null,
40251
+ priceId,
40252
+ quantity: 0,
40253
+ status: "synced",
40254
+ lastErrorCode: null,
40255
+ synced: true
40256
+ });
40257
+ return connectedAccountBillingView(0, state);
40258
+ }
40259
+ const error = new ConnectedAccountBillingError(
40260
+ "An active paid plan is required before connected accounts can be billed.",
40261
+ "paid_plan_required",
40262
+ 403
40263
+ );
40264
+ await persistFailure(user, actualQuantity, error);
40265
+ throw error;
40266
+ }
40267
+ const client2 = stripeClient ?? (() => {
40268
+ const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
40269
+ if (!secret2) {
40270
+ throw new ConnectedAccountBillingError(
40271
+ "Stripe is not configured.",
40272
+ "connected_account_price_not_configured",
40273
+ 503
40274
+ );
40275
+ }
40276
+ return new import_stripe.default(secret2, { apiVersion: STRIPE_API_VERSION });
40277
+ })();
40278
+ let currentItem;
40279
+ try {
40280
+ const subscription = await client2.subscriptions.retrieve(user.subscription_id);
40281
+ currentItem = findConnectedAccountItem(subscription, priceId);
40282
+ if (!liveSubscription(subscription)) {
40283
+ throw new ConnectedAccountBillingError(
40284
+ "The plan subscription is not active.",
40285
+ "subscription_not_active",
40286
+ 402
40287
+ );
40288
+ }
40289
+ if (!findBasePlanItem(subscription)) {
40290
+ throw new ConnectedAccountBillingError(
40291
+ "The base plan item could not be found on this subscription.",
40292
+ "subscription_plan_item_missing",
40293
+ 409
40294
+ );
40295
+ }
40296
+ const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
40297
+ let resultingItem = currentItem;
40298
+ if (actualQuantity !== currentQuantity) {
40299
+ const updated = await client2.subscriptions.update(subscription.id, {
40300
+ items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
40301
+ proration_behavior: "create_prorations"
40302
+ });
40303
+ resultingItem = findConnectedAccountItem(updated, priceId);
40304
+ }
40305
+ const state = await setConnectedAccountBillingState({
40306
+ userId: user.id,
40307
+ stripeSubscriptionId: subscription.id,
40308
+ stripeSubscriptionItemId: resultingItem?.id ?? null,
40309
+ priceId,
40310
+ quantity: actualQuantity,
40311
+ status: "synced",
40312
+ lastErrorCode: null,
40313
+ synced: true
40314
+ });
40315
+ return connectedAccountBillingView(actualQuantity, state);
40316
+ } catch (error) {
40317
+ await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
40318
+ if (error instanceof ConnectedAccountBillingError) throw error;
40319
+ throw new ConnectedAccountBillingError(
40320
+ "Unable to update connected-account billing in Stripe.",
40321
+ "stripe_connection_billing_failed",
40322
+ 503
40323
+ );
40324
+ }
40325
+ }
40326
+ async function currentConnectedAccountBillingView(userId, actualQuantity) {
40327
+ return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
40328
+ }
40329
+ var import_stripe, DEFAULT_CONNECTED_ACCOUNT_PRICE_ID, STRIPE_API_VERSION, ConnectedAccountBillingError;
40330
+ var init_connected_account_billing = __esm({
40331
+ "src/api/connected-account-billing.ts"() {
40332
+ "use strict";
40333
+ import_stripe = __toESM(require("stripe"), 1);
40334
+ init_db();
40335
+ init_rates();
40336
+ DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
40337
+ STRIPE_API_VERSION = "2026-02-25.clover";
40338
+ ConnectedAccountBillingError = class extends Error {
40339
+ constructor(message, code, status = 409) {
40340
+ super(message);
40341
+ this.code = code;
40342
+ this.status = status;
40343
+ this.name = "ConnectedAccountBillingError";
40344
+ }
40345
+ code;
40346
+ status;
40347
+ };
40348
+ }
40349
+ });
40350
+
40071
40351
  // src/api/stripe-routes.ts
40072
40352
  function linePriceId(line) {
40073
40353
  const l = line;
@@ -40081,16 +40361,17 @@ async function resolveUser2(customerId, emailFallback) {
40081
40361
  if (user) await setStripeCustomerId(user.id, customerId);
40082
40362
  return user;
40083
40363
  }
40084
- var import_stripe, import_hono19, stripe, stripeApp;
40364
+ var import_stripe2, import_hono19, stripe, stripeApp;
40085
40365
  var init_stripe_routes = __esm({
40086
40366
  "src/api/stripe-routes.ts"() {
40087
40367
  "use strict";
40088
- import_stripe = __toESM(require("stripe"), 1);
40368
+ import_stripe2 = __toESM(require("stripe"), 1);
40089
40369
  import_hono19 = require("hono");
40090
40370
  init_db();
40091
40371
  init_rates();
40092
40372
  init_memory();
40093
- stripe = new import_stripe.default(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
40373
+ init_connected_account_billing();
40374
+ stripe = new import_stripe2.default(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
40094
40375
  stripeApp = new import_hono19.Hono();
40095
40376
  stripeApp.post("/webhooks", async (c) => {
40096
40377
  const sig = c.req.header("stripe-signature");
@@ -40107,15 +40388,23 @@ var init_stripe_routes = __esm({
40107
40388
  const invoice = event.data.object;
40108
40389
  const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
40109
40390
  if (memLineId) return c.json({ received: true });
40110
- const lineTierId = invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
40111
- if (lineTierId) {
40112
- const tier = SUBSCRIPTION_TIERS[lineTierId];
40113
- const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
40114
- if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
40115
- await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
40116
- await setSubscriptionTier(user.id, tier.tier, tier.concurrency, invoice.subscription ?? user.subscription_id);
40117
- const credentials = await syncScheduledActionCredentials(user);
40118
- if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
40391
+ const invoiceHasTierLine = invoice.lines.data.some((l) => {
40392
+ const id = linePriceId(l);
40393
+ return id && id in SUBSCRIPTION_TIERS;
40394
+ });
40395
+ if (invoiceHasTierLine) {
40396
+ const subId = invoice.subscription;
40397
+ const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
40398
+ const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
40399
+ const tier = tierPriceId ? SUBSCRIPTION_TIERS[tierPriceId] : void 0;
40400
+ if (tier) {
40401
+ const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
40402
+ if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
40403
+ await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
40404
+ await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
40405
+ const credentials = await syncScheduledActionCredentials(user);
40406
+ if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
40407
+ }
40119
40408
  }
40120
40409
  }
40121
40410
  }
@@ -43484,8 +43773,8 @@ async function reconcileDiscoveredNangoConnections(identity, discovered) {
43484
43773
  sql: `
43485
43774
  INSERT INTO service_connections (
43486
43775
  id, user_id, provider_config_key, provider, transport, upstream_connection_id,
43487
- label, lifecycle_status, reconnect_required, legacy_owner_identity, created_at, updated_at
43488
- ) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
43776
+ label, lifecycle_status, reconnect_required, actions_enabled, legacy_owner_identity, created_at, updated_at
43777
+ ) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, 1, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
43489
43778
  ON CONFLICT(user_id, provider_config_key, upstream_connection_id) DO UPDATE SET
43490
43779
  provider = excluded.provider,
43491
43780
  label = COALESCE(excluded.label, service_connections.label),
@@ -45601,225 +45890,6 @@ var init_scheduler_integration_auth = __esm({
45601
45890
  }
45602
45891
  });
45603
45892
 
45604
- // src/api/connected-account-billing.ts
45605
- async function ensureConnectedAccountBillingSchema() {
45606
- await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
45607
- user_id INTEGER PRIMARY KEY,
45608
- stripe_subscription_id TEXT,
45609
- stripe_subscription_item_id TEXT,
45610
- price_id TEXT,
45611
- quantity INTEGER NOT NULL DEFAULT 0,
45612
- status TEXT NOT NULL DEFAULT 'pending',
45613
- last_error_code TEXT,
45614
- synced_at TEXT,
45615
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
45616
- )`);
45617
- }
45618
- async function getConnectedAccountBillingState(userId) {
45619
- await ensureConnectedAccountBillingSchema();
45620
- const result = await getDb().execute({
45621
- sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
45622
- args: [Number(userId)]
45623
- });
45624
- return result.rows[0] ? result.rows[0] : null;
45625
- }
45626
- async function setConnectedAccountBillingState(input) {
45627
- await ensureConnectedAccountBillingSchema();
45628
- await getDb().execute({
45629
- sql: `INSERT INTO connected_account_billing
45630
- (user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
45631
- VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
45632
- ON CONFLICT(user_id) DO UPDATE SET
45633
- stripe_subscription_id = excluded.stripe_subscription_id,
45634
- stripe_subscription_item_id = excluded.stripe_subscription_item_id,
45635
- price_id = excluded.price_id,
45636
- quantity = excluded.quantity,
45637
- status = excluded.status,
45638
- last_error_code = excluded.last_error_code,
45639
- synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
45640
- updated_at = datetime('now')`,
45641
- args: [
45642
- Number(input.userId),
45643
- input.stripeSubscriptionId,
45644
- input.stripeSubscriptionItemId,
45645
- input.priceId,
45646
- input.quantity,
45647
- input.status,
45648
- input.lastErrorCode,
45649
- input.synced ? 1 : 0,
45650
- input.synced ? 1 : 0
45651
- ]
45652
- });
45653
- const state = await getConnectedAccountBillingState(input.userId);
45654
- if (!state) throw new Error("connected account billing state was not persisted");
45655
- return state;
45656
- }
45657
- function connectedAccountPriceId() {
45658
- const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
45659
- if (configured) return configured;
45660
- return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
45661
- }
45662
- function connectedAccountBillingView(actualQuantity, state) {
45663
- const quantity = Math.max(0, Math.round(actualQuantity));
45664
- return {
45665
- provider: "nango",
45666
- billingMode: "flat_recurring_usd_plus_credits",
45667
- priceId: connectedAccountPriceId(),
45668
- unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
45669
- interval: "month",
45670
- quantity,
45671
- projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
45672
- status: state?.status ?? "pending",
45673
- lastErrorCode: state?.last_error_code ?? null,
45674
- syncedAt: state?.synced_at ?? null,
45675
- usage: CONNECTED_USAGE_RATE_POLICY.usage
45676
- };
45677
- }
45678
- function findBasePlanItem(subscription) {
45679
- return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
45680
- }
45681
- function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
45682
- return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
45683
- }
45684
- function liveSubscription(subscription) {
45685
- return subscription.status === "active" || subscription.status === "trialing";
45686
- }
45687
- function safeStripeErrorCode(error) {
45688
- if (error instanceof ConnectedAccountBillingError) return error.code;
45689
- return "stripe_connection_billing_failed";
45690
- }
45691
- async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
45692
- await setConnectedAccountBillingState({
45693
- userId: user.id,
45694
- stripeSubscriptionId: user.subscription_id,
45695
- stripeSubscriptionItemId: subscriptionItemId,
45696
- priceId: connectedAccountPriceId(),
45697
- quantity: actualQuantity,
45698
- status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
45699
- lastErrorCode: safeStripeErrorCode(error),
45700
- synced: false
45701
- });
45702
- }
45703
- async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
45704
- if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
45705
- throw new Error("actualQuantity must be a non-negative safe integer");
45706
- }
45707
- const priceId = connectedAccountPriceId();
45708
- if (!priceId) {
45709
- const error = new ConnectedAccountBillingError(
45710
- "Connected-account billing is not configured.",
45711
- "connected_account_price_not_configured",
45712
- 503
45713
- );
45714
- await persistFailure(user, actualQuantity, error);
45715
- throw error;
45716
- }
45717
- if (!user.subscription_id) {
45718
- if (actualQuantity === 0) {
45719
- const state = await setConnectedAccountBillingState({
45720
- userId: user.id,
45721
- stripeSubscriptionId: null,
45722
- stripeSubscriptionItemId: null,
45723
- priceId,
45724
- quantity: 0,
45725
- status: "synced",
45726
- lastErrorCode: null,
45727
- synced: true
45728
- });
45729
- return connectedAccountBillingView(0, state);
45730
- }
45731
- const error = new ConnectedAccountBillingError(
45732
- "An active paid plan is required before connected accounts can be billed.",
45733
- "paid_plan_required",
45734
- 403
45735
- );
45736
- await persistFailure(user, actualQuantity, error);
45737
- throw error;
45738
- }
45739
- const client2 = stripeClient ?? (() => {
45740
- const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
45741
- if (!secret2) {
45742
- throw new ConnectedAccountBillingError(
45743
- "Stripe is not configured.",
45744
- "connected_account_price_not_configured",
45745
- 503
45746
- );
45747
- }
45748
- return new import_stripe2.default(secret2, { apiVersion: STRIPE_API_VERSION });
45749
- })();
45750
- let currentItem;
45751
- try {
45752
- const subscription = await client2.subscriptions.retrieve(user.subscription_id);
45753
- currentItem = findConnectedAccountItem(subscription, priceId);
45754
- if (!liveSubscription(subscription)) {
45755
- throw new ConnectedAccountBillingError(
45756
- "The plan subscription is not active.",
45757
- "subscription_not_active",
45758
- 402
45759
- );
45760
- }
45761
- if (!findBasePlanItem(subscription)) {
45762
- throw new ConnectedAccountBillingError(
45763
- "The base plan item could not be found on this subscription.",
45764
- "subscription_plan_item_missing",
45765
- 409
45766
- );
45767
- }
45768
- const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
45769
- let resultingItem = currentItem;
45770
- if (actualQuantity !== currentQuantity) {
45771
- const updated = await client2.subscriptions.update(subscription.id, {
45772
- items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
45773
- proration_behavior: "create_prorations"
45774
- });
45775
- resultingItem = findConnectedAccountItem(updated, priceId);
45776
- }
45777
- const state = await setConnectedAccountBillingState({
45778
- userId: user.id,
45779
- stripeSubscriptionId: subscription.id,
45780
- stripeSubscriptionItemId: resultingItem?.id ?? null,
45781
- priceId,
45782
- quantity: actualQuantity,
45783
- status: "synced",
45784
- lastErrorCode: null,
45785
- synced: true
45786
- });
45787
- return connectedAccountBillingView(actualQuantity, state);
45788
- } catch (error) {
45789
- await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
45790
- if (error instanceof ConnectedAccountBillingError) throw error;
45791
- throw new ConnectedAccountBillingError(
45792
- "Unable to update connected-account billing in Stripe.",
45793
- "stripe_connection_billing_failed",
45794
- 503
45795
- );
45796
- }
45797
- }
45798
- async function currentConnectedAccountBillingView(userId, actualQuantity) {
45799
- return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
45800
- }
45801
- var import_stripe2, DEFAULT_CONNECTED_ACCOUNT_PRICE_ID, STRIPE_API_VERSION, ConnectedAccountBillingError;
45802
- var init_connected_account_billing = __esm({
45803
- "src/api/connected-account-billing.ts"() {
45804
- "use strict";
45805
- import_stripe2 = __toESM(require("stripe"), 1);
45806
- init_db();
45807
- init_rates();
45808
- DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
45809
- STRIPE_API_VERSION = "2026-02-25.clover";
45810
- ConnectedAccountBillingError = class extends Error {
45811
- constructor(message, code, status = 409) {
45812
- super(message);
45813
- this.code = code;
45814
- this.status = status;
45815
- this.name = "ConnectedAccountBillingError";
45816
- }
45817
- code;
45818
- status;
45819
- };
45820
- }
45821
- });
45822
-
45823
45893
  // src/api/webhook.ts
45824
45894
  async function deliverWebhook(url, payload, retries = 3) {
45825
45895
  for (let attempt = 1; attempt <= retries; attempt++) {
@@ -46193,6 +46263,22 @@ function schedulerIntegrationAuthError(c, error) {
46193
46263
  }
46194
46264
  return scheduleConnectionError(c, error, "The scheduler integration request failed.");
46195
46265
  }
46266
+ function toolResultText(result) {
46267
+ if (!result || typeof result !== "object") return null;
46268
+ const content = result.content;
46269
+ if (!Array.isArray(content)) return null;
46270
+ const textItem = content.find((item) => item && typeof item === "object" && item.type === "text");
46271
+ const text2 = textItem?.text;
46272
+ return typeof text2 === "string" ? text2 : null;
46273
+ }
46274
+ function parseEmailAddress(headerValue) {
46275
+ const value = headerValue?.trim();
46276
+ if (!value) return null;
46277
+ const angled = value.match(/^"?([^"<]*?)"?\s*<([^>]+)>$/);
46278
+ if (angled) return { email: angled[2].trim().toLowerCase(), name: angled[1].trim() || null };
46279
+ const bare = value.match(/^([^\s<>]+@[^\s<>]+)$/);
46280
+ return bare ? { email: bare[1].trim().toLowerCase(), name: null } : null;
46281
+ }
46196
46282
  async function forwardIntegrationAlias(c, targetPath) {
46197
46283
  const url = new URL(c.req.url);
46198
46284
  url.pathname = targetPath;
@@ -46237,6 +46323,22 @@ async function checkHarvestLimits(user, reuseLockId) {
46237
46323
  }
46238
46324
  return null;
46239
46325
  }
46326
+ async function chargeTierChangeNow(stripeClient, subscriptionId, customerId) {
46327
+ try {
46328
+ const draft = await stripeClient.invoices.create({
46329
+ customer: customerId,
46330
+ subscription: subscriptionId,
46331
+ auto_advance: true,
46332
+ collection_method: "charge_automatically"
46333
+ });
46334
+ const finalized = await stripeClient.invoices.finalizeInvoice(draft.id);
46335
+ if (finalized.amount_due <= 0 || finalized.status === "paid") return { ok: true, amountDue: finalized.amount_due };
46336
+ const paid = await stripeClient.invoices.pay(finalized.id);
46337
+ return { ok: paid.status === "paid", amountDue: paid.amount_due };
46338
+ } catch (err) {
46339
+ return { ok: false, amountDue: 0, error: err instanceof Error ? err.message : "Unable to charge the plan change immediately." };
46340
+ }
46341
+ }
46240
46342
  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;
46241
46343
  var init_server = __esm({
46242
46344
  "src/api/server.ts"() {
@@ -47066,7 +47168,7 @@ var init_server = __esm({
47066
47168
  user.email,
47067
47169
  connectionId,
47068
47170
  { channel: body.channel, text: body.text },
47069
- void 0,
47171
+ "send-message",
47070
47172
  connectedActionIdempotencyKey(c)
47071
47173
  );
47072
47174
  return c.json({ ok: true, result });
@@ -47086,7 +47188,7 @@ var init_server = __esm({
47086
47188
  user.email,
47087
47189
  connectionId,
47088
47190
  { to: body.to, subject: body.subject, body: body.body },
47089
- void 0,
47191
+ "send-message",
47090
47192
  connectedActionIdempotencyKey(c)
47091
47193
  );
47092
47194
  return c.json({ ok: true, result });
@@ -47094,6 +47196,60 @@ var init_server = __esm({
47094
47196
  return scheduleConnectionError(c, err, "Unable to send the email.");
47095
47197
  }
47096
47198
  });
47199
+ app.post("/schedule-connections/actions/gmail/search-contacts", auth2, requireIntegrationsTier, async (c) => {
47200
+ const user = c.get("user");
47201
+ const body = await c.req.json().catch(() => ({}));
47202
+ const connectionId = providerConfigKeyFrom(body.connectionId);
47203
+ const query = typeof body.query === "string" ? body.query.trim() : "";
47204
+ const maxMessages = Math.min(150, Math.max(1, Number.isFinite(body.maxMessages) ? Number(body.maxMessages) : 50));
47205
+ const pageToken = typeof body.pageToken === "string" ? body.pageToken : void 0;
47206
+ if (!connectionId || !query) return c.json({ ok: false, error: "connectionId and query are required." }, 400);
47207
+ try {
47208
+ const listRaw = await callScheduleConnectionRead(user.email, connectionId, "list-messages", { q: query, maxResults: maxMessages, pageToken });
47209
+ const listText = toolResultText(listRaw);
47210
+ const list = listText ? JSON.parse(listText) : {};
47211
+ const ids = (list.messages ?? []).map((m) => m.id);
47212
+ const messages = [];
47213
+ const contacts = /* @__PURE__ */ new Map();
47214
+ for (const id of ids) {
47215
+ const msgRaw = await callScheduleConnectionRead(user.email, connectionId, "get-message", { id, format: "metadata" });
47216
+ const msgText = toolResultText(msgRaw);
47217
+ if (!msgText) continue;
47218
+ const msg = JSON.parse(msgText);
47219
+ const headers = new Map((msg.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
47220
+ const date = headers.get("date") ?? null;
47221
+ const fromRaw = headers.get("from");
47222
+ const from = parseEmailAddress(fromRaw);
47223
+ const subject = headers.get("subject") ?? null;
47224
+ messages.push({ id, date, from: fromRaw ?? null, to: headers.get("to") ?? null, subject, snippet: msg.snippet ?? null });
47225
+ if (from) {
47226
+ const domain = from.email.split("@")[1] ?? null;
47227
+ const existing = contacts.get(from.email);
47228
+ if (existing) {
47229
+ existing.messageCount += 1;
47230
+ existing.name = existing.name ?? from.name;
47231
+ if (subject && existing.sampleSubjects.length < 5 && !existing.sampleSubjects.includes(subject)) existing.sampleSubjects.push(subject);
47232
+ if (date && (!existing.lastSeen || date > existing.lastSeen)) existing.lastSeen = date;
47233
+ if (date && (!existing.firstSeen || date < existing.firstSeen)) existing.firstSeen = date;
47234
+ } else {
47235
+ contacts.set(from.email, { email: from.email, name: from.name, domain, messageCount: 1, firstSeen: date, lastSeen: date, sampleSubjects: subject ? [subject] : [] });
47236
+ }
47237
+ }
47238
+ }
47239
+ const totalMatches = list.resultSizeEstimate;
47240
+ return c.json({
47241
+ ok: true,
47242
+ totalMatches,
47243
+ messagesFetched: messages.length,
47244
+ truncated: typeof totalMatches === "number" ? totalMatches > messages.length : Boolean(list.nextPageToken),
47245
+ nextPageToken: list.nextPageToken ?? null,
47246
+ messages,
47247
+ contacts: Array.from(contacts.values())
47248
+ });
47249
+ } catch (err) {
47250
+ return scheduleConnectionError(c, err, "Unable to search Gmail contacts.");
47251
+ }
47252
+ });
47097
47253
  app.post("/schedule-connections/actions/google-calendar/create-event", auth2, requireIntegrationsTier, async (c) => {
47098
47254
  const user = c.get("user");
47099
47255
  const body = await c.req.json().catch(() => ({}));
@@ -47112,7 +47268,7 @@ var init_server = __esm({
47112
47268
  start: { dateTime: body.startDateTime, timeZone },
47113
47269
  end: { dateTime: body.endDateTime, timeZone },
47114
47270
  attendees
47115
- }, void 0, connectedActionIdempotencyKey(c));
47271
+ }, "create-event", connectedActionIdempotencyKey(c));
47116
47272
  return c.json({ ok: true, result });
47117
47273
  } catch (err) {
47118
47274
  return scheduleConnectionError(c, err, "Unable to create the calendar event.");
@@ -47132,7 +47288,7 @@ var init_server = __esm({
47132
47288
  durationMinutes: typeof body.durationMinutes === "number" ? body.durationMinutes : 30,
47133
47289
  timezone: typeof body.timezone === "string" ? body.timezone : void 0,
47134
47290
  agenda: body.agenda
47135
- }, void 0, connectedActionIdempotencyKey(c));
47291
+ }, "create-meeting", connectedActionIdempotencyKey(c));
47136
47292
  return c.json({ ok: true, result });
47137
47293
  } catch (err) {
47138
47294
  return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
@@ -48139,7 +48295,16 @@ var init_server = __esm({
48139
48295
  items: [{ id: itemId, price: tier.price_id }],
48140
48296
  proration_behavior: "create_prorations"
48141
48297
  });
48142
- return c.json({ updated: true, tier: tier.tier });
48298
+ const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
48299
+ if (!billed.ok) {
48300
+ return c.json({
48301
+ updated: true,
48302
+ tier: tier.tier,
48303
+ billed: false,
48304
+ error: billed.error ?? "Plan changed, but the prorated charge could not be collected. Update your payment method and it will retry automatically."
48305
+ });
48306
+ }
48307
+ return c.json({ updated: true, tier: tier.tier, billed: true });
48143
48308
  }
48144
48309
  }
48145
48310
  const session = await stripeClient.checkout.sessions.create({
@@ -48256,7 +48421,16 @@ var init_server = __esm({
48256
48421
  items: [{ id: itemId, price: tier.price_id }],
48257
48422
  proration_behavior: "create_prorations"
48258
48423
  });
48259
- return c.json({ updated: true, tier: tier.tier, message: `Switched to ${tier.label} (prorated). Credits and concurrency update on the next invoice.` });
48424
+ const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
48425
+ if (!billed.ok) {
48426
+ return c.json({
48427
+ updated: true,
48428
+ tier: tier.tier,
48429
+ billed: false,
48430
+ 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.`
48431
+ });
48432
+ }
48433
+ 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.` });
48260
48434
  }
48261
48435
  }
48262
48436
  const session = await stripeClient.checkout.sessions.create({