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.
- package/dist/bin/api-server.cjs +1100 -458
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +1 -1
- 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 +87 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +3 -3
- package/dist/{chunk-YGRZU7IR.js → chunk-FZG427QD.js} +90 -3
- package/dist/chunk-FZG427QD.js.map +1 -0
- package/dist/{chunk-GWRIO6JT.js → chunk-R7EETU7Z.js} +134 -134
- package/dist/chunk-R7EETU7Z.js.map +1 -0
- package/dist/chunk-RD5JOAWS.js +7 -0
- package/dist/chunk-RD5JOAWS.js.map +1 -0
- package/dist/{extract-bundle-COS56ZDO.js → extract-bundle-U4D5LW5W.js} +4 -2
- package/dist/extract-bundle-U4D5LW5W.js.map +1 -0
- package/dist/{server-JNY4XPZE.js → server-E34MNLI3.js} +567 -28
- package/dist/server-E34MNLI3.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-EGWJ74EX.js +0 -7
- package/dist/chunk-EGWJ74EX.js.map +0 -1
- package/dist/chunk-GWRIO6JT.js.map +0 -1
- package/dist/chunk-YGRZU7IR.js.map +0 -1
- package/dist/extract-bundle-COS56ZDO.js.map +0 -1
- package/dist/server-JNY4XPZE.js.map +0 -1
|
@@ -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-
|
|
37
|
+
} from "./chunk-FZG427QD.js";
|
|
37
38
|
import {
|
|
38
39
|
auditImages,
|
|
40
|
+
buildLinkReport,
|
|
41
|
+
computeIssues,
|
|
39
42
|
getBlobStore
|
|
40
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
7523
|
-
const discovered = await
|
|
7524
|
-
const
|
|
7525
|
-
await setExtractJobTotal(jobId,
|
|
7526
|
-
return
|
|
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
|
|
7552
|
-
const
|
|
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-
|
|
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
|
|
20997
|
-
|
|
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
|
|
21026
|
-
|
|
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-
|
|
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-
|
|
22618
|
+
//# sourceMappingURL=server-E34MNLI3.js.map
|