mcp-scraper 0.32.1 → 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.
- package/README.md +1 -1
- package/dist/bin/api-server.cjs +434 -252
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +3 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +3 -3
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +44 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +3 -3
- package/dist/bin/paa-harvest.cjs +4 -2
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +2 -2
- package/dist/{chunk-RDCCU7CQ.js → chunk-AEMABLEL.js} +5 -4
- package/dist/chunk-AEMABLEL.js.map +1 -0
- package/dist/{chunk-2FY4VBAY.js → chunk-GNRHBIYZ.js} +44 -3
- package/dist/chunk-GNRHBIYZ.js.map +1 -0
- package/dist/{chunk-55M5CE5L.js → chunk-MKZIL4EA.js} +2 -2
- package/dist/{chunk-PTHY7NYD.js → chunk-V73MPRU6.js} +3 -1
- package/dist/chunk-V73MPRU6.js.map +1 -0
- package/dist/chunk-XF46AB5C.js +7 -0
- package/dist/chunk-XF46AB5C.js.map +1 -0
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-GXTYKIAX.js → server-NHLEIKDZ.js} +386 -246
- package/dist/server-NHLEIKDZ.js.map +1 -0
- package/dist/{worker-7DJYUACV.js → worker-ZZCZNY3R.js} +3 -3
- package/docs/mcp-tool-manifest.generated.json +165 -3
- package/package.json +1 -1
- package/dist/chunk-2FY4VBAY.js.map +0 -1
- package/dist/chunk-PTHY7NYD.js.map +0 -1
- package/dist/chunk-RDCCU7CQ.js.map +0 -1
- package/dist/chunk-VJBS6RPB.js +0 -7
- package/dist/chunk-VJBS6RPB.js.map +0 -1
- package/dist/server-GXTYKIAX.js.map +0 -1
- /package/dist/{chunk-55M5CE5L.js.map → chunk-MKZIL4EA.js.map} +0 -0
- /package/dist/{worker-7DJYUACV.js.map → worker-ZZCZNY3R.js.map} +0 -0
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
encodeUule,
|
|
16
16
|
harvest,
|
|
17
17
|
normalizeLocation,
|
|
18
|
-
resolveKernelProxyId
|
|
19
|
-
|
|
18
|
+
resolveKernelProxyId,
|
|
19
|
+
serpArrivalUrl
|
|
20
|
+
} from "./chunk-AEMABLEL.js";
|
|
20
21
|
import {
|
|
21
22
|
HttpMcpToolExecutor,
|
|
22
23
|
assessTranscriptSignal,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
sanitizeAttempts,
|
|
37
38
|
sanitizeHarvestResult,
|
|
38
39
|
transcribeMediaUrl
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-GNRHBIYZ.js";
|
|
40
41
|
import {
|
|
41
42
|
auditImages,
|
|
42
43
|
buildLinkReport,
|
|
@@ -65,7 +66,7 @@ import {
|
|
|
65
66
|
workflowOutputBaseDir,
|
|
66
67
|
workflowStepCount,
|
|
67
68
|
workflowSupportsSteps
|
|
68
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-MKZIL4EA.js";
|
|
69
70
|
import "./chunk-K443GQY5.js";
|
|
70
71
|
import {
|
|
71
72
|
DEFAULT_MAPS_PROXY_MODE,
|
|
@@ -76,8 +77,8 @@ import {
|
|
|
76
77
|
RawMapsHoursRowSchema,
|
|
77
78
|
RawMapsOverviewSchema,
|
|
78
79
|
RawMapsReviewStatsSchema
|
|
79
|
-
} from "./chunk-
|
|
80
|
-
import "./chunk-
|
|
80
|
+
} from "./chunk-V73MPRU6.js";
|
|
81
|
+
import "./chunk-XF46AB5C.js";
|
|
81
82
|
import {
|
|
82
83
|
completeExtractJob,
|
|
83
84
|
countSuccessfulPages,
|
|
@@ -13083,6 +13084,7 @@ import { Hono as Hono11 } from "hono";
|
|
|
13083
13084
|
// src/extractor/MapsSearchExtractor.ts
|
|
13084
13085
|
var LOCAL_RESULTS_PAGE_LIMIT = 8;
|
|
13085
13086
|
var LOCAL_RESULTS_WAIT_MS = 1e3;
|
|
13087
|
+
var MAPS_REDIRECT_BASE = "https://serp-redirector.vercel.app";
|
|
13086
13088
|
function buildGoogleLocalResultsUrl(input) {
|
|
13087
13089
|
const query = [input.query, input.location].filter(Boolean).join(" ");
|
|
13088
13090
|
const params = new URLSearchParams({ q: query, udm: "1", gl: input.gl, hl: input.hl, pws: "0" });
|
|
@@ -13095,6 +13097,22 @@ function buildGoogleOrganicSearchUrl(input) {
|
|
|
13095
13097
|
if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
|
|
13096
13098
|
return `https://www.google.com/search?${params.toString()}`;
|
|
13097
13099
|
}
|
|
13100
|
+
function mapsIdentifiersFromUrl(url) {
|
|
13101
|
+
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);
|
|
13102
|
+
if (fid) {
|
|
13103
|
+
try {
|
|
13104
|
+
return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: BigInt(fid[2]).toString() };
|
|
13105
|
+
} catch {
|
|
13106
|
+
return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: null };
|
|
13107
|
+
}
|
|
13108
|
+
}
|
|
13109
|
+
try {
|
|
13110
|
+
const decimal = new URL(url).searchParams.get("cid");
|
|
13111
|
+
return { cid: null, cidDecimal: decimal && /^\d+$/.test(decimal) ? decimal : null };
|
|
13112
|
+
} catch {
|
|
13113
|
+
return { cid: null, cidDecimal: null };
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13098
13116
|
var MapsSearchExtractor = class {
|
|
13099
13117
|
constructor(driver) {
|
|
13100
13118
|
this.driver = driver;
|
|
@@ -13104,21 +13122,23 @@ var MapsSearchExtractor = class {
|
|
|
13104
13122
|
const startMs = Date.now();
|
|
13105
13123
|
const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
|
|
13106
13124
|
const searchUrl = buildGoogleOrganicSearchUrl(options);
|
|
13125
|
+
const keepDefaultProxy = !options.forceDirectEgress && options.proxyMode !== "location" && options.proxyMode !== "configured" && !options.kernelProxyId;
|
|
13107
13126
|
const config = {
|
|
13108
13127
|
headless: options.headless,
|
|
13109
13128
|
kernelApiKey: options.kernelApiKey,
|
|
13110
13129
|
kernelProxyId: options.kernelProxyId,
|
|
13111
13130
|
kernelProxyResolution: options.kernelProxyResolution,
|
|
13112
13131
|
proxyMode: options.proxyMode,
|
|
13113
|
-
keepDefaultProxy
|
|
13132
|
+
keepDefaultProxy,
|
|
13114
13133
|
viewport: { width: 1280, height: 900 },
|
|
13115
13134
|
locale: `${options.hl}-${options.gl.toUpperCase()}`,
|
|
13116
13135
|
debug: options.debug
|
|
13117
13136
|
};
|
|
13137
|
+
const navUrl = options.serpRedirect ? serpArrivalUrl(searchUrl, process.env.SERP_REDIRECT_BASE?.trim() || MAPS_REDIRECT_BASE) : searchUrl;
|
|
13118
13138
|
try {
|
|
13119
13139
|
await this.driver.launch(config);
|
|
13120
13140
|
const page = this.driver.getPage();
|
|
13121
|
-
await page.goto(
|
|
13141
|
+
await page.goto(navUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
13122
13142
|
await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
|
|
13123
13143
|
if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
13124
13144
|
const reachedFinder = await this.clickIntoLocalFinder(page);
|
|
@@ -13168,13 +13188,15 @@ var MapsSearchExtractor = class {
|
|
|
13168
13188
|
for (const card of cards) {
|
|
13169
13189
|
if (results.length >= options.maxResults || seen.has(card.cardKey)) continue;
|
|
13170
13190
|
seen.add(card.cardKey);
|
|
13191
|
+
const cardIds = mapsIdentifiersFromUrl(card.directionsUrl ?? "");
|
|
13192
|
+
const cardPlaceUrl = cardIds.cidDecimal ? `https://www.google.com/maps?cid=${cardIds.cidDecimal}` : card.placeUrl;
|
|
13171
13193
|
const details = await this.openCardAndExtractDialog(page, card, options.includeServices);
|
|
13172
13194
|
results.push({
|
|
13173
13195
|
position: results.length + 1,
|
|
13174
13196
|
name: card.name,
|
|
13175
|
-
placeUrl: details?.placeUrl ??
|
|
13176
|
-
cid: details?.cid ??
|
|
13177
|
-
cidDecimal: details?.cidDecimal ??
|
|
13197
|
+
placeUrl: details?.placeUrl ?? cardPlaceUrl,
|
|
13198
|
+
cid: details?.cid ?? cardIds.cid,
|
|
13199
|
+
cidDecimal: details?.cidDecimal ?? cardIds.cidDecimal,
|
|
13178
13200
|
rating: details?.rating ?? card.rating,
|
|
13179
13201
|
reviewCount: details?.reviewCount ?? card.reviewCount,
|
|
13180
13202
|
category: details?.category ?? card.category,
|
|
@@ -13231,8 +13253,10 @@ var MapsSearchExtractor = class {
|
|
|
13231
13253
|
const phone = lines.map((line) => line.match(phonePattern)?.[0]).find((value) => Boolean(value)) ?? null;
|
|
13232
13254
|
const hoursStatus = normalize((lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? "").split("\xB7")[0] ?? "");
|
|
13233
13255
|
const address = lines.find((line) => addressPattern.test(line)) ?? null;
|
|
13234
|
-
const
|
|
13256
|
+
const anchorHrefs = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href);
|
|
13257
|
+
const websiteUrl = anchorHrefs.find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
|
|
13235
13258
|
if (!rating && !(phone && websiteUrl)) continue;
|
|
13259
|
+
const fidHref = anchorHrefs.find((href) => /(0x[0-9a-f]+):(0x[0-9a-f]+)/i.test(href)) ?? null;
|
|
13236
13260
|
const key = `${name.toLowerCase()}|${lines.join(" ").toLowerCase()}`;
|
|
13237
13261
|
if (seen.has(key)) continue;
|
|
13238
13262
|
seen.add(key);
|
|
@@ -13252,7 +13276,7 @@ var MapsSearchExtractor = class {
|
|
|
13252
13276
|
phone,
|
|
13253
13277
|
hoursStatus,
|
|
13254
13278
|
websiteUrl,
|
|
13255
|
-
directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
|
|
13279
|
+
directionsUrl: fidHref ?? `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
|
|
13256
13280
|
metadata: lines.slice(0, 20)
|
|
13257
13281
|
});
|
|
13258
13282
|
}
|
|
@@ -13978,7 +14002,9 @@ async function runMapsSearchWithRotation(options) {
|
|
|
13978
14002
|
...options,
|
|
13979
14003
|
proxyZip,
|
|
13980
14004
|
kernelProxyId: options.proxyMode === "none" ? void 0 : resolution.kernelProxyId,
|
|
13981
|
-
kernelProxyResolution: resolution.resolution
|
|
14005
|
+
kernelProxyResolution: resolution.resolution,
|
|
14006
|
+
serpRedirect: options.proxyMode === "none" && attemptIndex >= 1 && attemptIndex < maxAttempts - 1,
|
|
14007
|
+
forceDirectEgress: options.proxyMode === "none" && attemptIndex >= 1
|
|
13982
14008
|
};
|
|
13983
14009
|
const isHeadfulEscalation = attemptIndex === maxAttempts - 1;
|
|
13984
14010
|
const baseCtx = currentCostContext();
|
|
@@ -19548,9 +19574,222 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
|
|
|
19548
19574
|
}
|
|
19549
19575
|
|
|
19550
19576
|
// src/api/stripe-routes.ts
|
|
19551
|
-
import
|
|
19577
|
+
import Stripe2 from "stripe";
|
|
19552
19578
|
import { Hono as Hono19 } from "hono";
|
|
19553
|
-
|
|
19579
|
+
|
|
19580
|
+
// src/api/connected-account-billing.ts
|
|
19581
|
+
import Stripe from "stripe";
|
|
19582
|
+
var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
|
|
19583
|
+
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
19584
|
+
async function ensureConnectedAccountBillingSchema() {
|
|
19585
|
+
await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
|
|
19586
|
+
user_id INTEGER PRIMARY KEY,
|
|
19587
|
+
stripe_subscription_id TEXT,
|
|
19588
|
+
stripe_subscription_item_id TEXT,
|
|
19589
|
+
price_id TEXT,
|
|
19590
|
+
quantity INTEGER NOT NULL DEFAULT 0,
|
|
19591
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
19592
|
+
last_error_code TEXT,
|
|
19593
|
+
synced_at TEXT,
|
|
19594
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19595
|
+
)`);
|
|
19596
|
+
}
|
|
19597
|
+
async function getConnectedAccountBillingState(userId) {
|
|
19598
|
+
await ensureConnectedAccountBillingSchema();
|
|
19599
|
+
const result = await getDb().execute({
|
|
19600
|
+
sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
|
|
19601
|
+
args: [Number(userId)]
|
|
19602
|
+
});
|
|
19603
|
+
return result.rows[0] ? result.rows[0] : null;
|
|
19604
|
+
}
|
|
19605
|
+
async function setConnectedAccountBillingState(input) {
|
|
19606
|
+
await ensureConnectedAccountBillingSchema();
|
|
19607
|
+
await getDb().execute({
|
|
19608
|
+
sql: `INSERT INTO connected_account_billing
|
|
19609
|
+
(user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
|
|
19610
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
|
|
19611
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
19612
|
+
stripe_subscription_id = excluded.stripe_subscription_id,
|
|
19613
|
+
stripe_subscription_item_id = excluded.stripe_subscription_item_id,
|
|
19614
|
+
price_id = excluded.price_id,
|
|
19615
|
+
quantity = excluded.quantity,
|
|
19616
|
+
status = excluded.status,
|
|
19617
|
+
last_error_code = excluded.last_error_code,
|
|
19618
|
+
synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
|
|
19619
|
+
updated_at = datetime('now')`,
|
|
19620
|
+
args: [
|
|
19621
|
+
Number(input.userId),
|
|
19622
|
+
input.stripeSubscriptionId,
|
|
19623
|
+
input.stripeSubscriptionItemId,
|
|
19624
|
+
input.priceId,
|
|
19625
|
+
input.quantity,
|
|
19626
|
+
input.status,
|
|
19627
|
+
input.lastErrorCode,
|
|
19628
|
+
input.synced ? 1 : 0,
|
|
19629
|
+
input.synced ? 1 : 0
|
|
19630
|
+
]
|
|
19631
|
+
});
|
|
19632
|
+
const state = await getConnectedAccountBillingState(input.userId);
|
|
19633
|
+
if (!state) throw new Error("connected account billing state was not persisted");
|
|
19634
|
+
return state;
|
|
19635
|
+
}
|
|
19636
|
+
var ConnectedAccountBillingError = class extends Error {
|
|
19637
|
+
constructor(message, code, status = 409) {
|
|
19638
|
+
super(message);
|
|
19639
|
+
this.code = code;
|
|
19640
|
+
this.status = status;
|
|
19641
|
+
this.name = "ConnectedAccountBillingError";
|
|
19642
|
+
}
|
|
19643
|
+
code;
|
|
19644
|
+
status;
|
|
19645
|
+
};
|
|
19646
|
+
function connectedAccountPriceId() {
|
|
19647
|
+
const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
|
|
19648
|
+
if (configured) return configured;
|
|
19649
|
+
return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
|
|
19650
|
+
}
|
|
19651
|
+
function connectedAccountBillingView(actualQuantity, state) {
|
|
19652
|
+
const quantity = Math.max(0, Math.round(actualQuantity));
|
|
19653
|
+
return {
|
|
19654
|
+
provider: "nango",
|
|
19655
|
+
billingMode: "flat_recurring_usd_plus_credits",
|
|
19656
|
+
priceId: connectedAccountPriceId(),
|
|
19657
|
+
unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
19658
|
+
interval: "month",
|
|
19659
|
+
quantity,
|
|
19660
|
+
projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
19661
|
+
status: state?.status ?? "pending",
|
|
19662
|
+
lastErrorCode: state?.last_error_code ?? null,
|
|
19663
|
+
syncedAt: state?.synced_at ?? null,
|
|
19664
|
+
usage: CONNECTED_USAGE_RATE_POLICY.usage
|
|
19665
|
+
};
|
|
19666
|
+
}
|
|
19667
|
+
function findBasePlanItem(subscription) {
|
|
19668
|
+
return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
|
|
19669
|
+
}
|
|
19670
|
+
function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
|
|
19671
|
+
return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
|
|
19672
|
+
}
|
|
19673
|
+
function liveSubscription(subscription) {
|
|
19674
|
+
return subscription.status === "active" || subscription.status === "trialing";
|
|
19675
|
+
}
|
|
19676
|
+
function safeStripeErrorCode(error) {
|
|
19677
|
+
if (error instanceof ConnectedAccountBillingError) return error.code;
|
|
19678
|
+
return "stripe_connection_billing_failed";
|
|
19679
|
+
}
|
|
19680
|
+
async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
|
|
19681
|
+
await setConnectedAccountBillingState({
|
|
19682
|
+
userId: user.id,
|
|
19683
|
+
stripeSubscriptionId: user.subscription_id,
|
|
19684
|
+
stripeSubscriptionItemId: subscriptionItemId,
|
|
19685
|
+
priceId: connectedAccountPriceId(),
|
|
19686
|
+
quantity: actualQuantity,
|
|
19687
|
+
status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
|
|
19688
|
+
lastErrorCode: safeStripeErrorCode(error),
|
|
19689
|
+
synced: false
|
|
19690
|
+
});
|
|
19691
|
+
}
|
|
19692
|
+
async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
|
|
19693
|
+
if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
|
|
19694
|
+
throw new Error("actualQuantity must be a non-negative safe integer");
|
|
19695
|
+
}
|
|
19696
|
+
const priceId = connectedAccountPriceId();
|
|
19697
|
+
if (!priceId) {
|
|
19698
|
+
const error = new ConnectedAccountBillingError(
|
|
19699
|
+
"Connected-account billing is not configured.",
|
|
19700
|
+
"connected_account_price_not_configured",
|
|
19701
|
+
503
|
|
19702
|
+
);
|
|
19703
|
+
await persistFailure(user, actualQuantity, error);
|
|
19704
|
+
throw error;
|
|
19705
|
+
}
|
|
19706
|
+
if (!user.subscription_id) {
|
|
19707
|
+
if (actualQuantity === 0) {
|
|
19708
|
+
const state = await setConnectedAccountBillingState({
|
|
19709
|
+
userId: user.id,
|
|
19710
|
+
stripeSubscriptionId: null,
|
|
19711
|
+
stripeSubscriptionItemId: null,
|
|
19712
|
+
priceId,
|
|
19713
|
+
quantity: 0,
|
|
19714
|
+
status: "synced",
|
|
19715
|
+
lastErrorCode: null,
|
|
19716
|
+
synced: true
|
|
19717
|
+
});
|
|
19718
|
+
return connectedAccountBillingView(0, state);
|
|
19719
|
+
}
|
|
19720
|
+
const error = new ConnectedAccountBillingError(
|
|
19721
|
+
"An active paid plan is required before connected accounts can be billed.",
|
|
19722
|
+
"paid_plan_required",
|
|
19723
|
+
403
|
|
19724
|
+
);
|
|
19725
|
+
await persistFailure(user, actualQuantity, error);
|
|
19726
|
+
throw error;
|
|
19727
|
+
}
|
|
19728
|
+
const client2 = stripeClient ?? (() => {
|
|
19729
|
+
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
19730
|
+
if (!secret2) {
|
|
19731
|
+
throw new ConnectedAccountBillingError(
|
|
19732
|
+
"Stripe is not configured.",
|
|
19733
|
+
"connected_account_price_not_configured",
|
|
19734
|
+
503
|
|
19735
|
+
);
|
|
19736
|
+
}
|
|
19737
|
+
return new Stripe(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
19738
|
+
})();
|
|
19739
|
+
let currentItem;
|
|
19740
|
+
try {
|
|
19741
|
+
const subscription = await client2.subscriptions.retrieve(user.subscription_id);
|
|
19742
|
+
currentItem = findConnectedAccountItem(subscription, priceId);
|
|
19743
|
+
if (!liveSubscription(subscription)) {
|
|
19744
|
+
throw new ConnectedAccountBillingError(
|
|
19745
|
+
"The plan subscription is not active.",
|
|
19746
|
+
"subscription_not_active",
|
|
19747
|
+
402
|
|
19748
|
+
);
|
|
19749
|
+
}
|
|
19750
|
+
if (!findBasePlanItem(subscription)) {
|
|
19751
|
+
throw new ConnectedAccountBillingError(
|
|
19752
|
+
"The base plan item could not be found on this subscription.",
|
|
19753
|
+
"subscription_plan_item_missing",
|
|
19754
|
+
409
|
|
19755
|
+
);
|
|
19756
|
+
}
|
|
19757
|
+
const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
|
|
19758
|
+
let resultingItem = currentItem;
|
|
19759
|
+
if (actualQuantity !== currentQuantity) {
|
|
19760
|
+
const updated = await client2.subscriptions.update(subscription.id, {
|
|
19761
|
+
items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
|
|
19762
|
+
proration_behavior: "create_prorations"
|
|
19763
|
+
});
|
|
19764
|
+
resultingItem = findConnectedAccountItem(updated, priceId);
|
|
19765
|
+
}
|
|
19766
|
+
const state = await setConnectedAccountBillingState({
|
|
19767
|
+
userId: user.id,
|
|
19768
|
+
stripeSubscriptionId: subscription.id,
|
|
19769
|
+
stripeSubscriptionItemId: resultingItem?.id ?? null,
|
|
19770
|
+
priceId,
|
|
19771
|
+
quantity: actualQuantity,
|
|
19772
|
+
status: "synced",
|
|
19773
|
+
lastErrorCode: null,
|
|
19774
|
+
synced: true
|
|
19775
|
+
});
|
|
19776
|
+
return connectedAccountBillingView(actualQuantity, state);
|
|
19777
|
+
} catch (error) {
|
|
19778
|
+
await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
|
|
19779
|
+
if (error instanceof ConnectedAccountBillingError) throw error;
|
|
19780
|
+
throw new ConnectedAccountBillingError(
|
|
19781
|
+
"Unable to update connected-account billing in Stripe.",
|
|
19782
|
+
"stripe_connection_billing_failed",
|
|
19783
|
+
503
|
|
19784
|
+
);
|
|
19785
|
+
}
|
|
19786
|
+
}
|
|
19787
|
+
async function currentConnectedAccountBillingView(userId, actualQuantity) {
|
|
19788
|
+
return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
|
|
19789
|
+
}
|
|
19790
|
+
|
|
19791
|
+
// src/api/stripe-routes.ts
|
|
19792
|
+
var stripe = new Stripe2(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
19554
19793
|
var stripeApp = new Hono19();
|
|
19555
19794
|
stripeApp.post("/webhooks", async (c) => {
|
|
19556
19795
|
const sig = c.req.header("stripe-signature");
|
|
@@ -19567,15 +19806,23 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
19567
19806
|
const invoice = event.data.object;
|
|
19568
19807
|
const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
|
|
19569
19808
|
if (memLineId) return c.json({ received: true });
|
|
19570
|
-
const
|
|
19571
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19809
|
+
const invoiceHasTierLine = invoice.lines.data.some((l) => {
|
|
19810
|
+
const id = linePriceId(l);
|
|
19811
|
+
return id && id in SUBSCRIPTION_TIERS;
|
|
19812
|
+
});
|
|
19813
|
+
if (invoiceHasTierLine) {
|
|
19814
|
+
const subId = invoice.subscription;
|
|
19815
|
+
const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
|
|
19816
|
+
const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
|
|
19817
|
+
const tier = tierPriceId ? SUBSCRIPTION_TIERS[tierPriceId] : void 0;
|
|
19818
|
+
if (tier) {
|
|
19819
|
+
const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
|
|
19820
|
+
if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
|
|
19821
|
+
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
|
|
19822
|
+
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
|
|
19823
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
19824
|
+
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
19825
|
+
}
|
|
19579
19826
|
}
|
|
19580
19827
|
}
|
|
19581
19828
|
}
|
|
@@ -22826,8 +23073,8 @@ async function reconcileDiscoveredNangoConnections(identity, discovered) {
|
|
|
22826
23073
|
sql: `
|
|
22827
23074
|
INSERT INTO service_connections (
|
|
22828
23075
|
id, user_id, provider_config_key, provider, transport, upstream_connection_id,
|
|
22829
|
-
label, lifecycle_status, reconnect_required, legacy_owner_identity, created_at, updated_at
|
|
22830
|
-
) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
|
|
23076
|
+
label, lifecycle_status, reconnect_required, actions_enabled, legacy_owner_identity, created_at, updated_at
|
|
23077
|
+
) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, 1, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
|
|
22831
23078
|
ON CONFLICT(user_id, provider_config_key, upstream_connection_id) DO UPDATE SET
|
|
22832
23079
|
provider = excluded.provider,
|
|
22833
23080
|
label = COALESCE(excluded.label, service_connections.label),
|
|
@@ -24896,217 +25143,6 @@ async function verifySchedulerIntegrationRequest(request, rawBody) {
|
|
|
24896
25143
|
return { requestId };
|
|
24897
25144
|
}
|
|
24898
25145
|
|
|
24899
|
-
// src/api/connected-account-billing.ts
|
|
24900
|
-
import Stripe2 from "stripe";
|
|
24901
|
-
var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
|
|
24902
|
-
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
24903
|
-
async function ensureConnectedAccountBillingSchema() {
|
|
24904
|
-
await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
|
|
24905
|
-
user_id INTEGER PRIMARY KEY,
|
|
24906
|
-
stripe_subscription_id TEXT,
|
|
24907
|
-
stripe_subscription_item_id TEXT,
|
|
24908
|
-
price_id TEXT,
|
|
24909
|
-
quantity INTEGER NOT NULL DEFAULT 0,
|
|
24910
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
24911
|
-
last_error_code TEXT,
|
|
24912
|
-
synced_at TEXT,
|
|
24913
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
24914
|
-
)`);
|
|
24915
|
-
}
|
|
24916
|
-
async function getConnectedAccountBillingState(userId) {
|
|
24917
|
-
await ensureConnectedAccountBillingSchema();
|
|
24918
|
-
const result = await getDb().execute({
|
|
24919
|
-
sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
|
|
24920
|
-
args: [Number(userId)]
|
|
24921
|
-
});
|
|
24922
|
-
return result.rows[0] ? result.rows[0] : null;
|
|
24923
|
-
}
|
|
24924
|
-
async function setConnectedAccountBillingState(input) {
|
|
24925
|
-
await ensureConnectedAccountBillingSchema();
|
|
24926
|
-
await getDb().execute({
|
|
24927
|
-
sql: `INSERT INTO connected_account_billing
|
|
24928
|
-
(user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
|
|
24929
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
|
|
24930
|
-
ON CONFLICT(user_id) DO UPDATE SET
|
|
24931
|
-
stripe_subscription_id = excluded.stripe_subscription_id,
|
|
24932
|
-
stripe_subscription_item_id = excluded.stripe_subscription_item_id,
|
|
24933
|
-
price_id = excluded.price_id,
|
|
24934
|
-
quantity = excluded.quantity,
|
|
24935
|
-
status = excluded.status,
|
|
24936
|
-
last_error_code = excluded.last_error_code,
|
|
24937
|
-
synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
|
|
24938
|
-
updated_at = datetime('now')`,
|
|
24939
|
-
args: [
|
|
24940
|
-
Number(input.userId),
|
|
24941
|
-
input.stripeSubscriptionId,
|
|
24942
|
-
input.stripeSubscriptionItemId,
|
|
24943
|
-
input.priceId,
|
|
24944
|
-
input.quantity,
|
|
24945
|
-
input.status,
|
|
24946
|
-
input.lastErrorCode,
|
|
24947
|
-
input.synced ? 1 : 0,
|
|
24948
|
-
input.synced ? 1 : 0
|
|
24949
|
-
]
|
|
24950
|
-
});
|
|
24951
|
-
const state = await getConnectedAccountBillingState(input.userId);
|
|
24952
|
-
if (!state) throw new Error("connected account billing state was not persisted");
|
|
24953
|
-
return state;
|
|
24954
|
-
}
|
|
24955
|
-
var ConnectedAccountBillingError = class extends Error {
|
|
24956
|
-
constructor(message, code, status = 409) {
|
|
24957
|
-
super(message);
|
|
24958
|
-
this.code = code;
|
|
24959
|
-
this.status = status;
|
|
24960
|
-
this.name = "ConnectedAccountBillingError";
|
|
24961
|
-
}
|
|
24962
|
-
code;
|
|
24963
|
-
status;
|
|
24964
|
-
};
|
|
24965
|
-
function connectedAccountPriceId() {
|
|
24966
|
-
const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
|
|
24967
|
-
if (configured) return configured;
|
|
24968
|
-
return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
|
|
24969
|
-
}
|
|
24970
|
-
function connectedAccountBillingView(actualQuantity, state) {
|
|
24971
|
-
const quantity = Math.max(0, Math.round(actualQuantity));
|
|
24972
|
-
return {
|
|
24973
|
-
provider: "nango",
|
|
24974
|
-
billingMode: "flat_recurring_usd_plus_credits",
|
|
24975
|
-
priceId: connectedAccountPriceId(),
|
|
24976
|
-
unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
24977
|
-
interval: "month",
|
|
24978
|
-
quantity,
|
|
24979
|
-
projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
24980
|
-
status: state?.status ?? "pending",
|
|
24981
|
-
lastErrorCode: state?.last_error_code ?? null,
|
|
24982
|
-
syncedAt: state?.synced_at ?? null,
|
|
24983
|
-
usage: CONNECTED_USAGE_RATE_POLICY.usage
|
|
24984
|
-
};
|
|
24985
|
-
}
|
|
24986
|
-
function findBasePlanItem(subscription) {
|
|
24987
|
-
return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
|
|
24988
|
-
}
|
|
24989
|
-
function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
|
|
24990
|
-
return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
|
|
24991
|
-
}
|
|
24992
|
-
function liveSubscription(subscription) {
|
|
24993
|
-
return subscription.status === "active" || subscription.status === "trialing";
|
|
24994
|
-
}
|
|
24995
|
-
function safeStripeErrorCode(error) {
|
|
24996
|
-
if (error instanceof ConnectedAccountBillingError) return error.code;
|
|
24997
|
-
return "stripe_connection_billing_failed";
|
|
24998
|
-
}
|
|
24999
|
-
async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
|
|
25000
|
-
await setConnectedAccountBillingState({
|
|
25001
|
-
userId: user.id,
|
|
25002
|
-
stripeSubscriptionId: user.subscription_id,
|
|
25003
|
-
stripeSubscriptionItemId: subscriptionItemId,
|
|
25004
|
-
priceId: connectedAccountPriceId(),
|
|
25005
|
-
quantity: actualQuantity,
|
|
25006
|
-
status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
|
|
25007
|
-
lastErrorCode: safeStripeErrorCode(error),
|
|
25008
|
-
synced: false
|
|
25009
|
-
});
|
|
25010
|
-
}
|
|
25011
|
-
async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
|
|
25012
|
-
if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
|
|
25013
|
-
throw new Error("actualQuantity must be a non-negative safe integer");
|
|
25014
|
-
}
|
|
25015
|
-
const priceId = connectedAccountPriceId();
|
|
25016
|
-
if (!priceId) {
|
|
25017
|
-
const error = new ConnectedAccountBillingError(
|
|
25018
|
-
"Connected-account billing is not configured.",
|
|
25019
|
-
"connected_account_price_not_configured",
|
|
25020
|
-
503
|
|
25021
|
-
);
|
|
25022
|
-
await persistFailure(user, actualQuantity, error);
|
|
25023
|
-
throw error;
|
|
25024
|
-
}
|
|
25025
|
-
if (!user.subscription_id) {
|
|
25026
|
-
if (actualQuantity === 0) {
|
|
25027
|
-
const state = await setConnectedAccountBillingState({
|
|
25028
|
-
userId: user.id,
|
|
25029
|
-
stripeSubscriptionId: null,
|
|
25030
|
-
stripeSubscriptionItemId: null,
|
|
25031
|
-
priceId,
|
|
25032
|
-
quantity: 0,
|
|
25033
|
-
status: "synced",
|
|
25034
|
-
lastErrorCode: null,
|
|
25035
|
-
synced: true
|
|
25036
|
-
});
|
|
25037
|
-
return connectedAccountBillingView(0, state);
|
|
25038
|
-
}
|
|
25039
|
-
const error = new ConnectedAccountBillingError(
|
|
25040
|
-
"An active paid plan is required before connected accounts can be billed.",
|
|
25041
|
-
"paid_plan_required",
|
|
25042
|
-
403
|
|
25043
|
-
);
|
|
25044
|
-
await persistFailure(user, actualQuantity, error);
|
|
25045
|
-
throw error;
|
|
25046
|
-
}
|
|
25047
|
-
const client2 = stripeClient ?? (() => {
|
|
25048
|
-
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
25049
|
-
if (!secret2) {
|
|
25050
|
-
throw new ConnectedAccountBillingError(
|
|
25051
|
-
"Stripe is not configured.",
|
|
25052
|
-
"connected_account_price_not_configured",
|
|
25053
|
-
503
|
|
25054
|
-
);
|
|
25055
|
-
}
|
|
25056
|
-
return new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
25057
|
-
})();
|
|
25058
|
-
let currentItem;
|
|
25059
|
-
try {
|
|
25060
|
-
const subscription = await client2.subscriptions.retrieve(user.subscription_id);
|
|
25061
|
-
currentItem = findConnectedAccountItem(subscription, priceId);
|
|
25062
|
-
if (!liveSubscription(subscription)) {
|
|
25063
|
-
throw new ConnectedAccountBillingError(
|
|
25064
|
-
"The plan subscription is not active.",
|
|
25065
|
-
"subscription_not_active",
|
|
25066
|
-
402
|
|
25067
|
-
);
|
|
25068
|
-
}
|
|
25069
|
-
if (!findBasePlanItem(subscription)) {
|
|
25070
|
-
throw new ConnectedAccountBillingError(
|
|
25071
|
-
"The base plan item could not be found on this subscription.",
|
|
25072
|
-
"subscription_plan_item_missing",
|
|
25073
|
-
409
|
|
25074
|
-
);
|
|
25075
|
-
}
|
|
25076
|
-
const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
|
|
25077
|
-
let resultingItem = currentItem;
|
|
25078
|
-
if (actualQuantity !== currentQuantity) {
|
|
25079
|
-
const updated = await client2.subscriptions.update(subscription.id, {
|
|
25080
|
-
items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
|
|
25081
|
-
proration_behavior: "create_prorations"
|
|
25082
|
-
});
|
|
25083
|
-
resultingItem = findConnectedAccountItem(updated, priceId);
|
|
25084
|
-
}
|
|
25085
|
-
const state = await setConnectedAccountBillingState({
|
|
25086
|
-
userId: user.id,
|
|
25087
|
-
stripeSubscriptionId: subscription.id,
|
|
25088
|
-
stripeSubscriptionItemId: resultingItem?.id ?? null,
|
|
25089
|
-
priceId,
|
|
25090
|
-
quantity: actualQuantity,
|
|
25091
|
-
status: "synced",
|
|
25092
|
-
lastErrorCode: null,
|
|
25093
|
-
synced: true
|
|
25094
|
-
});
|
|
25095
|
-
return connectedAccountBillingView(actualQuantity, state);
|
|
25096
|
-
} catch (error) {
|
|
25097
|
-
await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
|
|
25098
|
-
if (error instanceof ConnectedAccountBillingError) throw error;
|
|
25099
|
-
throw new ConnectedAccountBillingError(
|
|
25100
|
-
"Unable to update connected-account billing in Stripe.",
|
|
25101
|
-
"stripe_connection_billing_failed",
|
|
25102
|
-
503
|
|
25103
|
-
);
|
|
25104
|
-
}
|
|
25105
|
-
}
|
|
25106
|
-
async function currentConnectedAccountBillingView(userId, actualQuantity) {
|
|
25107
|
-
return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
|
|
25108
|
-
}
|
|
25109
|
-
|
|
25110
25146
|
// src/api/server.ts
|
|
25111
25147
|
var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
25112
25148
|
var isProduction2 = secureCookies2;
|
|
@@ -26091,7 +26127,7 @@ app.post("/schedule-connections/actions/slack/send-message", auth2, requireInteg
|
|
|
26091
26127
|
user.email,
|
|
26092
26128
|
connectionId,
|
|
26093
26129
|
{ channel: body.channel, text: body.text },
|
|
26094
|
-
|
|
26130
|
+
"send-message",
|
|
26095
26131
|
connectedActionIdempotencyKey(c)
|
|
26096
26132
|
);
|
|
26097
26133
|
return c.json({ ok: true, result });
|
|
@@ -26111,7 +26147,7 @@ app.post("/schedule-connections/actions/gmail/send-message", auth2, requireInteg
|
|
|
26111
26147
|
user.email,
|
|
26112
26148
|
connectionId,
|
|
26113
26149
|
{ to: body.to, subject: body.subject, body: body.body },
|
|
26114
|
-
|
|
26150
|
+
"send-message",
|
|
26115
26151
|
connectedActionIdempotencyKey(c)
|
|
26116
26152
|
);
|
|
26117
26153
|
return c.json({ ok: true, result });
|
|
@@ -26119,6 +26155,76 @@ app.post("/schedule-connections/actions/gmail/send-message", auth2, requireInteg
|
|
|
26119
26155
|
return scheduleConnectionError(c, err, "Unable to send the email.");
|
|
26120
26156
|
}
|
|
26121
26157
|
});
|
|
26158
|
+
function toolResultText(result) {
|
|
26159
|
+
if (!result || typeof result !== "object") return null;
|
|
26160
|
+
const content = result.content;
|
|
26161
|
+
if (!Array.isArray(content)) return null;
|
|
26162
|
+
const textItem = content.find((item) => item && typeof item === "object" && item.type === "text");
|
|
26163
|
+
const text2 = textItem?.text;
|
|
26164
|
+
return typeof text2 === "string" ? text2 : null;
|
|
26165
|
+
}
|
|
26166
|
+
function parseEmailAddress(headerValue) {
|
|
26167
|
+
const value = headerValue?.trim();
|
|
26168
|
+
if (!value) return null;
|
|
26169
|
+
const angled = value.match(/^"?([^"<]*?)"?\s*<([^>]+)>$/);
|
|
26170
|
+
if (angled) return { email: angled[2].trim().toLowerCase(), name: angled[1].trim() || null };
|
|
26171
|
+
const bare = value.match(/^([^\s<>]+@[^\s<>]+)$/);
|
|
26172
|
+
return bare ? { email: bare[1].trim().toLowerCase(), name: null } : null;
|
|
26173
|
+
}
|
|
26174
|
+
app.post("/schedule-connections/actions/gmail/search-contacts", auth2, requireIntegrationsTier, async (c) => {
|
|
26175
|
+
const user = c.get("user");
|
|
26176
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26177
|
+
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
26178
|
+
const query = typeof body.query === "string" ? body.query.trim() : "";
|
|
26179
|
+
const maxMessages = Math.min(150, Math.max(1, Number.isFinite(body.maxMessages) ? Number(body.maxMessages) : 50));
|
|
26180
|
+
const pageToken = typeof body.pageToken === "string" ? body.pageToken : void 0;
|
|
26181
|
+
if (!connectionId || !query) return c.json({ ok: false, error: "connectionId and query are required." }, 400);
|
|
26182
|
+
try {
|
|
26183
|
+
const listRaw = await callScheduleConnectionRead(user.email, connectionId, "list-messages", { q: query, maxResults: maxMessages, pageToken });
|
|
26184
|
+
const listText = toolResultText(listRaw);
|
|
26185
|
+
const list = listText ? JSON.parse(listText) : {};
|
|
26186
|
+
const ids = (list.messages ?? []).map((m) => m.id);
|
|
26187
|
+
const messages = [];
|
|
26188
|
+
const contacts = /* @__PURE__ */ new Map();
|
|
26189
|
+
for (const id of ids) {
|
|
26190
|
+
const msgRaw = await callScheduleConnectionRead(user.email, connectionId, "get-message", { id, format: "metadata" });
|
|
26191
|
+
const msgText = toolResultText(msgRaw);
|
|
26192
|
+
if (!msgText) continue;
|
|
26193
|
+
const msg = JSON.parse(msgText);
|
|
26194
|
+
const headers = new Map((msg.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
26195
|
+
const date = headers.get("date") ?? null;
|
|
26196
|
+
const fromRaw = headers.get("from");
|
|
26197
|
+
const from = parseEmailAddress(fromRaw);
|
|
26198
|
+
const subject = headers.get("subject") ?? null;
|
|
26199
|
+
messages.push({ id, date, from: fromRaw ?? null, to: headers.get("to") ?? null, subject, snippet: msg.snippet ?? null });
|
|
26200
|
+
if (from) {
|
|
26201
|
+
const domain = from.email.split("@")[1] ?? null;
|
|
26202
|
+
const existing = contacts.get(from.email);
|
|
26203
|
+
if (existing) {
|
|
26204
|
+
existing.messageCount += 1;
|
|
26205
|
+
existing.name = existing.name ?? from.name;
|
|
26206
|
+
if (subject && existing.sampleSubjects.length < 5 && !existing.sampleSubjects.includes(subject)) existing.sampleSubjects.push(subject);
|
|
26207
|
+
if (date && (!existing.lastSeen || date > existing.lastSeen)) existing.lastSeen = date;
|
|
26208
|
+
if (date && (!existing.firstSeen || date < existing.firstSeen)) existing.firstSeen = date;
|
|
26209
|
+
} else {
|
|
26210
|
+
contacts.set(from.email, { email: from.email, name: from.name, domain, messageCount: 1, firstSeen: date, lastSeen: date, sampleSubjects: subject ? [subject] : [] });
|
|
26211
|
+
}
|
|
26212
|
+
}
|
|
26213
|
+
}
|
|
26214
|
+
const totalMatches = list.resultSizeEstimate;
|
|
26215
|
+
return c.json({
|
|
26216
|
+
ok: true,
|
|
26217
|
+
totalMatches,
|
|
26218
|
+
messagesFetched: messages.length,
|
|
26219
|
+
truncated: typeof totalMatches === "number" ? totalMatches > messages.length : Boolean(list.nextPageToken),
|
|
26220
|
+
nextPageToken: list.nextPageToken ?? null,
|
|
26221
|
+
messages,
|
|
26222
|
+
contacts: Array.from(contacts.values())
|
|
26223
|
+
});
|
|
26224
|
+
} catch (err) {
|
|
26225
|
+
return scheduleConnectionError(c, err, "Unable to search Gmail contacts.");
|
|
26226
|
+
}
|
|
26227
|
+
});
|
|
26122
26228
|
app.post("/schedule-connections/actions/google-calendar/create-event", auth2, requireIntegrationsTier, async (c) => {
|
|
26123
26229
|
const user = c.get("user");
|
|
26124
26230
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -26137,7 +26243,7 @@ app.post("/schedule-connections/actions/google-calendar/create-event", auth2, re
|
|
|
26137
26243
|
start: { dateTime: body.startDateTime, timeZone },
|
|
26138
26244
|
end: { dateTime: body.endDateTime, timeZone },
|
|
26139
26245
|
attendees
|
|
26140
|
-
},
|
|
26246
|
+
}, "create-event", connectedActionIdempotencyKey(c));
|
|
26141
26247
|
return c.json({ ok: true, result });
|
|
26142
26248
|
} catch (err) {
|
|
26143
26249
|
return scheduleConnectionError(c, err, "Unable to create the calendar event.");
|
|
@@ -26157,7 +26263,7 @@ app.post("/schedule-connections/actions/zoom/create-meeting", auth2, requireInte
|
|
|
26157
26263
|
durationMinutes: typeof body.durationMinutes === "number" ? body.durationMinutes : 30,
|
|
26158
26264
|
timezone: typeof body.timezone === "string" ? body.timezone : void 0,
|
|
26159
26265
|
agenda: body.agenda
|
|
26160
|
-
},
|
|
26266
|
+
}, "create-meeting", connectedActionIdempotencyKey(c));
|
|
26161
26267
|
return c.json({ ok: true, result });
|
|
26162
26268
|
} catch (err) {
|
|
26163
26269
|
return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
|
|
@@ -27185,6 +27291,22 @@ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, asy
|
|
|
27185
27291
|
return c.json({ error: message }, 500);
|
|
27186
27292
|
}
|
|
27187
27293
|
});
|
|
27294
|
+
async function chargeTierChangeNow(stripeClient, subscriptionId, customerId) {
|
|
27295
|
+
try {
|
|
27296
|
+
const draft = await stripeClient.invoices.create({
|
|
27297
|
+
customer: customerId,
|
|
27298
|
+
subscription: subscriptionId,
|
|
27299
|
+
auto_advance: true,
|
|
27300
|
+
collection_method: "charge_automatically"
|
|
27301
|
+
});
|
|
27302
|
+
const finalized = await stripeClient.invoices.finalizeInvoice(draft.id);
|
|
27303
|
+
if (finalized.amount_due <= 0 || finalized.status === "paid") return { ok: true, amountDue: finalized.amount_due };
|
|
27304
|
+
const paid = await stripeClient.invoices.pay(finalized.id);
|
|
27305
|
+
return { ok: paid.status === "paid", amountDue: paid.amount_due };
|
|
27306
|
+
} catch (err) {
|
|
27307
|
+
return { ok: false, amountDue: 0, error: err instanceof Error ? err.message : "Unable to charge the plan change immediately." };
|
|
27308
|
+
}
|
|
27309
|
+
}
|
|
27188
27310
|
app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
27189
27311
|
try {
|
|
27190
27312
|
const user = c.get("sessionUser");
|
|
@@ -27208,7 +27330,16 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
27208
27330
|
items: [{ id: itemId, price: tier.price_id }],
|
|
27209
27331
|
proration_behavior: "create_prorations"
|
|
27210
27332
|
});
|
|
27211
|
-
|
|
27333
|
+
const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
|
|
27334
|
+
if (!billed.ok) {
|
|
27335
|
+
return c.json({
|
|
27336
|
+
updated: true,
|
|
27337
|
+
tier: tier.tier,
|
|
27338
|
+
billed: false,
|
|
27339
|
+
error: billed.error ?? "Plan changed, but the prorated charge could not be collected. Update your payment method and it will retry automatically."
|
|
27340
|
+
});
|
|
27341
|
+
}
|
|
27342
|
+
return c.json({ updated: true, tier: tier.tier, billed: true });
|
|
27212
27343
|
}
|
|
27213
27344
|
}
|
|
27214
27345
|
const session = await stripeClient.checkout.sessions.create({
|
|
@@ -27325,7 +27456,16 @@ app.post("/billing/subscribe/terminal-checkout", auth2, async (c) => {
|
|
|
27325
27456
|
items: [{ id: itemId, price: tier.price_id }],
|
|
27326
27457
|
proration_behavior: "create_prorations"
|
|
27327
27458
|
});
|
|
27328
|
-
|
|
27459
|
+
const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
|
|
27460
|
+
if (!billed.ok) {
|
|
27461
|
+
return c.json({
|
|
27462
|
+
updated: true,
|
|
27463
|
+
tier: tier.tier,
|
|
27464
|
+
billed: false,
|
|
27465
|
+
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.`
|
|
27466
|
+
});
|
|
27467
|
+
}
|
|
27468
|
+
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.` });
|
|
27329
27469
|
}
|
|
27330
27470
|
}
|
|
27331
27471
|
const session = await stripeClient.checkout.sessions.create({
|
|
@@ -27424,7 +27564,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
27424
27564
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
27425
27565
|
return c.json({ error: "Unauthorized" }, 401);
|
|
27426
27566
|
}
|
|
27427
|
-
const { drainQueue } = await import("./worker-
|
|
27567
|
+
const { drainQueue } = await import("./worker-ZZCZNY3R.js");
|
|
27428
27568
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
27429
27569
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
27430
27570
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
|
|
@@ -27627,4 +27767,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
27627
27767
|
export {
|
|
27628
27768
|
app
|
|
27629
27769
|
};
|
|
27630
|
-
//# sourceMappingURL=server-
|
|
27770
|
+
//# sourceMappingURL=server-NHLEIKDZ.js.map
|