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