mcp-scraper 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ import {
22
22
  import {
23
23
  HttpMcpToolExecutor,
24
24
  MemoryMcpToolExecutor,
25
+ buildLinkGraph,
25
26
  buildPaaExtractorMcpServer,
26
27
  configureReportSaving,
27
28
  exportFanout,
@@ -33,11 +34,13 @@ import {
33
34
  registerSerpIntelligenceCaptureTools,
34
35
  sanitizeAttempts,
35
36
  sanitizeHarvestResult
36
- } from "./chunk-YGRZU7IR.js";
37
+ } from "./chunk-FZG427QD.js";
37
38
  import {
38
39
  auditImages,
40
+ buildLinkReport,
41
+ computeIssues,
39
42
  getBlobStore
40
- } from "./chunk-GWRIO6JT.js";
43
+ } from "./chunk-R7EETU7Z.js";
41
44
  import {
42
45
  browserServiceApiKey,
43
46
  browserServiceProfileName,
@@ -67,7 +70,7 @@ import {
67
70
  RawMapsOverviewSchema,
68
71
  RawMapsReviewStatsSchema
69
72
  } from "./chunk-XGIPATLV.js";
70
- import "./chunk-EGWJ74EX.js";
73
+ import "./chunk-RD5JOAWS.js";
71
74
  import {
72
75
  completeExtractJob,
73
76
  countSuccessfulPages,
@@ -4780,7 +4783,8 @@ async function spiderSite(opts) {
4780
4783
  totalFound: results.length,
4781
4784
  durationMs: Date.now() - startMs,
4782
4785
  truncated: queue.length > 0 || results.length >= maxUrls,
4783
- browserRetries
4786
+ browserRetries,
4787
+ sitemapUrls: sitemapUrls.slice(0, maxUrls)
4784
4788
  };
4785
4789
  }
4786
4790
 
@@ -4904,6 +4908,23 @@ async function crawlWithRotation(urls, opts) {
4904
4908
  return urls.map((u) => byUrl.get(u) ?? { url: u, html: null, status: 0, via: "browser" });
4905
4909
  }
4906
4910
 
4911
+ // src/api/seo-audit.ts
4912
+ function buildSeoAudit(pages, startUrl) {
4913
+ const { edges, metrics } = buildLinkGraph(pages, startUrl);
4914
+ const issues = computeIssues(pages, metrics);
4915
+ const { summary } = buildLinkReport(edges, [...metrics.values()], startUrl);
4916
+ return {
4917
+ issues,
4918
+ linkSummary: summary,
4919
+ pageMetrics: [...metrics.values()].map((m) => ({
4920
+ url: m.url,
4921
+ inboundInternal: m.inlinks,
4922
+ outboundInternal: m.outlinksInternal,
4923
+ orphan: m.orphan
4924
+ }))
4925
+ };
4926
+ }
4927
+
4907
4928
  // src/api/site-extractor.ts
4908
4929
  var PIXEL_WIDTHS = { i: 4, l: 4, j: 4, ".": 4, ",": 4, "'": 4, t: 6, f: 6, r: 6, " ": 4, m: 14, w: 13, W: 16, M: 16 };
4909
4930
  function estimatePixels(s) {
@@ -4947,6 +4968,7 @@ function emptyPageData(url, status, via) {
4947
4968
  imageLinks: [],
4948
4969
  indexable: status === 200,
4949
4970
  indexabilityReason: status === 200 ? null : "non-200",
4971
+ inSitemap: null,
4950
4972
  schemaTypes: [],
4951
4973
  canonicalUrl: null,
4952
4974
  internalLinks: 0,
@@ -4963,7 +4985,8 @@ function makeSeedSpider(startUrl, urls) {
4963
4985
  totalFound: urls.length,
4964
4986
  durationMs: 0,
4965
4987
  truncated: false,
4966
- browserRetries: 0
4988
+ browserRetries: 0,
4989
+ sitemapUrls: []
4967
4990
  };
4968
4991
  }
4969
4992
  var UA2 = "Mozilla/5.0 (compatible; ThorbitBot/1.0; +https://thorbit.ai)";
@@ -5134,6 +5157,7 @@ function parsePageData(url, html, status, via, resp) {
5134
5157
  imageLinks,
5135
5158
  indexable,
5136
5159
  indexabilityReason,
5160
+ inSitemap: null,
5137
5161
  schemaTypes,
5138
5162
  canonicalUrl,
5139
5163
  internalLinks,
@@ -5147,11 +5171,11 @@ async function extractPagesRotating(urls, opts) {
5147
5171
  const fetched = await crawlWithRotation(urls, { apiKey: opts.kernelApiKey, concurrency: opts.concurrency, urlsPerBrowser: opts.urlsPerBrowser });
5148
5172
  return fetched.map((r) => r.html ? parsePageData(r.url, r.html, r.status || 200, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }) : emptyPageData(r.url, r.status || null, "browser"));
5149
5173
  }
5150
- async function discoverUrls(startUrl, maxPages, kernelApiKey) {
5174
+ async function discoverUrlsWithSitemap(startUrl, maxPages, kernelApiKey) {
5151
5175
  const sitemapUrls = await discoverSitemapUrls2(startUrl, maxPages);
5152
- if (sitemapUrls.length > 0) return sitemapUrls;
5176
+ if (sitemapUrls.length > 0) return { urls: sitemapUrls, sitemapUrls };
5153
5177
  const spider = await spiderSite({ startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey });
5154
- return spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
5178
+ return { urls: spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url), sitemapUrls: spider.sitemapUrls };
5155
5179
  }
5156
5180
  async function fetchAndParse(url, kernelApiKey) {
5157
5181
  const startedAt = Date.now();
@@ -5210,6 +5234,7 @@ async function extractSite(opts) {
5210
5234
  let browserRetries = 0;
5211
5235
  let truncated = false;
5212
5236
  let spider;
5237
+ let sitemapUrls = [];
5213
5238
  if (rotating) {
5214
5239
  const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
5215
5240
  const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
@@ -5227,7 +5252,8 @@ async function extractSite(opts) {
5227
5252
  for (const u of opts.seedUrls) enqueue(u);
5228
5253
  } else {
5229
5254
  enqueue(opts.startUrl);
5230
- for (const u of await discoverSitemapUrls2(opts.startUrl, maxPages)) enqueue(u);
5255
+ sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
5256
+ for (const u of sitemapUrls) enqueue(u);
5231
5257
  }
5232
5258
  const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
5233
5259
  let done = 0;
@@ -5256,6 +5282,7 @@ async function extractSite(opts) {
5256
5282
  spider = makeSeedSpider(opts.startUrl, [...seen]);
5257
5283
  } else {
5258
5284
  spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
5285
+ sitemapUrls = spider.sitemapUrls;
5259
5286
  const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
5260
5287
  browserRetries = spider.browserRetries;
5261
5288
  await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
@@ -5265,10 +5292,17 @@ async function extractSite(opts) {
5265
5292
  });
5266
5293
  truncated = spider.truncated || pages.length >= maxPages;
5267
5294
  }
5295
+ const sitemapSet = sitemapUrls.length > 0 ? new Set(sitemapUrls.map((u) => normalizeUrl(u, opts.startUrl) ?? u)) : null;
5296
+ for (const p of pages) p.inSitemap = sitemapSet ? sitemapSet.has(normalizeUrl(p.url, opts.startUrl) ?? p.url) : null;
5268
5297
  let branding = null;
5269
5298
  if (opts.formats?.includes("branding") && opts.kernelApiKey) {
5270
5299
  branding = await extractBranding(opts.startUrl, opts.kernelApiKey).catch(() => null);
5271
5300
  }
5301
+ const seoAudit = opts.formats?.includes("issues") ? buildSeoAudit(pages, opts.startUrl) : null;
5302
+ let imageAudit = null;
5303
+ if (opts.formats?.includes("images")) {
5304
+ imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
5305
+ }
5272
5306
  return {
5273
5307
  startUrl: opts.startUrl,
5274
5308
  pages,
@@ -5277,7 +5311,9 @@ async function extractSite(opts) {
5277
5311
  durationMs: Date.now() - startMs,
5278
5312
  truncated,
5279
5313
  browserRetries,
5280
- branding
5314
+ branding,
5315
+ seoAudit,
5316
+ imageAudit
5281
5317
  };
5282
5318
  }
5283
5319
 
@@ -7519,12 +7555,13 @@ var siteExtractFn = inngest.createFunction(
7519
7555
  const maxPages = Number(job.options.maxPages ?? 1e4);
7520
7556
  const concurrency = Number(job.options.concurrency ?? 1);
7521
7557
  const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
7522
- const seed = await step.run("discover", async () => {
7523
- const discovered = await discoverUrls(job.startUrl, maxPages, key);
7524
- const out = discovered.length ? discovered : [job.startUrl];
7525
- await setExtractJobTotal(jobId, out.length);
7526
- return out;
7558
+ const seedRaw = await step.run("discover", async () => {
7559
+ const discovered = await discoverUrlsWithSitemap(job.startUrl, maxPages, key);
7560
+ const urls = discovered.urls.length ? discovered.urls : [job.startUrl];
7561
+ await setExtractJobTotal(jobId, urls.length);
7562
+ return { urls, sitemapUrls: discovered.sitemapUrls };
7527
7563
  });
7564
+ const seed = Array.isArray(seedRaw) ? { urls: seedRaw, sitemapUrls: [] } : seedRaw;
7528
7565
  const siteHost = (() => {
7529
7566
  try {
7530
7567
  return new URL(job.startUrl).hostname.replace(/^www\./, "");
@@ -7548,8 +7585,9 @@ var siteExtractFn = inngest.createFunction(
7548
7585
  return false;
7549
7586
  }
7550
7587
  };
7551
- const queue = [...seed];
7552
- const seen = new Set(seed.map(norm));
7588
+ const sitemapSet = seed.sitemapUrls.length > 0 ? new Set(seed.sitemapUrls.map(norm)) : null;
7589
+ const queue = [...seed.urls];
7590
+ const seen = new Set(seed.urls.map(norm));
7553
7591
  let crawled = 0;
7554
7592
  const MAX_BATCHES = Math.ceil(maxPages / BATCH) + 30;
7555
7593
  for (let i = 0; i < MAX_BATCHES && crawled < maxPages && crawled < queue.length; i++) {
@@ -7557,7 +7595,7 @@ var siteExtractFn = inngest.createFunction(
7557
7595
  if (!batch.length) break;
7558
7596
  const newLinks = await step.run(`crawl-batch-${i}`, async () => {
7559
7597
  const pages = await extractPagesRotating(batch, { kernelApiKey: key, concurrency, urlsPerBrowser });
7560
- await saveExtractPages(jobId, pages);
7598
+ await saveExtractPages(jobId, pages.map((p) => ({ ...p, inSitemap: sitemapSet ? sitemapSet.has(norm(p.url)) : null })));
7561
7599
  const found = [];
7562
7600
  for (const p of pages) for (const l of p.outlinks ?? []) {
7563
7601
  if (l.internal && l.href && sameSite(l.href)) found.push(l.href);
@@ -7581,9 +7619,13 @@ var siteExtractFn = inngest.createFunction(
7581
7619
  const pages = await getExtractedPages(jobId);
7582
7620
  return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
7583
7621
  }) : null;
7622
+ const seoAudit = Array.isArray(job.options.formats) && job.options.formats.includes("issues") ? await step.run("seo-audit", async () => {
7623
+ const pages = await getExtractedPages(jobId);
7624
+ return buildSeoAudit(pages, job.startUrl);
7625
+ }) : null;
7584
7626
  const artifacts = await step.run("finalize", async () => {
7585
- const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
7586
- const stored = await assembleExtractArtifacts(job, { branding, imageAudit });
7627
+ const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
7628
+ const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
7587
7629
  await completeExtractJob(jobId, stored);
7588
7630
  return stored;
7589
7631
  });
@@ -9089,7 +9131,7 @@ var ExtractSiteBodySchema = z11.object({
9089
9131
  rotateProxies: z11.boolean().optional(),
9090
9132
  rotateProxyEvery: z11.number().int().min(1).max(100).optional(),
9091
9133
  background: z11.boolean().optional(),
9092
- formats: z11.array(z11.enum(["markdown", "links", "json", "images", "branding"])).optional()
9134
+ formats: z11.array(z11.enum(["markdown", "links", "json", "images", "branding", "issues"])).optional()
9093
9135
  });
9094
9136
  var YoutubeHarvestBodySchema = z11.object({
9095
9137
  mode: z11.enum(["search", "channel"]),
@@ -20351,6 +20393,306 @@ async function dbUsage(identity, plan) {
20351
20393
  };
20352
20394
  }
20353
20395
 
20396
+ // src/api/nango-control.ts
20397
+ var DEFAULT_NANGO_CONTROL_URL = "https://mcp-scraper-scheduler.vercel.app";
20398
+ var DISABLED_NANGO_TOOLS = {
20399
+ "google-mail": /* @__PURE__ */ new Set(["list-filters"]),
20400
+ slack: /* @__PURE__ */ new Set(["search-files", "search-messages"])
20401
+ };
20402
+ var NangoControlError = class extends Error {
20403
+ status;
20404
+ constructor(message, status = 502) {
20405
+ super(message);
20406
+ this.name = "NangoControlError";
20407
+ this.status = status;
20408
+ }
20409
+ };
20410
+ var ScheduleConnectionValidationError = class extends Error {
20411
+ constructor(message) {
20412
+ super(message);
20413
+ this.name = "ScheduleConnectionValidationError";
20414
+ }
20415
+ };
20416
+ function controlBaseUrl() {
20417
+ return (process.env.NANGO_CONTROL_URL?.trim() || DEFAULT_NANGO_CONTROL_URL).replace(/\/$/, "");
20418
+ }
20419
+ function controlSecret() {
20420
+ const secret2 = process.env.SCHEDULE_INTEGRATIONS_SECRET?.trim();
20421
+ if (!secret2) throw new NangoControlError("Scheduled service connections are not configured.", 503);
20422
+ return secret2;
20423
+ }
20424
+ function isRecord(value) {
20425
+ return !!value && typeof value === "object" && !Array.isArray(value);
20426
+ }
20427
+ function unwrapData(value) {
20428
+ if (isRecord(value) && isRecord(value.data)) return value.data;
20429
+ return value;
20430
+ }
20431
+ function cleanString(value, max = 300) {
20432
+ if (typeof value !== "string") return null;
20433
+ const trimmed = value.trim();
20434
+ return trimmed ? trimmed.slice(0, max) : null;
20435
+ }
20436
+ function firstString(record, keys, max = 300) {
20437
+ for (const key of keys) {
20438
+ const value = cleanString(record[key], max);
20439
+ if (value) return value;
20440
+ }
20441
+ return null;
20442
+ }
20443
+ function cleanTools(value) {
20444
+ if (!Array.isArray(value)) return [];
20445
+ const tools = value.map((tool) => cleanString(tool, 200)).filter((tool) => !!tool);
20446
+ return [...new Set(tools)].slice(0, 200);
20447
+ }
20448
+ function cleanStringArray(value, maxItems = 20, maxLength = 100) {
20449
+ if (!Array.isArray(value)) return [];
20450
+ const items = value.map((item) => cleanString(item, maxLength)).filter((item) => !!item);
20451
+ return [...new Set(items)].slice(0, maxItems);
20452
+ }
20453
+ function cleanHttpsUrl(value) {
20454
+ const candidate = cleanString(value, 2e3);
20455
+ if (!candidate) return null;
20456
+ try {
20457
+ const url = new URL(candidate);
20458
+ return url.protocol === "https:" ? url.toString() : null;
20459
+ } catch {
20460
+ return null;
20461
+ }
20462
+ }
20463
+ function arrayFromPayload(value, keys) {
20464
+ const unwrapped = unwrapData(value);
20465
+ if (Array.isArray(unwrapped)) return unwrapped;
20466
+ if (!isRecord(unwrapped)) return [];
20467
+ for (const key of keys) {
20468
+ if (Array.isArray(unwrapped[key])) return unwrapped[key];
20469
+ }
20470
+ return [];
20471
+ }
20472
+ async function controlRequest(path5, init) {
20473
+ const response = await fetch(`${controlBaseUrl()}${path5}`, {
20474
+ ...init,
20475
+ headers: {
20476
+ accept: "application/json",
20477
+ authorization: `Bearer ${controlSecret()}`,
20478
+ ...init?.body ? { "content-type": "application/json" } : {},
20479
+ ...init?.headers ?? {}
20480
+ },
20481
+ signal: AbortSignal.timeout(2e4)
20482
+ }).catch((err) => {
20483
+ throw new NangoControlError(`Scheduled service connection control is unavailable: ${err instanceof Error ? err.message : "network error"}`);
20484
+ });
20485
+ const text = await response.text();
20486
+ let body = {};
20487
+ try {
20488
+ body = text ? JSON.parse(text) : {};
20489
+ } catch {
20490
+ body = {};
20491
+ }
20492
+ if (!response.ok) {
20493
+ const upstream = isRecord(body) ? cleanString(body.error, 300) : null;
20494
+ throw new NangoControlError(upstream || `Scheduled service connection control failed (${response.status}).`);
20495
+ }
20496
+ return body;
20497
+ }
20498
+ function sanitizeScheduleConnectionSelections(value) {
20499
+ if (value == null) return [];
20500
+ if (!Array.isArray(value)) throw new ScheduleConnectionValidationError("connections must be an array.");
20501
+ if (value.length > 20) throw new ScheduleConnectionValidationError("A scheduled action can use at most 20 service connections.");
20502
+ const selections = [];
20503
+ const seen = /* @__PURE__ */ new Set();
20504
+ for (const item of value) {
20505
+ if (!isRecord(item)) throw new ScheduleConnectionValidationError("Each service connection must be an object.");
20506
+ const connectionId = firstString(item, ["connectionId", "connection_id"]);
20507
+ const providerConfigKey = firstString(item, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id"]);
20508
+ const allowedTools = cleanTools(item.allowedTools ?? item.allowed_tools ?? item.allowedCapabilities ?? item.allowed_capabilities);
20509
+ if (!connectionId || !providerConfigKey) throw new ScheduleConnectionValidationError("Each service connection needs a connectionId and providerConfigKey.");
20510
+ if (allowedTools.length === 0) throw new ScheduleConnectionValidationError("Each selected service needs at least one allowed tool.");
20511
+ const key = `${providerConfigKey}:${connectionId}`;
20512
+ if (seen.has(key)) continue;
20513
+ seen.add(key);
20514
+ selections.push({ connectionId, providerConfigKey, allowedTools });
20515
+ }
20516
+ return selections;
20517
+ }
20518
+ async function getNangoCatalog() {
20519
+ const body = await controlRequest("/api/internal/nango/catalog");
20520
+ const rows = arrayFromPayload(body, ["providers", "catalog", "integrations", "services"]);
20521
+ const result = [];
20522
+ for (const row of rows) {
20523
+ if (!isRecord(row)) continue;
20524
+ const providerConfigKey = firstString(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "id"]);
20525
+ if (!providerConfigKey) continue;
20526
+ const provider = firstString(row, ["provider"]);
20527
+ const label = firstString(row, ["label", "displayName", "display_name", "name"]) || providerConfigKey;
20528
+ const description = firstString(row, ["description"], 500);
20529
+ const logoUrl = cleanHttpsUrl(row.logoUrl ?? row.logo_url ?? row.logo);
20530
+ const docsUrl = cleanHttpsUrl(row.docsUrl ?? row.docs_url ?? row.docs);
20531
+ const authMode = firstString(row, ["authMode", "auth_mode"], 100);
20532
+ const categories = cleanStringArray(row.categories);
20533
+ const disabledTools = DISABLED_NANGO_TOOLS[providerConfigKey];
20534
+ const safeDefaultAllowedTools = cleanTools(
20535
+ row.safeDefaultAllowedTools ?? row.safe_default_allowed_tools ?? row.defaultAllowedTools ?? row.default_allowed_tools ?? row.allowedTools ?? row.allowed_tools
20536
+ ).filter((tool) => !disabledTools?.has(tool));
20537
+ result.push({
20538
+ providerConfigKey,
20539
+ provider,
20540
+ label,
20541
+ description,
20542
+ logoUrl,
20543
+ docsUrl,
20544
+ authMode,
20545
+ categories,
20546
+ safeDefaultAllowedTools
20547
+ });
20548
+ }
20549
+ return result;
20550
+ }
20551
+ async function getNangoConnections(identity) {
20552
+ const query = new URLSearchParams({ identity }).toString();
20553
+ const body = await controlRequest(`/api/internal/nango/connections?${query}`);
20554
+ const rows = arrayFromPayload(body, ["connections"]);
20555
+ const result = [];
20556
+ for (const row of rows) {
20557
+ if (!isRecord(row)) continue;
20558
+ const connectionId = firstString(row, ["connectionId", "connection_id", "id"]);
20559
+ const providerConfigKey = firstString(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
20560
+ if (!connectionId || !providerConfigKey) continue;
20561
+ const provider = firstString(row, ["provider"]);
20562
+ const label = firstString(row, ["label", "accountLabel", "account_label", "displayName", "display_name", "endUserEmail", "end_user_email"]) || providerConfigKey;
20563
+ const rawStatus = firstString(row, ["status"], 64) || "connected";
20564
+ const reconnectRequired = row.reconnectRequired === true || row.reconnect_required === true || rawStatus === "needs_reauth" || rawStatus === "reauth_required" || rawStatus === "invalid" || rawStatus === "error" || rawStatus === "revoked" || rawStatus === "disabled";
20565
+ result.push({
20566
+ connectionId,
20567
+ providerConfigKey,
20568
+ provider,
20569
+ label,
20570
+ status: reconnectRequired ? "needs_reauth" : "connected",
20571
+ reconnectRequired,
20572
+ actionsEnabled: row.actionsEnabled === true || row.actions_enabled === true,
20573
+ createdAt: firstString(row, ["createdAt", "created_at"], 100),
20574
+ updatedAt: firstString(row, ["updatedAt", "updated_at"], 100)
20575
+ });
20576
+ }
20577
+ return result;
20578
+ }
20579
+ function sanitizeConnectSession(body) {
20580
+ const data = unwrapData(body);
20581
+ if (!isRecord(data)) throw new NangoControlError("The connection service returned an invalid connect session.");
20582
+ const connectLink = firstString(data, ["connectLink", "connect_link"], 2e3);
20583
+ if (!connectLink) throw new NangoControlError("The connection service did not return a connect link.");
20584
+ return {
20585
+ connectLink,
20586
+ sessionToken: firstString(data, ["sessionToken", "session_token", "token"], 2e3),
20587
+ expiresAt: firstString(data, ["expiresAt", "expires_at"], 100)
20588
+ };
20589
+ }
20590
+ async function createNangoConnectSession(identity, providerConfigKey) {
20591
+ const body = await controlRequest("/api/internal/nango/connect-session", {
20592
+ method: "POST",
20593
+ body: JSON.stringify({ identity, email: identity, provider: providerConfigKey })
20594
+ });
20595
+ return sanitizeConnectSession(body);
20596
+ }
20597
+ async function createNangoReconnectSession(identity, connectionId) {
20598
+ const body = await controlRequest("/api/internal/nango/reconnect-session", {
20599
+ method: "POST",
20600
+ body: JSON.stringify({ identity, connectionId, email: identity })
20601
+ });
20602
+ return sanitizeConnectSession(body);
20603
+ }
20604
+ function sanitizeBindingRows(body, defaultScheduleActionId) {
20605
+ const data = unwrapData(body);
20606
+ const flattened = [];
20607
+ if (isRecord(data) && isRecord(data.bindings) && !Array.isArray(data.bindings)) {
20608
+ for (const [scheduleActionId, value] of Object.entries(data.bindings)) {
20609
+ if (Array.isArray(value)) value.forEach((row) => flattened.push({ row, scheduleActionId }));
20610
+ }
20611
+ } else {
20612
+ const rows = arrayFromPayload(data, ["bindings", "connections"]);
20613
+ for (const row of rows) {
20614
+ if (isRecord(row) && Array.isArray(row.connections)) {
20615
+ const scheduleActionId = firstString(row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || defaultScheduleActionId;
20616
+ row.connections.forEach((connection) => flattened.push({ row: connection, scheduleActionId }));
20617
+ } else {
20618
+ flattened.push({ row, scheduleActionId: defaultScheduleActionId });
20619
+ }
20620
+ }
20621
+ }
20622
+ const result = [];
20623
+ for (const item of flattened) {
20624
+ if (!isRecord(item.row)) continue;
20625
+ const connectionId = firstString(item.row, ["connectionId", "connection_id"]);
20626
+ const providerConfigKey = firstString(item.row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
20627
+ if (!connectionId || !providerConfigKey) continue;
20628
+ result.push({
20629
+ scheduleActionId: firstString(item.row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || item.scheduleActionId,
20630
+ connectionId,
20631
+ providerConfigKey,
20632
+ allowedTools: cleanTools(item.row.allowedTools ?? item.row.allowed_tools ?? item.row.allowedCapabilities ?? item.row.allowed_capabilities),
20633
+ label: firstString(item.row, ["label", "accountLabel", "account_label", "displayName", "display_name"])
20634
+ });
20635
+ }
20636
+ return result;
20637
+ }
20638
+ async function getScheduleConnectionBindings(identity, scheduleActionId) {
20639
+ const query = new URLSearchParams({ identity, scheduleId: scheduleActionId });
20640
+ const body = await controlRequest(`/api/internal/nango/bindings?${query.toString()}`);
20641
+ return sanitizeBindingRows(body, scheduleActionId);
20642
+ }
20643
+ async function putScheduleConnectionBindings(identity, scheduleActionId, connections) {
20644
+ const body = await controlRequest("/api/internal/nango/bindings", {
20645
+ method: "PUT",
20646
+ body: JSON.stringify({
20647
+ identity,
20648
+ scheduleId: scheduleActionId,
20649
+ connections: connections.map((connection) => ({
20650
+ connectionId: connection.connectionId,
20651
+ allowedTools: connection.allowedTools
20652
+ }))
20653
+ })
20654
+ });
20655
+ const sanitized = sanitizeBindingRows(body, scheduleActionId);
20656
+ const expectedByConnectionId = new Map(connections.map((connection) => [
20657
+ connection.connectionId,
20658
+ {
20659
+ providerConfigKey: connection.providerConfigKey,
20660
+ allowedTools: [...connection.allowedTools].sort()
20661
+ }
20662
+ ]));
20663
+ const responseMatchesRequest = sanitized.length === connections.length && sanitized.every((binding) => {
20664
+ const expected = expectedByConnectionId.get(binding.connectionId);
20665
+ return !!expected && binding.scheduleActionId === scheduleActionId && binding.providerConfigKey === expected.providerConfigKey && [...binding.allowedTools].sort().join("\0") === expected.allowedTools.join("\0");
20666
+ });
20667
+ if (!responseMatchesRequest) {
20668
+ throw new NangoControlError("Scheduled service connection control returned an invalid binding response.");
20669
+ }
20670
+ return sanitized;
20671
+ }
20672
+ async function deleteScheduleConnectionBindings(identity, scheduleActionId) {
20673
+ await controlRequest("/api/internal/nango/bindings", {
20674
+ method: "DELETE",
20675
+ body: JSON.stringify({ identity, scheduleId: scheduleActionId })
20676
+ });
20677
+ }
20678
+ async function setScheduleConnectionActionsEnabled(identity, connectionId, enabled) {
20679
+ const body = await controlRequest("/api/internal/nango/connections/actions/enable", {
20680
+ method: "POST",
20681
+ body: JSON.stringify({ identity, connectionId, enabled })
20682
+ });
20683
+ const data = unwrapData(body);
20684
+ if (!isRecord(data) || !isRecord(data.connection)) return enabled;
20685
+ return data.connection.actionsEnabled === true;
20686
+ }
20687
+ async function callScheduleConnectionAction(identity, connectionId, input) {
20688
+ const body = await controlRequest("/api/internal/nango/connections/actions/call", {
20689
+ method: "POST",
20690
+ body: JSON.stringify({ identity, connectionId, input })
20691
+ });
20692
+ const data = unwrapData(body);
20693
+ return isRecord(data) ? data.result ?? data : data;
20694
+ }
20695
+
20354
20696
  // src/api/server.ts
20355
20697
  var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
20356
20698
  var isProduction2 = secureCookies2;
@@ -20458,6 +20800,7 @@ app.use("*", async (c, next) => {
20458
20800
  });
20459
20801
  function opForCostPath(p) {
20460
20802
  if (p === "/harvest" || p === "/harvest/sync") return "harvest";
20803
+ if (p === "/diff-page") return "diff_page";
20461
20804
  if (p === "/extract-url") return "page_scrape";
20462
20805
  if (p === "/extract-site") return "extract_site";
20463
20806
  if (p === "/map-urls") return "url_map";
@@ -20630,6 +20973,13 @@ app.get("/catalog", (c) => {
20630
20973
  c.header("Cache-Control", "public, max-age=60");
20631
20974
  return c.json(CATALOG);
20632
20975
  });
20976
+ app.get("/rates", (c) => {
20977
+ const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
20978
+ ...cost,
20979
+ ...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
20980
+ }));
20981
+ return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
20982
+ });
20633
20983
  app.get("/me", async (c) => {
20634
20984
  const token = getCookie2(c, "session");
20635
20985
  if (!token) return c.json({ authenticated: false });
@@ -20955,6 +21305,20 @@ app.post("/schedule-checkout", auth2, async (c) => {
20955
21305
  return c.json({ error: message }, 500);
20956
21306
  }
20957
21307
  });
21308
+ function scheduleConnectionError(c, err, fallback) {
21309
+ if (err instanceof ScheduleConnectionValidationError) return c.json({ ok: false, error: err.message }, 400);
21310
+ if (err instanceof NangoControlError) return c.json({ ok: false, error: err.message }, err.status);
21311
+ console.error("[schedule-connections]", err instanceof Error ? err.message : String(err));
21312
+ return c.json({ ok: false, error: fallback }, 502);
21313
+ }
21314
+ function providerConfigKeyFrom(value) {
21315
+ if (typeof value !== "string") return null;
21316
+ const key = value.trim();
21317
+ return key && key.length <= 200 ? key : null;
21318
+ }
21319
+ function bindingsForSchedule(bindings, scheduleActionId) {
21320
+ return bindings.filter((binding) => binding.scheduleActionId === scheduleActionId);
21321
+ }
20958
21322
  app.get("/schedule-status", auth2, async (c) => {
20959
21323
  try {
20960
21324
  const user = c.get("user");
@@ -20968,6 +21332,103 @@ app.get("/schedule-status", auth2, async (c) => {
20968
21332
  return c.json({ error: message }, 500);
20969
21333
  }
20970
21334
  });
21335
+ app.get("/schedule-connection-catalog", auth2, async (c) => {
21336
+ try {
21337
+ return c.json({ ok: true, catalog: await getNangoCatalog() });
21338
+ } catch (err) {
21339
+ return scheduleConnectionError(c, err, "Unable to load the service connection catalog.");
21340
+ }
21341
+ });
21342
+ app.get("/schedule-connections", auth2, async (c) => {
21343
+ try {
21344
+ const user = c.get("user");
21345
+ return c.json({ ok: true, connections: await getNangoConnections(user.email) });
21346
+ } catch (err) {
21347
+ return scheduleConnectionError(c, err, "Unable to load service connections.");
21348
+ }
21349
+ });
21350
+ app.post("/schedule-connections/session", auth2, async (c) => {
21351
+ const user = c.get("user");
21352
+ const body = await c.req.json().catch(() => ({}));
21353
+ const providerConfigKey = providerConfigKeyFrom(body.providerConfigKey);
21354
+ if (!providerConfigKey) return c.json({ ok: false, error: "providerConfigKey is required." }, 400);
21355
+ try {
21356
+ const session = await createNangoConnectSession(user.email, providerConfigKey);
21357
+ return c.json({ ok: true, ...session });
21358
+ } catch (err) {
21359
+ return scheduleConnectionError(c, err, "Unable to start the service connection flow.");
21360
+ }
21361
+ });
21362
+ app.post("/schedule-connections/:id/reconnect-session", auth2, async (c) => {
21363
+ const user = c.get("user");
21364
+ try {
21365
+ const session = await createNangoReconnectSession(user.email, c.req.param("id"));
21366
+ return c.json({ ok: true, ...session });
21367
+ } catch (err) {
21368
+ return scheduleConnectionError(c, err, "Unable to start the service reconnection flow.");
21369
+ }
21370
+ });
21371
+ app.post("/schedule-connections/:id/enable-actions", auth2, async (c) => {
21372
+ const user = c.get("user");
21373
+ const body = await c.req.json().catch(() => ({}));
21374
+ if (typeof body.enabled !== "boolean") return c.json({ ok: false, error: "enabled must be a boolean." }, 400);
21375
+ try {
21376
+ const actionsEnabled = await setScheduleConnectionActionsEnabled(user.email, c.req.param("id"), body.enabled);
21377
+ return c.json({ ok: true, actionsEnabled });
21378
+ } catch (err) {
21379
+ return scheduleConnectionError(c, err, "Unable to update this connection's action setting.");
21380
+ }
21381
+ });
21382
+ app.post("/schedule-connections/actions/slack/send-message", auth2, async (c) => {
21383
+ const user = c.get("user");
21384
+ const body = await c.req.json().catch(() => ({}));
21385
+ const connectionId = providerConfigKeyFrom(body.connectionId);
21386
+ if (!connectionId || typeof body.channel !== "string" || typeof body.text !== "string") {
21387
+ return c.json({ ok: false, error: "connectionId, channel, and text are required." }, 400);
21388
+ }
21389
+ try {
21390
+ const result = await callScheduleConnectionAction(user.email, connectionId, { channel: body.channel, text: body.text });
21391
+ return c.json({ ok: true, result });
21392
+ } catch (err) {
21393
+ return scheduleConnectionError(c, err, "Unable to send the Slack message.");
21394
+ }
21395
+ });
21396
+ app.post("/schedule-connections/actions/gmail/send-message", auth2, async (c) => {
21397
+ const user = c.get("user");
21398
+ const body = await c.req.json().catch(() => ({}));
21399
+ const connectionId = providerConfigKeyFrom(body.connectionId);
21400
+ if (!connectionId || typeof body.to !== "string" || typeof body.subject !== "string" || typeof body.body !== "string") {
21401
+ return c.json({ ok: false, error: "connectionId, to, subject, and body are required." }, 400);
21402
+ }
21403
+ try {
21404
+ const result = await callScheduleConnectionAction(user.email, connectionId, { to: body.to, subject: body.subject, body: body.body });
21405
+ return c.json({ ok: true, result });
21406
+ } catch (err) {
21407
+ return scheduleConnectionError(c, err, "Unable to send the email.");
21408
+ }
21409
+ });
21410
+ app.post("/schedule-connections/actions/google-calendar/create-event", auth2, async (c) => {
21411
+ const user = c.get("user");
21412
+ const body = await c.req.json().catch(() => ({}));
21413
+ const connectionId = providerConfigKeyFrom(body.connectionId);
21414
+ if (!connectionId || typeof body.summary !== "string" || typeof body.startDateTime !== "string" || typeof body.endDateTime !== "string") {
21415
+ return c.json({ ok: false, error: "connectionId, summary, startDateTime, and endDateTime are required." }, 400);
21416
+ }
21417
+ const timeZone = typeof body.timeZone === "string" ? body.timeZone : void 0;
21418
+ try {
21419
+ const result = await callScheduleConnectionAction(user.email, connectionId, {
21420
+ calendarId: typeof body.calendarId === "string" ? body.calendarId : "primary",
21421
+ summary: body.summary,
21422
+ description: typeof body.description === "string" ? body.description : void 0,
21423
+ location: typeof body.location === "string" ? body.location : void 0,
21424
+ start: { dateTime: body.startDateTime, timeZone },
21425
+ end: { dateTime: body.endDateTime, timeZone }
21426
+ });
21427
+ return c.json({ ok: true, result });
21428
+ } catch (err) {
21429
+ return scheduleConnectionError(c, err, "Unable to create the calendar event.");
21430
+ }
21431
+ });
20971
21432
  app.post("/schedule-link", auth2, async (c) => {
20972
21433
  try {
20973
21434
  const user = c.get("user");
@@ -20986,15 +21447,58 @@ app.get("/schedule-actions", auth2, async (c) => {
20986
21447
  const { key, error } = await getOrCreateUserMemoryKey(user);
20987
21448
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
20988
21449
  const r = await memoryCall("listScheduledActionsTool", {}, key);
20989
- return c.json(r);
21450
+ if (!r.ok || !Array.isArray(r.actions)) return c.json(r);
21451
+ try {
21452
+ const actions = await Promise.all(r.actions.map(async (action) => {
21453
+ const id = typeof action.id === "string" ? action.id : "";
21454
+ if (!id) return { ...action, connections: [] };
21455
+ const bindings = await getScheduleConnectionBindings(user.email, id);
21456
+ return { ...action, connections: bindingsForSchedule(bindings, id) };
21457
+ }));
21458
+ return c.json({
21459
+ ...r,
21460
+ actions
21461
+ });
21462
+ } catch (err) {
21463
+ console.warn("[schedule-actions] connection bindings unavailable:", err instanceof Error ? err.message : String(err));
21464
+ return c.json({
21465
+ ...r,
21466
+ actions: r.actions.map((action) => ({ ...action, connections: [] })),
21467
+ connectionBindingsUnavailable: true
21468
+ });
21469
+ }
20990
21470
  });
20991
21471
  app.post("/schedule-actions", auth2, async (c) => {
20992
21472
  const user = c.get("user");
20993
21473
  const { key, error } = await getOrCreateUserMemoryKey(user);
20994
21474
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
20995
21475
  const body = await c.req.json().catch(() => ({}));
20996
- const r = await memoryCall("createScheduledActionTool", body, key);
20997
- return c.json(r);
21476
+ const hasConnections = Object.prototype.hasOwnProperty.call(body, "connections");
21477
+ let connections;
21478
+ try {
21479
+ connections = sanitizeScheduleConnectionSelections(body.connections);
21480
+ } catch (err) {
21481
+ return scheduleConnectionError(c, err, "Invalid service connections.");
21482
+ }
21483
+ const actionBody = { ...body };
21484
+ delete actionBody.connections;
21485
+ const r = await memoryCall("createScheduledActionTool", actionBody, key);
21486
+ if (!r.ok || !hasConnections) return c.json(r);
21487
+ if (connections.length === 0) return c.json({ ...r, connections: [] });
21488
+ if (!r.id) {
21489
+ console.error("[schedule-actions] create returned no id while service connections were requested");
21490
+ return c.json({ ok: false, error: "The scheduled action was created without a usable id; service connections were not attached." }, 502);
21491
+ }
21492
+ try {
21493
+ const bound = await putScheduleConnectionBindings(user.email, r.id, connections);
21494
+ return c.json({ ...r, connections: bound });
21495
+ } catch (err) {
21496
+ const rollback = await memoryCall("deleteScheduledActionTool", { id: r.id }, key).catch(() => ({ ok: false }));
21497
+ if (!rollback.ok) console.error("[schedule-actions] failed to compensate action after connection binding failure", r.id);
21498
+ const response = scheduleConnectionError(c, err, "The scheduled action was not created because its service connections could not be attached.");
21499
+ response.headers.set("x-schedule-create-rolled-back", rollback.ok ? "true" : "false");
21500
+ return response;
21501
+ }
20998
21502
  });
20999
21503
  app.post("/schedule-actions/propose", auth2, async (c) => {
21000
21504
  const user = c.get("user");
@@ -21004,6 +21508,33 @@ app.post("/schedule-actions/propose", auth2, async (c) => {
21004
21508
  const r = await memoryCall("proposeScheduledActionTool", body, key);
21005
21509
  return c.json(r);
21006
21510
  });
21511
+ app.get("/schedule-actions/:id/connections", auth2, async (c) => {
21512
+ try {
21513
+ const user = c.get("user");
21514
+ const scheduleActionId = c.req.param("id");
21515
+ const bindings = await getScheduleConnectionBindings(user.email, scheduleActionId);
21516
+ return c.json({ ok: true, connections: bindingsForSchedule(bindings, scheduleActionId) });
21517
+ } catch (err) {
21518
+ return scheduleConnectionError(c, err, "Unable to load scheduled action service connections.");
21519
+ }
21520
+ });
21521
+ app.put("/schedule-actions/:id/connections", auth2, async (c) => {
21522
+ const user = c.get("user");
21523
+ const body = await c.req.json().catch(() => ({}));
21524
+ let connections;
21525
+ try {
21526
+ connections = sanitizeScheduleConnectionSelections(body.connections);
21527
+ } catch (err) {
21528
+ return scheduleConnectionError(c, err, "Invalid service connections.");
21529
+ }
21530
+ try {
21531
+ const scheduleActionId = c.req.param("id");
21532
+ const bindings = await putScheduleConnectionBindings(user.email, scheduleActionId, connections);
21533
+ return c.json({ ok: true, connections: bindings });
21534
+ } catch (err) {
21535
+ return scheduleConnectionError(c, err, "Unable to update scheduled action service connections.");
21536
+ }
21537
+ });
21007
21538
  app.post("/schedule-actions/:id/pause", auth2, async (c) => {
21008
21539
  const user = c.get("user");
21009
21540
  const { key, error } = await getOrCreateUserMemoryKey(user);
@@ -21022,8 +21553,16 @@ app.delete("/schedule-actions/:id", auth2, async (c) => {
21022
21553
  const user = c.get("user");
21023
21554
  const { key, error } = await getOrCreateUserMemoryKey(user);
21024
21555
  if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
21025
- const r = await memoryCall("deleteScheduledActionTool", { id: c.req.param("id") }, key);
21026
- return c.json(r);
21556
+ const scheduleActionId = c.req.param("id");
21557
+ const r = await memoryCall("deleteScheduledActionTool", { id: scheduleActionId }, key);
21558
+ if (!r.ok) return c.json(r);
21559
+ try {
21560
+ await deleteScheduleConnectionBindings(user.email, scheduleActionId);
21561
+ return c.json(r);
21562
+ } catch (err) {
21563
+ console.error("[schedule-actions] action deleted but connection binding cleanup failed", scheduleActionId, err instanceof Error ? err.message : String(err));
21564
+ return c.json({ ...r, connectionBindingCleanupPending: true });
21565
+ }
21027
21566
  });
21028
21567
  app.post("/schedule-link/revoke", auth2, async (c) => {
21029
21568
  try {
@@ -21905,7 +22444,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
21905
22444
  }
21906
22445
  const jobId = c.req.param("id");
21907
22446
  const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-NVSZH35Y.js");
21908
- const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
22447
+ const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
21909
22448
  const job = await getExtractJob2(jobId);
21910
22449
  if (!job) return c.json({ error: "job not found" }, 404);
21911
22450
  const stored = await assembleExtractArtifacts(job, {});
@@ -22076,4 +22615,4 @@ app.get("/blog/:slug/", (c) => {
22076
22615
  export {
22077
22616
  app
22078
22617
  };
22079
- //# sourceMappingURL=server-JNY4XPZE.js.map
22618
+ //# sourceMappingURL=server-E34MNLI3.js.map