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
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
createHarvestAttemptRecorder,
|
|
7
7
|
harvestProblemResponse,
|
|
8
8
|
serializeHarvestProblem
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-C2OD4ELT.js";
|
|
10
10
|
import {
|
|
11
11
|
BrowserDriver,
|
|
12
12
|
MapsSelectors,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
harvest,
|
|
16
16
|
resolveKernelProxyId,
|
|
17
17
|
serpArrivalUrl
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-A54ELKJK.js";
|
|
19
19
|
import {
|
|
20
20
|
HttpMcpToolExecutor,
|
|
21
21
|
assessTranscriptSignal,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
sanitizeAttempts,
|
|
36
36
|
sanitizeHarvestResult,
|
|
37
37
|
transcribeMediaUrl
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-6ZUK4H66.js";
|
|
39
39
|
import {
|
|
40
40
|
auditImages,
|
|
41
41
|
buildLinkReport,
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
recordKernelSession,
|
|
53
53
|
recordVendorUsage,
|
|
54
54
|
runWithCostContext
|
|
55
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-WJJTGRK4.js";
|
|
56
56
|
import {
|
|
57
57
|
csvRecords,
|
|
58
58
|
listWorkflowDefinitions,
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
RawMapsOverviewSchema,
|
|
77
77
|
RawMapsReviewStatsSchema
|
|
78
78
|
} from "./chunk-V73MPRU6.js";
|
|
79
|
-
import "./chunk-
|
|
79
|
+
import "./chunk-F65IIUHI.js";
|
|
80
80
|
import {
|
|
81
81
|
completeExtractJob,
|
|
82
82
|
countSuccessfulPages,
|
|
@@ -87,11 +87,12 @@ import {
|
|
|
87
87
|
saveExtractPages,
|
|
88
88
|
setExtractJobTotal,
|
|
89
89
|
settleExtractJob
|
|
90
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-XL6YIWJ3.js";
|
|
91
91
|
import {
|
|
92
92
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
93
93
|
CONCURRENCY_PRICE_ID,
|
|
94
|
-
|
|
94
|
+
CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS,
|
|
95
|
+
CONNECTED_ACTIVE_CONNECTION_MONTHLY_MC,
|
|
95
96
|
CONNECTED_COMPUTE_CREDITS_PER_SECOND,
|
|
96
97
|
CONNECTED_FUNCTION_RUN_CREDITS,
|
|
97
98
|
CONNECTED_FUNCTION_RUN_MC,
|
|
@@ -123,9 +124,10 @@ import {
|
|
|
123
124
|
concurrencySlotBillingInfo,
|
|
124
125
|
getPublicRatesPayload,
|
|
125
126
|
insufficientBalanceResponse,
|
|
127
|
+
mcToCredits,
|
|
126
128
|
serpActualCostMc,
|
|
127
129
|
vendorCostUsdToMc
|
|
128
|
-
} from "./chunk-
|
|
130
|
+
} from "./chunk-LKIP7VDN.js";
|
|
129
131
|
import {
|
|
130
132
|
CaptchaError,
|
|
131
133
|
ExtractionError,
|
|
@@ -211,7 +213,7 @@ import {
|
|
|
211
213
|
settleDebitMcIdempotent,
|
|
212
214
|
upsertPageSnapshot,
|
|
213
215
|
verifyPassword
|
|
214
|
-
} from "./chunk-
|
|
216
|
+
} from "./chunk-AIK7WQD5.js";
|
|
215
217
|
|
|
216
218
|
// src/blog/registry.ts
|
|
217
219
|
var posts = [
|
|
@@ -7369,7 +7371,7 @@ var siteExtractFn = inngest.createFunction(
|
|
|
7369
7371
|
return buildSeoAudit(pages, job.startUrl);
|
|
7370
7372
|
}) : null;
|
|
7371
7373
|
const artifacts = await step.run("finalize", async () => {
|
|
7372
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
7374
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-YNXTFCC3.js");
|
|
7373
7375
|
const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
|
|
7374
7376
|
await completeExtractJob(jobId, stored);
|
|
7375
7377
|
return stored;
|
|
@@ -13079,15 +13081,115 @@ videoApp.post("/status", createApiKeyAuth(), async (c) => {
|
|
|
13079
13081
|
// src/api/maps-routes.ts
|
|
13080
13082
|
import { Hono as Hono11 } from "hono";
|
|
13081
13083
|
|
|
13084
|
+
// src/extractor/GoogleBusinessPanel.ts
|
|
13085
|
+
function clickNamedBusinessCardInBrowser(businessName) {
|
|
13086
|
+
const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
13087
|
+
const expected = normalize(businessName);
|
|
13088
|
+
const headings = Array.from(document.querySelectorAll('div[role="heading"], h3')).filter((heading) => normalize(heading.textContent) === expected);
|
|
13089
|
+
for (const heading of headings) {
|
|
13090
|
+
if (heading.closest('[role="dialog"], dialog')) continue;
|
|
13091
|
+
const directlyClickable = heading.closest(
|
|
13092
|
+
'button, a, [role="button"], [role="link"], [jsaction]'
|
|
13093
|
+
);
|
|
13094
|
+
if (directlyClickable) {
|
|
13095
|
+
directlyClickable.click();
|
|
13096
|
+
return true;
|
|
13097
|
+
}
|
|
13098
|
+
let target = heading;
|
|
13099
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
13100
|
+
const parent = target.parentElement;
|
|
13101
|
+
if (!parent || parent.querySelectorAll('div[role="heading"], h3').length > 1) break;
|
|
13102
|
+
target = parent;
|
|
13103
|
+
}
|
|
13104
|
+
target.click();
|
|
13105
|
+
return true;
|
|
13106
|
+
}
|
|
13107
|
+
return false;
|
|
13108
|
+
}
|
|
13109
|
+
function clickBusinessPanelRowInBrowser(input) {
|
|
13110
|
+
const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
13111
|
+
const expectedBusiness = normalize(input.businessName).toLowerCase();
|
|
13112
|
+
const visible = (element) => element.getAttribute("aria-hidden") !== "true" && (element.offsetParent !== null || element.getClientRects().length > 0);
|
|
13113
|
+
const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => visible(dialog) && normalize(dialog.innerText).toLowerCase().includes(expectedBusiness)).reverse();
|
|
13114
|
+
for (const dialog of dialogs) {
|
|
13115
|
+
const row = Array.from(
|
|
13116
|
+
dialog.querySelectorAll('button, a, [role="button"]')
|
|
13117
|
+
).find((element) => visible(element) && normalize(element.innerText).startsWith(input.prefix));
|
|
13118
|
+
if (row) {
|
|
13119
|
+
row.click();
|
|
13120
|
+
return true;
|
|
13121
|
+
}
|
|
13122
|
+
}
|
|
13123
|
+
return false;
|
|
13124
|
+
}
|
|
13125
|
+
function extractOpenBusinessPanelDialogItemsInBrowser(expectedHeading) {
|
|
13126
|
+
const normalize = (value) => (value ?? "").trim();
|
|
13127
|
+
const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => dialog.getAttribute("aria-hidden") !== "true").reverse();
|
|
13128
|
+
const matchingDialog = dialogs.find((dialog) => {
|
|
13129
|
+
const heading2 = dialog.querySelector('h1, h2, [role="heading"]');
|
|
13130
|
+
return normalize(heading2?.innerText || heading2?.textContent).startsWith(expectedHeading);
|
|
13131
|
+
});
|
|
13132
|
+
if (!matchingDialog) return [];
|
|
13133
|
+
const heading = matchingDialog.querySelector('h1, h2, [role="heading"]');
|
|
13134
|
+
const headingLines = new Set(
|
|
13135
|
+
normalize(heading?.innerText || heading?.textContent).split("\n").map((value) => value.trim()).filter(Boolean)
|
|
13136
|
+
);
|
|
13137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13138
|
+
const items = [];
|
|
13139
|
+
matchingDialog.querySelectorAll("*").forEach((element) => {
|
|
13140
|
+
if (heading?.contains(element)) return;
|
|
13141
|
+
if (element.children.length > 0) return;
|
|
13142
|
+
const value = normalize(element.innerText || element.textContent);
|
|
13143
|
+
if (!value || headingLines.has(value) || seen.has(value)) return;
|
|
13144
|
+
if (/^(back|close|done|thanks)$/i.test(value)) return;
|
|
13145
|
+
if (/^serves\s.*following/i.test(value)) return;
|
|
13146
|
+
seen.add(value);
|
|
13147
|
+
items.push(value);
|
|
13148
|
+
});
|
|
13149
|
+
return items;
|
|
13150
|
+
}
|
|
13151
|
+
function closeOpenBusinessPanelDialogInBrowser(expectedHeading) {
|
|
13152
|
+
const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).filter((dialog) => dialog.getAttribute("aria-hidden") !== "true").reverse();
|
|
13153
|
+
const matchingDialog = dialogs.find((dialog) => {
|
|
13154
|
+
const heading = dialog.querySelector('h1, h2, [role="heading"]');
|
|
13155
|
+
return (heading?.innerText || heading?.textContent || "").trim().startsWith(expectedHeading);
|
|
13156
|
+
});
|
|
13157
|
+
const close = matchingDialog?.querySelector(
|
|
13158
|
+
'[aria-label*="Close" i], button[aria-label*="close" i]'
|
|
13159
|
+
);
|
|
13160
|
+
close?.click();
|
|
13161
|
+
return Boolean(close);
|
|
13162
|
+
}
|
|
13163
|
+
async function clickPanelRowWithRetry(page, businessName, prefix) {
|
|
13164
|
+
const input = { businessName, prefix };
|
|
13165
|
+
if (await page.evaluate(clickBusinessPanelRowInBrowser, input)) return true;
|
|
13166
|
+
await page.waitForTimeout(1500);
|
|
13167
|
+
return page.evaluate(clickBusinessPanelRowInBrowser, input);
|
|
13168
|
+
}
|
|
13169
|
+
async function extractExpandedList(page, businessName, rowPrefix, dialogHeading) {
|
|
13170
|
+
if (!await clickPanelRowWithRetry(page, businessName, rowPrefix)) return [];
|
|
13171
|
+
await page.waitForTimeout(500);
|
|
13172
|
+
const items = await page.evaluate(
|
|
13173
|
+
extractOpenBusinessPanelDialogItemsInBrowser,
|
|
13174
|
+
dialogHeading
|
|
13175
|
+
);
|
|
13176
|
+
await page.evaluate(closeOpenBusinessPanelDialogInBrowser, dialogHeading);
|
|
13177
|
+
await page.waitForTimeout(250);
|
|
13178
|
+
return items;
|
|
13179
|
+
}
|
|
13180
|
+
async function extractExpandedGoogleBusinessPanelServices(page, businessName) {
|
|
13181
|
+
const services = await extractExpandedList(page, businessName, "Services:", "Services");
|
|
13182
|
+
const areasServed = await extractExpandedList(page, businessName, "Serves ", "Areas Served");
|
|
13183
|
+
return { services, areasServed };
|
|
13184
|
+
}
|
|
13185
|
+
function completeCollapsedPanelValues(values) {
|
|
13186
|
+
return values.filter((value) => !/(?:\.{3}|…)\s*$/.test(value.trim()));
|
|
13187
|
+
}
|
|
13188
|
+
|
|
13082
13189
|
// src/extractor/MapsSearchExtractor.ts
|
|
13083
13190
|
var LOCAL_RESULTS_PAGE_LIMIT = 8;
|
|
13084
13191
|
var LOCAL_RESULTS_WAIT_MS = 1e3;
|
|
13085
13192
|
var MAPS_REDIRECT_BASE = "https://serp-redirector.vercel.app";
|
|
13086
|
-
function buildGoogleLocalResultsUrl(input) {
|
|
13087
|
-
const query = [input.query, input.location].filter(Boolean).join(" ");
|
|
13088
|
-
const params = new URLSearchParams({ q: query, udm: "1", gl: input.gl, hl: input.hl, pws: "0" });
|
|
13089
|
-
return `https://www.google.com/search?${params.toString()}`;
|
|
13090
|
-
}
|
|
13091
13193
|
function buildGoogleOrganicSearchUrl(input) {
|
|
13092
13194
|
const query = [input.query, input.location].filter(Boolean).join(" ");
|
|
13093
13195
|
const params = new URLSearchParams({ q: query, gl: input.gl, hl: input.hl, pws: "0" });
|
|
@@ -13354,6 +13456,11 @@ var MapsSearchExtractor = class {
|
|
|
13354
13456
|
areasServed
|
|
13355
13457
|
};
|
|
13356
13458
|
}, includeServices);
|
|
13459
|
+
if (includeServices) {
|
|
13460
|
+
const expanded = await extractExpandedGoogleBusinessPanelServices(page, card.name);
|
|
13461
|
+
details.services = expanded.services.length > 0 ? expanded.services : completeCollapsedPanelValues(details.services);
|
|
13462
|
+
details.areasServed = expanded.areasServed.length > 0 ? expanded.areasServed : completeCollapsedPanelValues(details.areasServed);
|
|
13463
|
+
}
|
|
13357
13464
|
await this.closeDialog(page);
|
|
13358
13465
|
return details;
|
|
13359
13466
|
}
|
|
@@ -13398,6 +13505,7 @@ var MapsSearchExtractor = class {
|
|
|
13398
13505
|
};
|
|
13399
13506
|
|
|
13400
13507
|
// src/extractor/MapsNavigator.ts
|
|
13508
|
+
var BUSINESS_PANEL_REDIRECT_BASE = "https://serp-redirector.vercel.app";
|
|
13401
13509
|
var MapsNavigator = class {
|
|
13402
13510
|
constructor(page) {
|
|
13403
13511
|
this.page = page;
|
|
@@ -13455,26 +13563,67 @@ var MapsNavigator = class {
|
|
|
13455
13563
|
await this.page.waitForSelector('[role="main"] h2', { timeout: 5e3 }).catch(() => {
|
|
13456
13564
|
});
|
|
13457
13565
|
}
|
|
13458
|
-
async navigateToSerpBusinessPanel(businessName, location2, gl, hl) {
|
|
13459
|
-
const searchUrl =
|
|
13460
|
-
|
|
13566
|
+
async navigateToSerpBusinessPanel(businessName, location2, gl, hl, category) {
|
|
13567
|
+
const searchUrl = buildGoogleOrganicSearchUrl({
|
|
13568
|
+
query: category?.trim() || businessName,
|
|
13569
|
+
location: location2,
|
|
13570
|
+
gl,
|
|
13571
|
+
hl
|
|
13461
13572
|
});
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
return
|
|
13473
|
-
}
|
|
13474
|
-
|
|
13475
|
-
await
|
|
13573
|
+
const arrivalUrl = serpArrivalUrl(
|
|
13574
|
+
searchUrl,
|
|
13575
|
+
process.env.SERP_REDIRECT_BASE?.trim() || BUSINESS_PANEL_REDIRECT_BASE
|
|
13576
|
+
);
|
|
13577
|
+
await this.page.goto(arrivalUrl, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch(() => {
|
|
13578
|
+
});
|
|
13579
|
+
await this.page.waitForTimeout(2e3);
|
|
13580
|
+
if (await this.isGoogleChallenge()) return false;
|
|
13581
|
+
await this.waitForNamedBusinessCard(businessName, 5e3);
|
|
13582
|
+
if (await this.page.evaluate(clickNamedBusinessCardInBrowser, businessName)) {
|
|
13583
|
+
return this.waitForBusinessDialog(businessName);
|
|
13584
|
+
}
|
|
13585
|
+
const moreBusinesses = this.page.getByRole("link", { name: /more (businesses|places|results)/i }).or(this.page.getByRole("button", { name: /more (businesses|places|results)/i })).first();
|
|
13586
|
+
if (!await moreBusinesses.isVisible({ timeout: 5e3 }).catch(() => false)) return false;
|
|
13587
|
+
await moreBusinesses.click({ timeout: 1e4 }).catch(() => {
|
|
13588
|
+
});
|
|
13589
|
+
await this.page.waitForTimeout(3e3);
|
|
13590
|
+
if (await this.isGoogleChallenge()) return false;
|
|
13591
|
+
await this.waitForNamedBusinessCard(businessName, 3e3);
|
|
13592
|
+
if (!await this.page.evaluate(clickNamedBusinessCardInBrowser, businessName)) {
|
|
13593
|
+
const searchBox = this.page.locator('textarea[name="q"]:visible, input[name="q"]:visible').first();
|
|
13594
|
+
if (!await searchBox.isVisible({ timeout: 5e3 }).catch(() => false)) return false;
|
|
13595
|
+
await searchBox.fill(`${businessName} ${location2}`);
|
|
13596
|
+
await searchBox.press("Enter");
|
|
13597
|
+
await this.page.waitForTimeout(3e3);
|
|
13598
|
+
if (await this.isGoogleChallenge()) return false;
|
|
13599
|
+
await this.waitForNamedBusinessCard(businessName, 1e4);
|
|
13600
|
+
if (!await this.page.evaluate(clickNamedBusinessCardInBrowser, businessName)) return false;
|
|
13601
|
+
}
|
|
13602
|
+
return this.waitForBusinessDialog(businessName);
|
|
13603
|
+
}
|
|
13604
|
+
async isGoogleChallenge() {
|
|
13605
|
+
return this.page.evaluate(() => {
|
|
13606
|
+
const text2 = (document.body?.innerText ?? "").slice(0, 2e3);
|
|
13607
|
+
return /unusual traffic|captcha|recaptcha|about this page/i.test(text2) || /\/sorry\//.test(location.href);
|
|
13608
|
+
}).catch(() => true);
|
|
13609
|
+
}
|
|
13610
|
+
async waitForNamedBusinessCard(businessName, timeout) {
|
|
13611
|
+
if (typeof this.page.waitForFunction !== "function") return;
|
|
13612
|
+
await this.page.waitForFunction(
|
|
13613
|
+
(expectedName) => {
|
|
13614
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
13615
|
+
const expected = normalize(expectedName);
|
|
13616
|
+
return Array.from(document.querySelectorAll('div[role="heading"], h3')).some((element) => normalize(element.textContent ?? "") === expected);
|
|
13617
|
+
},
|
|
13618
|
+
businessName,
|
|
13619
|
+
{ timeout }
|
|
13620
|
+
).catch(() => void 0);
|
|
13621
|
+
}
|
|
13622
|
+
async waitForBusinessDialog(businessName) {
|
|
13623
|
+
const dialog = this.page.locator('dialog:visible, [role="dialog"]:visible').filter({ hasText: businessName }).first();
|
|
13624
|
+
await dialog.waitFor({ state: "visible", timeout: 1e4 }).catch(() => {
|
|
13476
13625
|
});
|
|
13477
|
-
return
|
|
13626
|
+
return dialog.isVisible().catch(() => false);
|
|
13478
13627
|
}
|
|
13479
13628
|
buildReviewsUrl(placeUrl) {
|
|
13480
13629
|
const fid = placeUrl.match(/!1s(0x[0-9a-f]+:0x[0-9a-f]+)/i);
|
|
@@ -13519,6 +13668,71 @@ var MapsNavigator = class {
|
|
|
13519
13668
|
}
|
|
13520
13669
|
};
|
|
13521
13670
|
|
|
13671
|
+
// src/extractor/GoogleBusinessPanelRotation.ts
|
|
13672
|
+
async function extractGoogleBusinessPanelServicesWithRotation(options) {
|
|
13673
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 5);
|
|
13674
|
+
for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
|
|
13675
|
+
const driver = new BrowserDriver();
|
|
13676
|
+
const attemptNumber = attemptIndex + 1;
|
|
13677
|
+
const useDefaultProxy = attemptIndex < 2;
|
|
13678
|
+
const useHeadful = attemptIndex === maxAttempts - 1;
|
|
13679
|
+
try {
|
|
13680
|
+
await driver.launch({
|
|
13681
|
+
headless: useHeadful ? false : options.headless,
|
|
13682
|
+
headlessMode: useHeadful ? "headful" : options.headless ? "headless" : "headful",
|
|
13683
|
+
kernelApiKey: options.kernelApiKey,
|
|
13684
|
+
keepDefaultProxy: useDefaultProxy,
|
|
13685
|
+
proxyMode: "none",
|
|
13686
|
+
viewport: { width: 1280, height: 900 },
|
|
13687
|
+
locale: `${options.hl}-${options.gl.toUpperCase()}`
|
|
13688
|
+
});
|
|
13689
|
+
const page = driver.getPage();
|
|
13690
|
+
const opened = await new MapsNavigator(page).navigateToSerpBusinessPanel(
|
|
13691
|
+
options.businessName,
|
|
13692
|
+
options.location,
|
|
13693
|
+
options.gl,
|
|
13694
|
+
options.hl,
|
|
13695
|
+
options.category
|
|
13696
|
+
);
|
|
13697
|
+
if (!opened) {
|
|
13698
|
+
console.warn(JSON.stringify({
|
|
13699
|
+
event: "maps_place_services_panel_not_opened",
|
|
13700
|
+
attempt_number: attemptNumber,
|
|
13701
|
+
max_attempts: maxAttempts,
|
|
13702
|
+
keep_default_proxy: useDefaultProxy,
|
|
13703
|
+
headless_mode: useHeadful ? "headful" : options.headless ? "headless" : "headful",
|
|
13704
|
+
final_url: page.url()
|
|
13705
|
+
}));
|
|
13706
|
+
continue;
|
|
13707
|
+
}
|
|
13708
|
+
const result = await extractExpandedGoogleBusinessPanelServices(
|
|
13709
|
+
page,
|
|
13710
|
+
options.businessName
|
|
13711
|
+
);
|
|
13712
|
+
return {
|
|
13713
|
+
...result,
|
|
13714
|
+
opened: true,
|
|
13715
|
+
attempts: attemptNumber
|
|
13716
|
+
};
|
|
13717
|
+
} catch (error) {
|
|
13718
|
+
console.warn(JSON.stringify({
|
|
13719
|
+
event: "maps_place_services_attempt_failed",
|
|
13720
|
+
attempt_number: attemptNumber,
|
|
13721
|
+
max_attempts: maxAttempts,
|
|
13722
|
+
message: error instanceof Error ? error.message : String(error)
|
|
13723
|
+
}));
|
|
13724
|
+
} finally {
|
|
13725
|
+
await driver.close().catch(() => void 0);
|
|
13726
|
+
}
|
|
13727
|
+
}
|
|
13728
|
+
return {
|
|
13729
|
+
services: [],
|
|
13730
|
+
areasServed: [],
|
|
13731
|
+
opened: false,
|
|
13732
|
+
attempts: maxAttempts
|
|
13733
|
+
};
|
|
13734
|
+
}
|
|
13735
|
+
|
|
13522
13736
|
// src/extractor/MapsReviewCollector.ts
|
|
13523
13737
|
var REVIEW_SCROLL_BUDGET_MS = 45e3;
|
|
13524
13738
|
var REVIEW_SCROLL_STEP_MS = 1200;
|
|
@@ -13650,14 +13864,33 @@ var MapsExtractor = class {
|
|
|
13650
13864
|
let servicesStatus = "not_requested";
|
|
13651
13865
|
if (options.includeServices) {
|
|
13652
13866
|
try {
|
|
13653
|
-
|
|
13867
|
+
let opened = await navigator.navigateToSerpBusinessPanel(
|
|
13868
|
+
options.businessName,
|
|
13869
|
+
options.location,
|
|
13870
|
+
options.gl,
|
|
13871
|
+
options.hl,
|
|
13872
|
+
overview.category ?? void 0
|
|
13873
|
+
);
|
|
13654
13874
|
if (opened) {
|
|
13655
|
-
const result = await
|
|
13875
|
+
const result = await extractExpandedGoogleBusinessPanelServices(page, options.businessName);
|
|
13656
13876
|
services = result.services;
|
|
13657
13877
|
areasServed = result.areasServed;
|
|
13658
13878
|
servicesStatus = services.length > 0 || areasServed.length > 0 ? "collected" : "none_exist";
|
|
13659
13879
|
} else {
|
|
13660
|
-
|
|
13880
|
+
await this.driver.close().catch(() => void 0);
|
|
13881
|
+
const result = await extractGoogleBusinessPanelServicesWithRotation({
|
|
13882
|
+
businessName: options.businessName,
|
|
13883
|
+
location: options.location,
|
|
13884
|
+
category: overview.category ?? void 0,
|
|
13885
|
+
gl: options.gl,
|
|
13886
|
+
hl: options.hl,
|
|
13887
|
+
headless: options.headless,
|
|
13888
|
+
kernelApiKey: options.kernelApiKey
|
|
13889
|
+
});
|
|
13890
|
+
opened = result.opened;
|
|
13891
|
+
services = result.services;
|
|
13892
|
+
areasServed = result.areasServed;
|
|
13893
|
+
servicesStatus = opened ? services.length > 0 || areasServed.length > 0 ? "collected" : "none_exist" : "unavailable";
|
|
13661
13894
|
}
|
|
13662
13895
|
} catch (err) {
|
|
13663
13896
|
console.warn("[MapsExtractor] services/areas-served extraction failed:", err instanceof Error ? err.message : String(err));
|
|
@@ -13870,18 +14103,6 @@ var MapsExtractor = class {
|
|
|
13870
14103
|
}
|
|
13871
14104
|
return result.data;
|
|
13872
14105
|
}
|
|
13873
|
-
async extractServicesAndAreasServed(page) {
|
|
13874
|
-
return page.evaluate(() => {
|
|
13875
|
-
const visible = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).find((dialog) => dialog.offsetParent !== null);
|
|
13876
|
-
if (!visible) return { services: [], areasServed: [] };
|
|
13877
|
-
const values = Array.from(visible.querySelectorAll("button")).map((button) => button.innerText.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
13878
|
-
const servicesText = values.find((value) => /^services\s*:/i.test(value));
|
|
13879
|
-
const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
|
|
13880
|
-
const served = values.find((value) => /^serves\b/i.test(value));
|
|
13881
|
-
const areasServed = served ? [served.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
|
|
13882
|
-
return { services, areasServed };
|
|
13883
|
-
});
|
|
13884
|
-
}
|
|
13885
14106
|
};
|
|
13886
14107
|
|
|
13887
14108
|
// src/maps-search-rotation.ts
|
|
@@ -19574,13 +19795,11 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
|
|
|
19574
19795
|
}
|
|
19575
19796
|
|
|
19576
19797
|
// src/api/stripe-routes.ts
|
|
19577
|
-
import
|
|
19798
|
+
import Stripe from "stripe";
|
|
19578
19799
|
import { Hono as Hono19 } from "hono";
|
|
19579
19800
|
|
|
19580
19801
|
// src/api/connected-account-billing.ts
|
|
19581
|
-
import Stripe from "stripe";
|
|
19582
19802
|
var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
|
|
19583
|
-
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
19584
19803
|
async function ensureConnectedAccountBillingSchema() {
|
|
19585
19804
|
await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
|
|
19586
19805
|
user_id INTEGER PRIMARY KEY,
|
|
@@ -19634,14 +19853,16 @@ async function setConnectedAccountBillingState(input) {
|
|
|
19634
19853
|
return state;
|
|
19635
19854
|
}
|
|
19636
19855
|
var ConnectedAccountBillingError = class extends Error {
|
|
19637
|
-
constructor(message, code, status = 409) {
|
|
19856
|
+
constructor(message, code, status = 409, details) {
|
|
19638
19857
|
super(message);
|
|
19639
19858
|
this.code = code;
|
|
19640
19859
|
this.status = status;
|
|
19860
|
+
this.details = details;
|
|
19641
19861
|
this.name = "ConnectedAccountBillingError";
|
|
19642
19862
|
}
|
|
19643
19863
|
code;
|
|
19644
19864
|
status;
|
|
19865
|
+
details;
|
|
19645
19866
|
};
|
|
19646
19867
|
function connectedAccountPriceId() {
|
|
19647
19868
|
const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
|
|
@@ -19652,12 +19873,12 @@ function connectedAccountBillingView(actualQuantity, state) {
|
|
|
19652
19873
|
const quantity = Math.max(0, Math.round(actualQuantity));
|
|
19653
19874
|
return {
|
|
19654
19875
|
provider: "nango",
|
|
19655
|
-
billingMode: "
|
|
19876
|
+
billingMode: "flat_recurring_credits",
|
|
19656
19877
|
priceId: connectedAccountPriceId(),
|
|
19657
|
-
|
|
19878
|
+
unitAmountCredits: CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS,
|
|
19658
19879
|
interval: "month",
|
|
19659
19880
|
quantity,
|
|
19660
|
-
|
|
19881
|
+
projectedMonthlyCredits: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS,
|
|
19661
19882
|
status: state?.status ?? "pending",
|
|
19662
19883
|
lastErrorCode: state?.last_error_code ?? null,
|
|
19663
19884
|
syncedAt: state?.synced_at ?? null,
|
|
@@ -19667,29 +19888,23 @@ function connectedAccountBillingView(actualQuantity, state) {
|
|
|
19667
19888
|
function findBasePlanItem(subscription) {
|
|
19668
19889
|
return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
|
|
19669
19890
|
}
|
|
19670
|
-
function
|
|
19671
|
-
return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
|
|
19672
|
-
}
|
|
19673
|
-
function liveSubscription(subscription) {
|
|
19674
|
-
return subscription.status === "active" || subscription.status === "trialing";
|
|
19675
|
-
}
|
|
19676
|
-
function safeStripeErrorCode(error) {
|
|
19891
|
+
function safeErrorCode(error) {
|
|
19677
19892
|
if (error instanceof ConnectedAccountBillingError) return error.code;
|
|
19678
19893
|
return "stripe_connection_billing_failed";
|
|
19679
19894
|
}
|
|
19680
|
-
async function persistFailure(user,
|
|
19895
|
+
async function persistFailure(user, quantityToPersist, error) {
|
|
19681
19896
|
await setConnectedAccountBillingState({
|
|
19682
19897
|
userId: user.id,
|
|
19683
19898
|
stripeSubscriptionId: user.subscription_id,
|
|
19684
|
-
stripeSubscriptionItemId:
|
|
19899
|
+
stripeSubscriptionItemId: null,
|
|
19685
19900
|
priceId: connectedAccountPriceId(),
|
|
19686
|
-
quantity:
|
|
19901
|
+
quantity: quantityToPersist,
|
|
19687
19902
|
status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
|
|
19688
|
-
lastErrorCode:
|
|
19903
|
+
lastErrorCode: safeErrorCode(error),
|
|
19689
19904
|
synced: false
|
|
19690
19905
|
});
|
|
19691
19906
|
}
|
|
19692
|
-
async function reconcileConnectedAccountBilling(user, actualQuantity
|
|
19907
|
+
async function reconcileConnectedAccountBilling(user, actualQuantity) {
|
|
19693
19908
|
if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
|
|
19694
19909
|
throw new Error("actualQuantity must be a non-negative safe integer");
|
|
19695
19910
|
}
|
|
@@ -19703,7 +19918,7 @@ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClie
|
|
|
19703
19918
|
await persistFailure(user, actualQuantity, error);
|
|
19704
19919
|
throw error;
|
|
19705
19920
|
}
|
|
19706
|
-
if (!user.subscription_id) {
|
|
19921
|
+
if (!user.subscription_id || !(user.subscription_tier ?? "") || !Object.values(SUBSCRIPTION_TIERS).some((t) => t.tier === user.subscription_tier)) {
|
|
19707
19922
|
if (actualQuantity === 0) {
|
|
19708
19923
|
const state = await setConnectedAccountBillingState({
|
|
19709
19924
|
userId: user.id,
|
|
@@ -19725,48 +19940,38 @@ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClie
|
|
|
19725
19940
|
await persistFailure(user, actualQuantity, error);
|
|
19726
19941
|
throw error;
|
|
19727
19942
|
}
|
|
19728
|
-
const
|
|
19729
|
-
|
|
19730
|
-
if (!secret2) {
|
|
19731
|
-
throw new ConnectedAccountBillingError(
|
|
19732
|
-
"Stripe is not configured.",
|
|
19733
|
-
"connected_account_price_not_configured",
|
|
19734
|
-
503
|
|
19735
|
-
);
|
|
19736
|
-
}
|
|
19737
|
-
return new Stripe(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
19738
|
-
})();
|
|
19739
|
-
let currentItem;
|
|
19943
|
+
const existingState = await getConnectedAccountBillingState(user.id);
|
|
19944
|
+
const currentQuantity = Math.max(0, existingState?.quantity ?? 0);
|
|
19740
19945
|
try {
|
|
19741
|
-
|
|
19742
|
-
|
|
19743
|
-
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
|
|
19747
|
-
|
|
19748
|
-
|
|
19749
|
-
}
|
|
19750
|
-
if (!findBasePlanItem(subscription)) {
|
|
19751
|
-
throw new ConnectedAccountBillingError(
|
|
19752
|
-
"The base plan item could not be found on this subscription.",
|
|
19753
|
-
"subscription_plan_item_missing",
|
|
19754
|
-
409
|
|
19946
|
+
if (actualQuantity > currentQuantity) {
|
|
19947
|
+
const newConnections = actualQuantity - currentQuantity;
|
|
19948
|
+
const feeMc = newConnections * CONNECTED_ACTIVE_CONNECTION_MONTHLY_MC;
|
|
19949
|
+
const debit = await debitMc(
|
|
19950
|
+
user.id,
|
|
19951
|
+
feeMc,
|
|
19952
|
+
LedgerOperation.CONNECTED_ACCOUNT_FEE,
|
|
19953
|
+
`${newConnections} connected account${newConnections === 1 ? "" : "s"} at ${CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS} credits/month`
|
|
19755
19954
|
);
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
|
|
19761
|
-
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
|
|
19955
|
+
if (!debit.ok) {
|
|
19956
|
+
const requiredMc = feeMc;
|
|
19957
|
+
const error = new ConnectedAccountBillingError(
|
|
19958
|
+
`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.`,
|
|
19959
|
+
"insufficient_credits",
|
|
19960
|
+
402,
|
|
19961
|
+
{
|
|
19962
|
+
balanceCredits: mcToCredits(debit.balance_mc),
|
|
19963
|
+
requiredCredits: mcToCredits(requiredMc),
|
|
19964
|
+
shortByCredits: mcToCredits(Math.max(0, requiredMc - debit.balance_mc))
|
|
19965
|
+
}
|
|
19966
|
+
);
|
|
19967
|
+
await persistFailure(user, currentQuantity, error);
|
|
19968
|
+
throw error;
|
|
19969
|
+
}
|
|
19765
19970
|
}
|
|
19766
19971
|
const state = await setConnectedAccountBillingState({
|
|
19767
19972
|
userId: user.id,
|
|
19768
|
-
stripeSubscriptionId:
|
|
19769
|
-
stripeSubscriptionItemId:
|
|
19973
|
+
stripeSubscriptionId: user.subscription_id,
|
|
19974
|
+
stripeSubscriptionItemId: null,
|
|
19770
19975
|
priceId,
|
|
19771
19976
|
quantity: actualQuantity,
|
|
19772
19977
|
status: "synced",
|
|
@@ -19775,10 +19980,10 @@ async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClie
|
|
|
19775
19980
|
});
|
|
19776
19981
|
return connectedAccountBillingView(actualQuantity, state);
|
|
19777
19982
|
} catch (error) {
|
|
19778
|
-
await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
|
|
19779
19983
|
if (error instanceof ConnectedAccountBillingError) throw error;
|
|
19984
|
+
await persistFailure(user, currentQuantity, error);
|
|
19780
19985
|
throw new ConnectedAccountBillingError(
|
|
19781
|
-
"Unable to update connected-account billing
|
|
19986
|
+
"Unable to update connected-account billing.",
|
|
19782
19987
|
"stripe_connection_billing_failed",
|
|
19783
19988
|
503
|
|
19784
19989
|
);
|
|
@@ -19789,7 +19994,7 @@ async function currentConnectedAccountBillingView(userId, actualQuantity) {
|
|
|
19789
19994
|
}
|
|
19790
19995
|
|
|
19791
19996
|
// src/api/stripe-routes.ts
|
|
19792
|
-
var stripe = new
|
|
19997
|
+
var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
19793
19998
|
var stripeApp = new Hono19();
|
|
19794
19999
|
stripeApp.post("/webhooks", async (c) => {
|
|
19795
20000
|
const sig = c.req.header("stripe-signature");
|
|
@@ -19818,17 +20023,41 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
19818
20023
|
if (tier) {
|
|
19819
20024
|
const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
|
|
19820
20025
|
if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
|
|
19821
|
-
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
|
|
20026
|
+
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id, { neverExpires: tier.credits_never_expire });
|
|
19822
20027
|
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
|
|
20028
|
+
if (tier.includes_memory) {
|
|
20029
|
+
const subscriptionId = subId ?? user.subscription_id;
|
|
20030
|
+
await setMemoryPlan(user.id, "pro", subscriptionId ?? null);
|
|
20031
|
+
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "pro" }));
|
|
20032
|
+
}
|
|
19823
20033
|
const credentials = await syncScheduledActionCredentials(user);
|
|
19824
20034
|
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
20035
|
+
const connectionState = await getConnectedAccountBillingState(user.id);
|
|
20036
|
+
const activeConnections = Math.max(0, connectionState?.quantity ?? 0);
|
|
20037
|
+
if (activeConnections > 0) {
|
|
20038
|
+
const feeMc = activeConnections * CONNECTED_ACTIVE_CONNECTION_MONTHLY_MC;
|
|
20039
|
+
const feeDescription = `${activeConnections} connected account${activeConnections === 1 ? "" : "s"} renewal at ${CONNECTED_ACTIVE_CONNECTION_MONTHLY_CREDITS} credits/month`;
|
|
20040
|
+
const feeDebit = await debitMcIdempotent(user.id, feeMc, LedgerOperation.CONNECTED_ACCOUNT_FEE, feeDescription, `connection-fee:${invoice.id}`);
|
|
20041
|
+
if (!feeDebit.ok) {
|
|
20042
|
+
await setConnectedAccountBillingState({
|
|
20043
|
+
userId: user.id,
|
|
20044
|
+
stripeSubscriptionId: user.subscription_id,
|
|
20045
|
+
stripeSubscriptionItemId: null,
|
|
20046
|
+
priceId: connectionState?.price_id ?? null,
|
|
20047
|
+
quantity: activeConnections,
|
|
20048
|
+
status: "blocked",
|
|
20049
|
+
lastErrorCode: "insufficient_credits",
|
|
20050
|
+
synced: false
|
|
20051
|
+
});
|
|
20052
|
+
}
|
|
20053
|
+
}
|
|
19825
20054
|
}
|
|
19826
20055
|
}
|
|
19827
20056
|
}
|
|
19828
20057
|
}
|
|
19829
20058
|
if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") {
|
|
19830
20059
|
const sub = event.data.object;
|
|
19831
|
-
const priceId = sub.items.data[0]?.price?.id;
|
|
20060
|
+
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
19832
20061
|
if (priceId && priceId in MEMORY_PLANS) {
|
|
19833
20062
|
const mem = MEMORY_PLANS[priceId];
|
|
19834
20063
|
const user2 = await resolveUser2(sub.customer, sub.customer_email);
|
|
@@ -19847,6 +20076,14 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
19847
20076
|
const live = sub.status === "active" || sub.status === "trialing";
|
|
19848
20077
|
const newTier = live ? tier.tier : null;
|
|
19849
20078
|
await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
|
|
20079
|
+
if (tier.includes_memory) {
|
|
20080
|
+
const newMemoryPlan = live ? "pro" : "free";
|
|
20081
|
+
await setMemoryPlan(user.id, newMemoryPlan, live ? sub.id : null);
|
|
20082
|
+
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: newMemoryPlan }));
|
|
20083
|
+
} else if (user.memory_subscription_id === sub.id) {
|
|
20084
|
+
await setMemoryPlan(user.id, "free", null);
|
|
20085
|
+
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "free" }));
|
|
20086
|
+
}
|
|
19850
20087
|
if (live) {
|
|
19851
20088
|
const credentials = await syncScheduledActionCredentials(user);
|
|
19852
20089
|
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
@@ -19858,7 +20095,19 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
19858
20095
|
}
|
|
19859
20096
|
if (event.type === "customer.subscription.deleted") {
|
|
19860
20097
|
const sub = event.data.object;
|
|
19861
|
-
const priceId = sub.items.data[0]?.price?.id;
|
|
20098
|
+
const priceId = findBasePlanItem(sub)?.price?.id ?? sub.items.data[0]?.price?.id;
|
|
20099
|
+
if (priceId && priceId in SUBSCRIPTION_TIERS) {
|
|
20100
|
+
const tier = SUBSCRIPTION_TIERS[priceId];
|
|
20101
|
+
const user2 = await getUserByStripeCustomerId(sub.customer);
|
|
20102
|
+
if (user2) {
|
|
20103
|
+
await setSubscriptionTier(user2.id, null, 0, null);
|
|
20104
|
+
if (tier.includes_memory || user2.memory_subscription_id === sub.id) {
|
|
20105
|
+
await setMemoryPlan(user2.id, "free", null);
|
|
20106
|
+
await syncMemoryKeyPlan(user2, resolveEffectiveMemoryPlan({ ...user2, memory_plan: "free" }));
|
|
20107
|
+
}
|
|
20108
|
+
}
|
|
20109
|
+
return c.json({ received: true });
|
|
20110
|
+
}
|
|
19862
20111
|
if (priceId && priceId in MEMORY_PLANS || await resolveUserByMemorySubscription(sub.id)) {
|
|
19863
20112
|
const user2 = await resolveUserByMemorySubscription(sub.id) ?? await getUserByStripeCustomerId(sub.customer);
|
|
19864
20113
|
if (user2) {
|
|
@@ -19869,9 +20118,7 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
19869
20118
|
}
|
|
19870
20119
|
const user = await getUserByStripeCustomerId(sub.customer);
|
|
19871
20120
|
if (!user) return c.json({ received: true });
|
|
19872
|
-
if (priceId
|
|
19873
|
-
await setSubscriptionTier(user.id, null, 0, null);
|
|
19874
|
-
} else if (priceId === CONCURRENCY_PRICE_ID) {
|
|
20121
|
+
if (priceId === CONCURRENCY_PRICE_ID) {
|
|
19875
20122
|
await setExtraConcurrencySlots(user.id, Math.max(0, user.extra_concurrency_slots - 1));
|
|
19876
20123
|
await setConcurrencySubId(user.id, null);
|
|
19877
20124
|
}
|
|
@@ -19964,9 +20211,9 @@ function negotiateScope(requested, user, resource) {
|
|
|
19964
20211
|
const filtered = wanted.filter((s) => MEMORY_SCOPES.includes(s));
|
|
19965
20212
|
const base = filtered.length > 0 ? filtered : ["memory.read", "memory.write"];
|
|
19966
20213
|
const allowed = base.filter((s) => s === "memory.admin" ? operator : true);
|
|
19967
|
-
const
|
|
19968
|
-
if (operator && !
|
|
19969
|
-
return
|
|
20214
|
+
const unique2 = [...new Set(allowed)];
|
|
20215
|
+
if (operator && !unique2.includes("memory.admin")) unique2.push("memory.admin");
|
|
20216
|
+
return unique2.join(" ");
|
|
19970
20217
|
}
|
|
19971
20218
|
async function sessionUser(token) {
|
|
19972
20219
|
if (!token) return null;
|
|
@@ -21688,50 +21935,262 @@ loadActions();
|
|
|
21688
21935
|
// src/api/resend-inbound-routes.ts
|
|
21689
21936
|
import { Hono as Hono24 } from "hono";
|
|
21690
21937
|
import { Resend } from "resend";
|
|
21691
|
-
|
|
21692
|
-
|
|
21693
|
-
|
|
21694
|
-
|
|
21695
|
-
|
|
21696
|
-
|
|
21697
|
-
|
|
21698
|
-
|
|
21699
|
-
|
|
21700
|
-
|
|
21701
|
-
|
|
21702
|
-
|
|
21703
|
-
|
|
21704
|
-
|
|
21705
|
-
|
|
21938
|
+
|
|
21939
|
+
// src/api/resend-support-thread.ts
|
|
21940
|
+
import { createHash as createHash7 } from "crypto";
|
|
21941
|
+
var SUPPORT_TAGS = [
|
|
21942
|
+
{
|
|
21943
|
+
tag: "support",
|
|
21944
|
+
central: true,
|
|
21945
|
+
reusable: true,
|
|
21946
|
+
description: "Customer support work, context, and follow-up."
|
|
21947
|
+
},
|
|
21948
|
+
{
|
|
21949
|
+
tag: "customer-support",
|
|
21950
|
+
central: true,
|
|
21951
|
+
reusable: true,
|
|
21952
|
+
description: "Inbound and in-product MCP Scraper customer support requests."
|
|
21953
|
+
},
|
|
21954
|
+
{
|
|
21955
|
+
tag: "inbound-email",
|
|
21956
|
+
central: true,
|
|
21957
|
+
reusable: true,
|
|
21958
|
+
description: "Messages received through an inbound email integration."
|
|
21959
|
+
}
|
|
21960
|
+
];
|
|
21961
|
+
var ISSUE_TAGS = [
|
|
21962
|
+
...SUPPORT_TAGS,
|
|
21963
|
+
{
|
|
21964
|
+
tag: "product-feedback",
|
|
21965
|
+
central: true,
|
|
21966
|
+
reusable: true,
|
|
21967
|
+
description: "Customer feedback and reported product problems."
|
|
21968
|
+
}
|
|
21969
|
+
];
|
|
21970
|
+
function cleanLine(value, fallback, max = 240) {
|
|
21971
|
+
const clean2 = String(value ?? "").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
21972
|
+
return (clean2 || fallback).slice(0, max);
|
|
21706
21973
|
}
|
|
21707
21974
|
function quoteUntrusted(value) {
|
|
21708
|
-
const clean2 = value.replace(/\0/g, "").trim().slice(0, 16e3);
|
|
21975
|
+
const clean2 = String(value ?? "").replace(/\0/g, "").trim().slice(0, 16e3);
|
|
21709
21976
|
if (!clean2) return "> _(No plain-text body was available. Review the forwarded email.)_";
|
|
21710
21977
|
return clean2.split("\n").map((line) => `> ${line}`).join("\n");
|
|
21711
21978
|
}
|
|
21712
|
-
function
|
|
21713
|
-
return
|
|
21979
|
+
function shortHash(value, length = 24) {
|
|
21980
|
+
return createHash7("sha256").update(value).digest("hex").slice(0, length);
|
|
21714
21981
|
}
|
|
21715
|
-
function
|
|
21716
|
-
return value.replace(/[^a-
|
|
21982
|
+
function safePathPart(value) {
|
|
21983
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "contact";
|
|
21717
21984
|
}
|
|
21718
|
-
function
|
|
21719
|
-
const
|
|
21720
|
-
|
|
21721
|
-
const
|
|
21722
|
-
const
|
|
21723
|
-
const
|
|
21724
|
-
|
|
21725
|
-
const
|
|
21726
|
-
const
|
|
21985
|
+
function parseMailbox(value) {
|
|
21986
|
+
const raw = cleanLine(value, "", 500);
|
|
21987
|
+
if (!raw) return null;
|
|
21988
|
+
const angle = raw.match(/^(.*?)<([^<>]+)>$/);
|
|
21989
|
+
const candidate = (angle?.[2] ?? raw).trim().toLowerCase();
|
|
21990
|
+
const emailMatch = candidate.match(/[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?/i);
|
|
21991
|
+
if (!emailMatch) return null;
|
|
21992
|
+
const email = emailMatch[0].toLowerCase();
|
|
21993
|
+
const suppliedName = angle?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
21994
|
+
return { email, name: cleanLine(suppliedName, email) };
|
|
21995
|
+
}
|
|
21996
|
+
function supportSender(email) {
|
|
21997
|
+
return parseMailbox(email.reply_to?.[0]) ?? parseMailbox(email.from);
|
|
21998
|
+
}
|
|
21999
|
+
function normalizeSupportSubject(value) {
|
|
22000
|
+
let subject = cleanLine(value, "(no subject)", 240);
|
|
22001
|
+
for (let i = 0; i < 8; i += 1) {
|
|
22002
|
+
const next = subject.replace(/^\[(?:support|external)\]\s*/i, "").replace(/^(?:re|fw|fwd)\s*:\s*/i, "").trim();
|
|
22003
|
+
if (next === subject) break;
|
|
22004
|
+
subject = next;
|
|
22005
|
+
}
|
|
22006
|
+
return subject || "(no subject)";
|
|
22007
|
+
}
|
|
22008
|
+
function supportThreadToken(customerEmail, normalizedSubject) {
|
|
22009
|
+
return shortHash(`${customerEmail.trim().toLowerCase()}
|
|
22010
|
+
${normalizeSupportSubject(normalizedSubject).toLowerCase()}`);
|
|
22011
|
+
}
|
|
22012
|
+
function supportThreadAddress(token, receivingDomain) {
|
|
22013
|
+
const safeToken = token.toLowerCase().replace(/[^a-f0-9]/g, "").slice(0, 24);
|
|
22014
|
+
const domain = receivingDomain.trim().toLowerCase();
|
|
22015
|
+
if (!safeToken || !/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(domain)) throw new Error("invalid support thread address");
|
|
22016
|
+
return `support+${safeToken}@${domain}`;
|
|
22017
|
+
}
|
|
22018
|
+
function isAutomatedMailbox(email) {
|
|
22019
|
+
const local = email.toLowerCase().split("@", 1)[0];
|
|
22020
|
+
return /^(?:no-?reply|do-?not-?reply|mailer-daemon|postmaster|dmarc|bounce|notifications?)$/.test(local);
|
|
22021
|
+
}
|
|
22022
|
+
function extractReplyText(value) {
|
|
22023
|
+
const text2 = String(value ?? "").replace(/\0/g, "").trim();
|
|
22024
|
+
if (!text2) return "";
|
|
22025
|
+
const lines = text2.split(/\r?\n/);
|
|
22026
|
+
const kept = [];
|
|
22027
|
+
for (const line of lines) {
|
|
22028
|
+
if (/^On .+ wrote:\s*$/i.test(line.trim())) break;
|
|
22029
|
+
if (/^-{2,}\s*Forwarded message\s*-{2,}$/i.test(line.trim())) break;
|
|
22030
|
+
if (/^From:\s+MCP Scraper Support\b/i.test(line.trim())) break;
|
|
22031
|
+
if (/^>\s?/.test(line)) break;
|
|
22032
|
+
kept.push(line);
|
|
22033
|
+
}
|
|
22034
|
+
return kept.join("\n").trim().slice(0, 16e3);
|
|
22035
|
+
}
|
|
22036
|
+
function personPath(identity) {
|
|
22037
|
+
const local = identity.email.split("@")[0];
|
|
22038
|
+
return `contacts/${safePathPart(identity.name === identity.email ? local : identity.name)}-${shortHash(identity.email, 12)}`;
|
|
22039
|
+
}
|
|
22040
|
+
function issuePath(token) {
|
|
22041
|
+
return `support/threads/${token}`;
|
|
22042
|
+
}
|
|
22043
|
+
function communicationPath(email) {
|
|
22044
|
+
const date = cleanLine(email.created_at, (/* @__PURE__ */ new Date()).toISOString()).slice(0, 10).replace(/[^0-9-]/g, "");
|
|
22045
|
+
const id = email.id.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100) || shortHash(email.message_id || email.id);
|
|
22046
|
+
return `Email/${date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}-${id}`;
|
|
22047
|
+
}
|
|
22048
|
+
function noteRef(vault, path5) {
|
|
22049
|
+
return `${vault}::${path5.replace(/\.md$/i, "")}.md`;
|
|
22050
|
+
}
|
|
22051
|
+
function unique(values) {
|
|
22052
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))];
|
|
22053
|
+
}
|
|
22054
|
+
function searchPath(source) {
|
|
22055
|
+
if (!source?.startsWith("note:")) return null;
|
|
22056
|
+
const path5 = source.slice("note:".length).trim();
|
|
22057
|
+
return path5 || null;
|
|
22058
|
+
}
|
|
22059
|
+
async function getNote(memoryKey, vault, path5) {
|
|
22060
|
+
const result = await memoryCall("getTool", { vault, path: path5 }, memoryKey);
|
|
22061
|
+
return result.ok && result.note ? result.note : null;
|
|
22062
|
+
}
|
|
22063
|
+
async function inspectTagVocabulary(memoryKey) {
|
|
22064
|
+
const result = await memoryCall("listTagsTool", {}, memoryKey);
|
|
22065
|
+
if (!result.ok) throw new Error(result.error ?? "tag vocabulary could not be inspected");
|
|
22066
|
+
}
|
|
22067
|
+
async function captureNewNote(memoryKey, input) {
|
|
22068
|
+
const existing = await getNote(memoryKey, input.vault, input.path);
|
|
22069
|
+
if (existing) return existing;
|
|
22070
|
+
const prepared = await memoryCall("prepareMemoryWriteTool", {
|
|
22071
|
+
title: input.title,
|
|
22072
|
+
content: input.content,
|
|
22073
|
+
source: input.source,
|
|
22074
|
+
type: input.type,
|
|
22075
|
+
vault: input.vault,
|
|
22076
|
+
tagCandidates: input.tagCandidates,
|
|
22077
|
+
maxLinks: 4
|
|
22078
|
+
}, memoryKey);
|
|
22079
|
+
if (!prepared.ok) throw new Error(prepared.error ?? `${input.vault} memory preparation failed`);
|
|
22080
|
+
const resolvedTags = unique(
|
|
22081
|
+
(prepared.tagResolutions ?? []).filter((item) => item.action !== "omit").map((item) => item.tag ?? item.candidate)
|
|
22082
|
+
);
|
|
22083
|
+
const capture = await memoryCall("memoryCaptureTool", {
|
|
22084
|
+
vault: input.vault,
|
|
22085
|
+
path: input.path,
|
|
22086
|
+
title: input.title,
|
|
22087
|
+
content: input.content,
|
|
22088
|
+
props: { ...input.props, tags: resolvedTags },
|
|
22089
|
+
tagDecisions: input.tagCandidates
|
|
22090
|
+
}, memoryKey);
|
|
22091
|
+
if (!capture.ok && !capture.conflict) {
|
|
22092
|
+
const detail = JSON.stringify(capture);
|
|
22093
|
+
throw new Error(`${capture.error ?? `${input.vault} memory capture failed`}: ${detail.slice(0, 2e3)}`);
|
|
22094
|
+
}
|
|
22095
|
+
const stored = await getNote(memoryKey, input.vault, input.path);
|
|
22096
|
+
if (!stored) throw new Error(`${input.vault} memory capture could not be read back`);
|
|
22097
|
+
return stored;
|
|
22098
|
+
}
|
|
22099
|
+
async function findOrCreatePerson(memoryKey, customer, occurredAt) {
|
|
22100
|
+
const result = await memoryCall("searchTool", {
|
|
22101
|
+
vault: "People",
|
|
22102
|
+
query: customer.email,
|
|
22103
|
+
queries: [
|
|
22104
|
+
customer.email,
|
|
22105
|
+
`contact ${customer.email}`,
|
|
22106
|
+
`support relationship ${customer.email}`
|
|
22107
|
+
],
|
|
22108
|
+
metadata: { email: customer.email },
|
|
22109
|
+
rerankTopN: 10
|
|
22110
|
+
}, memoryKey);
|
|
22111
|
+
if (!result.ok) throw new Error(result.error ?? "People lookup failed");
|
|
22112
|
+
for (const candidate of result.results ?? []) {
|
|
22113
|
+
const path6 = searchPath(candidate.source);
|
|
22114
|
+
if (!path6 || candidate.vault !== "People") continue;
|
|
22115
|
+
const note = await getNote(memoryKey, "People", path6);
|
|
22116
|
+
if (note && String(note.props?.email ?? "").toLowerCase() === customer.email) return note;
|
|
22117
|
+
}
|
|
22118
|
+
const path5 = personPath(customer);
|
|
22119
|
+
const existing = await getNote(memoryKey, "People", path5);
|
|
22120
|
+
if (existing) return existing;
|
|
22121
|
+
const title = customer.name === customer.email ? customer.email : `${customer.name} \u2014 ${customer.email}`;
|
|
22122
|
+
const summary = `MCP Scraper support contact identified by the verified sender email ${customer.email}.`;
|
|
21727
22123
|
const content = [
|
|
22124
|
+
"## Identity",
|
|
22125
|
+
"",
|
|
22126
|
+
`- Name: ${customer.name}`,
|
|
22127
|
+
`- Email: ${customer.email}`,
|
|
22128
|
+
"",
|
|
22129
|
+
"## Relationship",
|
|
22130
|
+
"",
|
|
22131
|
+
"MCP Scraper support contact. The relationship is not classified beyond the verified email exchange.",
|
|
22132
|
+
"",
|
|
22133
|
+
"## Known Context",
|
|
22134
|
+
"",
|
|
22135
|
+
"This person contacted MCP Scraper support. Durable facts should be added only after review.",
|
|
22136
|
+
"",
|
|
22137
|
+
"## Interaction History",
|
|
22138
|
+
"",
|
|
22139
|
+
"Linked Communication records provide the message history.",
|
|
22140
|
+
"",
|
|
22141
|
+
"## Open Loops",
|
|
22142
|
+
"",
|
|
22143
|
+
"Review the linked support thread and respond if needed.",
|
|
22144
|
+
"",
|
|
22145
|
+
"## Tasks",
|
|
22146
|
+
"",
|
|
22147
|
+
"_No task was inferred automatically._",
|
|
22148
|
+
"",
|
|
22149
|
+
"## Links",
|
|
22150
|
+
"",
|
|
22151
|
+
"_Communication and issue links are derived from linked records._"
|
|
22152
|
+
].join("\n");
|
|
22153
|
+
return captureNewNote(memoryKey, {
|
|
22154
|
+
vault: "People",
|
|
22155
|
+
path: path5,
|
|
22156
|
+
title,
|
|
22157
|
+
content,
|
|
22158
|
+
source: `resend:sender:${customer.email}`,
|
|
22159
|
+
type: "person",
|
|
22160
|
+
tagCandidates: SUPPORT_TAGS,
|
|
22161
|
+
props: {
|
|
22162
|
+
status: "active",
|
|
22163
|
+
type: "person",
|
|
22164
|
+
summary,
|
|
22165
|
+
pinned: false,
|
|
22166
|
+
source_type: "channel",
|
|
22167
|
+
source_ref: `resend:sender:${customer.email}`,
|
|
22168
|
+
related: [],
|
|
22169
|
+
related_vault_notes: [],
|
|
22170
|
+
embed: true,
|
|
22171
|
+
embed_priority: "high",
|
|
22172
|
+
embedding_summary: summary,
|
|
22173
|
+
person_name: customer.name,
|
|
22174
|
+
relationship: "MCP Scraper support contact",
|
|
22175
|
+
email: customer.email,
|
|
22176
|
+
memories: [],
|
|
22177
|
+
source_refs: [`resend:sender:${customer.email}`],
|
|
22178
|
+
last_seen: occurredAt,
|
|
22179
|
+
last_email_at: occurredAt,
|
|
22180
|
+
last_contact_at: occurredAt
|
|
22181
|
+
}
|
|
22182
|
+
});
|
|
22183
|
+
}
|
|
22184
|
+
function buildIssueContent(email, customer, personPathValue, threadAddressValue) {
|
|
22185
|
+
const subject = normalizeSupportSubject(email.subject);
|
|
22186
|
+
return [
|
|
21728
22187
|
"## Observation",
|
|
21729
22188
|
"",
|
|
21730
|
-
`
|
|
22189
|
+
`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.`,
|
|
21731
22190
|
"",
|
|
21732
22191
|
"## Impact",
|
|
21733
22192
|
"",
|
|
21734
|
-
"A customer or prospect may be waiting for a response. The
|
|
22193
|
+
"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.",
|
|
21735
22194
|
"",
|
|
21736
22195
|
"## Confirmation",
|
|
21737
22196
|
"",
|
|
@@ -21741,125 +22200,368 @@ function buildSupportIssueNote(email) {
|
|
|
21741
22200
|
"",
|
|
21742
22201
|
"## Evidence",
|
|
21743
22202
|
"",
|
|
21744
|
-
`-
|
|
21745
|
-
`-
|
|
21746
|
-
`-
|
|
21747
|
-
`-
|
|
21748
|
-
`- Message ID: ${cleanLine(email.message_id, "not supplied")}`,
|
|
21749
|
-
`- Resend email ID: ${email.id}`,
|
|
21750
|
-
`- Attachments: ${attachments}`,
|
|
21751
|
-
"",
|
|
21752
|
-
"### Message body",
|
|
22203
|
+
`- Customer email: ${customer.email}`,
|
|
22204
|
+
`- Original subject: ${cleanLine(email.subject, "(no subject)")}`,
|
|
22205
|
+
`- Thread address: ${threadAddressValue}`,
|
|
22206
|
+
`- Person: ${noteRef("People", personPathValue)}`,
|
|
21753
22207
|
"",
|
|
21754
|
-
|
|
22208
|
+
"Thread messages are preserved as linked Communication records and summarized here as they arrive.",
|
|
21755
22209
|
"",
|
|
21756
22210
|
"## Resolution",
|
|
21757
22211
|
"",
|
|
21758
|
-
"Pending review and a reply
|
|
22212
|
+
"Pending review and a reply. If this is a confirmed product issue, update the status and link the resulting task or Improvement Log receipt.",
|
|
21759
22213
|
"",
|
|
21760
22214
|
"## Tasks",
|
|
21761
22215
|
"",
|
|
21762
|
-
"- [ ] Review the
|
|
22216
|
+
"- [ ] Review the complete linked conversation.",
|
|
21763
22217
|
"- [ ] Reply to the sender.",
|
|
21764
|
-
"- [ ]
|
|
21765
|
-
"- [ ] Link any resulting task or Improvement Log receipt.",
|
|
22218
|
+
"- [ ] Classify the thread as a question, product defect, billing concern, feedback, or another issue.",
|
|
21766
22219
|
"",
|
|
21767
22220
|
"## Links",
|
|
21768
22221
|
"",
|
|
21769
|
-
|
|
22222
|
+
`- Person: ${noteRef("People", personPathValue)}`
|
|
21770
22223
|
].join("\n");
|
|
21771
|
-
|
|
21772
|
-
|
|
21773
|
-
|
|
21774
|
-
const
|
|
21775
|
-
|
|
21776
|
-
|
|
21777
|
-
|
|
21778
|
-
|
|
21779
|
-
];
|
|
21780
|
-
const prepared = await memoryCall("prepareMemoryWriteTool", {
|
|
21781
|
-
title: note.title,
|
|
21782
|
-
content: note.content,
|
|
21783
|
-
source: `resend:email:${email.id}`,
|
|
21784
|
-
type: "user_friction",
|
|
21785
|
-
vault: "Issues",
|
|
21786
|
-
tagCandidates,
|
|
21787
|
-
maxLinks: 4
|
|
21788
|
-
}, memoryKey);
|
|
21789
|
-
if (!prepared.ok) throw new Error(prepared.error ?? "memory preparation failed");
|
|
21790
|
-
const resolvedTags = (prepared.tagResolutions ?? []).filter((item) => item.action !== "omit").map((item) => item.tag ?? item.candidate).filter((tag) => Boolean(tag));
|
|
21791
|
-
const capture = await memoryCall("memoryCaptureTool", {
|
|
22224
|
+
}
|
|
22225
|
+
async function findOrCreateIssue(memoryKey, email, customer, personPathValue, token, threadAddressValue) {
|
|
22226
|
+
const path5 = issuePath(token);
|
|
22227
|
+
const existing = await getNote(memoryKey, "Issues", path5);
|
|
22228
|
+
if (existing) return existing;
|
|
22229
|
+
const subject = normalizeSupportSubject(email.subject);
|
|
22230
|
+
const summary = `Support conversation with ${customer.email} about \u201C${subject}\u201D; full history is linked through Communication records.`;
|
|
22231
|
+
return captureNewNote(memoryKey, {
|
|
21792
22232
|
vault: "Issues",
|
|
21793
|
-
path:
|
|
21794
|
-
title:
|
|
21795
|
-
content:
|
|
22233
|
+
path: path5,
|
|
22234
|
+
title: `Support thread \u2014 ${subject} \u2014 ${customer.email}`.slice(0, 240),
|
|
22235
|
+
content: buildIssueContent(email, customer, personPathValue, threadAddressValue),
|
|
22236
|
+
source: `resend:thread:${token}`,
|
|
22237
|
+
type: "user_friction",
|
|
22238
|
+
tagCandidates: ISSUE_TAGS,
|
|
21796
22239
|
props: {
|
|
21797
22240
|
status: "ai_observed",
|
|
21798
|
-
|
|
21799
|
-
|
|
22241
|
+
type: "user_friction",
|
|
22242
|
+
summary,
|
|
21800
22243
|
pinned: false,
|
|
21801
|
-
source_type: "
|
|
21802
|
-
source_ref: `resend:
|
|
22244
|
+
source_type: "thread",
|
|
22245
|
+
source_ref: `resend:thread:${token}`,
|
|
21803
22246
|
related: [],
|
|
21804
|
-
related_vault_notes: [],
|
|
22247
|
+
related_vault_notes: [noteRef("People", personPathValue)],
|
|
21805
22248
|
embed: true,
|
|
21806
22249
|
embed_priority: "high",
|
|
21807
|
-
embedding_summary:
|
|
21808
|
-
type: "user_friction",
|
|
22250
|
+
embedding_summary: summary,
|
|
21809
22251
|
severity: "untriaged",
|
|
21810
22252
|
system: "MCP Scraper customer support",
|
|
21811
22253
|
observed_at: email.created_at,
|
|
21812
22254
|
detected_by: "resend-email-webhook",
|
|
21813
|
-
reproducible: false
|
|
22255
|
+
reproducible: false,
|
|
22256
|
+
person_refs: [personPathValue],
|
|
22257
|
+
customer_email: customer.email,
|
|
22258
|
+
customer_name: customer.name,
|
|
22259
|
+
normalized_subject: subject,
|
|
22260
|
+
thread_token: token,
|
|
22261
|
+
thread_address: threadAddressValue,
|
|
22262
|
+
communication_refs: [],
|
|
22263
|
+
message_count: 0,
|
|
22264
|
+
attention_state: "needs_reply"
|
|
22265
|
+
}
|
|
22266
|
+
});
|
|
22267
|
+
}
|
|
22268
|
+
function buildCommunicationContent(email, customer, direction, personPathValue, issuePathValue, parentPath) {
|
|
22269
|
+
const sender = supportSender(email);
|
|
22270
|
+
const body = direction === "outbound" ? extractReplyText(email.text) : email.text;
|
|
22271
|
+
const participants = direction === "inbound" ? `- From: ${sender?.email ?? email.from}
|
|
22272
|
+
- To: MCP Scraper support` : `- From: MCP Scraper support
|
|
22273
|
+
- To: ${customer.email}`;
|
|
22274
|
+
return [
|
|
22275
|
+
"## Summary",
|
|
22276
|
+
"",
|
|
22277
|
+
`${direction === "inbound" ? "Inbound" : "Outbound"} support email in the \u201C${normalizeSupportSubject(email.subject)}\u201D thread.`,
|
|
22278
|
+
"",
|
|
22279
|
+
"## Participants",
|
|
22280
|
+
"",
|
|
22281
|
+
participants,
|
|
22282
|
+
`- Person: ${noteRef("People", personPathValue)}`,
|
|
22283
|
+
"",
|
|
22284
|
+
"## Message",
|
|
22285
|
+
"",
|
|
22286
|
+
quoteUntrusted(body),
|
|
22287
|
+
"",
|
|
22288
|
+
"## Decision",
|
|
22289
|
+
"",
|
|
22290
|
+
"_No decision was inferred automatically._",
|
|
22291
|
+
"",
|
|
22292
|
+
"## Follow Up Tasks",
|
|
22293
|
+
"",
|
|
22294
|
+
direction === "inbound" ? "- [ ] Review and reply." : "- [ ] Wait for the customer response or close the thread after review.",
|
|
22295
|
+
"",
|
|
22296
|
+
"## Links",
|
|
22297
|
+
"",
|
|
22298
|
+
`- Issue: ${noteRef("Issues", issuePathValue)}`,
|
|
22299
|
+
`- Person: ${noteRef("People", personPathValue)}`,
|
|
22300
|
+
...parentPath ? [`- Previous message: [[${parentPath}]]`] : []
|
|
22301
|
+
].join("\n");
|
|
22302
|
+
}
|
|
22303
|
+
async function captureCommunication(memoryKey, email, customer, direction, personPathValue, issuePathValue, token, parentPath) {
|
|
22304
|
+
const path5 = communicationPath(email);
|
|
22305
|
+
const existing = await getNote(memoryKey, "Communication", path5);
|
|
22306
|
+
if (existing) return existing;
|
|
22307
|
+
const subject = normalizeSupportSubject(email.subject);
|
|
22308
|
+
const summary = `${direction === "inbound" ? "Inbound message from" : "Outbound reply to"} ${customer.email} in the support thread about \u201C${subject}\u201D.`;
|
|
22309
|
+
const providerMessageRef = cleanLine(email.message_id, email.id, 500);
|
|
22310
|
+
return captureNewNote(memoryKey, {
|
|
22311
|
+
vault: "Communication",
|
|
22312
|
+
path: path5,
|
|
22313
|
+
title: `${direction === "inbound" ? "Inbound" : "Outbound"} email \u2014 ${subject} \u2014 ${customer.email}`.slice(0, 240),
|
|
22314
|
+
content: buildCommunicationContent(email, customer, direction, personPathValue, issuePathValue, parentPath),
|
|
22315
|
+
source: `resend:email:${email.id}`,
|
|
22316
|
+
type: "email",
|
|
22317
|
+
tagCandidates: SUPPORT_TAGS,
|
|
22318
|
+
props: {
|
|
22319
|
+
status: direction === "inbound" ? "inbox" : "responded",
|
|
22320
|
+
type: "email",
|
|
22321
|
+
summary,
|
|
22322
|
+
pinned: false,
|
|
22323
|
+
source_type: "channel",
|
|
22324
|
+
source_ref: `resend:email:${email.id}`,
|
|
22325
|
+
related: parentPath ? [`[[${parentPath}]]`] : [],
|
|
22326
|
+
related_vault_notes: [
|
|
22327
|
+
noteRef("People", personPathValue),
|
|
22328
|
+
noteRef("Issues", issuePathValue)
|
|
22329
|
+
],
|
|
22330
|
+
embed: true,
|
|
22331
|
+
embed_priority: "high",
|
|
22332
|
+
embedding_summary: summary,
|
|
22333
|
+
channel: "email",
|
|
22334
|
+
participants: [customer.email, "support@updates.mcpscraper.dev"],
|
|
22335
|
+
person_refs: [personPathValue],
|
|
22336
|
+
occurred_at: email.created_at,
|
|
22337
|
+
direction,
|
|
22338
|
+
attention_state: direction === "inbound" ? "needs_reply" : "waiting",
|
|
22339
|
+
provider: "resend",
|
|
22340
|
+
provider_connection_ref: "resend:updates.mcpscraper.dev",
|
|
22341
|
+
provider_message_ref: providerMessageRef,
|
|
22342
|
+
provider_thread_ref: token,
|
|
22343
|
+
thread_ref: token,
|
|
22344
|
+
issue_ref: issuePathValue,
|
|
22345
|
+
sender_email: supportSender(email)?.email ?? email.from,
|
|
22346
|
+
recipient_emails: email.to ?? [],
|
|
22347
|
+
parentMessageId: parentPath,
|
|
22348
|
+
parent_communication_ref: parentPath
|
|
22349
|
+
}
|
|
22350
|
+
});
|
|
22351
|
+
}
|
|
22352
|
+
function appendIssueEvidence(content, email, communicationPathValue, direction) {
|
|
22353
|
+
const marker = `- Resend email ID: ${email.id}`;
|
|
22354
|
+
if (content.includes(marker)) return content;
|
|
22355
|
+
const block = [
|
|
22356
|
+
`### ${direction === "inbound" ? "Inbound" : "Outbound"} email \u2014 ${cleanLine(email.created_at, (/* @__PURE__ */ new Date()).toISOString())}`,
|
|
22357
|
+
"",
|
|
22358
|
+
`- Communication: ${noteRef("Communication", communicationPathValue)}`,
|
|
22359
|
+
`- Subject: ${cleanLine(email.subject, "(no subject)")}`,
|
|
22360
|
+
marker,
|
|
22361
|
+
`- Message ID: ${cleanLine(email.message_id, "not supplied", 500)}`,
|
|
22362
|
+
"",
|
|
22363
|
+
quoteUntrusted(direction === "outbound" ? extractReplyText(email.text) : email.text),
|
|
22364
|
+
""
|
|
22365
|
+
].join("\n");
|
|
22366
|
+
const anchor = "\n## Resolution";
|
|
22367
|
+
return content.includes(anchor) ? content.replace(anchor, `
|
|
22368
|
+
${block}${anchor}`) : `${content}
|
|
22369
|
+
|
|
22370
|
+
${block}`;
|
|
22371
|
+
}
|
|
22372
|
+
async function updateIssueThread(memoryKey, issuePathValue, email, communicationPathValue, direction, personPathValue) {
|
|
22373
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
22374
|
+
const current = await getNote(memoryKey, "Issues", issuePathValue);
|
|
22375
|
+
if (!current) throw new Error("support thread issue disappeared before update");
|
|
22376
|
+
const currentRefs = unique(Array.isArray(current.props?.communication_refs) ? current.props.communication_refs : []);
|
|
22377
|
+
const alreadyLinked = currentRefs.includes(communicationPathValue);
|
|
22378
|
+
const communicationRefs = alreadyLinked ? currentRefs : [...currentRefs, communicationPathValue];
|
|
22379
|
+
const relatedVaultNotes = unique([
|
|
22380
|
+
...Array.isArray(current.props?.related_vault_notes) ? current.props.related_vault_notes : [],
|
|
22381
|
+
noteRef("People", personPathValue),
|
|
22382
|
+
...communicationRefs.map((path5) => noteRef("Communication", path5))
|
|
22383
|
+
]);
|
|
22384
|
+
const result = await memoryCall("putTool", {
|
|
22385
|
+
vault: "Issues",
|
|
22386
|
+
path: issuePathValue,
|
|
22387
|
+
title: current.title,
|
|
22388
|
+
content: appendIssueEvidence(current.content, email, communicationPathValue, direction),
|
|
22389
|
+
baseRevision: current.revision,
|
|
22390
|
+
props: {
|
|
22391
|
+
related_vault_notes: relatedVaultNotes,
|
|
22392
|
+
communication_refs: communicationRefs,
|
|
22393
|
+
last_communication_ref: communicationPathValue,
|
|
22394
|
+
last_message_at: email.created_at,
|
|
22395
|
+
message_count: communicationRefs.length,
|
|
22396
|
+
attention_state: direction === "inbound" ? "needs_reply" : "waiting"
|
|
22397
|
+
}
|
|
22398
|
+
}, memoryKey);
|
|
22399
|
+
if (result.ok) return;
|
|
22400
|
+
if (!result.conflict || attempt === 1) throw new Error(result.error ?? "support thread issue update failed");
|
|
22401
|
+
}
|
|
22402
|
+
}
|
|
22403
|
+
async function getSupportThreadByToken(memoryKey, token) {
|
|
22404
|
+
const safeToken = token.toLowerCase().replace(/[^a-f0-9]/g, "").slice(0, 24);
|
|
22405
|
+
if (safeToken.length !== 24) return null;
|
|
22406
|
+
const path5 = issuePath(safeToken);
|
|
22407
|
+
const issue = await getNote(memoryKey, "Issues", path5);
|
|
22408
|
+
if (!issue) return null;
|
|
22409
|
+
const customerEmail = parseMailbox(String(issue.props?.customer_email ?? ""));
|
|
22410
|
+
const personRefs = Array.isArray(issue.props?.person_refs) ? issue.props.person_refs : [];
|
|
22411
|
+
const personPathValue = personRefs.find((value) => typeof value === "string");
|
|
22412
|
+
const threadAddressValue = String(issue.props?.thread_address ?? "");
|
|
22413
|
+
if (!customerEmail || !personPathValue || !threadAddressValue) return null;
|
|
22414
|
+
return {
|
|
22415
|
+
issue,
|
|
22416
|
+
issuePath: path5,
|
|
22417
|
+
threadToken: safeToken,
|
|
22418
|
+
threadAddress: threadAddressValue,
|
|
22419
|
+
customer: {
|
|
22420
|
+
email: customerEmail.email,
|
|
22421
|
+
name: cleanLine(String(issue.props?.customer_name ?? ""), customerEmail.email)
|
|
21814
22422
|
},
|
|
21815
|
-
|
|
21816
|
-
|
|
21817
|
-
|
|
21818
|
-
|
|
21819
|
-
|
|
21820
|
-
|
|
22423
|
+
personPath: personPathValue,
|
|
22424
|
+
normalizedSubject: cleanLine(String(issue.props?.normalized_subject ?? ""), "(no subject)")
|
|
22425
|
+
};
|
|
22426
|
+
}
|
|
22427
|
+
async function captureSupportMessage(memoryKey, email, options) {
|
|
22428
|
+
await inspectTagVocabulary(memoryKey);
|
|
22429
|
+
const customer = options.customer ?? supportSender(email);
|
|
22430
|
+
if (!customer) throw new Error("support sender does not contain a usable email address");
|
|
22431
|
+
const normalizedSubject = options.existingThread?.normalizedSubject ?? normalizeSupportSubject(email.subject);
|
|
22432
|
+
const token = options.existingThread?.threadToken ?? supportThreadToken(customer.email, normalizedSubject);
|
|
22433
|
+
const threadAddressValue = options.existingThread?.threadAddress ?? supportThreadAddress(token, options.receivingDomain);
|
|
22434
|
+
const person = options.existingThread ? await getNote(memoryKey, "People", options.existingThread.personPath) : await findOrCreatePerson(memoryKey, customer, email.created_at);
|
|
22435
|
+
if (!person) throw new Error("support person could not be resolved");
|
|
22436
|
+
const issue = options.existingThread?.issue ?? await findOrCreateIssue(
|
|
22437
|
+
memoryKey,
|
|
22438
|
+
email,
|
|
22439
|
+
customer,
|
|
22440
|
+
person.path,
|
|
22441
|
+
token,
|
|
22442
|
+
threadAddressValue
|
|
22443
|
+
);
|
|
22444
|
+
const parentPath = typeof issue.props?.last_communication_ref === "string" ? issue.props.last_communication_ref : void 0;
|
|
22445
|
+
const communication = await captureCommunication(
|
|
22446
|
+
memoryKey,
|
|
22447
|
+
email,
|
|
22448
|
+
customer,
|
|
22449
|
+
options.direction,
|
|
22450
|
+
person.path,
|
|
22451
|
+
issue.path,
|
|
22452
|
+
token,
|
|
22453
|
+
parentPath
|
|
22454
|
+
);
|
|
22455
|
+
await updateIssueThread(memoryKey, issue.path, email, communication.path, options.direction, person.path);
|
|
22456
|
+
return {
|
|
22457
|
+
issuePath: issue.path,
|
|
22458
|
+
communicationPath: communication.path,
|
|
22459
|
+
personPath: person.path,
|
|
22460
|
+
parentCommunicationPath: parentPath,
|
|
22461
|
+
threadToken: token,
|
|
22462
|
+
threadAddress: threadAddressValue,
|
|
22463
|
+
customerEmail: customer.email,
|
|
22464
|
+
normalizedSubject,
|
|
22465
|
+
direction: options.direction
|
|
22466
|
+
};
|
|
22467
|
+
}
|
|
22468
|
+
|
|
22469
|
+
// src/api/resend-inbound-routes.ts
|
|
22470
|
+
var SUPPORT_ADDRESS = "support@mcpscraper.dev";
|
|
22471
|
+
var DMARC_ADDRESS = "dmarc@mcpscraper.dev";
|
|
22472
|
+
var FORWARD_FROM = "MCP Scraper Support <support@updates.mcpscraper.dev>";
|
|
22473
|
+
function receivingDomains(values) {
|
|
22474
|
+
return values.map((domain) => domain?.trim().toLowerCase()).filter((domain) => Boolean(domain));
|
|
22475
|
+
}
|
|
22476
|
+
function extractSupportThreadToken(recipients, ...domainsInput) {
|
|
22477
|
+
const domains = receivingDomains(domainsInput);
|
|
22478
|
+
for (const recipient of recipients) {
|
|
22479
|
+
const address = parseMailbox(recipient)?.email ?? recipient.trim().toLowerCase();
|
|
22480
|
+
const match = address.match(/^support\+([a-f0-9]{24})@(.+)$/i);
|
|
22481
|
+
if (match && domains.includes(match[2].toLowerCase())) return match[1].toLowerCase();
|
|
21821
22482
|
}
|
|
21822
|
-
|
|
22483
|
+
return null;
|
|
22484
|
+
}
|
|
22485
|
+
function classifyInboundRecipients(recipients, ...domainsInput) {
|
|
22486
|
+
const normalized = recipients.map((address) => parseMailbox(address)?.email ?? address.trim().toLowerCase());
|
|
22487
|
+
const domains = receivingDomains(domainsInput);
|
|
22488
|
+
const supportAddresses = [SUPPORT_ADDRESS, ...domains.map((domain) => `support@${domain}`)];
|
|
22489
|
+
const dmarcAddresses = [DMARC_ADDRESS, ...domains.map((domain) => `dmarc@${domain}`)];
|
|
22490
|
+
if (dmarcAddresses.some((address) => normalized.includes(address))) return "dmarc";
|
|
22491
|
+
if (extractSupportThreadToken(recipients, ...domains)) return "support-thread";
|
|
22492
|
+
if (supportAddresses.some((address) => normalized.includes(address))) return "support";
|
|
22493
|
+
return "ignore";
|
|
21823
22494
|
}
|
|
21824
|
-
|
|
22495
|
+
function escapeHtml2(value) {
|
|
22496
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
22497
|
+
}
|
|
22498
|
+
function safeEmailId(value) {
|
|
22499
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 100) || "unknown-email";
|
|
22500
|
+
}
|
|
22501
|
+
async function receivedAttachments(resend, email) {
|
|
21825
22502
|
const attachmentResult = await resend.emails.receiving.attachments.list({ emailId: email.id });
|
|
21826
22503
|
if (attachmentResult.error) throw new Error(`attachment retrieval failed: ${attachmentResult.error.message}`);
|
|
21827
|
-
|
|
22504
|
+
return (attachmentResult.data?.data ?? []).filter((item) => item.content_disposition !== "inline").map((item) => ({
|
|
21828
22505
|
path: item.download_url,
|
|
21829
22506
|
filename: item.filename ?? "attachment",
|
|
21830
22507
|
contentId: item.content_id
|
|
21831
22508
|
}));
|
|
21832
|
-
|
|
21833
|
-
|
|
22509
|
+
}
|
|
22510
|
+
async function forwardSupportEmail(resend, email, destination, threadAddress) {
|
|
22511
|
+
const attachments = await receivedAttachments(resend, email);
|
|
22512
|
+
const sender = supportSender(email);
|
|
22513
|
+
const subject = normalizeSupportSubject(email.subject);
|
|
21834
22514
|
const headerText = [
|
|
21835
|
-
|
|
22515
|
+
`Support message received at ${(email.to ?? []).join(", ")}`,
|
|
21836
22516
|
`From: ${email.from}`,
|
|
21837
|
-
`
|
|
22517
|
+
`Customer email: ${sender?.email ?? email.from}`,
|
|
21838
22518
|
`Received: ${email.created_at}`,
|
|
22519
|
+
`Thread reply address: ${threadAddress}`,
|
|
21839
22520
|
""
|
|
21840
22521
|
].join("\n");
|
|
21841
22522
|
const headerHtml = [
|
|
21842
22523
|
'<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">',
|
|
21843
|
-
|
|
22524
|
+
"<strong>Support message received</strong><br>",
|
|
21844
22525
|
`From: ${escapeHtml2(email.from)}<br>`,
|
|
21845
|
-
`
|
|
21846
|
-
`Received: ${escapeHtml2(email.created_at)}
|
|
22526
|
+
`Customer email: ${escapeHtml2(sender?.email ?? email.from)}<br>`,
|
|
22527
|
+
`Received: ${escapeHtml2(email.created_at)}<br>`,
|
|
22528
|
+
`Thread reply address: ${escapeHtml2(threadAddress)}`,
|
|
21847
22529
|
"</div>"
|
|
21848
22530
|
].join("");
|
|
21849
22531
|
const sent = await resend.emails.send({
|
|
21850
22532
|
from: FORWARD_FROM,
|
|
21851
22533
|
to: destination,
|
|
21852
|
-
replyTo:
|
|
21853
|
-
subject: `[
|
|
22534
|
+
replyTo: threadAddress,
|
|
22535
|
+
subject: `[Support] ${subject}`,
|
|
21854
22536
|
text: `${headerText}${email.text || "(HTML-only message; view the HTML version.)"}`,
|
|
21855
22537
|
html: email.html ? `${headerHtml}${email.html}` : void 0,
|
|
21856
22538
|
attachments: attachments.length ? attachments : void 0,
|
|
22539
|
+
headers: { "X-MCP-Support-Thread": threadAddress },
|
|
21857
22540
|
tags: [
|
|
21858
|
-
{ name: "category", value:
|
|
22541
|
+
{ name: "category", value: "support_forward" },
|
|
21859
22542
|
{ name: "inbound_id", value: safeEmailId(email.id) }
|
|
21860
22543
|
]
|
|
21861
|
-
}, { idempotencyKey: `mcp-inbound-${
|
|
21862
|
-
if (sent.error) throw new Error(`forward failed: ${sent.error.message}`);
|
|
22544
|
+
}, { idempotencyKey: `mcp-inbound-support-${safeEmailId(email.id)}` });
|
|
22545
|
+
if (sent.error) throw new Error(`support forward failed: ${sent.error.message}`);
|
|
22546
|
+
return sent.data.id;
|
|
22547
|
+
}
|
|
22548
|
+
async function relayOperatorReply(resend, email, customerEmail, threadAddress, normalizedSubject) {
|
|
22549
|
+
const attachments = await receivedAttachments(resend, email);
|
|
22550
|
+
const replyText = extractReplyText(email.text) || "(No plain-text reply was available.)";
|
|
22551
|
+
const sent = await resend.emails.send({
|
|
22552
|
+
from: FORWARD_FROM,
|
|
22553
|
+
to: customerEmail,
|
|
22554
|
+
replyTo: threadAddress,
|
|
22555
|
+
subject: `Re: ${normalizeSupportSubject(normalizedSubject)}`,
|
|
22556
|
+
text: replyText,
|
|
22557
|
+
attachments: attachments.length ? attachments : void 0,
|
|
22558
|
+
headers: { "X-MCP-Support-Thread": threadAddress },
|
|
22559
|
+
tags: [
|
|
22560
|
+
{ name: "category", value: "support_reply" },
|
|
22561
|
+
{ name: "inbound_id", value: safeEmailId(email.id) }
|
|
22562
|
+
]
|
|
22563
|
+
}, { idempotencyKey: `mcp-support-reply-${safeEmailId(email.id)}` });
|
|
22564
|
+
if (sent.error) throw new Error(`support reply relay failed: ${sent.error.message}`);
|
|
21863
22565
|
return sent.data.id;
|
|
21864
22566
|
}
|
|
21865
22567
|
var resendInboundApp = new Hono24();
|
|
@@ -21885,32 +22587,99 @@ resendInboundApp.post("/webhooks", async (c) => {
|
|
|
21885
22587
|
}
|
|
21886
22588
|
if (event.type !== "email.received") return c.json({ ok: true, ignored: true });
|
|
21887
22589
|
const receivedEvent = event;
|
|
21888
|
-
const
|
|
21889
|
-
receivedEvent.data.to ?? [],
|
|
22590
|
+
const domains = [
|
|
21890
22591
|
process.env.RESEND_RECEIVING_DOMAIN,
|
|
21891
22592
|
process.env.RESEND_MANAGED_RECEIVING_DOMAIN
|
|
21892
|
-
|
|
21893
|
-
|
|
22593
|
+
];
|
|
22594
|
+
const route = classifyInboundRecipients(receivedEvent.data.to ?? [], ...domains);
|
|
22595
|
+
if (route === "ignore") return c.json({ ok: true, route, ignored: true });
|
|
22596
|
+
if (route === "dmarc") {
|
|
22597
|
+
return c.json({ ok: true, route, ignored: true, reason: "dmarc-report-suppressed" });
|
|
22598
|
+
}
|
|
21894
22599
|
const received = await resend.emails.receiving.get(receivedEvent.data.email_id);
|
|
21895
22600
|
if (received.error || !received.data) {
|
|
21896
22601
|
console.error("[resend/inbound] could not retrieve received email", receivedEvent.data.email_id);
|
|
21897
22602
|
return c.json({ error: "received email could not be retrieved" }, 502);
|
|
21898
22603
|
}
|
|
22604
|
+
const destination = process.env.SUPPORT_FORWARD_TO?.trim();
|
|
22605
|
+
const memoryKey = process.env.SUPPORT_INBOX_MEMORY_KEY?.trim();
|
|
22606
|
+
const receivingDomain = process.env.RESEND_RECEIVING_DOMAIN?.trim();
|
|
22607
|
+
if (!destination || !memoryKey || !receivingDomain) {
|
|
22608
|
+
return c.json({ error: "support routing is not configured" }, 503);
|
|
22609
|
+
}
|
|
21899
22610
|
try {
|
|
22611
|
+
const email = received.data;
|
|
22612
|
+
const sender = supportSender(email);
|
|
22613
|
+
if (!sender || isAutomatedMailbox(sender.email)) {
|
|
22614
|
+
return c.json({ ok: true, route, ignored: true, reason: "automated-or-unusable-sender" });
|
|
22615
|
+
}
|
|
21900
22616
|
if (route === "support") {
|
|
21901
|
-
const
|
|
21902
|
-
|
|
21903
|
-
|
|
21904
|
-
|
|
21905
|
-
|
|
21906
|
-
|
|
21907
|
-
|
|
21908
|
-
|
|
22617
|
+
const captured = await captureSupportMessage(memoryKey, email, {
|
|
22618
|
+
receivingDomain,
|
|
22619
|
+
direction: "inbound",
|
|
22620
|
+
customer: sender
|
|
22621
|
+
});
|
|
22622
|
+
const forwardedId = await forwardSupportEmail(resend, received.data, destination, captured.threadAddress);
|
|
22623
|
+
return c.json({
|
|
22624
|
+
ok: true,
|
|
22625
|
+
route,
|
|
22626
|
+
direction: "inbound",
|
|
22627
|
+
forwardedId,
|
|
22628
|
+
issuePath: captured.issuePath,
|
|
22629
|
+
communicationPath: captured.communicationPath,
|
|
22630
|
+
personPath: captured.personPath,
|
|
22631
|
+
threadAddress: captured.threadAddress
|
|
22632
|
+
});
|
|
22633
|
+
}
|
|
22634
|
+
const token = extractSupportThreadToken(receivedEvent.data.to ?? [], ...domains);
|
|
22635
|
+
const thread = token ? await getSupportThreadByToken(memoryKey, token) : null;
|
|
22636
|
+
if (!thread) return c.json({ ok: true, route, ignored: true, reason: "unknown-support-thread" });
|
|
22637
|
+
const operator = parseMailbox(destination);
|
|
22638
|
+
if (operator && sender.email === operator.email) {
|
|
22639
|
+
const captured = await captureSupportMessage(memoryKey, email, {
|
|
22640
|
+
receivingDomain,
|
|
22641
|
+
direction: "outbound",
|
|
22642
|
+
customer: thread.customer,
|
|
22643
|
+
existingThread: thread
|
|
22644
|
+
});
|
|
22645
|
+
const relayedId = await relayOperatorReply(
|
|
22646
|
+
resend,
|
|
22647
|
+
received.data,
|
|
22648
|
+
thread.customer.email,
|
|
22649
|
+
captured.threadAddress,
|
|
22650
|
+
thread.normalizedSubject
|
|
22651
|
+
);
|
|
22652
|
+
return c.json({
|
|
22653
|
+
ok: true,
|
|
22654
|
+
route,
|
|
22655
|
+
direction: "outbound",
|
|
22656
|
+
relayedId,
|
|
22657
|
+
issuePath: captured.issuePath,
|
|
22658
|
+
communicationPath: captured.communicationPath,
|
|
22659
|
+
personPath: captured.personPath,
|
|
22660
|
+
threadAddress: captured.threadAddress
|
|
22661
|
+
});
|
|
22662
|
+
}
|
|
22663
|
+
if (sender.email === thread.customer.email) {
|
|
22664
|
+
const captured = await captureSupportMessage(memoryKey, email, {
|
|
22665
|
+
receivingDomain,
|
|
22666
|
+
direction: "inbound",
|
|
22667
|
+
customer: thread.customer,
|
|
22668
|
+
existingThread: thread
|
|
22669
|
+
});
|
|
22670
|
+
const forwardedId = await forwardSupportEmail(resend, received.data, destination, captured.threadAddress);
|
|
22671
|
+
return c.json({
|
|
22672
|
+
ok: true,
|
|
22673
|
+
route,
|
|
22674
|
+
direction: "inbound",
|
|
22675
|
+
forwardedId,
|
|
22676
|
+
issuePath: captured.issuePath,
|
|
22677
|
+
communicationPath: captured.communicationPath,
|
|
22678
|
+
personPath: captured.personPath,
|
|
22679
|
+
threadAddress: captured.threadAddress
|
|
22680
|
+
});
|
|
21909
22681
|
}
|
|
21910
|
-
|
|
21911
|
-
if (!destination) return c.json({ error: "DMARC routing is not configured" }, 503);
|
|
21912
|
-
const forwardedId = await forwardReceivedEmail(resend, received.data, destination, "DMARC");
|
|
21913
|
-
return c.json({ ok: true, route, forwardedId });
|
|
22682
|
+
return c.json({ ok: true, route, ignored: true, reason: "unrecognized-thread-sender" });
|
|
21914
22683
|
} catch (err) {
|
|
21915
22684
|
console.error("[resend/inbound] processing failed", receivedEvent.data.email_id, err instanceof Error ? err.message : String(err));
|
|
21916
22685
|
return c.json({ error: "inbound email processing failed" }, 502);
|
|
@@ -21953,16 +22722,16 @@ function startSiteAuditWorker() {
|
|
|
21953
22722
|
// src/api/server.ts
|
|
21954
22723
|
import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
21955
22724
|
import { getCookie as getCookie2, setCookie as setCookie2, deleteCookie } from "hono/cookie";
|
|
21956
|
-
import
|
|
22725
|
+
import Stripe2 from "stripe";
|
|
21957
22726
|
|
|
21958
22727
|
// src/api/page-diff.ts
|
|
21959
|
-
import { createHash as
|
|
22728
|
+
import { createHash as createHash8 } from "crypto";
|
|
21960
22729
|
import { diffLines } from "diff";
|
|
21961
22730
|
var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
|
|
21962
22731
|
var MAX_DIFF_HUNKS = 200;
|
|
21963
22732
|
var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
|
|
21964
22733
|
function sha256Hex(value) {
|
|
21965
|
-
return
|
|
22734
|
+
return createHash8("sha256").update(value).digest("hex");
|
|
21966
22735
|
}
|
|
21967
22736
|
function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
|
|
21968
22737
|
if (value.length <= maxChars) return { value, truncated: false };
|
|
@@ -22069,7 +22838,7 @@ async function persistScrapeBody(user, opts) {
|
|
|
22069
22838
|
}
|
|
22070
22839
|
|
|
22071
22840
|
// src/api/connection-memory-import.ts
|
|
22072
|
-
import { createHash as
|
|
22841
|
+
import { createHash as createHash9 } from "crypto";
|
|
22073
22842
|
var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
|
|
22074
22843
|
var CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
|
|
22075
22844
|
var CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS = 256e3;
|
|
@@ -22099,7 +22868,7 @@ function canonicalJson(value) {
|
|
|
22099
22868
|
return JSON.stringify(value);
|
|
22100
22869
|
}
|
|
22101
22870
|
function sha2562(value) {
|
|
22102
|
-
return
|
|
22871
|
+
return createHash9("sha256").update(value).digest("hex");
|
|
22103
22872
|
}
|
|
22104
22873
|
function sensitiveKey(key) {
|
|
22105
22874
|
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -22941,10 +23710,10 @@ async function dbUsage(identity, plan) {
|
|
|
22941
23710
|
}
|
|
22942
23711
|
|
|
22943
23712
|
// src/api/nango-control.ts
|
|
22944
|
-
import { createHash as
|
|
23713
|
+
import { createHash as createHash11, randomUUID as randomUUID12 } from "crypto";
|
|
22945
23714
|
|
|
22946
23715
|
// src/api/main-nango-transport.ts
|
|
22947
|
-
import { createHash as
|
|
23716
|
+
import { createHash as createHash10, randomUUID as randomUUID11 } from "crypto";
|
|
22948
23717
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
22949
23718
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
22950
23719
|
|
|
@@ -23536,7 +24305,7 @@ async function listNangoToolsDirect(identity, connectionId) {
|
|
|
23536
24305
|
});
|
|
23537
24306
|
const readTools = tools.filter((tool) => classifyTool(tool) === "read").map((tool) => tool.name);
|
|
23538
24307
|
const actionTools = tools.filter((tool) => classifyTool(tool) === "action").map((tool) => tool.name);
|
|
23539
|
-
const revision =
|
|
24308
|
+
const revision = createHash10("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
|
|
23540
24309
|
await updateServiceConnectionTools(connection.id, readTools, actionTools, revision);
|
|
23541
24310
|
const refreshed = await getOwnedServiceConnection(identity, connection.id);
|
|
23542
24311
|
return { connection: refreshed ?? { ...connection, readTools, actionTools, toolRevision: revision }, tools };
|
|
@@ -23557,7 +24326,7 @@ async function callNangoToolDirect(args) {
|
|
|
23557
24326
|
ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION
|
|
23558
24327
|
});
|
|
23559
24328
|
const requestId = args.requestId?.trim() || randomUUID11();
|
|
23560
|
-
const idempotencyKey = `main-nango:${
|
|
24329
|
+
const idempotencyKey = `main-nango:${createHash10("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
|
|
23561
24330
|
const startedAt = /* @__PURE__ */ new Date();
|
|
23562
24331
|
const started = performance.now();
|
|
23563
24332
|
let result;
|
|
@@ -23582,7 +24351,7 @@ async function callNangoToolDirect(args) {
|
|
|
23582
24351
|
toolName: args.tool,
|
|
23583
24352
|
operationKind: args.operationKind ?? args.classification,
|
|
23584
24353
|
outcome: providerError ? "error" : "partial",
|
|
23585
|
-
requestId: requestId.length <= 200 ? requestId :
|
|
24354
|
+
requestId: requestId.length <= 200 ? requestId : createHash10("sha256").update(requestId).digest("hex"),
|
|
23586
24355
|
startedAt: startedAt.toISOString(),
|
|
23587
24356
|
completedAt: completedAt.toISOString()
|
|
23588
24357
|
}
|
|
@@ -23619,7 +24388,7 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
|
|
|
23619
24388
|
providerContractHash: MAIN_INTEGRATION_CONTRACT_HASH,
|
|
23620
24389
|
protocolVersion: null,
|
|
23621
24390
|
schemaSource: "live_tools_list",
|
|
23622
|
-
schemaHash:
|
|
24391
|
+
schemaHash: createHash10("sha256").update(JSON.stringify(projected)).digest("hex"),
|
|
23623
24392
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
23624
24393
|
};
|
|
23625
24394
|
}
|
|
@@ -24344,7 +25113,7 @@ async function setScheduleConnectionActionsEnabled(identity, connectionId, enabl
|
|
|
24344
25113
|
return data.connection.actionsEnabled === true;
|
|
24345
25114
|
}
|
|
24346
25115
|
async function callScheduleConnectionAction(identity, connectionId, input, tool, idempotencyKey) {
|
|
24347
|
-
const requestId = `main-connected-action:${
|
|
25116
|
+
const requestId = `main-connected-action:${createHash11("sha256").update(identity).update("\0").update(idempotencyKey?.trim() || randomUUID12()).digest("hex")}`;
|
|
24348
25117
|
if (mainOwnsIntegrations()) {
|
|
24349
25118
|
const selectedTool = tool?.trim();
|
|
24350
25119
|
if (!selectedTool) throw new NangoControlError("An action tool is required.", 400, "invalid_request", false);
|
|
@@ -24486,7 +25255,7 @@ function canonicalJson2(value) {
|
|
|
24486
25255
|
return JSON.stringify(value);
|
|
24487
25256
|
}
|
|
24488
25257
|
function projectedToolSchemaHash(tool) {
|
|
24489
|
-
return
|
|
25258
|
+
return createHash11("sha256").update(canonicalJson2(tool)).digest("hex");
|
|
24490
25259
|
}
|
|
24491
25260
|
async function describeNangoTool(identity, connectionId, tool, fresh) {
|
|
24492
25261
|
if (mainOwnsIntegrations()) {
|
|
@@ -25072,7 +25841,7 @@ async function callResendExportPage(identity, input) {
|
|
|
25072
25841
|
}
|
|
25073
25842
|
|
|
25074
25843
|
// src/api/scheduler-integration-auth.ts
|
|
25075
|
-
import { createHash as
|
|
25844
|
+
import { createHash as createHash12, createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
25076
25845
|
var MAX_CLOCK_SKEW_MS = 5 * 60 * 1e3;
|
|
25077
25846
|
var SchedulerIntegrationAuthError = class extends Error {
|
|
25078
25847
|
code;
|
|
@@ -25088,7 +25857,7 @@ function signingSecret() {
|
|
|
25088
25857
|
return secret2;
|
|
25089
25858
|
}
|
|
25090
25859
|
function schedulerIntegrationSignature(args) {
|
|
25091
|
-
const bodyHash =
|
|
25860
|
+
const bodyHash = createHash12("sha256").update(args.body).digest("hex");
|
|
25092
25861
|
return createHmac4("sha256", args.secret).update(`${args.method.toUpperCase()}
|
|
25093
25862
|
${args.path}
|
|
25094
25863
|
${args.timestamp}
|
|
@@ -25237,7 +26006,7 @@ var requirePaidSchedulingTier = createMiddleware3(async (c, next) => {
|
|
|
25237
26006
|
return next();
|
|
25238
26007
|
});
|
|
25239
26008
|
var app = new Hono25();
|
|
25240
|
-
var
|
|
26009
|
+
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
25241
26010
|
function requireStripeSecret() {
|
|
25242
26011
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
25243
26012
|
if (secret2) return secret2;
|
|
@@ -25247,7 +26016,7 @@ function requireStripeSecret() {
|
|
|
25247
26016
|
async function createSignupStripeCustomer(email) {
|
|
25248
26017
|
const secret2 = requireStripeSecret();
|
|
25249
26018
|
if (!secret2) return null;
|
|
25250
|
-
const stripeClient = new
|
|
26019
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
25251
26020
|
const customer = await stripeClient.customers.create({
|
|
25252
26021
|
email,
|
|
25253
26022
|
description: "MCP Scraper free account",
|
|
@@ -25319,7 +26088,7 @@ app.post("/auth/register", requireAllowedOrigin, async (c) => {
|
|
|
25319
26088
|
const user = await createUser(normalizedEmail, void 0, password, stripeCustomerId ?? void 0);
|
|
25320
26089
|
if (stripeCustomerId) {
|
|
25321
26090
|
try {
|
|
25322
|
-
const stripeClient = new
|
|
26091
|
+
const stripeClient = new Stripe2(process.env.STRIPE_SECRET_KEY, { apiVersion: STRIPE_API_VERSION });
|
|
25323
26092
|
await stripeClient.customers.update(stripeCustomerId, {
|
|
25324
26093
|
metadata: {
|
|
25325
26094
|
app: "mcp-scraper",
|
|
@@ -25387,7 +26156,7 @@ app.post("/account/delete", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
25387
26156
|
}
|
|
25388
26157
|
const secret2 = requireStripeSecret();
|
|
25389
26158
|
if (secret2 && user.stripe_customer_id) {
|
|
25390
|
-
const stripeClient = new
|
|
26159
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
25391
26160
|
const subs = await stripeClient.subscriptions.list({ customer: user.stripe_customer_id, status: "all", limit: 100 });
|
|
25392
26161
|
for (const sub of subs.data) {
|
|
25393
26162
|
if (sub.status === "active" || sub.status === "trialing" || sub.status === "past_due") {
|
|
@@ -25712,7 +26481,7 @@ app.post("/memory/checkout", auth2, async (c) => {
|
|
|
25712
26481
|
if (!priceId || !MEMORY_PLANS[priceId]) return c.json({ error: "Invalid priceId \u2014 must be a memory plan price." }, 400);
|
|
25713
26482
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
25714
26483
|
if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
|
|
25715
|
-
const stripeClient = new
|
|
26484
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
25716
26485
|
let customerId = user.stripe_customer_id;
|
|
25717
26486
|
if (!customerId) {
|
|
25718
26487
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -25747,7 +26516,7 @@ app.post("/memory/portal", auth2, async (c) => {
|
|
|
25747
26516
|
if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
|
|
25748
26517
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
25749
26518
|
if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
|
|
25750
|
-
const stripeClient = new
|
|
26519
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
25751
26520
|
const session = await stripeClient.billingPortal.sessions.create({
|
|
25752
26521
|
customer: user.stripe_customer_id,
|
|
25753
26522
|
return_url: `${appOrigin()}/memory`
|
|
@@ -25965,10 +26734,8 @@ app.post("/schedule-connections/session", auth2, requireIntegrationsTier, async
|
|
|
25965
26734
|
if (!providerConfigKey) return c.json({ ok: false, error: "providerConfigKey is required." }, 400);
|
|
25966
26735
|
try {
|
|
25967
26736
|
if (providerConfigKey !== "resend") {
|
|
25968
|
-
await
|
|
25969
|
-
|
|
25970
|
-
billableNangoConnectionCount(await getNangoConnections(user.email, { summaryOnly: true }))
|
|
25971
|
-
);
|
|
26737
|
+
const existingCount = billableNangoConnectionCount(await getNangoConnections(user.email, { summaryOnly: true }));
|
|
26738
|
+
await reconcileConnectedAccountBilling(user, existingCount + 1);
|
|
25972
26739
|
}
|
|
25973
26740
|
const session = providerConfigKey === "resend" ? await createResendConnectSession(user.email, resendOAuthCallbackUrl()) : await createNangoConnectSession(user.email, providerConfigKey);
|
|
25974
26741
|
return c.json({ ok: true, ...session });
|
|
@@ -27266,7 +28033,7 @@ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, asy
|
|
|
27266
28033
|
if (user.concurrency_stripe_sub_id) return c.json({ error: "Already subscribed \u2014 cancel existing slot first to change." }, 409);
|
|
27267
28034
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
27268
28035
|
if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
|
|
27269
|
-
const stripeClient = new
|
|
28036
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
27270
28037
|
let customerId = user.stripe_customer_id;
|
|
27271
28038
|
if (!customerId) {
|
|
27272
28039
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -27312,10 +28079,10 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
27312
28079
|
const user = c.get("sessionUser");
|
|
27313
28080
|
const raw = await c.req.json().catch(() => ({}));
|
|
27314
28081
|
const tier = SUBSCRIPTION_TIER_BY_KEY[String(raw.tier ?? "")];
|
|
27315
|
-
if (!tier) return c.json({ error: "Invalid tier.
|
|
28082
|
+
if (!tier) return c.json({ error: "Invalid subscription tier." }, 400);
|
|
27316
28083
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
27317
28084
|
if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
|
|
27318
|
-
const stripeClient = new
|
|
28085
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
27319
28086
|
let customerId = user.stripe_customer_id;
|
|
27320
28087
|
if (!customerId) {
|
|
27321
28088
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -27367,7 +28134,7 @@ app.post("/billing/portal", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
27367
28134
|
if (!user.stripe_customer_id) return c.json({ error: "No billing account yet \u2014 subscribe first." }, 409);
|
|
27368
28135
|
const secret2 = requireStripeSecret();
|
|
27369
28136
|
if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
|
|
27370
|
-
const stripeClient = new
|
|
28137
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
27371
28138
|
const session = await stripeClient.billingPortal.sessions.create({
|
|
27372
28139
|
customer: user.stripe_customer_id,
|
|
27373
28140
|
return_url: `${appOrigin()}/billing`
|
|
@@ -27397,7 +28164,7 @@ app.post("/billing/concurrency/terminal-checkout", auth2, async (c) => {
|
|
|
27397
28164
|
}
|
|
27398
28165
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
27399
28166
|
if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
|
|
27400
|
-
const stripeClient = new
|
|
28167
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
27401
28168
|
let customerId = user.stripe_customer_id;
|
|
27402
28169
|
if (!customerId) {
|
|
27403
28170
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -27441,7 +28208,7 @@ app.post("/billing/subscribe/terminal-checkout", auth2, async (c) => {
|
|
|
27441
28208
|
if (!tier) return c.json({ error: "Invalid tier. Choose starter, growth, or scale.", tiers: Object.keys(SUBSCRIPTION_TIER_BY_KEY) }, 400);
|
|
27442
28209
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
27443
28210
|
if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
|
|
27444
|
-
const stripeClient = new
|
|
28211
|
+
const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
27445
28212
|
let customerId = user.stripe_customer_id;
|
|
27446
28213
|
if (!customerId) {
|
|
27447
28214
|
const customer = await stripeClient.customers.create({ email: user.email });
|
|
@@ -27502,7 +28269,7 @@ app.post("/billing/concurrency/cancel", requireAllowedOrigin, sessionAuth, async
|
|
|
27502
28269
|
if (!user.concurrency_stripe_sub_id) return c.json({ error: "No active concurrency subscription." }, 404);
|
|
27503
28270
|
const stripeSecret = process.env.STRIPE_SECRET_KEY?.trim();
|
|
27504
28271
|
if (!stripeSecret) return c.json({ error: "Stripe is not configured." }, 503);
|
|
27505
|
-
const stripeClient = new
|
|
28272
|
+
const stripeClient = new Stripe2(stripeSecret, { apiVersion: STRIPE_API_VERSION });
|
|
27506
28273
|
await stripeClient.subscriptions.cancel(user.concurrency_stripe_sub_id);
|
|
27507
28274
|
const nextSlots = Math.max(0, user.extra_concurrency_slots - 1);
|
|
27508
28275
|
await setExtraConcurrencySlots(user.id, nextSlots);
|
|
@@ -27564,7 +28331,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
27564
28331
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
27565
28332
|
return c.json({ error: "Unauthorized" }, 401);
|
|
27566
28333
|
}
|
|
27567
|
-
const { drainQueue } = await import("./worker-
|
|
28334
|
+
const { drainQueue } = await import("./worker-BJ64GCDE.js");
|
|
27568
28335
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
27569
28336
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
27570
28337
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
|
|
@@ -27592,15 +28359,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
27592
28359
|
return c.json({ error: "Unauthorized" }, 401);
|
|
27593
28360
|
}
|
|
27594
28361
|
const jobId = c.req.param("id");
|
|
27595
|
-
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-
|
|
27596
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
28362
|
+
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-ZDTO7ODZ.js");
|
|
28363
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-YNXTFCC3.js");
|
|
27597
28364
|
const job = await getExtractJob2(jobId);
|
|
27598
28365
|
if (!job) return c.json({ error: "job not found" }, 404);
|
|
27599
28366
|
const stored = await assembleExtractArtifacts(job, {});
|
|
27600
28367
|
await completeExtractJob2(jobId, stored);
|
|
27601
28368
|
let settlement = "already_settled_or_refunded";
|
|
27602
28369
|
if (job.billedMc == null && job.userId != null) {
|
|
27603
|
-
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
28370
|
+
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-ZDTO7ODZ.js");
|
|
27604
28371
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
27605
28372
|
const successful = await countSuccessfulPages2(jobId);
|
|
27606
28373
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -27767,4 +28534,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
27767
28534
|
export {
|
|
27768
28535
|
app
|
|
27769
28536
|
};
|
|
27770
|
-
//# sourceMappingURL=server-
|
|
28537
|
+
//# sourceMappingURL=server-SHOYO52X.js.map
|