mcp-scraper 0.40.2 → 0.41.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/README.md +2 -2
- package/dist/bin/api-server.cjs +2264 -337
- 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 +180 -92
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +6 -5
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs +31 -9
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +4 -4
- package/dist/{chunk-XORPNO3Z.js → chunk-27DUCRAZ.js} +2 -2
- package/dist/{chunk-44HZLHDV.js → chunk-5UN33CGU.js} +38 -1
- package/dist/chunk-5UN33CGU.js.map +1 -0
- package/dist/{chunk-X2LKCX6H.js → chunk-6J57U6HA.js} +3 -3
- package/dist/chunk-BBI7RGOT.js +99 -0
- package/dist/chunk-BBI7RGOT.js.map +1 -0
- package/dist/{chunk-NVXNEOUQ.js → chunk-CS4HE6QY.js} +47 -11
- package/dist/chunk-CS4HE6QY.js.map +1 -0
- package/dist/{chunk-3PIWJS6Y.js → chunk-EHES33KB.js} +2 -2
- package/dist/{chunk-GUVKHCKE.js → chunk-IQTT7CAB.js} +94 -3
- package/dist/chunk-IQTT7CAB.js.map +1 -0
- package/dist/{chunk-7XBBFBYY.js → chunk-JVIW4GK2.js} +44 -12
- package/dist/chunk-JVIW4GK2.js.map +1 -0
- package/dist/chunk-OOB35KFT.js +7 -0
- package/dist/chunk-OOB35KFT.js.map +1 -0
- package/dist/{chunk-LP6E462I.js → chunk-SIE5LZ2V.js} +74 -106
- package/dist/chunk-SIE5LZ2V.js.map +1 -0
- package/dist/{db-W3CP562I.js → db-LVVU6NOK.js} +16 -2
- package/dist/{extract-bundle-K4PG3RZJ.js → extract-bundle-SKXEDG4Z.js} +4 -4
- package/dist/index.cjs +31 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/{location-data-repository-RLQX6SNM.js → location-data-repository-NBTCOA7B.js} +3 -3
- package/dist/{server-KM3CFWCF.js → server-2JJPCZH4.js} +1812 -162
- package/dist/server-2JJPCZH4.js.map +1 -0
- package/dist/{site-extract-repository-2SMMFKKL.js → site-extract-repository-6GX72U4N.js} +4 -4
- package/dist/{worker-FXAGFYOE.js → worker-5RWLFSAJ.js} +27 -28
- package/dist/worker-5RWLFSAJ.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-44HZLHDV.js.map +0 -1
- package/dist/chunk-7XBBFBYY.js.map +0 -1
- package/dist/chunk-GUVKHCKE.js.map +0 -1
- package/dist/chunk-LP6E462I.js.map +0 -1
- package/dist/chunk-NVXNEOUQ.js.map +0 -1
- package/dist/chunk-ZUJLSICT.js +0 -7
- package/dist/chunk-ZUJLSICT.js.map +0 -1
- package/dist/server-KM3CFWCF.js.map +0 -1
- package/dist/worker-FXAGFYOE.js.map +0 -1
- /package/dist/{chunk-XORPNO3Z.js.map → chunk-27DUCRAZ.js.map} +0 -0
- /package/dist/{chunk-X2LKCX6H.js.map → chunk-6J57U6HA.js.map} +0 -0
- /package/dist/{chunk-3PIWJS6Y.js.map → chunk-EHES33KB.js.map} +0 -0
- /package/dist/{db-W3CP562I.js.map → db-LVVU6NOK.js.map} +0 -0
- /package/dist/{extract-bundle-K4PG3RZJ.js.map → extract-bundle-SKXEDG4Z.js.map} +0 -0
- /package/dist/{location-data-repository-RLQX6SNM.js.map → location-data-repository-NBTCOA7B.js.map} +0 -0
- /package/dist/{site-extract-repository-2SMMFKKL.js.map → site-extract-repository-6GX72U4N.js.map} +0 -0
|
@@ -9,27 +9,38 @@ import {
|
|
|
9
9
|
importHostedCensusPlacesCsv,
|
|
10
10
|
importHostedZipGroupsCsv,
|
|
11
11
|
listActiveHostedLocationDatasets
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-27DUCRAZ.js";
|
|
13
13
|
import {
|
|
14
14
|
classifyHarvestProblem,
|
|
15
15
|
createHarvestAttemptRecorder,
|
|
16
|
+
deliverWebhook,
|
|
16
17
|
harvestProblemResponse,
|
|
17
18
|
serializeHarvestProblem
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-CS4HE6QY.js";
|
|
19
20
|
import {
|
|
21
|
+
AIModeSelectors,
|
|
22
|
+
AIOverviewSelectors,
|
|
20
23
|
BACKUP_LAST_RESORT_ATTEMPTS,
|
|
21
24
|
BrowserDriver,
|
|
25
|
+
ForumSelectors,
|
|
26
|
+
LocalPackSelectors,
|
|
22
27
|
MapsSelectors,
|
|
28
|
+
OrganicSelectors,
|
|
29
|
+
PAASelectors,
|
|
30
|
+
VideoSelectors,
|
|
31
|
+
WhatPeopleSayingSelectors,
|
|
23
32
|
backupProxyAvailable,
|
|
24
33
|
buildYouTubeChannelVideosUrl,
|
|
25
34
|
cleanupBackupProxyId,
|
|
26
35
|
createBackupProxyIdSafe,
|
|
27
36
|
deleteKernelProxyId,
|
|
37
|
+
encodeUule,
|
|
28
38
|
harvest,
|
|
39
|
+
normalizeLocation,
|
|
29
40
|
resolveBackupKernelProxyId,
|
|
30
41
|
resolveKernelProxyId,
|
|
31
42
|
serpArrivalUrl
|
|
32
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-JVIW4GK2.js";
|
|
33
44
|
import {
|
|
34
45
|
CreateEditorialReadingRoomInputSchema,
|
|
35
46
|
HttpMcpToolExecutor,
|
|
@@ -55,10 +66,8 @@ import {
|
|
|
55
66
|
registerSerpIntelligenceCaptureTools,
|
|
56
67
|
renewConnectedDataArtifactDownload,
|
|
57
68
|
renewDirectoryArtifactDownload,
|
|
58
|
-
sanitizeAttempts,
|
|
59
|
-
sanitizeHarvestResult,
|
|
60
69
|
transcribeMediaUrl
|
|
61
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-SIE5LZ2V.js";
|
|
62
71
|
import {
|
|
63
72
|
auditImageUrls,
|
|
64
73
|
auditImages,
|
|
@@ -69,6 +78,11 @@ import {
|
|
|
69
78
|
renewPrivateArtifactDownload,
|
|
70
79
|
validatePublicHttpUrl
|
|
71
80
|
} from "./chunk-345BQXZH.js";
|
|
81
|
+
import {
|
|
82
|
+
sanitizeAttempts,
|
|
83
|
+
sanitizeHarvestResult,
|
|
84
|
+
sanitizeOutboundDiagnostics
|
|
85
|
+
} from "./chunk-BBI7RGOT.js";
|
|
72
86
|
import {
|
|
73
87
|
browserServiceApiKey,
|
|
74
88
|
browserServiceProfileName,
|
|
@@ -78,7 +92,7 @@ import {
|
|
|
78
92
|
recordKernelSession,
|
|
79
93
|
recordVendorUsage,
|
|
80
94
|
runWithCostContext
|
|
81
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-EHES33KB.js";
|
|
82
96
|
import {
|
|
83
97
|
listWorkflowDefinitions,
|
|
84
98
|
runWorkflow,
|
|
@@ -105,7 +119,7 @@ import {
|
|
|
105
119
|
RawMapsOverviewSchema,
|
|
106
120
|
RawMapsReviewStatsSchema
|
|
107
121
|
} from "./chunk-CB5C3BPB.js";
|
|
108
|
-
import "./chunk-
|
|
122
|
+
import "./chunk-OOB35KFT.js";
|
|
109
123
|
import {
|
|
110
124
|
abandonExtractSettlement,
|
|
111
125
|
countSuccessfulPages,
|
|
@@ -128,7 +142,7 @@ import {
|
|
|
128
142
|
setExtractJobTotal,
|
|
129
143
|
settleExtractJob,
|
|
130
144
|
terminalExtractJobStatus
|
|
131
|
-
} from "./chunk-
|
|
145
|
+
} from "./chunk-6J57U6HA.js";
|
|
132
146
|
import {
|
|
133
147
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
134
148
|
CONCURRENCY_PRICE_ID,
|
|
@@ -181,14 +195,18 @@ import {
|
|
|
181
195
|
CaptchaError,
|
|
182
196
|
MapsSearchIncompletePageError,
|
|
183
197
|
RECAPTCHA_INSTRUCTIONS,
|
|
198
|
+
normalizePublicErrorCode,
|
|
199
|
+
publicErrorMessage,
|
|
184
200
|
sanitizeVendorName
|
|
185
|
-
} from "./chunk-
|
|
201
|
+
} from "./chunk-5UN33CGU.js";
|
|
186
202
|
import {
|
|
187
203
|
SiteAuditJobRowSchema,
|
|
188
204
|
advanceWorkflowRunStep,
|
|
189
205
|
appendWorkflowArtifacts,
|
|
190
206
|
cancelJob,
|
|
191
207
|
checkRateLimit,
|
|
208
|
+
checkpointJob,
|
|
209
|
+
claimInngestPaaJob,
|
|
192
210
|
claimStripeEvent,
|
|
193
211
|
completeJob,
|
|
194
212
|
completeStripeEvent,
|
|
@@ -197,6 +215,7 @@ import {
|
|
|
197
215
|
consumePasswordResetToken,
|
|
198
216
|
countActiveUsers,
|
|
199
217
|
createJob,
|
|
218
|
+
createJobWithId,
|
|
200
219
|
createPasswordResetToken,
|
|
201
220
|
createRunningJob,
|
|
202
221
|
createUser,
|
|
@@ -212,6 +231,7 @@ import {
|
|
|
212
231
|
failJob,
|
|
213
232
|
failStripeEvent,
|
|
214
233
|
failWorkflowRunRecord,
|
|
234
|
+
finishHarvestAttempt,
|
|
215
235
|
getBillingLedgerSummary,
|
|
216
236
|
getClient,
|
|
217
237
|
getDb,
|
|
@@ -240,7 +260,10 @@ import {
|
|
|
240
260
|
listHarvestAttempts,
|
|
241
261
|
listInboxMessages,
|
|
242
262
|
listJobs,
|
|
263
|
+
listPendingInngestPaaJobs,
|
|
264
|
+
listRecentTerminalInngestPaaJobs,
|
|
243
265
|
listRequestEvents,
|
|
266
|
+
listStaleRunningInngestPaaJobs,
|
|
244
267
|
listUsers,
|
|
245
268
|
listUsersDueForConnectedAccountBillingReconciliation,
|
|
246
269
|
listWorkflowArtifacts,
|
|
@@ -248,6 +271,7 @@ import {
|
|
|
248
271
|
listWorkflowSchedules,
|
|
249
272
|
logRequestEvent,
|
|
250
273
|
markInboxMessageOpened,
|
|
274
|
+
markJobBillingSettled,
|
|
251
275
|
markWorkflowRunRunning,
|
|
252
276
|
markWorkflowScheduleRan,
|
|
253
277
|
patchWorkflowSchedule,
|
|
@@ -271,9 +295,10 @@ import {
|
|
|
271
295
|
setUserMemoryCreds,
|
|
272
296
|
settleDebitMcIdempotent,
|
|
273
297
|
softDeleteInboxMessage,
|
|
298
|
+
startHarvestAttempt,
|
|
274
299
|
upsertPageSnapshot,
|
|
275
300
|
verifyPassword
|
|
276
|
-
} from "./chunk-
|
|
301
|
+
} from "./chunk-IQTT7CAB.js";
|
|
277
302
|
|
|
278
303
|
// src/blog/registry.ts
|
|
279
304
|
var posts = [
|
|
@@ -3590,6 +3615,7 @@ async function generateOgImage(post) {
|
|
|
3590
3615
|
|
|
3591
3616
|
// src/api/server.ts
|
|
3592
3617
|
import { Resend as Resend3 } from "resend";
|
|
3618
|
+
import { randomUUID as randomUUID18 } from "crypto";
|
|
3593
3619
|
|
|
3594
3620
|
// src/api/kpo-extractor.ts
|
|
3595
3621
|
import TurndownService from "turndown";
|
|
@@ -8618,7 +8644,7 @@ var siteExtractFn = inngest.createFunction(
|
|
|
8618
8644
|
});
|
|
8619
8645
|
}) : null;
|
|
8620
8646
|
const finalized = await step.run("finalize", async () => {
|
|
8621
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
8647
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-SKXEDG4Z.js");
|
|
8622
8648
|
const current = await getExtractJob(jobId2);
|
|
8623
8649
|
if (!current) throw new Error("extract job disappeared before finalization");
|
|
8624
8650
|
const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
|
|
@@ -9560,7 +9586,7 @@ async function resolveUsZipsPath(requestedPath) {
|
|
|
9560
9586
|
async function loadZipGroups(stateAbbr, requestedPath, warnings, sourceMode) {
|
|
9561
9587
|
const mode = resolvedLocationDataMode({ zipSource: sourceMode });
|
|
9562
9588
|
if (mode === "hosted") {
|
|
9563
|
-
const { getHostedZipGroups } = await import("./location-data-repository-
|
|
9589
|
+
const { getHostedZipGroups } = await import("./location-data-repository-NBTCOA7B.js");
|
|
9564
9590
|
const hosted = await getHostedZipGroups(stateAbbr);
|
|
9565
9591
|
if (!hosted.dataset) {
|
|
9566
9592
|
throw new DirectoryLocationError(
|
|
@@ -9623,7 +9649,7 @@ async function resolveDirectoryMarkets(options, config = {}) {
|
|
|
9623
9649
|
const sourceUrl = censusStateUrl(state.fips);
|
|
9624
9650
|
const warnings = [];
|
|
9625
9651
|
if (resolvedLocationDataMode(config) === "hosted") {
|
|
9626
|
-
const { queryHostedLocationMarkets } = await import("./location-data-repository-
|
|
9652
|
+
const { queryHostedLocationMarkets } = await import("./location-data-repository-NBTCOA7B.js");
|
|
9627
9653
|
const hosted = await queryHostedLocationMarkets({
|
|
9628
9654
|
state: state.abbr,
|
|
9629
9655
|
minPopulation: options.minPopulation,
|
|
@@ -10957,6 +10983,908 @@ var directoryWorkflowFn = inngest.createFunction(
|
|
|
10957
10983
|
}
|
|
10958
10984
|
);
|
|
10959
10985
|
|
|
10986
|
+
// src/api/paa-capture-artifacts.ts
|
|
10987
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
10988
|
+
var PAA_CAPTURE_ARTIFACT_PREFIX = "paa-captures";
|
|
10989
|
+
function paaCaptureArtifactPolicy() {
|
|
10990
|
+
return {
|
|
10991
|
+
prefix: PAA_CAPTURE_ARTIFACT_PREFIX,
|
|
10992
|
+
artifactTtlMs: 7 * DAY_MS,
|
|
10993
|
+
downloadTtlMs: 15 * 60 * 1e3,
|
|
10994
|
+
token: process.env.PAA_CAPTURE_BLOB_READ_WRITE_TOKEN ?? process.env.BLOB_READ_WRITE_TOKEN ?? null
|
|
10995
|
+
};
|
|
10996
|
+
}
|
|
10997
|
+
async function createPaaRawDomArtifact(input) {
|
|
10998
|
+
return createPrivateArtifact({
|
|
10999
|
+
policy: paaCaptureArtifactPolicy(),
|
|
11000
|
+
ownerId: String(input.userId),
|
|
11001
|
+
artifactKey: `${input.jobId}-attempt-${input.attempt}.html.gz`,
|
|
11002
|
+
createdAt: input.createdAt ?? /* @__PURE__ */ new Date(),
|
|
11003
|
+
filename: `${input.jobId}-serp.html.gz`,
|
|
11004
|
+
contentType: "application/gzip",
|
|
11005
|
+
content: input.gzip
|
|
11006
|
+
});
|
|
11007
|
+
}
|
|
11008
|
+
async function tryCreatePaaRawDomArtifact(input) {
|
|
11009
|
+
try {
|
|
11010
|
+
return {
|
|
11011
|
+
artifact: await createPaaRawDomArtifact(input),
|
|
11012
|
+
status: "stored"
|
|
11013
|
+
};
|
|
11014
|
+
} catch (error) {
|
|
11015
|
+
console.error("[paa-capture-artifact] private DOM preservation unavailable:", error instanceof Error ? error.message : String(error));
|
|
11016
|
+
return { artifact: null, status: "unavailable" };
|
|
11017
|
+
}
|
|
11018
|
+
}
|
|
11019
|
+
|
|
11020
|
+
// src/api/paa-harvest-settlement.ts
|
|
11021
|
+
function capturedQuestionCount(result) {
|
|
11022
|
+
if (!result || typeof result !== "object") return 0;
|
|
11023
|
+
const value = result;
|
|
11024
|
+
if (typeof value.totalQuestions === "number") return value.totalQuestions;
|
|
11025
|
+
return Array.isArray(value.progress?.records) ? value.progress.records.length : 0;
|
|
11026
|
+
}
|
|
11027
|
+
function finalCost(result, heldMc) {
|
|
11028
|
+
const questions = capturedQuestionCount(result);
|
|
11029
|
+
if (questions <= 0) return 0;
|
|
11030
|
+
return Math.min(heldMc, MC_COSTS.paa_base + questions * MC_COSTS.paa);
|
|
11031
|
+
}
|
|
11032
|
+
async function settlePaaHarvestJob(jobOrId) {
|
|
11033
|
+
const job = typeof jobOrId === "string" ? await getJob(jobOrId) : jobOrId;
|
|
11034
|
+
if (!job) return false;
|
|
11035
|
+
const options = job.options;
|
|
11036
|
+
const debitKey2 = options.billingDebitKey;
|
|
11037
|
+
const heldMc = Number(options.billingHoldMc ?? 0);
|
|
11038
|
+
if (!debitKey2 || !Number.isSafeInteger(heldMc) || heldMc <= 0) return false;
|
|
11039
|
+
const settlement = await settleDebitMcIdempotent(
|
|
11040
|
+
job.user_id,
|
|
11041
|
+
debitKey2,
|
|
11042
|
+
finalCost(job.result, heldMc),
|
|
11043
|
+
LedgerOperation.PAA_REFUND,
|
|
11044
|
+
"durable PAA harvest settlement",
|
|
11045
|
+
"paa_harvest"
|
|
11046
|
+
);
|
|
11047
|
+
await markJobBillingSettled(job.id, settlement.final_amount_mc);
|
|
11048
|
+
return true;
|
|
11049
|
+
}
|
|
11050
|
+
|
|
11051
|
+
// src/paa/durable-capture.ts
|
|
11052
|
+
import Kernel4 from "@onkernel/sdk";
|
|
11053
|
+
import { createHash as createHash2 } from "crypto";
|
|
11054
|
+
var PAA_INVOCATION_BUDGET_MS = 28e4;
|
|
11055
|
+
var PAA_BROWSER_WORK_BUDGET_MS = 25e4;
|
|
11056
|
+
var PAA_SOLVER_BUDGET_MS = 6e4;
|
|
11057
|
+
function remainingSeconds(deadlineMs, ceiling) {
|
|
11058
|
+
return Math.max(1, Math.min(ceiling, Math.floor((deadlineMs - Date.now()) / 1e3)));
|
|
11059
|
+
}
|
|
11060
|
+
async function execute(kernel, sessionId, code, deadlineMs, ceilingSeconds) {
|
|
11061
|
+
if (Date.now() >= deadlineMs) throw new Error("paa_work_deadline_exhausted");
|
|
11062
|
+
const response = await kernel.browsers.playwright.execute(sessionId, {
|
|
11063
|
+
code,
|
|
11064
|
+
timeout_sec: remainingSeconds(deadlineMs, ceilingSeconds)
|
|
11065
|
+
});
|
|
11066
|
+
if (!response.success || response.result === null || response.result === void 0) {
|
|
11067
|
+
throw new Error(response.error || response.stderr || "co-located browser execution failed");
|
|
11068
|
+
}
|
|
11069
|
+
return response.result;
|
|
11070
|
+
}
|
|
11071
|
+
function navigationCode(searchUrl) {
|
|
11072
|
+
return `
|
|
11073
|
+
const wait = ms => page.waitForTimeout(ms);
|
|
11074
|
+
const block = async () => {
|
|
11075
|
+
if (/google\\.[^/]+\\/sorry\\//i.test(page.url())) return 'captcha';
|
|
11076
|
+
if (await page.locator(${JSON.stringify(PAASelectors.captchaMarker)}).count().catch(() => 0)) return 'captcha';
|
|
11077
|
+
const title = await page.title().catch(() => '');
|
|
11078
|
+
const text = await page.locator('body').innerText({ timeout: 2000 }).catch(() => '');
|
|
11079
|
+
if (/recaptcha|unusual traffic|are you a robot|about this page|detected unusual traffic/i.test(text)) return 'captcha';
|
|
11080
|
+
if (/\\b403\\b.{0,120}forbidden|access denied|permission to access/i.test(title + '\\n' + text.slice(0, 3000))) return 'soft_block';
|
|
11081
|
+
return null;
|
|
11082
|
+
};
|
|
11083
|
+
await page.goto(${JSON.stringify(searchUrl)}, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
11084
|
+
let captchaObserved = false;
|
|
11085
|
+
let solverCleared = false;
|
|
11086
|
+
let blocked = await block();
|
|
11087
|
+
const solverDeadline = Date.now() + ${PAA_SOLVER_BUDGET_MS};
|
|
11088
|
+
while (blocked === 'captcha' && Date.now() < solverDeadline) {
|
|
11089
|
+
captchaObserved = true;
|
|
11090
|
+
await wait(4000 + Math.floor(Math.random() * 1500));
|
|
11091
|
+
blocked = await block();
|
|
11092
|
+
if (!blocked) solverCleared = true;
|
|
11093
|
+
}
|
|
11094
|
+
let count = await page.locator(${JSON.stringify(PAASelectors.item)}).count().catch(() => 0);
|
|
11095
|
+
for (let scan = 0; !blocked && count === 0 && scan < 7; scan += 1) {
|
|
11096
|
+
await page.mouse.wheel(0, 520 + Math.floor(Math.random() * 420));
|
|
11097
|
+
await wait(350 + Math.floor(Math.random() * 500));
|
|
11098
|
+
count = await page.locator(${JSON.stringify(PAASelectors.item)}).count().catch(() => 0);
|
|
11099
|
+
}
|
|
11100
|
+
await page.evaluate((itemSelector) => {
|
|
11101
|
+
const state = window;
|
|
11102
|
+
if (state.__mcpTrustedListenerInstalled) return;
|
|
11103
|
+
document.addEventListener('click', event => {
|
|
11104
|
+
if (event.target instanceof Element && event.target.closest(itemSelector)) {
|
|
11105
|
+
state.__mcpLastPaaClickTrusted = event.isTrusted;
|
|
11106
|
+
}
|
|
11107
|
+
}, { capture: true });
|
|
11108
|
+
state.__mcpTrustedListenerInstalled = true;
|
|
11109
|
+
}, ${JSON.stringify(PAASelectors.item)}).catch(() => {});
|
|
11110
|
+
return {
|
|
11111
|
+
outcome: blocked || (count > 0 ? 'ready' : 'no_paa'),
|
|
11112
|
+
finalUrl: page.url(),
|
|
11113
|
+
title: await page.title().catch(() => ''),
|
|
11114
|
+
captchaObserved,
|
|
11115
|
+
solverCleared,
|
|
11116
|
+
environment: await page.evaluate(() => ({
|
|
11117
|
+
userAgent: navigator.userAgent,
|
|
11118
|
+
language: navigator.language,
|
|
11119
|
+
platform: navigator.platform,
|
|
11120
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
11121
|
+
innerWidth: window.innerWidth,
|
|
11122
|
+
innerHeight: window.innerHeight,
|
|
11123
|
+
outerWidth: window.outerWidth,
|
|
11124
|
+
outerHeight: window.outerHeight,
|
|
11125
|
+
screenWidth: window.screen.width,
|
|
11126
|
+
screenHeight: window.screen.height,
|
|
11127
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
11128
|
+
})).catch(() => null),
|
|
11129
|
+
};
|
|
11130
|
+
`;
|
|
11131
|
+
}
|
|
11132
|
+
function recordReaderSource() {
|
|
11133
|
+
return `
|
|
11134
|
+
const clean = value => (value || '').replace(/\\u00a0/g, ' ').replace(/[ \\t]+/g, ' ').replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
11135
|
+
const externalHref = anchor => {
|
|
11136
|
+
const href = anchor && anchor.href || '';
|
|
11137
|
+
if (!/^https?:\\/\\//i.test(href)) return '';
|
|
11138
|
+
try {
|
|
11139
|
+
const url = new URL(href);
|
|
11140
|
+
if (/(^|\\.)google(usercontent)?\\.[a-z.]+$/i.test(url.hostname)) {
|
|
11141
|
+
const redirected = url.searchParams.get('q') || url.searchParams.get('url');
|
|
11142
|
+
return redirected && redirected.startsWith('http') ? redirected : '';
|
|
11143
|
+
}
|
|
11144
|
+
} catch {}
|
|
11145
|
+
return href;
|
|
11146
|
+
};
|
|
11147
|
+
const sourceLabel = value => clean(value)
|
|
11148
|
+
.replace(/\\s*\\(\\+\\d+\\)\\s*-\\s*View related links.*$/i, '')
|
|
11149
|
+
.replace(/\\s*-\\s*Opens in new tab.*$/i, '').trim();
|
|
11150
|
+
const readRecords = () => Array.from(document.querySelectorAll(SEL.item)).map(item => {
|
|
11151
|
+
const question = clean(item.getAttribute(SEL.itemDataQ)
|
|
11152
|
+
|| item.getAttribute(SEL.itemDataInitQ)
|
|
11153
|
+
|| item.querySelector(SEL.itemQuestionEl)?.innerText);
|
|
11154
|
+
const expanded = item.classList.contains(SEL.expandedClass)
|
|
11155
|
+
|| item.querySelector(SEL.clickTarget)?.getAttribute('aria-expanded') === 'true';
|
|
11156
|
+
const container = expanded ? item.querySelector(SEL.answerContainer) : null;
|
|
11157
|
+
const answerElement = container?.querySelector(SEL.aiAnswerBody) || container;
|
|
11158
|
+
const anchors = expanded ? Array.from(answerElement?.querySelectorAll(
|
|
11159
|
+
SEL.aiCitation + ',' + SEL.aiSourceCard + ',a[href]'
|
|
11160
|
+
) || []) : [];
|
|
11161
|
+
const byUrl = new Map();
|
|
11162
|
+
for (const anchor of anchors) {
|
|
11163
|
+
const url = externalHref(anchor);
|
|
11164
|
+
if (!url || byUrl.has(url)) continue;
|
|
11165
|
+
let site = '';
|
|
11166
|
+
try { site = new URL(url).hostname.replace(/^www\\./, ''); } catch {}
|
|
11167
|
+
byUrl.set(url, { title: sourceLabel(anchor.getAttribute('aria-label')) || clean(anchor.textContent), site, url });
|
|
11168
|
+
}
|
|
11169
|
+
const sources = Array.from(byUrl.values());
|
|
11170
|
+
let sourceTitle = expanded ? clean(item.querySelector(SEL.sourceTitle)?.textContent) : '';
|
|
11171
|
+
let sourceSite = expanded ? clean(item.querySelector(SEL.sourceSite)?.textContent) : '';
|
|
11172
|
+
let sourceCite = expanded ? externalHref(item.querySelector(SEL.sourceCite)) : '';
|
|
11173
|
+
const primary = sources.find(source => source.title.length > 2) || sources[0];
|
|
11174
|
+
if (!sourceCite && primary) {
|
|
11175
|
+
sourceTitle ||= primary.title;
|
|
11176
|
+
sourceSite ||= primary.site;
|
|
11177
|
+
sourceCite = primary.url;
|
|
11178
|
+
}
|
|
11179
|
+
return {
|
|
11180
|
+
question,
|
|
11181
|
+
answer: clean(answerElement?.innerText || answerElement?.textContent),
|
|
11182
|
+
sourceTitle,
|
|
11183
|
+
sourceSite,
|
|
11184
|
+
sourceCite,
|
|
11185
|
+
sources,
|
|
11186
|
+
depth: 1,
|
|
11187
|
+
parentQuestion: null,
|
|
11188
|
+
};
|
|
11189
|
+
}).filter(record => record.question);
|
|
11190
|
+
`;
|
|
11191
|
+
}
|
|
11192
|
+
function expansionCode(clickedQuestions) {
|
|
11193
|
+
return `
|
|
11194
|
+
const SEL = ${JSON.stringify(PAASelectors)};
|
|
11195
|
+
${recordReaderSource()}
|
|
11196
|
+
const deliveryBlock = async () => {
|
|
11197
|
+
if (/google\\.[^/]+\\/sorry\\//i.test(page.url())) return 'captcha';
|
|
11198
|
+
if (await page.locator(SEL.captchaMarker).count().catch(() => 0)) return 'captcha';
|
|
11199
|
+
const text = await page.locator('body').innerText({ timeout: 2000 }).catch(() => '');
|
|
11200
|
+
if (/recaptcha|unusual traffic|are you a robot|about this page/i.test(text)) return 'captcha';
|
|
11201
|
+
if (/\\b403\\b.{0,120}forbidden|access denied|permission to access/i.test(text.slice(0, 3000))) return 'soft_block';
|
|
11202
|
+
return null;
|
|
11203
|
+
};
|
|
11204
|
+
const blocked = await deliveryBlock();
|
|
11205
|
+
const records = await page.evaluate(({ SEL }) => {
|
|
11206
|
+
${recordReaderSource()}
|
|
11207
|
+
return readRecords();
|
|
11208
|
+
}, { SEL });
|
|
11209
|
+
if (blocked) return {
|
|
11210
|
+
outcome: blocked, targetQuestion: null, records, newQuestions: [],
|
|
11211
|
+
finalUrl: page.url(), title: await page.title().catch(() => ''),
|
|
11212
|
+
interactionTrusted: null, clickError: null,
|
|
11213
|
+
};
|
|
11214
|
+
const clicked = new Set(${JSON.stringify(clickedQuestions)});
|
|
11215
|
+
const targetQuestion = records.map(record => record.question).find(question => !clicked.has(question)) || null;
|
|
11216
|
+
if (!targetQuestion) return {
|
|
11217
|
+
outcome: 'exhausted', targetQuestion: null, records, newQuestions: [],
|
|
11218
|
+
finalUrl: page.url(), title: await page.title().catch(() => ''),
|
|
11219
|
+
interactionTrusted: null, clickError: null,
|
|
11220
|
+
};
|
|
11221
|
+
const items = page.locator(SEL.item);
|
|
11222
|
+
let targetIndex = -1;
|
|
11223
|
+
for (let index = 0, count = await items.count().catch(() => 0); index < count; index += 1) {
|
|
11224
|
+
const question = await items.nth(index).evaluate((item, SEL) =>
|
|
11225
|
+
item.getAttribute(SEL.itemDataQ)
|
|
11226
|
+
|| item.getAttribute(SEL.itemDataInitQ)
|
|
11227
|
+
|| item.querySelector(SEL.itemQuestionEl)?.innerText?.trim()
|
|
11228
|
+
|| '', SEL).catch(() => '');
|
|
11229
|
+
if (question.replace(/\\s+/g, ' ').trim() === targetQuestion) { targetIndex = index; break; }
|
|
11230
|
+
}
|
|
11231
|
+
if (targetIndex < 0) return {
|
|
11232
|
+
outcome: 'exhausted', targetQuestion, records, newQuestions: [],
|
|
11233
|
+
finalUrl: page.url(), title: await page.title().catch(() => ''),
|
|
11234
|
+
interactionTrusted: null, clickError: 'question disappeared before click',
|
|
11235
|
+
};
|
|
11236
|
+
const beforeQuestions = new Set(records.map(record => record.question));
|
|
11237
|
+
const item = items.nth(targetIndex);
|
|
11238
|
+
await item.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
|
|
11239
|
+
await page.waitForTimeout(180 + Math.floor(Math.random() * 420));
|
|
11240
|
+
await page.evaluate(() => { window.__mcpLastPaaClickTrusted = null; }).catch(() => {});
|
|
11241
|
+
let clickError = null;
|
|
11242
|
+
try {
|
|
11243
|
+
await item.locator(SEL.clickTarget).first().click({
|
|
11244
|
+
timeout: 6000,
|
|
11245
|
+
delay: 55 + Math.floor(Math.random() * 120),
|
|
11246
|
+
});
|
|
11247
|
+
} catch (error) {
|
|
11248
|
+
clickError = String(error && error.message || error).slice(0, 240);
|
|
11249
|
+
}
|
|
11250
|
+
const interactionTrusted = await page.evaluate(() => window.__mcpLastPaaClickTrusted ?? null).catch(() => null);
|
|
11251
|
+
await page.waitForTimeout(650 + Math.floor(Math.random() * 750));
|
|
11252
|
+
const afterRecords = await page.evaluate(({ SEL }) => {
|
|
11253
|
+
${recordReaderSource()}
|
|
11254
|
+
return readRecords();
|
|
11255
|
+
}, { SEL });
|
|
11256
|
+
return {
|
|
11257
|
+
outcome: clickError ? 'exhausted' : 'clicked',
|
|
11258
|
+
targetQuestion,
|
|
11259
|
+
records: afterRecords,
|
|
11260
|
+
newQuestions: afterRecords.map(record => record.question).filter(question => !beforeQuestions.has(question)),
|
|
11261
|
+
finalUrl: page.url(),
|
|
11262
|
+
title: await page.title().catch(() => ''),
|
|
11263
|
+
interactionTrusted,
|
|
11264
|
+
clickError,
|
|
11265
|
+
};
|
|
11266
|
+
`;
|
|
11267
|
+
}
|
|
11268
|
+
function finalCaptureCode(maxQuestions, parentByQuestion) {
|
|
11269
|
+
const surfaces = {
|
|
11270
|
+
aiOverview: AIOverviewSelectors,
|
|
11271
|
+
aiMode: AIModeSelectors,
|
|
11272
|
+
organic: OrganicSelectors,
|
|
11273
|
+
localPack: LocalPackSelectors,
|
|
11274
|
+
video: VideoSelectors,
|
|
11275
|
+
forum: ForumSelectors,
|
|
11276
|
+
whatPeopleSaying: WhatPeopleSayingSelectors
|
|
11277
|
+
};
|
|
11278
|
+
return `
|
|
11279
|
+
const SEL = ${JSON.stringify(PAASelectors)};
|
|
11280
|
+
const SURF = ${JSON.stringify(surfaces)};
|
|
11281
|
+
const PARENTS = ${JSON.stringify(parentByQuestion)};
|
|
11282
|
+
${recordReaderSource()}
|
|
11283
|
+
const capture = await page.evaluate(({ SEL, SURF, PARENTS, maxQuestions }) => {
|
|
11284
|
+
${recordReaderSource()}
|
|
11285
|
+
const unique = (items, key) => {
|
|
11286
|
+
const seen = new Set();
|
|
11287
|
+
return items.filter(item => { const value = key(item); if (!value || seen.has(value)) return false; seen.add(value); return true; });
|
|
11288
|
+
};
|
|
11289
|
+
const paa = readRecords().map(record => {
|
|
11290
|
+
const parentQuestion = PARENTS[record.question] || null;
|
|
11291
|
+
let depth = 1, parent = parentQuestion;
|
|
11292
|
+
const seen = new Set();
|
|
11293
|
+
while (parent && !seen.has(parent)) { seen.add(parent); depth += 1; parent = PARENTS[parent] || null; }
|
|
11294
|
+
return { ...record, depth, parentQuestion };
|
|
11295
|
+
}).slice(0, maxQuestions);
|
|
11296
|
+
const organicResults = [];
|
|
11297
|
+
document.querySelectorAll(SURF.organic.result).forEach(card => {
|
|
11298
|
+
const titleElement = card.querySelector(SURF.organic.title);
|
|
11299
|
+
const title = clean(titleElement?.textContent);
|
|
11300
|
+
const link = titleElement?.closest('a');
|
|
11301
|
+
const url = externalHref(link);
|
|
11302
|
+
if (!title || !url) return;
|
|
11303
|
+
let domain = '';
|
|
11304
|
+
try { domain = new URL(url).hostname.replace(/^www\\./, ''); } catch {}
|
|
11305
|
+
const rating = card.querySelector(SURF.organic.ratingWrap);
|
|
11306
|
+
organicResults.push({
|
|
11307
|
+
position: organicResults.length + 1, title, url, domain,
|
|
11308
|
+
cite: clean(card.querySelector(SURF.organic.cite)?.textContent) || null,
|
|
11309
|
+
snippet: clean(card.querySelector(SURF.organic.snippet)?.textContent) || null,
|
|
11310
|
+
isRedditStyle: Boolean(card.querySelector(SURF.organic.redditCite)),
|
|
11311
|
+
inlineRating: rating ? {
|
|
11312
|
+
value: clean(rating.querySelector(SURF.organic.ratingValue)?.textContent),
|
|
11313
|
+
count: clean(rating.querySelector(SURF.organic.reviewCount)?.textContent),
|
|
11314
|
+
} : null,
|
|
11315
|
+
});
|
|
11316
|
+
});
|
|
11317
|
+
const localPack = [];
|
|
11318
|
+
document.querySelectorAll(SURF.localPack.card).forEach(card => {
|
|
11319
|
+
const name = clean(card.querySelector(SURF.localPack.name)?.textContent);
|
|
11320
|
+
if (!name) return;
|
|
11321
|
+
const links = Array.from(card.querySelectorAll('a[href]'));
|
|
11322
|
+
let cid = card.querySelector('a[data-cid]')?.getAttribute('data-cid') || null;
|
|
11323
|
+
for (const link of links) {
|
|
11324
|
+
if (cid) break;
|
|
11325
|
+
const match = link.href.match(/[?&]cid=(\\d+)/);
|
|
11326
|
+
if (match) cid = match[1];
|
|
11327
|
+
}
|
|
11328
|
+
localPack.push({
|
|
11329
|
+
position: localPack.length + 1, name, cid,
|
|
11330
|
+
rating: clean(card.querySelector(SURF.localPack.ratingValue)?.textContent) || null,
|
|
11331
|
+
reviewCount: clean(card.querySelector(SURF.localPack.reviewCount)?.textContent).replace(/[()]/g, '') || null,
|
|
11332
|
+
metadata: [], websiteUrl: links.map(externalHref).find(Boolean) || null,
|
|
11333
|
+
directionsUrl: links.find(link => link.href.includes('google.com/maps'))?.href || null,
|
|
11334
|
+
});
|
|
11335
|
+
});
|
|
11336
|
+
const videos = unique(Array.from(document.querySelectorAll(SURF.video.item)).map(link => {
|
|
11337
|
+
const url = externalHref(link);
|
|
11338
|
+
const raw = clean(link.textContent);
|
|
11339
|
+
const platform = /youtu/i.test(url) ? 'YouTube' : /tiktok/i.test(url) ? 'TikTok' : /instagram/i.test(url) ? 'Instagram' : /facebook/i.test(url) ? 'Facebook' : '';
|
|
11340
|
+
return { type: 'video', title: raw, channel: '', platform, duration: raw.match(/\\b\\d{1,2}:\\d{2}\\b/)?.[0] || '', url };
|
|
11341
|
+
}).filter(item => item.title && item.url), item => item.url);
|
|
11342
|
+
const forums = unique(Array.from(document.querySelectorAll(SURF.forum.item)).map(link => ({
|
|
11343
|
+
title: clean(link.querySelector(SURF.forum.title)?.textContent),
|
|
11344
|
+
source: clean(link.querySelector(SURF.forum.source)?.textContent),
|
|
11345
|
+
url: externalHref(link),
|
|
11346
|
+
})).filter(item => item.title && item.url), item => item.url);
|
|
11347
|
+
const socialCards = Array.from(document.querySelectorAll(SURF.whatPeopleSaying.card));
|
|
11348
|
+
const whatPeopleSaying = unique(socialCards.map(card => {
|
|
11349
|
+
const link = card.querySelector(SURF.whatPeopleSaying.cardLink);
|
|
11350
|
+
const url = externalHref(link);
|
|
11351
|
+
const title = clean(card.querySelector(SURF.whatPeopleSaying.titleH1)?.textContent || card.querySelector(SURF.whatPeopleSaying.titleDiv)?.textContent);
|
|
11352
|
+
const source = clean(card.querySelector(SURF.whatPeopleSaying.ytChannel)?.textContent || card.querySelector(SURF.whatPeopleSaying.source)?.textContent);
|
|
11353
|
+
const platform = clean(card.querySelector(SURF.whatPeopleSaying.platformBadge)?.textContent) || (/youtube/i.test(url) ? 'YouTube' : '');
|
|
11354
|
+
const identity = (platform + source + url).toLowerCase();
|
|
11355
|
+
const type = identity.includes('reddit') ? 'reddit' : identity.includes('facebook') ? 'facebook' : identity.includes('instagram') ? 'instagram' : identity.includes('tiktok') ? 'tiktok' : identity.includes('youtube') ? 'youtube' : 'unknown';
|
|
11356
|
+
return { type, title, url, source, platform, popularComment: null, engagement: '', date: '', duration: null, authorNote: null };
|
|
11357
|
+
}).filter(item => item.title || item.url), item => item.url || item.type + ':' + item.title);
|
|
11358
|
+
const aioRoot = document.querySelector(SURF.aiOverview.root) || document.querySelector(SURF.aiOverview.legacyRoot);
|
|
11359
|
+
const aimRoot = document.querySelector(SURF.aiMode.root);
|
|
11360
|
+
const citations = root => unique(Array.from(root?.querySelectorAll('a[href]') || []).map(link => ({
|
|
11361
|
+
text: clean(link.textContent || link.getAttribute('aria-label')), href: externalHref(link),
|
|
11362
|
+
})).filter(item => item.href), item => item.href);
|
|
11363
|
+
const aiOverview = { detected: Boolean(aioRoot), text: aioRoot ? clean(aioRoot.innerText || aioRoot.textContent) : null, citations: citations(aioRoot), expanded: true, fullyExpanded: true, sections: [] };
|
|
11364
|
+
const aiMode = { detected: Boolean(aimRoot), text: aimRoot ? clean(aimRoot.innerText || aimRoot.textContent) : null, citations: citations(aimRoot) };
|
|
11365
|
+
const html = document.documentElement.outerHTML;
|
|
11366
|
+
const kgIds = unique(Array.from(html.matchAll(/\\/g\\/[a-zA-Z0-9_-]{5,20}/g)).map(match => match[0]), value => value);
|
|
11367
|
+
const cids = unique([
|
|
11368
|
+
...localPack.map(item => item.cid).filter(Boolean),
|
|
11369
|
+
...Array.from(html.matchAll(/[?&]cid=(\\d+)/g)).map(match => match[1]),
|
|
11370
|
+
], value => value);
|
|
11371
|
+
const gcids = unique(Array.from(html.matchAll(/gcid:[a-zA-Z0-9_]+/g)).map(match => match[0]), value => value);
|
|
11372
|
+
const entities = localPack.filter(item => item.cid).map(item => ({ name: item.name, kgId: null, cid: item.cid, gcid: null }));
|
|
11373
|
+
const headings = unique(Array.from(document.querySelectorAll('h1,h2,h3,[role="heading"]')).map(node => clean(node.textContent)).filter(text => text.length > 1 && text.length < 160), value => value);
|
|
11374
|
+
const relatedSearches = unique(Array.from(document.querySelectorAll('a[href*="/search?"]')).map(link => ({
|
|
11375
|
+
text: clean(link.textContent), url: link.href,
|
|
11376
|
+
})).filter(item => item.text && /related/i.test(item.url + ' ' + item.text)), item => item.url);
|
|
11377
|
+
const counts = {
|
|
11378
|
+
peopleAlsoAsk: paa.length, organic: organicResults.length, localPack: localPack.length,
|
|
11379
|
+
videos: videos.length, discussions: forums.length, whatPeopleSaying: whatPeopleSaying.length,
|
|
11380
|
+
aiOverview: aiOverview.detected ? 1 : 0, aiMode: aiMode.detected ? 1 : 0,
|
|
11381
|
+
relatedSearches: relatedSearches.length,
|
|
11382
|
+
};
|
|
11383
|
+
return {
|
|
11384
|
+
paa, organicResults: unique(organicResults, item => item.url), localPack: unique(localPack, item => item.cid || item.name),
|
|
11385
|
+
videos, forums, whatPeopleSaying, aiOverview, aiMode,
|
|
11386
|
+
surface: window.google?.sn === 'aim' ? 'aim' : window.google?.sn === 'web' ? 'web' : 'unknown',
|
|
11387
|
+
entityIds: { entities, kgIds, cids, gcids },
|
|
11388
|
+
serpFeatures: { sectionHeadings: headings, detected: Object.fromEntries(Object.entries(counts).map(([key, count]) => [key, count > 0])), counts },
|
|
11389
|
+
relatedSearches,
|
|
11390
|
+
};
|
|
11391
|
+
}, { SEL, SURF, PARENTS, maxQuestions: ${maxQuestions} });
|
|
11392
|
+
return capture;
|
|
11393
|
+
`;
|
|
11394
|
+
}
|
|
11395
|
+
var RAW_PREPARE_CODE = `
|
|
11396
|
+
const result = await page.evaluate(async () => {
|
|
11397
|
+
const source = new TextEncoder().encode(document.documentElement.outerHTML);
|
|
11398
|
+
const compressed = typeof CompressionStream === 'undefined'
|
|
11399
|
+
? source
|
|
11400
|
+
: new Uint8Array(await new Response(new Blob([source]).stream().pipeThrough(new CompressionStream('gzip'))).arrayBuffer());
|
|
11401
|
+
const pieces = [];
|
|
11402
|
+
for (let offset = 0; offset < compressed.length; offset += 32768) {
|
|
11403
|
+
pieces.push(String.fromCharCode(...compressed.subarray(offset, offset + 32768)));
|
|
11404
|
+
}
|
|
11405
|
+
window.__mcpPaaRawDom = btoa(pieces.join(''));
|
|
11406
|
+
return { encoding: typeof CompressionStream === 'undefined' ? 'plain' : 'gzip', chars: window.__mcpPaaRawDom.length };
|
|
11407
|
+
});
|
|
11408
|
+
return result;
|
|
11409
|
+
`;
|
|
11410
|
+
function rawChunkCode(offset, length) {
|
|
11411
|
+
return `return await page.evaluate(({ offset, length }) => ({ offset, chunk: window.__mcpPaaRawDom.slice(offset, offset + length) }), { offset: ${offset}, length: ${length} });`;
|
|
11412
|
+
}
|
|
11413
|
+
function buildPublicResult(input) {
|
|
11414
|
+
const extractedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11415
|
+
const selected = input.records;
|
|
11416
|
+
const flat = selected.map((record) => ({
|
|
11417
|
+
seed_query: input.query,
|
|
11418
|
+
question: record.question,
|
|
11419
|
+
answer: record.answer,
|
|
11420
|
+
source_title: record.sourceTitle,
|
|
11421
|
+
source_site: record.sourceSite,
|
|
11422
|
+
source_cite: record.sourceCite,
|
|
11423
|
+
depth: record.depth,
|
|
11424
|
+
parent_question: record.parentQuestion ?? "",
|
|
11425
|
+
extracted_at: extractedAt
|
|
11426
|
+
}));
|
|
11427
|
+
const tree = selected.map((record) => ({
|
|
11428
|
+
question: record.question,
|
|
11429
|
+
answer: record.answer || null,
|
|
11430
|
+
sourceTitle: record.sourceTitle || null,
|
|
11431
|
+
sourceSite: record.sourceSite || null,
|
|
11432
|
+
sourceCite: record.sourceCite || null,
|
|
11433
|
+
depth: record.depth,
|
|
11434
|
+
parentQuestion: record.parentQuestion,
|
|
11435
|
+
children: []
|
|
11436
|
+
}));
|
|
11437
|
+
return {
|
|
11438
|
+
seed: input.query,
|
|
11439
|
+
location: input.location,
|
|
11440
|
+
extractedAt,
|
|
11441
|
+
diagnostics: {
|
|
11442
|
+
completionStatus: selected.length > 0 ? "paa_found" : "no_paa",
|
|
11443
|
+
problem: null,
|
|
11444
|
+
resultQuality: input.partial ? "partial" : "complete",
|
|
11445
|
+
degradedResult: input.partial,
|
|
11446
|
+
retryRecommended: false,
|
|
11447
|
+
completeness: {
|
|
11448
|
+
paaWithoutAnswer: selected.filter((record) => !record.answer).length,
|
|
11449
|
+
paaWithoutSource: selected.filter((record) => !record.sourceCite).length,
|
|
11450
|
+
paaAnswersRecovered: 0,
|
|
11451
|
+
aioShareCaptured: null
|
|
11452
|
+
}
|
|
11453
|
+
},
|
|
11454
|
+
totalQuestions: selected.length,
|
|
11455
|
+
surface: input.capture.surface,
|
|
11456
|
+
aiOverview: input.capture.aiOverview,
|
|
11457
|
+
aiMode: input.capture.aiMode,
|
|
11458
|
+
whatPeopleSaying: input.capture.whatPeopleSaying,
|
|
11459
|
+
tree,
|
|
11460
|
+
flat,
|
|
11461
|
+
videos: input.capture.videos,
|
|
11462
|
+
forums: input.capture.forums,
|
|
11463
|
+
organicResults: input.capture.organicResults,
|
|
11464
|
+
localPack: input.capture.localPack,
|
|
11465
|
+
entityIds: input.capture.entityIds,
|
|
11466
|
+
serpFeatures: input.capture.serpFeatures,
|
|
11467
|
+
relatedSearches: input.capture.relatedSearches,
|
|
11468
|
+
stats: {
|
|
11469
|
+
seed: input.query,
|
|
11470
|
+
totalQuestions: selected.length,
|
|
11471
|
+
maxDepthReached: selected.reduce((max, item) => Math.max(max, item.depth), 0),
|
|
11472
|
+
durationMs: input.durationMs,
|
|
11473
|
+
errorCount: input.partial ? 1 : 0
|
|
11474
|
+
},
|
|
11475
|
+
capture: {
|
|
11476
|
+
execution: "kernel_colocated_playwright",
|
|
11477
|
+
stealth: true,
|
|
11478
|
+
managedProxy: "kernel_default",
|
|
11479
|
+
metadataOverrides: false,
|
|
11480
|
+
locationSignal: input.location ? "uule_only" : "none",
|
|
11481
|
+
attempt: input.attempt,
|
|
11482
|
+
finalUrl: input.navigation.finalUrl,
|
|
11483
|
+
captchaObserved: input.navigation.captchaObserved,
|
|
11484
|
+
solverCleared: input.navigation.solverCleared,
|
|
11485
|
+
environment: input.navigation.environment,
|
|
11486
|
+
rawDomSha256: input.rawDomSha256
|
|
11487
|
+
}
|
|
11488
|
+
};
|
|
11489
|
+
}
|
|
11490
|
+
async function runDurablePaaCapture(input) {
|
|
11491
|
+
const startedAt = input.startedAtMs ?? Date.now();
|
|
11492
|
+
const workDeadline = startedAt + PAA_BROWSER_WORK_BUDGET_MS;
|
|
11493
|
+
const location2 = input.location?.trim() || null;
|
|
11494
|
+
const params = new URLSearchParams({
|
|
11495
|
+
q: input.query,
|
|
11496
|
+
gl: input.gl ?? "us",
|
|
11497
|
+
hl: input.hl ?? "en",
|
|
11498
|
+
pws: "0"
|
|
11499
|
+
});
|
|
11500
|
+
if (location2) params.set("uule", encodeUule(normalizeLocation(location2)));
|
|
11501
|
+
const searchUrl = `https://www.google.com/search?${params.toString()}`;
|
|
11502
|
+
const kernel = new Kernel4({ apiKey: input.apiKey });
|
|
11503
|
+
let lastCheckpoint = {
|
|
11504
|
+
phase: "navigating",
|
|
11505
|
+
attempt: 1,
|
|
11506
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11507
|
+
records: [],
|
|
11508
|
+
clickedQuestions: [],
|
|
11509
|
+
finalUrl: null,
|
|
11510
|
+
captchaObserved: false,
|
|
11511
|
+
solverCleared: false,
|
|
11512
|
+
error: null
|
|
11513
|
+
};
|
|
11514
|
+
const persist = async (patch) => {
|
|
11515
|
+
lastCheckpoint = { ...lastCheckpoint, ...patch, observedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11516
|
+
await input.onCheckpoint?.(lastCheckpoint);
|
|
11517
|
+
};
|
|
11518
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
11519
|
+
if (Date.now() >= workDeadline) break;
|
|
11520
|
+
let sessionId = null;
|
|
11521
|
+
const records = /* @__PURE__ */ new Map();
|
|
11522
|
+
const clicked = /* @__PURE__ */ new Set();
|
|
11523
|
+
const parents = {};
|
|
11524
|
+
let navigation = null;
|
|
11525
|
+
try {
|
|
11526
|
+
await persist({ phase: "navigating", attempt, records: [], clickedQuestions: [], error: null });
|
|
11527
|
+
const browser = await kernel.browsers.create({
|
|
11528
|
+
stealth: true,
|
|
11529
|
+
timeout_seconds: Math.max(60, Math.min(280, Math.ceil((workDeadline - Date.now()) / 1e3) + 20))
|
|
11530
|
+
});
|
|
11531
|
+
sessionId = browser.session_id;
|
|
11532
|
+
navigation = await execute(kernel, sessionId, navigationCode(searchUrl), workDeadline, 110);
|
|
11533
|
+
await persist({
|
|
11534
|
+
phase: navigation.outcome === "ready" ? "expanding" : "blocked",
|
|
11535
|
+
finalUrl: navigation.finalUrl,
|
|
11536
|
+
captchaObserved: navigation.captchaObserved,
|
|
11537
|
+
solverCleared: navigation.solverCleared
|
|
11538
|
+
});
|
|
11539
|
+
if (navigation.outcome === "captcha" || navigation.outcome === "soft_block") {
|
|
11540
|
+
if (attempt === 1 && records.size === 0) continue;
|
|
11541
|
+
throw new Error(navigation.outcome);
|
|
11542
|
+
}
|
|
11543
|
+
let stagnant = 0;
|
|
11544
|
+
for (let step = 0; step < input.maxQuestions * 2 && Date.now() < workDeadline - 35e3; step += 1) {
|
|
11545
|
+
const observed = await execute(
|
|
11546
|
+
kernel,
|
|
11547
|
+
sessionId,
|
|
11548
|
+
expansionCode(Array.from(clicked)),
|
|
11549
|
+
workDeadline,
|
|
11550
|
+
25
|
|
11551
|
+
);
|
|
11552
|
+
const beforeSize = records.size;
|
|
11553
|
+
for (const record of observed.records) {
|
|
11554
|
+
const existing = records.get(record.question);
|
|
11555
|
+
records.set(record.question, {
|
|
11556
|
+
...record,
|
|
11557
|
+
answer: record.answer || existing?.answer || "",
|
|
11558
|
+
sourceTitle: record.sourceTitle || existing?.sourceTitle || "",
|
|
11559
|
+
sourceSite: record.sourceSite || existing?.sourceSite || "",
|
|
11560
|
+
sourceCite: record.sourceCite || existing?.sourceCite || "",
|
|
11561
|
+
sources: Array.from(new Map([...existing?.sources ?? [], ...record.sources].map((source) => [source.url, source])).values()),
|
|
11562
|
+
parentQuestion: parents[record.question] ?? null,
|
|
11563
|
+
depth: parents[record.question] ? 2 : 1
|
|
11564
|
+
});
|
|
11565
|
+
}
|
|
11566
|
+
if (observed.outcome === "clicked" && observed.targetQuestion) {
|
|
11567
|
+
clicked.add(observed.targetQuestion);
|
|
11568
|
+
for (const question of observed.newQuestions) {
|
|
11569
|
+
if (!parents[question]) parents[question] = observed.targetQuestion;
|
|
11570
|
+
}
|
|
11571
|
+
}
|
|
11572
|
+
const selected2 = Array.from(records.values()).slice(0, input.maxQuestions);
|
|
11573
|
+
await persist({
|
|
11574
|
+
phase: "expanding",
|
|
11575
|
+
records: selected2,
|
|
11576
|
+
clickedQuestions: Array.from(clicked),
|
|
11577
|
+
finalUrl: observed.finalUrl,
|
|
11578
|
+
captchaObserved: lastCheckpoint.captchaObserved || observed.outcome === "captcha"
|
|
11579
|
+
});
|
|
11580
|
+
if (observed.outcome === "captcha" || observed.outcome === "soft_block") break;
|
|
11581
|
+
if (selected2.length >= input.maxQuestions && selected2.every((record) => clicked.has(record.question))) break;
|
|
11582
|
+
stagnant = records.size === beforeSize || observed.outcome === "exhausted" ? stagnant + 1 : 0;
|
|
11583
|
+
if (stagnant >= 4) break;
|
|
11584
|
+
}
|
|
11585
|
+
const finalCapture = await execute(
|
|
11586
|
+
kernel,
|
|
11587
|
+
sessionId,
|
|
11588
|
+
finalCaptureCode(input.maxQuestions, parents),
|
|
11589
|
+
workDeadline,
|
|
11590
|
+
45
|
|
11591
|
+
);
|
|
11592
|
+
for (const record of finalCapture.paa) {
|
|
11593
|
+
const existing = records.get(record.question);
|
|
11594
|
+
records.set(record.question, {
|
|
11595
|
+
...record,
|
|
11596
|
+
answer: record.answer || existing?.answer || "",
|
|
11597
|
+
sourceTitle: record.sourceTitle || existing?.sourceTitle || "",
|
|
11598
|
+
sourceSite: record.sourceSite || existing?.sourceSite || "",
|
|
11599
|
+
sourceCite: record.sourceCite || existing?.sourceCite || "",
|
|
11600
|
+
sources: Array.from(new Map([...existing?.sources ?? [], ...record.sources].map((source) => [source.url, source])).values()),
|
|
11601
|
+
parentQuestion: parents[record.question] ?? record.parentQuestion,
|
|
11602
|
+
depth: record.depth
|
|
11603
|
+
});
|
|
11604
|
+
}
|
|
11605
|
+
let rawDomGzip = null;
|
|
11606
|
+
let rawDomSha256 = null;
|
|
11607
|
+
if (Date.now() < workDeadline - 12e3) {
|
|
11608
|
+
const prepared = await execute(
|
|
11609
|
+
kernel,
|
|
11610
|
+
sessionId,
|
|
11611
|
+
RAW_PREPARE_CODE,
|
|
11612
|
+
workDeadline,
|
|
11613
|
+
35
|
|
11614
|
+
);
|
|
11615
|
+
const chunks = [];
|
|
11616
|
+
for (let offset = 0; offset < prepared.chars && Date.now() < workDeadline - 5e3; offset += 18e4) {
|
|
11617
|
+
const part = await execute(
|
|
11618
|
+
kernel,
|
|
11619
|
+
sessionId,
|
|
11620
|
+
rawChunkCode(offset, 18e4),
|
|
11621
|
+
workDeadline,
|
|
11622
|
+
15
|
|
11623
|
+
);
|
|
11624
|
+
if (part.offset !== offset) throw new Error("raw_dom_chunk_offset_mismatch");
|
|
11625
|
+
chunks.push(part.chunk);
|
|
11626
|
+
}
|
|
11627
|
+
if (chunks.join("").length === prepared.chars) {
|
|
11628
|
+
const transported = Buffer.from(chunks.join(""), "base64");
|
|
11629
|
+
if (prepared.encoding === "gzip") rawDomGzip = transported;
|
|
11630
|
+
else {
|
|
11631
|
+
const { gzipSync } = await import("zlib");
|
|
11632
|
+
rawDomGzip = gzipSync(transported);
|
|
11633
|
+
}
|
|
11634
|
+
rawDomSha256 = createHash2("sha256").update(rawDomGzip).digest("hex");
|
|
11635
|
+
}
|
|
11636
|
+
}
|
|
11637
|
+
const selected = Array.from(records.values()).slice(0, input.maxQuestions);
|
|
11638
|
+
const partial = selected.length < input.maxQuestions || selected.some((record) => !record.answer || !record.sourceCite);
|
|
11639
|
+
await persist({ phase: "captured", records: selected, clickedQuestions: Array.from(clicked) });
|
|
11640
|
+
return {
|
|
11641
|
+
result: buildPublicResult({
|
|
11642
|
+
query: input.query,
|
|
11643
|
+
location: location2,
|
|
11644
|
+
capture: finalCapture,
|
|
11645
|
+
records: selected,
|
|
11646
|
+
navigation,
|
|
11647
|
+
durationMs: Date.now() - startedAt,
|
|
11648
|
+
partial,
|
|
11649
|
+
attempt,
|
|
11650
|
+
rawDomSha256
|
|
11651
|
+
}),
|
|
11652
|
+
rawDomGzip,
|
|
11653
|
+
rawDomSha256,
|
|
11654
|
+
attempt,
|
|
11655
|
+
partial,
|
|
11656
|
+
checkpoint: lastCheckpoint
|
|
11657
|
+
};
|
|
11658
|
+
} catch (error) {
|
|
11659
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11660
|
+
await persist({
|
|
11661
|
+
phase: records.size > 0 ? "captured" : "failed",
|
|
11662
|
+
records: Array.from(records.values()).slice(0, input.maxQuestions),
|
|
11663
|
+
clickedQuestions: Array.from(clicked),
|
|
11664
|
+
error: message.slice(0, 500)
|
|
11665
|
+
});
|
|
11666
|
+
if (records.size > 0) {
|
|
11667
|
+
const emptyCapture = {
|
|
11668
|
+
paa: [],
|
|
11669
|
+
organicResults: [],
|
|
11670
|
+
localPack: [],
|
|
11671
|
+
videos: [],
|
|
11672
|
+
forums: [],
|
|
11673
|
+
whatPeopleSaying: [],
|
|
11674
|
+
aiOverview: { detected: false, text: null, citations: [] },
|
|
11675
|
+
aiMode: { detected: false, text: null, citations: [] },
|
|
11676
|
+
surface: "unknown",
|
|
11677
|
+
entityIds: { entities: [], kgIds: [], cids: [], gcids: [] },
|
|
11678
|
+
serpFeatures: { sectionHeadings: [], detected: {}, counts: {} },
|
|
11679
|
+
relatedSearches: []
|
|
11680
|
+
};
|
|
11681
|
+
return {
|
|
11682
|
+
result: buildPublicResult({
|
|
11683
|
+
query: input.query,
|
|
11684
|
+
location: location2,
|
|
11685
|
+
capture: emptyCapture,
|
|
11686
|
+
records: Array.from(records.values()).slice(0, input.maxQuestions),
|
|
11687
|
+
navigation: navigation ?? {
|
|
11688
|
+
outcome: "soft_block",
|
|
11689
|
+
finalUrl: lastCheckpoint.finalUrl ?? "",
|
|
11690
|
+
title: "",
|
|
11691
|
+
captchaObserved: lastCheckpoint.captchaObserved,
|
|
11692
|
+
solverCleared: lastCheckpoint.solverCleared,
|
|
11693
|
+
environment: null
|
|
11694
|
+
},
|
|
11695
|
+
durationMs: Date.now() - startedAt,
|
|
11696
|
+
partial: true,
|
|
11697
|
+
attempt,
|
|
11698
|
+
rawDomSha256: null
|
|
11699
|
+
}),
|
|
11700
|
+
rawDomGzip: null,
|
|
11701
|
+
rawDomSha256: null,
|
|
11702
|
+
attempt,
|
|
11703
|
+
partial: true,
|
|
11704
|
+
checkpoint: lastCheckpoint
|
|
11705
|
+
};
|
|
11706
|
+
}
|
|
11707
|
+
if (attempt === 2 || Date.now() >= workDeadline - 35e3) throw error;
|
|
11708
|
+
} finally {
|
|
11709
|
+
if (sessionId) {
|
|
11710
|
+
await kernel.browsers.deleteByID(sessionId).catch(() => void 0);
|
|
11711
|
+
}
|
|
11712
|
+
}
|
|
11713
|
+
}
|
|
11714
|
+
throw new Error("paa_work_deadline_exhausted_before_capture");
|
|
11715
|
+
}
|
|
11716
|
+
|
|
11717
|
+
// src/inngest/functions/paa-harvest.ts
|
|
11718
|
+
function questionCount(result) {
|
|
11719
|
+
if (!result || typeof result !== "object") return 0;
|
|
11720
|
+
const value = result;
|
|
11721
|
+
if (typeof value.totalQuestions === "number") return value.totalQuestions;
|
|
11722
|
+
return Array.isArray(value.progress?.records) ? value.progress.records.length : 0;
|
|
11723
|
+
}
|
|
11724
|
+
var paaHarvestFn = inngest.createFunction(
|
|
11725
|
+
{
|
|
11726
|
+
id: "paa-harvest",
|
|
11727
|
+
retries: 0,
|
|
11728
|
+
triggers: [{ event: "mcp-scraper/paa.requested" }],
|
|
11729
|
+
onFailure: async ({ event }) => {
|
|
11730
|
+
const jobId2 = event?.data?.event?.data?.jobId;
|
|
11731
|
+
if (!jobId2) return;
|
|
11732
|
+
const current = await getJob(jobId2);
|
|
11733
|
+
if (!current) return;
|
|
11734
|
+
if (current.status === "done" || current.status === "failed" || current.status === "cancelled") {
|
|
11735
|
+
await settlePaaHarvestJob(jobId2).catch((error) => {
|
|
11736
|
+
console.error("[paa-harvest/onFailure] terminal settlement pending:", error instanceof Error ? error.message : String(error));
|
|
11737
|
+
});
|
|
11738
|
+
return;
|
|
11739
|
+
}
|
|
11740
|
+
const message = String(event?.data?.error?.message ?? "durable PAA invocation failed").slice(0, 1e3);
|
|
11741
|
+
await failJob(jobId2, message);
|
|
11742
|
+
await settlePaaHarvestJob(jobId2).catch((error) => {
|
|
11743
|
+
console.error("[paa-harvest/onFailure] settlement pending:", error instanceof Error ? error.message : String(error));
|
|
11744
|
+
});
|
|
11745
|
+
}
|
|
11746
|
+
},
|
|
11747
|
+
async ({ event }) => {
|
|
11748
|
+
const invocationStartedAt = Date.now();
|
|
11749
|
+
const job = await claimInngestPaaJob(event.data.jobId);
|
|
11750
|
+
if (!job) return { jobId: event.data.jobId, status: "missing_or_not_paa" };
|
|
11751
|
+
if (job.status === "done" || job.status === "failed" || job.status === "cancelled") {
|
|
11752
|
+
await settlePaaHarvestJob(job.id);
|
|
11753
|
+
return { jobId: job.id, status: job.status };
|
|
11754
|
+
}
|
|
11755
|
+
const options = job.options;
|
|
11756
|
+
const maxQuestions = Math.max(1, Math.min(100, Number(options.maxQuestions ?? 30)));
|
|
11757
|
+
const apiKey = browserServiceApiKey();
|
|
11758
|
+
if (!apiKey) throw new Error("browser service is not configured");
|
|
11759
|
+
const attemptStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11760
|
+
await startHarvestAttempt({
|
|
11761
|
+
jobId: job.id,
|
|
11762
|
+
userId: job.user_id,
|
|
11763
|
+
attemptNumber: 1,
|
|
11764
|
+
maxAttempts: 2,
|
|
11765
|
+
query: options.query ?? job.query,
|
|
11766
|
+
location: options.location ?? null,
|
|
11767
|
+
maxQuestions,
|
|
11768
|
+
startedAt: attemptStartedAt
|
|
11769
|
+
});
|
|
11770
|
+
const checkpointState = { value: null };
|
|
11771
|
+
try {
|
|
11772
|
+
const capture = await runDurablePaaCapture({
|
|
11773
|
+
apiKey,
|
|
11774
|
+
query: options.query ?? job.query,
|
|
11775
|
+
location: options.location ?? null,
|
|
11776
|
+
gl: options.gl ?? "us",
|
|
11777
|
+
hl: options.hl ?? "en",
|
|
11778
|
+
maxQuestions,
|
|
11779
|
+
startedAtMs: invocationStartedAt,
|
|
11780
|
+
onCheckpoint: async (checkpoint) => {
|
|
11781
|
+
checkpointState.value = checkpoint;
|
|
11782
|
+
const persisted = await checkpointJob(job.id, {
|
|
11783
|
+
progress: checkpoint,
|
|
11784
|
+
durable: {
|
|
11785
|
+
invocationBudgetMs: PAA_INVOCATION_BUDGET_MS,
|
|
11786
|
+
workBudgetMs: 25e4,
|
|
11787
|
+
cleanupReserveMs: 3e4
|
|
11788
|
+
}
|
|
11789
|
+
});
|
|
11790
|
+
if (!persisted) throw new Error("job_left_running_state_during_capture");
|
|
11791
|
+
}
|
|
11792
|
+
});
|
|
11793
|
+
const artifactResult = capture.rawDomGzip ? await tryCreatePaaRawDomArtifact({
|
|
11794
|
+
userId: job.user_id,
|
|
11795
|
+
jobId: job.id,
|
|
11796
|
+
attempt: capture.attempt,
|
|
11797
|
+
gzip: capture.rawDomGzip
|
|
11798
|
+
}) : { artifact: null, status: "unavailable" };
|
|
11799
|
+
const artifact = artifactResult.artifact;
|
|
11800
|
+
const artifactMissing = capture.rawDomGzip !== null && artifactResult.status !== "stored";
|
|
11801
|
+
const result = {
|
|
11802
|
+
...capture.result,
|
|
11803
|
+
diagnostics: {
|
|
11804
|
+
...capture.result.diagnostics,
|
|
11805
|
+
...artifactMissing ? {
|
|
11806
|
+
resultQuality: "partial",
|
|
11807
|
+
degradedResult: true,
|
|
11808
|
+
retryRecommended: false
|
|
11809
|
+
} : {}
|
|
11810
|
+
},
|
|
11811
|
+
capture: {
|
|
11812
|
+
...capture.result.capture,
|
|
11813
|
+
rawDomArtifact: artifact,
|
|
11814
|
+
rawDomArtifactStatus: artifactResult.status
|
|
11815
|
+
}
|
|
11816
|
+
};
|
|
11817
|
+
await completeJob(job.id, result);
|
|
11818
|
+
await finishHarvestAttempt({
|
|
11819
|
+
jobId: job.id,
|
|
11820
|
+
attemptNumber: 1,
|
|
11821
|
+
outcome: capture.partial ? "partial" : "paa_found",
|
|
11822
|
+
kernelSessionId: null,
|
|
11823
|
+
questionCount: questionCount(result),
|
|
11824
|
+
durationMs: Date.now() - invocationStartedAt,
|
|
11825
|
+
error: capture.checkpoint.error,
|
|
11826
|
+
willRetry: false,
|
|
11827
|
+
kernelDeleteStarted: true,
|
|
11828
|
+
kernelDeleteSucceeded: true,
|
|
11829
|
+
kernelDeleteError: null,
|
|
11830
|
+
browserCloseSucceeded: true,
|
|
11831
|
+
browserCloseError: null,
|
|
11832
|
+
debug: {
|
|
11833
|
+
execution: "kernel_colocated_playwright",
|
|
11834
|
+
stealth: true,
|
|
11835
|
+
attemptUsed: capture.attempt,
|
|
11836
|
+
rawDomStored: Boolean(artifact)
|
|
11837
|
+
},
|
|
11838
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11839
|
+
});
|
|
11840
|
+
await settlePaaHarvestJob(job.id);
|
|
11841
|
+
if (job.callback_url) {
|
|
11842
|
+
await deliverWebhook(job.callback_url, { job_id: job.id, status: "done", result });
|
|
11843
|
+
}
|
|
11844
|
+
return {
|
|
11845
|
+
jobId: job.id,
|
|
11846
|
+
status: "done",
|
|
11847
|
+
totalQuestions: questionCount(result),
|
|
11848
|
+
partial: capture.partial,
|
|
11849
|
+
artifactStored: Boolean(artifact)
|
|
11850
|
+
};
|
|
11851
|
+
} catch (error) {
|
|
11852
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11853
|
+
const latestCheckpoint = checkpointState.value;
|
|
11854
|
+
await finishHarvestAttempt({
|
|
11855
|
+
jobId: job.id,
|
|
11856
|
+
attemptNumber: 1,
|
|
11857
|
+
outcome: latestCheckpoint?.records.length ? "partial_session_lost" : "failed_before_capture",
|
|
11858
|
+
kernelSessionId: null,
|
|
11859
|
+
questionCount: latestCheckpoint?.records.length ?? 0,
|
|
11860
|
+
durationMs: Date.now() - invocationStartedAt,
|
|
11861
|
+
error: message.slice(0, 1e3),
|
|
11862
|
+
willRetry: false,
|
|
11863
|
+
kernelDeleteStarted: true,
|
|
11864
|
+
kernelDeleteSucceeded: null,
|
|
11865
|
+
kernelDeleteError: null,
|
|
11866
|
+
browserCloseSucceeded: null,
|
|
11867
|
+
browserCloseError: null,
|
|
11868
|
+
debug: latestCheckpoint,
|
|
11869
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11870
|
+
});
|
|
11871
|
+
await failJob(job.id, message.slice(0, 1e3));
|
|
11872
|
+
await settlePaaHarvestJob(job.id).catch((settlementError) => {
|
|
11873
|
+
console.error("[paa-harvest] settlement pending:", settlementError instanceof Error ? settlementError.message : String(settlementError));
|
|
11874
|
+
});
|
|
11875
|
+
if (job.callback_url) {
|
|
11876
|
+
await deliverWebhook(job.callback_url, {
|
|
11877
|
+
job_id: job.id,
|
|
11878
|
+
status: "failed",
|
|
11879
|
+
error: message,
|
|
11880
|
+
progress: latestCheckpoint
|
|
11881
|
+
});
|
|
11882
|
+
}
|
|
11883
|
+
throw error;
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11886
|
+
);
|
|
11887
|
+
|
|
10960
11888
|
// src/api/site-extract-start.ts
|
|
10961
11889
|
async function prepareSiteExtractStart(input) {
|
|
10962
11890
|
const existing = await getExtractJobByIdempotencyKey(input.userId, input.idempotencyKey);
|
|
@@ -11607,7 +12535,7 @@ import { Hono as Hono3 } from "hono";
|
|
|
11607
12535
|
import { z as z12 } from "zod";
|
|
11608
12536
|
|
|
11609
12537
|
// src/api/unified-billing.ts
|
|
11610
|
-
import { createHash as
|
|
12538
|
+
import { createHash as createHash3, createHmac, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
11611
12539
|
|
|
11612
12540
|
// src/api/scheduling-access.ts
|
|
11613
12541
|
var PAID_PLAN_REQUIRED_CODE = "paid_plan_required";
|
|
@@ -11708,7 +12636,7 @@ function tokenFor(id, idempotencyKey2) {
|
|
|
11708
12636
|
return createHmac("sha256", billingSecret()).update(id).update(":").update(idempotencyKey2).digest("base64url");
|
|
11709
12637
|
}
|
|
11710
12638
|
function tokenHash(token2) {
|
|
11711
|
-
return
|
|
12639
|
+
return createHash3("sha256").update(token2).digest("hex");
|
|
11712
12640
|
}
|
|
11713
12641
|
function verifyToken(row, token2) {
|
|
11714
12642
|
const expected = Buffer.from(row.token_hash, "hex");
|
|
@@ -12274,7 +13202,7 @@ async function syncScheduledActionCredentials(user) {
|
|
|
12274
13202
|
}
|
|
12275
13203
|
|
|
12276
13204
|
// src/api/connected-usage-billing.ts
|
|
12277
|
-
import { createHash as
|
|
13205
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
|
|
12278
13206
|
import { z as z11 } from "zod";
|
|
12279
13207
|
|
|
12280
13208
|
// src/api/connected-cost-telemetry.ts
|
|
@@ -12349,7 +13277,7 @@ var ConnectedUsageBillingError = class extends Error {
|
|
|
12349
13277
|
details;
|
|
12350
13278
|
};
|
|
12351
13279
|
function hash(value) {
|
|
12352
|
-
return
|
|
13280
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
12353
13281
|
}
|
|
12354
13282
|
function eventKey(idempotencyKey2) {
|
|
12355
13283
|
return `connected-usage:${hash(idempotencyKey2)}`;
|
|
@@ -18363,12 +19291,15 @@ function mapsErrorResponse(c, err, errorCode) {
|
|
|
18363
19291
|
const msg = err instanceof Error ? err.message : String(err);
|
|
18364
19292
|
const rotationError = err instanceof MapsSearchRotationError ? err : null;
|
|
18365
19293
|
const blocked = msg.includes("CAPTCHA") || msg.includes("blocked");
|
|
18366
|
-
const
|
|
18367
|
-
|
|
19294
|
+
const publicCode = normalizePublicErrorCode(
|
|
19295
|
+
rotationError?.errorCode ?? (blocked ? "captcha_or_blocked" : "service_unavailable")
|
|
19296
|
+
);
|
|
19297
|
+
const retryable = publicCode === "service_unavailable" || rotationError?.retryable === true || blocked;
|
|
19298
|
+
const message = publicErrorMessage(publicCode);
|
|
18368
19299
|
return c.json({
|
|
18369
19300
|
error: message,
|
|
18370
19301
|
message,
|
|
18371
|
-
error_code:
|
|
19302
|
+
error_code: publicCode,
|
|
18372
19303
|
retryable
|
|
18373
19304
|
}, retryable ? 503 : 500);
|
|
18374
19305
|
}
|
|
@@ -18879,7 +19810,7 @@ g2App.post("/reviews", createApiKeyAuth(), async (c) => {
|
|
|
18879
19810
|
});
|
|
18880
19811
|
|
|
18881
19812
|
// src/api/directory-routes.ts
|
|
18882
|
-
import { createHash as
|
|
19813
|
+
import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
|
|
18883
19814
|
import { Hono as Hono16 } from "hono";
|
|
18884
19815
|
|
|
18885
19816
|
// src/api/directory-workflow-dispatch.ts
|
|
@@ -18919,7 +19850,7 @@ function safeOptions(options) {
|
|
|
18919
19850
|
return safe;
|
|
18920
19851
|
}
|
|
18921
19852
|
function requestFingerprint(options) {
|
|
18922
|
-
return
|
|
19853
|
+
return createHash5("sha256").update(JSON.stringify(safeOptions(options))).digest("hex");
|
|
18923
19854
|
}
|
|
18924
19855
|
function idempotencyKey(raw) {
|
|
18925
19856
|
if (raw !== void 0) {
|
|
@@ -18931,7 +19862,7 @@ function idempotencyKey(raw) {
|
|
|
18931
19862
|
return { ok: true, key: `directory-${randomUUID7()}` };
|
|
18932
19863
|
}
|
|
18933
19864
|
function debitKeyFor(userId, responseKey) {
|
|
18934
|
-
const digest =
|
|
19865
|
+
const digest = createHash5("sha256").update(String(userId)).update("\0").update(responseKey).digest("hex");
|
|
18935
19866
|
return `directory-workflow:${userId}:${digest}`;
|
|
18936
19867
|
}
|
|
18937
19868
|
function jobId() {
|
|
@@ -19044,7 +19975,7 @@ async function runSynchronously(c, user, options, plan, responseKey) {
|
|
|
19044
19975
|
const csv = renderDirectoryWorkflowCsv(result);
|
|
19045
19976
|
const artifact = await createDirectoryCsvArtifact({
|
|
19046
19977
|
ownerId: String(user.id),
|
|
19047
|
-
jobId:
|
|
19978
|
+
jobId: createHash5("sha256").update(debitKey2).digest("hex").slice(0, 32),
|
|
19048
19979
|
createdAt: result.extractedAt,
|
|
19049
19980
|
filename: `${options.state}-${options.query}-directory.csv`,
|
|
19050
19981
|
csv,
|
|
@@ -19460,6 +20391,53 @@ locationDataApp.post("/admin/sync-census", async (c) => {
|
|
|
19460
20391
|
}
|
|
19461
20392
|
});
|
|
19462
20393
|
|
|
20394
|
+
// src/api/paa-harvest-dispatch.ts
|
|
20395
|
+
async function dispatchPaaHarvest(jobId2) {
|
|
20396
|
+
await inngest.send({
|
|
20397
|
+
id: `paa-harvest:${jobId2}`,
|
|
20398
|
+
name: "mcp-scraper/paa.requested",
|
|
20399
|
+
data: { jobId: jobId2 }
|
|
20400
|
+
});
|
|
20401
|
+
}
|
|
20402
|
+
async function redispatchPendingPaaHarvests(limit = 25) {
|
|
20403
|
+
const jobs = await listPendingInngestPaaJobs(limit);
|
|
20404
|
+
let dispatched = 0;
|
|
20405
|
+
let failed = 0;
|
|
20406
|
+
for (const job of jobs) {
|
|
20407
|
+
try {
|
|
20408
|
+
await dispatchPaaHarvest(job.id);
|
|
20409
|
+
dispatched += 1;
|
|
20410
|
+
} catch {
|
|
20411
|
+
failed += 1;
|
|
20412
|
+
}
|
|
20413
|
+
}
|
|
20414
|
+
return { checked: jobs.length, dispatched, failed };
|
|
20415
|
+
}
|
|
20416
|
+
|
|
20417
|
+
// src/api/paa-harvest-reconciliation.ts
|
|
20418
|
+
async function reconcilePaaHarvestSettlements(limit = 25) {
|
|
20419
|
+
const stale = await listStaleRunningInngestPaaJobs(limit);
|
|
20420
|
+
let staleFailed = 0;
|
|
20421
|
+
for (const job of stale) {
|
|
20422
|
+
try {
|
|
20423
|
+
await failJob(job.id, "durable PAA invocation exceeded its recovery window; preserved progress remains in result");
|
|
20424
|
+
staleFailed += 1;
|
|
20425
|
+
} catch {
|
|
20426
|
+
}
|
|
20427
|
+
}
|
|
20428
|
+
const jobs = await listRecentTerminalInngestPaaJobs(limit);
|
|
20429
|
+
let settled = 0;
|
|
20430
|
+
let failed = 0;
|
|
20431
|
+
for (const job of jobs) {
|
|
20432
|
+
try {
|
|
20433
|
+
if (await settlePaaHarvestJob(job)) settled += 1;
|
|
20434
|
+
} catch {
|
|
20435
|
+
failed += 1;
|
|
20436
|
+
}
|
|
20437
|
+
}
|
|
20438
|
+
return { checked: jobs.length, staleFailed, settled, failed };
|
|
20439
|
+
}
|
|
20440
|
+
|
|
19463
20441
|
// src/api/directory-workflow-reconciliation.ts
|
|
19464
20442
|
async function reconcileDirectoryWorkflowSettlements(limit = 25) {
|
|
19465
20443
|
const expiredStarts = await listExpiredDirectoryWorkflowStarts(limit);
|
|
@@ -20060,17 +21038,17 @@ workflowApp.post("/cron/dispatch", async (c) => {
|
|
|
20060
21038
|
|
|
20061
21039
|
// src/api/serp-intelligence-routes.ts
|
|
20062
21040
|
import { Hono as Hono19 } from "hono";
|
|
20063
|
-
import { createHash as
|
|
21041
|
+
import { createHash as createHash7, randomUUID as randomUUID8 } from "crypto";
|
|
20064
21042
|
|
|
20065
21043
|
// src/serp-intelligence/page-snapshot-extractor.ts
|
|
20066
|
-
import { createHash as
|
|
21044
|
+
import { createHash as createHash6 } from "crypto";
|
|
20067
21045
|
import pLimit4 from "p-limit";
|
|
20068
21046
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
20069
21047
|
var DEFAULT_MAX_CONCURRENCY = 2;
|
|
20070
21048
|
var DEFAULT_MAX_CONTENT_CHARS = 25e4;
|
|
20071
21049
|
function sha256(value) {
|
|
20072
21050
|
if (!value) return null;
|
|
20073
|
-
return
|
|
21051
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
20074
21052
|
}
|
|
20075
21053
|
function countWords(markdown) {
|
|
20076
21054
|
const matches = markdown.trim().match(/\b[\p{L}\p{N}][\p{L}\p{N}'-]*\b/gu);
|
|
@@ -20846,8 +21824,8 @@ async function logRequestEventBestEffort(input) {
|
|
|
20846
21824
|
}
|
|
20847
21825
|
function captureBillingKeys(userId, suppliedKey, body) {
|
|
20848
21826
|
const responseKey = suppliedKey?.trim() || randomUUID8();
|
|
20849
|
-
const requestFingerprint2 =
|
|
20850
|
-
const keyDigest =
|
|
21827
|
+
const requestFingerprint2 = createHash7("sha256").update(JSON.stringify(body)).digest("hex");
|
|
21828
|
+
const keyDigest = createHash7("sha256").update(String(userId)).update("\0").update(responseKey).digest("hex");
|
|
20851
21829
|
return {
|
|
20852
21830
|
responseKey,
|
|
20853
21831
|
debitKey: `serp-capture:${userId}:${keyDigest}`,
|
|
@@ -22116,7 +23094,7 @@ async function listReplayRows(sessionId) {
|
|
|
22116
23094
|
}
|
|
22117
23095
|
|
|
22118
23096
|
// src/services/browser-agent/browser-agent-service.ts
|
|
22119
|
-
import
|
|
23097
|
+
import Kernel5 from "@onkernel/sdk";
|
|
22120
23098
|
import { chromium as playwrightChromium } from "playwright";
|
|
22121
23099
|
|
|
22122
23100
|
// src/services/fanout/cdp-capture.ts
|
|
@@ -22819,7 +23797,7 @@ var DEFAULT_TIMEOUT_SECONDS = 600;
|
|
|
22819
23797
|
function client() {
|
|
22820
23798
|
const apiKey = browserServiceApiKey();
|
|
22821
23799
|
if (!apiKey) throw new Error("Browser backend API key is required");
|
|
22822
|
-
return new
|
|
23800
|
+
return new Kernel5({ apiKey });
|
|
22823
23801
|
}
|
|
22824
23802
|
function isProfileConflict(err) {
|
|
22825
23803
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -24299,7 +25277,7 @@ async function resolveUser3(customerId, emailFallback) {
|
|
|
24299
25277
|
// src/api/oauth-routes.ts
|
|
24300
25278
|
import { Hono as Hono23 } from "hono";
|
|
24301
25279
|
import { getCookie, setCookie } from "hono/cookie";
|
|
24302
|
-
import { createHash as
|
|
25280
|
+
import { createHash as createHash8, randomBytes as randomBytes2, randomUUID as randomUUID11 } from "crypto";
|
|
24303
25281
|
import { importPKCS8, exportJWK, SignJWT } from "jose";
|
|
24304
25282
|
var ISSUER = "https://mcpscraper.dev";
|
|
24305
25283
|
var RESOURCE = () => process.env.MCP_MEMORY_RESOURCE_URL ?? "https://memory.mcpscraper.dev/mcp";
|
|
@@ -24332,7 +25310,7 @@ async function getKeys() {
|
|
|
24332
25310
|
const privateKey = await importPKCS8(pem, "RS256", { extractable: true });
|
|
24333
25311
|
const full = await exportJWK(privateKey);
|
|
24334
25312
|
const publicJwk = { kty: full.kty, n: full.n, e: full.e };
|
|
24335
|
-
const kid =
|
|
25313
|
+
const kid = createHash8("sha256").update(JSON.stringify({ e: publicJwk.e, kty: publicJwk.kty, n: publicJwk.n })).digest("base64url").slice(0, 16);
|
|
24336
25314
|
publicJwk.kid = kid;
|
|
24337
25315
|
publicJwk.alg = "RS256";
|
|
24338
25316
|
publicJwk.use = "sig";
|
|
@@ -24642,7 +25620,7 @@ oauthApp.post("/oauth/authorize", async (c) => {
|
|
|
24642
25620
|
});
|
|
24643
25621
|
function pkceMatches(verifier, challenge) {
|
|
24644
25622
|
if (!verifier) return false;
|
|
24645
|
-
const computed =
|
|
25623
|
+
const computed = createHash8("sha256").update(verifier).digest("base64url");
|
|
24646
25624
|
return computed === challenge;
|
|
24647
25625
|
}
|
|
24648
25626
|
async function mintAccessToken(identity, scope, plan, audience) {
|
|
@@ -26095,7 +27073,7 @@ import { Hono as Hono27 } from "hono";
|
|
|
26095
27073
|
import { Resend } from "resend";
|
|
26096
27074
|
|
|
26097
27075
|
// src/api/resend-support-thread.ts
|
|
26098
|
-
import { createHash as
|
|
27076
|
+
import { createHash as createHash9 } from "crypto";
|
|
26099
27077
|
var SUPPORT_TAGS = [
|
|
26100
27078
|
{
|
|
26101
27079
|
tag: "support",
|
|
@@ -26135,7 +27113,7 @@ function quoteUntrusted(value) {
|
|
|
26135
27113
|
return clean2.split("\n").map((line) => `> ${line}`).join("\n");
|
|
26136
27114
|
}
|
|
26137
27115
|
function shortHash(value, length = 24) {
|
|
26138
|
-
return
|
|
27116
|
+
return createHash9("sha256").update(value).digest("hex").slice(0, length);
|
|
26139
27117
|
}
|
|
26140
27118
|
function safePathPart(value) {
|
|
26141
27119
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "contact";
|
|
@@ -27634,7 +28612,7 @@ import { Hono as Hono29 } from "hono";
|
|
|
27634
28612
|
import { z as z29 } from "zod";
|
|
27635
28613
|
|
|
27636
28614
|
// src/editorial-reading-room/render.ts
|
|
27637
|
-
import { createHash as
|
|
28615
|
+
import { createHash as createHash10 } from "crypto";
|
|
27638
28616
|
import { readFileSync as readFileSync3 } from "fs";
|
|
27639
28617
|
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
27640
28618
|
import { Marked, Renderer } from "marked";
|
|
@@ -27767,7 +28745,7 @@ function renderEditorialReadingRoom(input, now = /* @__PURE__ */ new Date()) {
|
|
|
27767
28745
|
articleCount: articles.length,
|
|
27768
28746
|
wordCount: totalWordCount,
|
|
27769
28747
|
bytes,
|
|
27770
|
-
sha256:
|
|
28748
|
+
sha256: createHash10("sha256").update(html).digest("hex"),
|
|
27771
28749
|
warnings
|
|
27772
28750
|
};
|
|
27773
28751
|
}
|
|
@@ -28000,13 +28978,13 @@ async function reconcileSiteExtractSettlements(limit = 25) {
|
|
|
28000
28978
|
}
|
|
28001
28979
|
|
|
28002
28980
|
// src/api/page-diff.ts
|
|
28003
|
-
import { createHash as
|
|
28981
|
+
import { createHash as createHash11 } from "crypto";
|
|
28004
28982
|
import { diffLines } from "diff";
|
|
28005
28983
|
var MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
|
|
28006
28984
|
var MAX_DIFF_HUNKS = 200;
|
|
28007
28985
|
var MAX_DIFF_LINES_PER_RESPONSE = 2e3;
|
|
28008
28986
|
function sha256Hex(value) {
|
|
28009
|
-
return
|
|
28987
|
+
return createHash11("sha256").update(value).digest("hex");
|
|
28010
28988
|
}
|
|
28011
28989
|
function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
|
|
28012
28990
|
if (value.length <= maxChars) return { value, truncated: false };
|
|
@@ -28528,7 +29506,7 @@ function inferredWaybackCapturedAt(content) {
|
|
|
28528
29506
|
}
|
|
28529
29507
|
|
|
28530
29508
|
// src/api/connection-memory-import.ts
|
|
28531
|
-
import { createHash as
|
|
29509
|
+
import { createHash as createHash12 } from "crypto";
|
|
28532
29510
|
var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
|
|
28533
29511
|
var CONNECTION_MEMORY_IMPORT_MAX_RESULT_BYTES = 1e6;
|
|
28534
29512
|
var CONNECTION_MEMORY_IMPORT_MAX_STRING_CHARS = 256e3;
|
|
@@ -28558,7 +29536,7 @@ function canonicalJson(value) {
|
|
|
28558
29536
|
return JSON.stringify(value);
|
|
28559
29537
|
}
|
|
28560
29538
|
function sha2562(value) {
|
|
28561
|
-
return
|
|
29539
|
+
return createHash12("sha256").update(value).digest("hex");
|
|
28562
29540
|
}
|
|
28563
29541
|
function sensitiveKey(key) {
|
|
28564
29542
|
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -28846,6 +29824,7 @@ var CONNECTED_DATA_DATASETS = [
|
|
|
28846
29824
|
"calendar_events",
|
|
28847
29825
|
"zoom_recordings",
|
|
28848
29826
|
"zoom_transcripts",
|
|
29827
|
+
"slack_channel_messages",
|
|
28849
29828
|
"meta_ads_insights",
|
|
28850
29829
|
"search_console_performance",
|
|
28851
29830
|
"resend_data",
|
|
@@ -28872,8 +29851,9 @@ function resolveConnectedDataRange(args) {
|
|
|
28872
29851
|
const from = args.from ? new Date(args.from) : new Date(to.getTime() - (args.lastDays ?? 7) * 864e5);
|
|
28873
29852
|
if (!Number.isFinite(from.getTime())) throw new ConnectedDataExportValidationError("from must be an RFC3339 timestamp.");
|
|
28874
29853
|
if (from.getTime() >= to.getTime()) throw new ConnectedDataExportValidationError("from must be earlier than to.");
|
|
28875
|
-
|
|
28876
|
-
|
|
29854
|
+
const maxRangeDays = args.maxRangeDays === void 0 ? 90 : args.maxRangeDays;
|
|
29855
|
+
if (maxRangeDays !== null && to.getTime() - from.getTime() > maxRangeDays * 864e5) {
|
|
29856
|
+
throw new ConnectedDataExportValidationError(`A connected-data export can cover at most ${maxRangeDays} days per request.`);
|
|
28877
29857
|
}
|
|
28878
29858
|
if (to.getTime() > now.getTime() + 5 * 6e4) {
|
|
28879
29859
|
throw new ConnectedDataExportValidationError("to cannot be in the future.");
|
|
@@ -28883,10 +29863,18 @@ function resolveConnectedDataRange(args) {
|
|
|
28883
29863
|
function resolveConnectedDataExportRequest(args) {
|
|
28884
29864
|
const continuation = args.continuation;
|
|
28885
29865
|
if (!continuation) {
|
|
29866
|
+
const scope2 = resolveConnectedDataExportScope(args.requestedDataset, args.scope);
|
|
28886
29867
|
return {
|
|
28887
29868
|
dataset: args.requestedDataset,
|
|
28888
|
-
range: resolveConnectedDataRange({
|
|
28889
|
-
|
|
29869
|
+
range: resolveConnectedDataRange({
|
|
29870
|
+
from: args.from,
|
|
29871
|
+
to: args.to,
|
|
29872
|
+
lastDays: args.lastDays,
|
|
29873
|
+
now: args.now,
|
|
29874
|
+
maxRangeDays: args.requestedDataset === "slack_channel_messages" ? null : 90
|
|
29875
|
+
}),
|
|
29876
|
+
...args.cursor ? { cursor: args.cursor } : {},
|
|
29877
|
+
...scope2 ? { scope: scope2 } : {}
|
|
28890
29878
|
};
|
|
28891
29879
|
}
|
|
28892
29880
|
const supported = CONNECTED_DATA_DATASETS.filter((dataset2) => dataset2 !== "auto");
|
|
@@ -28909,10 +29897,38 @@ function resolveConnectedDataExportRequest(args) {
|
|
|
28909
29897
|
throw new ConnectedDataExportValidationError("dataset conflicts with continuation.");
|
|
28910
29898
|
}
|
|
28911
29899
|
const dataset = continuation.dataset;
|
|
29900
|
+
const scope = resolveConnectedDataExportScope(dataset, continuation.scope);
|
|
29901
|
+
if (args.scope && JSON.stringify(resolveConnectedDataExportScope(dataset, args.scope)) !== JSON.stringify(scope)) {
|
|
29902
|
+
throw new ConnectedDataExportValidationError("scope conflicts with continuation.");
|
|
29903
|
+
}
|
|
28912
29904
|
return {
|
|
28913
29905
|
dataset,
|
|
28914
|
-
range: resolveConnectedDataRange({
|
|
28915
|
-
|
|
29906
|
+
range: resolveConnectedDataRange({
|
|
29907
|
+
from: continuation.from,
|
|
29908
|
+
to: continuation.to,
|
|
29909
|
+
now: args.now,
|
|
29910
|
+
maxRangeDays: dataset === "slack_channel_messages" ? null : 90
|
|
29911
|
+
}),
|
|
29912
|
+
cursor: continuation.cursor,
|
|
29913
|
+
...scope ? { scope } : {}
|
|
29914
|
+
};
|
|
29915
|
+
}
|
|
29916
|
+
function resolveConnectedDataExportScope(dataset, scope) {
|
|
29917
|
+
if (dataset !== "slack_channel_messages") {
|
|
29918
|
+
if (scope?.slack) {
|
|
29919
|
+
throw new ConnectedDataExportValidationError("Slack export scope requires dataset slack_channel_messages.");
|
|
29920
|
+
}
|
|
29921
|
+
return void 0;
|
|
29922
|
+
}
|
|
29923
|
+
const channelId = scope?.slack?.channelId?.trim();
|
|
29924
|
+
if (!channelId || !/^[A-Za-z0-9_-]{2,100}$/.test(channelId)) {
|
|
29925
|
+
throw new ConnectedDataExportValidationError("Slack channel export requires a valid channelId.");
|
|
29926
|
+
}
|
|
29927
|
+
return {
|
|
29928
|
+
slack: {
|
|
29929
|
+
channelId,
|
|
29930
|
+
includeThreads: scope?.slack?.includeThreads !== false
|
|
29931
|
+
}
|
|
28916
29932
|
};
|
|
28917
29933
|
}
|
|
28918
29934
|
function previewRecord(value) {
|
|
@@ -28941,7 +29957,14 @@ function previewRecord(value) {
|
|
|
28941
29957
|
"createdAt",
|
|
28942
29958
|
"updatedAt",
|
|
28943
29959
|
"hasTranscript",
|
|
28944
|
-
"contentTruncated"
|
|
29960
|
+
"contentTruncated",
|
|
29961
|
+
"channelId",
|
|
29962
|
+
"threadTs",
|
|
29963
|
+
"parentMessageTs",
|
|
29964
|
+
"occurredAt",
|
|
29965
|
+
"userId",
|
|
29966
|
+
"text",
|
|
29967
|
+
"replyCount"
|
|
28945
29968
|
]) {
|
|
28946
29969
|
if (record[key] === void 0) continue;
|
|
28947
29970
|
const item = record[key];
|
|
@@ -28984,7 +30007,8 @@ async function collectConnectedDataExport(args) {
|
|
|
28984
30007
|
dataset: args.dataset,
|
|
28985
30008
|
from: args.range.from,
|
|
28986
30009
|
to: args.range.to,
|
|
28987
|
-
pageSize: Math.min(25, maxItems - records.length),
|
|
30010
|
+
pageSize: Math.min(args.dataset === "slack_channel_messages" ? 200 : 25, maxItems - records.length),
|
|
30011
|
+
...args.scope ? { scope: args.scope } : {},
|
|
28988
30012
|
...cursor !== void 0 && cursor !== null ? { cursor } : {}
|
|
28989
30013
|
});
|
|
28990
30014
|
pages++;
|
|
@@ -29029,6 +30053,7 @@ async function collectConnectedDataExport(args) {
|
|
|
29029
30053
|
providerConfigKey,
|
|
29030
30054
|
dataset,
|
|
29031
30055
|
range: args.range,
|
|
30056
|
+
...args.scope ? { scope: args.scope } : {},
|
|
29032
30057
|
complete,
|
|
29033
30058
|
counts: { pages, listed, exported: records.length, failed },
|
|
29034
30059
|
untrustedContent: true,
|
|
@@ -29048,6 +30073,7 @@ ${lines.length ? `${lines.join("\n")}
|
|
|
29048
30073
|
providerConfigKey,
|
|
29049
30074
|
dataset,
|
|
29050
30075
|
range: args.range,
|
|
30076
|
+
...args.scope ? { scope: args.scope } : {},
|
|
29051
30077
|
counts: { pages, listed, exported: records.length, failed, bytes },
|
|
29052
30078
|
complete,
|
|
29053
30079
|
...!artifact ? { records } : {},
|
|
@@ -29057,7 +30083,8 @@ ${lines.length ? `${lines.join("\n")}
|
|
|
29057
30083
|
cursor: continuationCursor,
|
|
29058
30084
|
from: args.range.from,
|
|
29059
30085
|
to: args.range.to,
|
|
29060
|
-
dataset
|
|
30086
|
+
dataset,
|
|
30087
|
+
...args.scope ? { scope: args.scope } : {}
|
|
29061
30088
|
} : null,
|
|
29062
30089
|
warnings: [...warnings],
|
|
29063
30090
|
untrustedContent: true
|
|
@@ -29227,6 +30254,97 @@ async function getFreeCreditBreakdown(userId) {
|
|
|
29227
30254
|
|
|
29228
30255
|
// src/api/memory-db.ts
|
|
29229
30256
|
import { neon } from "@neondatabase/serverless";
|
|
30257
|
+
|
|
30258
|
+
// src/api/memory-universe.ts
|
|
30259
|
+
var SEP = "\n";
|
|
30260
|
+
var WIKILINK = /\[\[([^\][]+)\]\]/g;
|
|
30261
|
+
var HASH = /#[^]*$/;
|
|
30262
|
+
var CONNECTION_RE = /^Connection - (.+) - ([A-Za-z0-9_-]+)$/;
|
|
30263
|
+
function stripExt(value) {
|
|
30264
|
+
return value.replace(/\.(md|markdown|txt)$/i, "");
|
|
30265
|
+
}
|
|
30266
|
+
function vaultLabel(vault) {
|
|
30267
|
+
const provider = vault.match(CONNECTION_RE)?.[1];
|
|
30268
|
+
if (!provider) return vault;
|
|
30269
|
+
return provider.split(/[\s_-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
30270
|
+
}
|
|
30271
|
+
function noteTags(props) {
|
|
30272
|
+
const tags = props.tags;
|
|
30273
|
+
return Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [];
|
|
30274
|
+
}
|
|
30275
|
+
function wikiTargets(content) {
|
|
30276
|
+
const targets = [];
|
|
30277
|
+
for (const match of content.matchAll(WIKILINK)) {
|
|
30278
|
+
const target = match[1].split("|", 1)[0].replace(HASH, "").trim();
|
|
30279
|
+
if (target) targets.push(target);
|
|
30280
|
+
}
|
|
30281
|
+
return targets;
|
|
30282
|
+
}
|
|
30283
|
+
function buildResolver(notes) {
|
|
30284
|
+
const index = /* @__PURE__ */ new Map();
|
|
30285
|
+
const add = (key, note) => {
|
|
30286
|
+
const normalized = key.trim().toLowerCase();
|
|
30287
|
+
if (normalized && !index.has(normalized)) index.set(normalized, note);
|
|
30288
|
+
};
|
|
30289
|
+
for (const note of notes) add(`${note.vault}/${note.path}`, note);
|
|
30290
|
+
for (const note of notes) add(note.path, note);
|
|
30291
|
+
for (const note of notes) add(stripExt(note.path), note);
|
|
30292
|
+
for (const note of notes) if (note.title) add(note.title, note);
|
|
30293
|
+
for (const note of notes) {
|
|
30294
|
+
const base = note.path.split("/").pop() ?? note.path;
|
|
30295
|
+
add(base, note);
|
|
30296
|
+
add(stripExt(base), note);
|
|
30297
|
+
}
|
|
30298
|
+
return (target) => {
|
|
30299
|
+
const normalized = target.trim().toLowerCase();
|
|
30300
|
+
return index.get(normalized) ?? index.get(stripExt(normalized)) ?? null;
|
|
30301
|
+
};
|
|
30302
|
+
}
|
|
30303
|
+
function buildUniverseData(notes) {
|
|
30304
|
+
const resolver = buildResolver(notes);
|
|
30305
|
+
const nodes = [];
|
|
30306
|
+
const indexById = /* @__PURE__ */ new Map();
|
|
30307
|
+
const nodeFor = (id, label, group, ghost, extra) => {
|
|
30308
|
+
const existing = indexById.get(id);
|
|
30309
|
+
if (existing !== void 0) return existing;
|
|
30310
|
+
const index = nodes.length;
|
|
30311
|
+
indexById.set(id, index);
|
|
30312
|
+
nodes.push({ id, label, group, ghost, ...extra });
|
|
30313
|
+
return index;
|
|
30314
|
+
};
|
|
30315
|
+
for (const note of notes) {
|
|
30316
|
+
nodeFor(
|
|
30317
|
+
`${note.vault}${SEP}${note.path}`,
|
|
30318
|
+
note.title.trim() || note.path.split("/").pop() || note.path,
|
|
30319
|
+
vaultLabel(note.vault),
|
|
30320
|
+
false,
|
|
30321
|
+
{ sub: note.path, tags: noteTags(note.props), updatedAt: note.updatedAt }
|
|
30322
|
+
);
|
|
30323
|
+
}
|
|
30324
|
+
const links = [];
|
|
30325
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
30326
|
+
for (const note of notes) {
|
|
30327
|
+
const from = indexById.get(`${note.vault}${SEP}${note.path}`);
|
|
30328
|
+
if (from === void 0) continue;
|
|
30329
|
+
for (const target of wikiTargets(note.content)) {
|
|
30330
|
+
const resolved = resolver(target);
|
|
30331
|
+
const to = resolved ? nodeFor(
|
|
30332
|
+
`${resolved.vault}${SEP}${resolved.path}`,
|
|
30333
|
+
resolved.title.trim() || resolved.path.split("/").pop() || resolved.path,
|
|
30334
|
+
vaultLabel(resolved.vault),
|
|
30335
|
+
false
|
|
30336
|
+
) : nodeFor(`ghost:${target.toLowerCase()}`, target, "", true);
|
|
30337
|
+
if (to === from) continue;
|
|
30338
|
+
const edgeKey = from < to ? `${from}:${to}` : `${to}:${from}`;
|
|
30339
|
+
if (seenEdges.has(edgeKey)) continue;
|
|
30340
|
+
seenEdges.add(edgeKey);
|
|
30341
|
+
links.push({ s: from, t: to });
|
|
30342
|
+
}
|
|
30343
|
+
}
|
|
30344
|
+
return { nodes, links, total: notes.length, built: notes.length, failed: 0 };
|
|
30345
|
+
}
|
|
30346
|
+
|
|
30347
|
+
// src/api/memory-db.ts
|
|
29230
30348
|
var _sql = null;
|
|
29231
30349
|
function sql() {
|
|
29232
30350
|
if (_sql) return _sql;
|
|
@@ -29371,6 +30489,35 @@ async function dbNote(identity, vault, path5) {
|
|
|
29371
30489
|
capturedAt: r.captured_at
|
|
29372
30490
|
};
|
|
29373
30491
|
}
|
|
30492
|
+
async function dbUniverse(identity, includeConnections = false) {
|
|
30493
|
+
const entitled = await entitledVaultRows(identity);
|
|
30494
|
+
const selected = entitled.filter(
|
|
30495
|
+
(entry) => includeConnections || !/^Connection - (.+) - ([A-Za-z0-9_-]+)$/.test(entry.vault)
|
|
30496
|
+
);
|
|
30497
|
+
if (selected.length === 0) return buildUniverseData([]);
|
|
30498
|
+
const logicalByPhysical = new Map(selected.map((entry) => [entry.physical, entry.vault]));
|
|
30499
|
+
const physicals = [...logicalByPhysical.keys()];
|
|
30500
|
+
const rows = await sql().query(
|
|
30501
|
+
`SELECT vault, path, title, content, updated_at, props
|
|
30502
|
+
FROM mem_notes
|
|
30503
|
+
WHERE vault = ANY($1)
|
|
30504
|
+
ORDER BY updated_at DESC`,
|
|
30505
|
+
[physicals]
|
|
30506
|
+
);
|
|
30507
|
+
const notes = rows.flatMap((row) => {
|
|
30508
|
+
const vault = logicalByPhysical.get(row.vault);
|
|
30509
|
+
if (!vault) return [];
|
|
30510
|
+
return [{
|
|
30511
|
+
vault,
|
|
30512
|
+
path: row.path,
|
|
30513
|
+
title: row.title,
|
|
30514
|
+
content: row.content,
|
|
30515
|
+
updatedAt: row.updated_at,
|
|
30516
|
+
props: row.props && typeof row.props === "object" ? row.props : {}
|
|
30517
|
+
}];
|
|
30518
|
+
});
|
|
30519
|
+
return buildUniverseData(notes);
|
|
30520
|
+
}
|
|
29374
30521
|
var FREE_COST_CAP_USD = 1;
|
|
29375
30522
|
async function dbUsage(identity, plan) {
|
|
29376
30523
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -29400,10 +30547,379 @@ async function dbUsage(identity, plan) {
|
|
|
29400
30547
|
}
|
|
29401
30548
|
|
|
29402
30549
|
// src/api/nango-control.ts
|
|
29403
|
-
import { createHash as
|
|
30550
|
+
import { createHash as createHash14, randomUUID as randomUUID17 } from "crypto";
|
|
30551
|
+
|
|
30552
|
+
// src/api/slack-archive-analysis.ts
|
|
30553
|
+
function isRecord2(value) {
|
|
30554
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
30555
|
+
}
|
|
30556
|
+
function cleanString(value) {
|
|
30557
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
30558
|
+
}
|
|
30559
|
+
function slackWeekStart(value) {
|
|
30560
|
+
if (!value) return null;
|
|
30561
|
+
const date = new Date(value);
|
|
30562
|
+
if (!Number.isFinite(date.getTime())) return null;
|
|
30563
|
+
const daysSinceMonday = (date.getUTCDay() + 6) % 7;
|
|
30564
|
+
date.setUTCDate(date.getUTCDate() - daysSinceMonday);
|
|
30565
|
+
return date.toISOString().slice(0, 10);
|
|
30566
|
+
}
|
|
30567
|
+
function normalizedUrl(value) {
|
|
30568
|
+
const candidate = cleanString(value)?.split("|", 1)[0]?.replace(/[),.;!?]+$/, "");
|
|
30569
|
+
if (!candidate || !/^https?:\/\//i.test(candidate)) return null;
|
|
30570
|
+
try {
|
|
30571
|
+
return new URL(candidate).toString();
|
|
30572
|
+
} catch {
|
|
30573
|
+
return null;
|
|
30574
|
+
}
|
|
30575
|
+
}
|
|
30576
|
+
function deriveSlackMessageIndexFields(args) {
|
|
30577
|
+
const fileNames = args.files.flatMap((file) => {
|
|
30578
|
+
if (!isRecord2(file)) return [];
|
|
30579
|
+
const name = cleanString(file.name) ?? cleanString(file.title) ?? cleanString(file.id);
|
|
30580
|
+
return name ? [name] : [];
|
|
30581
|
+
});
|
|
30582
|
+
const urls = /* @__PURE__ */ new Set();
|
|
30583
|
+
const text2 = args.text ?? "";
|
|
30584
|
+
for (const match of text2.matchAll(/<(https?:\/\/[^>|]+)(?:\|[^>]*)?>/gi)) {
|
|
30585
|
+
const url = normalizedUrl(match[1]);
|
|
30586
|
+
if (url) urls.add(url);
|
|
30587
|
+
}
|
|
30588
|
+
for (const match of text2.matchAll(/https?:\/\/[^\s<>()]+/gi)) {
|
|
30589
|
+
const url = normalizedUrl(match[0]);
|
|
30590
|
+
if (url) urls.add(url);
|
|
30591
|
+
}
|
|
30592
|
+
const attachments = Array.isArray(args.providerData.attachments) ? args.providerData.attachments : [];
|
|
30593
|
+
for (const attachment of attachments) {
|
|
30594
|
+
if (!isRecord2(attachment)) continue;
|
|
30595
|
+
for (const key of ["url", "title_link", "from_url", "service_url"]) {
|
|
30596
|
+
const url = normalizedUrl(attachment[key]);
|
|
30597
|
+
if (url) urls.add(url);
|
|
30598
|
+
}
|
|
30599
|
+
}
|
|
30600
|
+
const profile = isRecord2(args.providerData.user_profile) ? args.providerData.user_profile : isRecord2(args.providerData.profile) ? args.providerData.profile : null;
|
|
30601
|
+
const authorName = cleanString(profile?.display_name) ?? cleanString(profile?.real_name) ?? cleanString(args.providerData.username) ?? cleanString(args.providerData.user_name);
|
|
30602
|
+
return {
|
|
30603
|
+
weekStart: slackWeekStart(args.occurredAt),
|
|
30604
|
+
authorName,
|
|
30605
|
+
fileCount: args.files.length,
|
|
30606
|
+
hasFiles: args.files.length > 0,
|
|
30607
|
+
fileNames,
|
|
30608
|
+
linkCount: urls.size,
|
|
30609
|
+
links: [...urls]
|
|
30610
|
+
};
|
|
30611
|
+
}
|
|
30612
|
+
|
|
30613
|
+
// src/api/slack-connected-data-export.ts
|
|
30614
|
+
var CURSOR_PREFIX = "slack1.";
|
|
30615
|
+
var MAX_CURSOR_BYTES = 64 * 1024;
|
|
30616
|
+
var MAX_PENDING_THREADS = 200;
|
|
30617
|
+
var THREAD_CONCURRENCY = 3;
|
|
30618
|
+
var MAX_SLACK_RETRIES = 3;
|
|
30619
|
+
function isRecord3(value) {
|
|
30620
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
30621
|
+
}
|
|
30622
|
+
function cleanString2(value, maxLength = 1e4) {
|
|
30623
|
+
return typeof value === "string" && value.trim() ? value.trim().slice(0, maxLength) : null;
|
|
30624
|
+
}
|
|
30625
|
+
function unwrapToolResult(value) {
|
|
30626
|
+
if (!isRecord3(value)) return value;
|
|
30627
|
+
if (isRecord3(value.structuredContent)) return value.structuredContent;
|
|
30628
|
+
if (Array.isArray(value.content)) {
|
|
30629
|
+
const textPart = value.content.find((part) => isRecord3(part) && part.type === "text" && typeof part.text === "string");
|
|
30630
|
+
if (isRecord3(textPart) && typeof textPart.text === "string") {
|
|
30631
|
+
try {
|
|
30632
|
+
return unwrapToolResult(JSON.parse(textPart.text));
|
|
30633
|
+
} catch {
|
|
30634
|
+
return textPart.text;
|
|
30635
|
+
}
|
|
30636
|
+
}
|
|
30637
|
+
}
|
|
30638
|
+
if (isRecord3(value.result)) return unwrapToolResult(value.result);
|
|
30639
|
+
if (isRecord3(value.data)) return unwrapToolResult(value.data);
|
|
30640
|
+
return value;
|
|
30641
|
+
}
|
|
30642
|
+
function messagesFromToolResult(value) {
|
|
30643
|
+
const root = unwrapToolResult(value);
|
|
30644
|
+
if (Array.isArray(root)) return root.filter(isRecord3);
|
|
30645
|
+
if (!isRecord3(root)) return [];
|
|
30646
|
+
for (const key of ["messages", "replies", "items", "data"]) {
|
|
30647
|
+
if (Array.isArray(root[key])) return root[key].filter(isRecord3);
|
|
30648
|
+
}
|
|
30649
|
+
return [];
|
|
30650
|
+
}
|
|
30651
|
+
function nextCursorFromToolResult(value) {
|
|
30652
|
+
const root = unwrapToolResult(value);
|
|
30653
|
+
if (!isRecord3(root)) return null;
|
|
30654
|
+
const metadata = isRecord3(root.response_metadata) ? root.response_metadata : isRecord3(root.responseMetadata) ? root.responseMetadata : isRecord3(root.metadata) ? root.metadata : null;
|
|
30655
|
+
return cleanString2(
|
|
30656
|
+
root.next_cursor ?? root.nextCursor ?? metadata?.next_cursor ?? metadata?.nextCursor,
|
|
30657
|
+
1e4
|
|
30658
|
+
);
|
|
30659
|
+
}
|
|
30660
|
+
function slackTimestampToIso(value) {
|
|
30661
|
+
const timestamp = cleanString2(value, 64);
|
|
30662
|
+
if (!timestamp || !/^\d{1,20}(?:\.\d{1,9})?$/.test(timestamp)) return null;
|
|
30663
|
+
const milliseconds = Number(timestamp) * 1e3;
|
|
30664
|
+
if (!Number.isFinite(milliseconds)) return null;
|
|
30665
|
+
try {
|
|
30666
|
+
return new Date(milliseconds).toISOString();
|
|
30667
|
+
} catch {
|
|
30668
|
+
return null;
|
|
30669
|
+
}
|
|
30670
|
+
}
|
|
30671
|
+
function normalizeSlackMessage(row, channelId, parentMessageTs) {
|
|
30672
|
+
const timestamp = cleanString2(row.ts, 64);
|
|
30673
|
+
const replyCount = typeof row.reply_count === "number" && Number.isFinite(row.reply_count) ? Math.max(0, Math.floor(row.reply_count)) : 0;
|
|
30674
|
+
const rowThreadTs = cleanString2(row.thread_ts, 64);
|
|
30675
|
+
const inferredParentMessageTs = parentMessageTs ?? (rowThreadTs && rowThreadTs !== timestamp ? rowThreadTs : null);
|
|
30676
|
+
const threadTs = inferredParentMessageTs ?? rowThreadTs ?? (replyCount > 0 ? timestamp : null);
|
|
30677
|
+
const occurredAt = slackTimestampToIso(timestamp);
|
|
30678
|
+
const files = Array.isArray(row.files) ? row.files : [];
|
|
30679
|
+
const text2 = typeof row.text === "string" ? row.text : null;
|
|
30680
|
+
return {
|
|
30681
|
+
recordType: inferredParentMessageTs ? "slack_thread_reply" : "slack_message",
|
|
30682
|
+
dataset: "slack_channel_messages",
|
|
30683
|
+
id: timestamp ? `${channelId}:${timestamp}` : `${channelId}:unknown`,
|
|
30684
|
+
providerRecordId: timestamp,
|
|
30685
|
+
channelId,
|
|
30686
|
+
threadTs,
|
|
30687
|
+
parentMessageTs: inferredParentMessageTs,
|
|
30688
|
+
isThreadReply: inferredParentMessageTs !== null,
|
|
30689
|
+
occurredAt,
|
|
30690
|
+
userId: cleanString2(row.user, 100) ?? cleanString2(row.bot_id, 100),
|
|
30691
|
+
text: text2,
|
|
30692
|
+
subtype: cleanString2(row.subtype, 100),
|
|
30693
|
+
replyCount,
|
|
30694
|
+
files,
|
|
30695
|
+
reactions: Array.isArray(row.reactions) ? row.reactions : [],
|
|
30696
|
+
providerData: row,
|
|
30697
|
+
...deriveSlackMessageIndexFields({
|
|
30698
|
+
occurredAt,
|
|
30699
|
+
text: text2,
|
|
30700
|
+
files,
|
|
30701
|
+
providerData: row
|
|
30702
|
+
})
|
|
30703
|
+
};
|
|
30704
|
+
}
|
|
30705
|
+
function encodeCursor(state) {
|
|
30706
|
+
const encoded = `${CURSOR_PREFIX}${Buffer.from(JSON.stringify(state)).toString("base64url")}`;
|
|
30707
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_CURSOR_BYTES) {
|
|
30708
|
+
throw new Error("slack_export_cursor_too_large");
|
|
30709
|
+
}
|
|
30710
|
+
return encoded;
|
|
30711
|
+
}
|
|
30712
|
+
function validProviderCursor(value) {
|
|
30713
|
+
return value === null || typeof value === "string" && value.length > 0 && value.length <= 1e4;
|
|
30714
|
+
}
|
|
30715
|
+
function decodeCursor(cursor, channelId, includeThreads) {
|
|
30716
|
+
if (!cursor) {
|
|
30717
|
+
return { version: 1, phase: "history", channelId, includeThreads, historyCursor: null };
|
|
30718
|
+
}
|
|
30719
|
+
if (!cursor.startsWith(CURSOR_PREFIX) || Buffer.byteLength(cursor, "utf8") > MAX_CURSOR_BYTES) {
|
|
30720
|
+
throw new Error("slack_export_cursor_invalid");
|
|
30721
|
+
}
|
|
30722
|
+
let parsed;
|
|
30723
|
+
try {
|
|
30724
|
+
parsed = JSON.parse(Buffer.from(cursor.slice(CURSOR_PREFIX.length), "base64url").toString("utf8"));
|
|
30725
|
+
} catch {
|
|
30726
|
+
throw new Error("slack_export_cursor_invalid");
|
|
30727
|
+
}
|
|
30728
|
+
if (!isRecord3(parsed) || parsed.version !== 1 || parsed.channelId !== channelId || parsed.includeThreads !== includeThreads || !validProviderCursor(parsed.historyCursor)) {
|
|
30729
|
+
throw new Error("slack_export_cursor_invalid");
|
|
30730
|
+
}
|
|
30731
|
+
if (parsed.phase === "history") {
|
|
30732
|
+
return {
|
|
30733
|
+
version: 1,
|
|
30734
|
+
phase: "history",
|
|
30735
|
+
channelId,
|
|
30736
|
+
includeThreads,
|
|
30737
|
+
historyCursor: parsed.historyCursor
|
|
30738
|
+
};
|
|
30739
|
+
}
|
|
30740
|
+
if (parsed.phase !== "threads" || includeThreads !== true || !Array.isArray(parsed.historyMessageTs) || !Array.isArray(parsed.threads) || parsed.threads.length < 1 || parsed.threads.length > MAX_PENDING_THREADS) {
|
|
30741
|
+
throw new Error("slack_export_cursor_invalid");
|
|
30742
|
+
}
|
|
30743
|
+
const historyMessageTs = parsed.historyMessageTs.flatMap((value) => {
|
|
30744
|
+
const timestamp = cleanString2(value, 64);
|
|
30745
|
+
return timestamp && /^\d{1,20}(?:\.\d{1,9})?$/.test(timestamp) ? [timestamp] : [];
|
|
30746
|
+
});
|
|
30747
|
+
if (historyMessageTs.length !== parsed.historyMessageTs.length || historyMessageTs.length > MAX_PENDING_THREADS) {
|
|
30748
|
+
throw new Error("slack_export_cursor_invalid");
|
|
30749
|
+
}
|
|
30750
|
+
const threads = parsed.threads.flatMap((value) => {
|
|
30751
|
+
if (!isRecord3(value)) return [];
|
|
30752
|
+
const threadTs = cleanString2(value.threadTs, 64);
|
|
30753
|
+
if (!threadTs || !/^\d{1,20}(?:\.\d{1,9})?$/.test(threadTs) || !validProviderCursor(value.cursor)) return [];
|
|
30754
|
+
return [{ threadTs, cursor: value.cursor }];
|
|
30755
|
+
});
|
|
30756
|
+
if (threads.length !== parsed.threads.length) throw new Error("slack_export_cursor_invalid");
|
|
30757
|
+
return {
|
|
30758
|
+
version: 1,
|
|
30759
|
+
phase: "threads",
|
|
30760
|
+
channelId,
|
|
30761
|
+
includeThreads: true,
|
|
30762
|
+
historyCursor: parsed.historyCursor,
|
|
30763
|
+
historyMessageTs,
|
|
30764
|
+
threads
|
|
30765
|
+
};
|
|
30766
|
+
}
|
|
30767
|
+
function slackBoundaryTimestamp(iso, offsetMicroseconds) {
|
|
30768
|
+
const milliseconds = new Date(iso).getTime();
|
|
30769
|
+
if (!Number.isFinite(milliseconds)) throw new Error("slack_export_range_invalid");
|
|
30770
|
+
return Math.max(0, milliseconds / 1e3 + offsetMicroseconds / 1e6).toFixed(6);
|
|
30771
|
+
}
|
|
30772
|
+
function errorText(error) {
|
|
30773
|
+
if (error instanceof Error) return `${error.name} ${error.message}`;
|
|
30774
|
+
if (typeof error === "string") return error;
|
|
30775
|
+
try {
|
|
30776
|
+
return JSON.stringify(error);
|
|
30777
|
+
} catch {
|
|
30778
|
+
return "";
|
|
30779
|
+
}
|
|
30780
|
+
}
|
|
30781
|
+
function retryAfterMilliseconds(error, attempt) {
|
|
30782
|
+
const text2 = errorText(error);
|
|
30783
|
+
if (!/429|rate.?limit|resource_exhausted/i.test(text2)) return null;
|
|
30784
|
+
if (isRecord3(error) && typeof error.retryAfterMs === "number" && Number.isFinite(error.retryAfterMs)) {
|
|
30785
|
+
return Math.min(Math.max(error.retryAfterMs, 1e3), 6e4);
|
|
30786
|
+
}
|
|
30787
|
+
const explicit = text2.match(/retry[_ -]?after[^0-9]{0,20}(\d{1,4})/i);
|
|
30788
|
+
const seconds = explicit ? Number(explicit[1]) : attempt;
|
|
30789
|
+
return Math.min(Math.max(seconds, 1) * 1e3, 6e4);
|
|
30790
|
+
}
|
|
30791
|
+
async function callSlackToolWithRetry(dependencies, args) {
|
|
30792
|
+
for (let attempt = 1; attempt <= MAX_SLACK_RETRIES; attempt += 1) {
|
|
30793
|
+
try {
|
|
30794
|
+
return await dependencies.callTool(args);
|
|
30795
|
+
} catch (error) {
|
|
30796
|
+
const delay = retryAfterMilliseconds(error, attempt);
|
|
30797
|
+
if (delay === null || attempt === MAX_SLACK_RETRIES) throw error;
|
|
30798
|
+
await (dependencies.sleep ?? ((milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))))(delay);
|
|
30799
|
+
}
|
|
30800
|
+
}
|
|
30801
|
+
throw new Error("slack_export_retry_exhausted");
|
|
30802
|
+
}
|
|
30803
|
+
function nextAfterThreads(state) {
|
|
30804
|
+
if (state.threads.length > 0) return { nextCursor: encodeCursor(state), complete: false };
|
|
30805
|
+
if (state.historyCursor) {
|
|
30806
|
+
return {
|
|
30807
|
+
nextCursor: encodeCursor({
|
|
30808
|
+
version: 1,
|
|
30809
|
+
phase: "history",
|
|
30810
|
+
channelId: state.channelId,
|
|
30811
|
+
includeThreads: true,
|
|
30812
|
+
historyCursor: state.historyCursor
|
|
30813
|
+
}),
|
|
30814
|
+
complete: false
|
|
30815
|
+
};
|
|
30816
|
+
}
|
|
30817
|
+
return { nextCursor: null, complete: true };
|
|
30818
|
+
}
|
|
30819
|
+
async function exportSlackChannelPage(input, dependencies) {
|
|
30820
|
+
const channelId = input.scope?.slack?.channelId?.trim();
|
|
30821
|
+
if (!channelId) throw new Error("slack_export_channel_required");
|
|
30822
|
+
const includeThreads = input.scope?.slack?.includeThreads !== false;
|
|
30823
|
+
const state = decodeCursor(input.cursor, channelId, includeThreads);
|
|
30824
|
+
const pageSize = Math.max(1, Math.min(200, Math.floor(input.pageSize)));
|
|
30825
|
+
if (state.phase === "history") {
|
|
30826
|
+
const result = await callSlackToolWithRetry(dependencies, {
|
|
30827
|
+
tool: "get-conversation-history",
|
|
30828
|
+
input: {
|
|
30829
|
+
channel_id: channelId,
|
|
30830
|
+
limit: pageSize,
|
|
30831
|
+
oldest: slackBoundaryTimestamp(input.from, -1),
|
|
30832
|
+
latest: slackBoundaryTimestamp(input.to, 0),
|
|
30833
|
+
inclusive: false,
|
|
30834
|
+
...state.historyCursor ? { cursor: state.historyCursor } : {}
|
|
30835
|
+
},
|
|
30836
|
+
requestId: `slack-export:${input.connectionId}:${channelId}:history:${state.historyCursor ?? "start"}`
|
|
30837
|
+
});
|
|
30838
|
+
const messages = messagesFromToolResult(result).slice(0, pageSize);
|
|
30839
|
+
const historyCursor = nextCursorFromToolResult(result);
|
|
30840
|
+
const records2 = messages.map((message) => normalizeSlackMessage(message, channelId, null));
|
|
30841
|
+
const threadIds = includeThreads ? [...new Set(messages.flatMap((message) => {
|
|
30842
|
+
const replyCount = typeof message.reply_count === "number" ? message.reply_count : 0;
|
|
30843
|
+
const timestamp = cleanString2(message.ts, 64);
|
|
30844
|
+
return replyCount > 0 && timestamp ? [timestamp] : [];
|
|
30845
|
+
}))].slice(0, MAX_PENDING_THREADS) : [];
|
|
30846
|
+
if (threadIds.length > 0) {
|
|
30847
|
+
return {
|
|
30848
|
+
providerConfigKey: "slack",
|
|
30849
|
+
dataset: "slack_channel_messages",
|
|
30850
|
+
records: records2,
|
|
30851
|
+
nextCursor: encodeCursor({
|
|
30852
|
+
version: 1,
|
|
30853
|
+
phase: "threads",
|
|
30854
|
+
channelId,
|
|
30855
|
+
includeThreads: true,
|
|
30856
|
+
historyCursor,
|
|
30857
|
+
historyMessageTs: messages.flatMap((message) => cleanString2(message.ts, 64) ?? []),
|
|
30858
|
+
threads: threadIds.map((threadTs) => ({ threadTs, cursor: null }))
|
|
30859
|
+
}),
|
|
30860
|
+
complete: false,
|
|
30861
|
+
counts: { listed: messages.length, exported: records2.length, failed: 0 },
|
|
30862
|
+
warnings: threadIds.length === MAX_PENDING_THREADS ? ["A history page reached the per-page threaded-parent safety limit; narrow the export range if threads are missing."] : [],
|
|
30863
|
+
untrustedContent: true
|
|
30864
|
+
};
|
|
30865
|
+
}
|
|
30866
|
+
return {
|
|
30867
|
+
providerConfigKey: "slack",
|
|
30868
|
+
dataset: "slack_channel_messages",
|
|
30869
|
+
records: records2,
|
|
30870
|
+
nextCursor: historyCursor ? encodeCursor({ ...state, historyCursor }) : null,
|
|
30871
|
+
complete: historyCursor === null,
|
|
30872
|
+
counts: { listed: messages.length, exported: records2.length, failed: 0 },
|
|
30873
|
+
warnings: [],
|
|
30874
|
+
untrustedContent: true
|
|
30875
|
+
};
|
|
30876
|
+
}
|
|
30877
|
+
const batchSize = Math.min(THREAD_CONCURRENCY, state.threads.length, pageSize);
|
|
30878
|
+
const batch = state.threads.slice(0, batchSize);
|
|
30879
|
+
const perThreadLimit = Math.max(1, Math.min(100, Math.floor(pageSize / batchSize)));
|
|
30880
|
+
const results = await Promise.all(batch.map((thread) => callSlackToolWithRetry(dependencies, {
|
|
30881
|
+
tool: "get-thread-replies",
|
|
30882
|
+
input: {
|
|
30883
|
+
channel_id: channelId,
|
|
30884
|
+
thread_ts: thread.threadTs,
|
|
30885
|
+
limit: perThreadLimit,
|
|
30886
|
+
...thread.cursor ? { cursor: thread.cursor } : {}
|
|
30887
|
+
},
|
|
30888
|
+
requestId: `slack-export:${input.connectionId}:${channelId}:thread:${thread.threadTs}:${thread.cursor ?? "start"}`
|
|
30889
|
+
})));
|
|
30890
|
+
const records = [];
|
|
30891
|
+
const stillPending = [];
|
|
30892
|
+
let listed = 0;
|
|
30893
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
30894
|
+
const thread = batch[index];
|
|
30895
|
+
const messages = messagesFromToolResult(results[index]);
|
|
30896
|
+
listed += messages.length;
|
|
30897
|
+
records.push(...messages.filter((message) => {
|
|
30898
|
+
const timestamp = cleanString2(message.ts, 64);
|
|
30899
|
+
return timestamp !== thread.threadTs && !state.historyMessageTs.includes(timestamp ?? "");
|
|
30900
|
+
}).map((message) => normalizeSlackMessage(message, channelId, thread.threadTs)));
|
|
30901
|
+
const cursor = nextCursorFromToolResult(results[index]);
|
|
30902
|
+
if (cursor) stillPending.push({ threadTs: thread.threadTs, cursor });
|
|
30903
|
+
}
|
|
30904
|
+
const nextState = {
|
|
30905
|
+
...state,
|
|
30906
|
+
threads: [...stillPending, ...state.threads.slice(batchSize)]
|
|
30907
|
+
};
|
|
30908
|
+
const next = nextAfterThreads(nextState);
|
|
30909
|
+
return {
|
|
30910
|
+
providerConfigKey: "slack",
|
|
30911
|
+
dataset: "slack_channel_messages",
|
|
30912
|
+
records,
|
|
30913
|
+
nextCursor: next.nextCursor,
|
|
30914
|
+
complete: next.complete,
|
|
30915
|
+
counts: { listed, exported: records.length, failed: 0 },
|
|
30916
|
+
warnings: [],
|
|
30917
|
+
untrustedContent: true
|
|
30918
|
+
};
|
|
30919
|
+
}
|
|
29404
30920
|
|
|
29405
30921
|
// src/api/main-nango-transport.ts
|
|
29406
|
-
import { createHash as
|
|
30922
|
+
import { createHash as createHash13, randomUUID as randomUUID16 } from "crypto";
|
|
29407
30923
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
29408
30924
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
29409
30925
|
|
|
@@ -29678,12 +31194,14 @@ var MainNangoTransportError = class extends Error {
|
|
|
29678
31194
|
status;
|
|
29679
31195
|
code;
|
|
29680
31196
|
retryable;
|
|
29681
|
-
|
|
31197
|
+
retryAfterMs;
|
|
31198
|
+
constructor(message, status, code, retryable = status === 429 || status >= 500, retryAfterMs = null) {
|
|
29682
31199
|
super(message);
|
|
29683
31200
|
this.name = "MainNangoTransportError";
|
|
29684
31201
|
this.status = status;
|
|
29685
31202
|
this.code = code;
|
|
29686
31203
|
this.retryable = retryable;
|
|
31204
|
+
this.retryAfterMs = retryAfterMs;
|
|
29687
31205
|
}
|
|
29688
31206
|
};
|
|
29689
31207
|
function nangoClientErrorText(error) {
|
|
@@ -29695,6 +31213,13 @@ function nangoClientErrorText(error) {
|
|
|
29695
31213
|
return "";
|
|
29696
31214
|
}
|
|
29697
31215
|
}
|
|
31216
|
+
function retryAfterMsFromText(value) {
|
|
31217
|
+
const milliseconds = value.match(/retry[_ -]?after[_ -]?ms[^0-9]{0,20}(\d{1,7})/i);
|
|
31218
|
+
if (milliseconds) return Math.min(Math.max(Number(milliseconds[1]), 1e3), 6e4);
|
|
31219
|
+
const seconds = value.match(/retry[_ -]?after[^0-9]{0,20}(\d{1,4})/i);
|
|
31220
|
+
if (seconds) return Math.min(Math.max(Number(seconds[1]), 1) * 1e3, 6e4);
|
|
31221
|
+
return null;
|
|
31222
|
+
}
|
|
29698
31223
|
function normalizeNangoClientError(error) {
|
|
29699
31224
|
if (error instanceof MainNangoTransportError) return error;
|
|
29700
31225
|
const detail = nangoClientErrorText(error);
|
|
@@ -29715,7 +31240,13 @@ function normalizeNangoClientError(error) {
|
|
|
29715
31240
|
);
|
|
29716
31241
|
}
|
|
29717
31242
|
if (/RESOURCE_EXHAUSTED|rate.?limit|status["']?\s*:\s*429/i.test(detail)) {
|
|
29718
|
-
return new MainNangoTransportError(
|
|
31243
|
+
return new MainNangoTransportError(
|
|
31244
|
+
"The connected provider is temporarily rate limited.",
|
|
31245
|
+
429,
|
|
31246
|
+
"upstream_rate_limited",
|
|
31247
|
+
true,
|
|
31248
|
+
retryAfterMsFromText(detail)
|
|
31249
|
+
);
|
|
29719
31250
|
}
|
|
29720
31251
|
if (/UNAUTHENTICATED|invalid_grant|status["']?\s*:\s*401/i.test(detail)) {
|
|
29721
31252
|
return new MainNangoTransportError("The service connection requires reconnection.", 409, "connection_inactive", false);
|
|
@@ -29780,7 +31311,9 @@ async function nangoRequest(path5, options = {}) {
|
|
|
29780
31311
|
throw new MainNangoTransportError(
|
|
29781
31312
|
status === 429 ? "The connected provider is temporarily rate limited." : "Nango request failed.",
|
|
29782
31313
|
status,
|
|
29783
|
-
status === 429 ? "upstream_rate_limited" : status === 404 ? "connection_not_found" : "connection_transport_unavailable"
|
|
31314
|
+
status === 429 ? "upstream_rate_limited" : status === 404 ? "connection_not_found" : "connection_transport_unavailable",
|
|
31315
|
+
status === 429 || status >= 500,
|
|
31316
|
+
status === 429 ? retryAfterMsFromText(`retry-after: ${response.headers.get("retry-after") ?? ""}`) : null
|
|
29784
31317
|
);
|
|
29785
31318
|
}
|
|
29786
31319
|
return payload;
|
|
@@ -29995,7 +31528,7 @@ async function listNangoToolsDirect(identity, connectionId) {
|
|
|
29995
31528
|
});
|
|
29996
31529
|
const readTools = tools.filter((tool) => classifyTool(tool) === "read").map((tool) => tool.name);
|
|
29997
31530
|
const actionTools = tools.filter((tool) => classifyTool(tool) === "action").map((tool) => tool.name);
|
|
29998
|
-
const revision =
|
|
31531
|
+
const revision = createHash13("sha256").update(JSON.stringify(tools.map((tool) => ({ name: tool.name, inputSchema: tool.inputSchema })))).digest("hex");
|
|
29999
31532
|
await updateServiceConnectionTools(connection.id, readTools, actionTools, revision);
|
|
30000
31533
|
const refreshed = await getOwnedServiceConnection(identity, connection.id);
|
|
30001
31534
|
return { connection: refreshed ?? { ...connection, readTools, actionTools, toolRevision: revision }, tools };
|
|
@@ -30016,7 +31549,7 @@ async function callNangoToolDirect(args) {
|
|
|
30016
31549
|
ratePolicyVersion: CONNECTED_USAGE_RATE_POLICY_VERSION
|
|
30017
31550
|
});
|
|
30018
31551
|
const requestId = args.requestId?.trim() || randomUUID16();
|
|
30019
|
-
const idempotencyKey2 = `main-nango:${
|
|
31552
|
+
const idempotencyKey2 = `main-nango:${createHash13("sha256").update(args.identity.toLowerCase()).update("\0").update(connection.id).update("\0").update(args.tool).update("\0").update(requestId).digest("hex")}`;
|
|
30020
31553
|
const startedAt = /* @__PURE__ */ new Date();
|
|
30021
31554
|
const started = performance.now();
|
|
30022
31555
|
let result;
|
|
@@ -30041,7 +31574,7 @@ async function callNangoToolDirect(args) {
|
|
|
30041
31574
|
toolName: args.tool,
|
|
30042
31575
|
operationKind: args.operationKind ?? args.classification,
|
|
30043
31576
|
outcome: providerError ? "error" : "partial",
|
|
30044
|
-
requestId: requestId.length <= 200 ? requestId :
|
|
31577
|
+
requestId: requestId.length <= 200 ? requestId : createHash13("sha256").update(requestId).digest("hex"),
|
|
30045
31578
|
startedAt: startedAt.toISOString(),
|
|
30046
31579
|
completedAt: completedAt.toISOString()
|
|
30047
31580
|
}
|
|
@@ -30078,7 +31611,7 @@ async function describeNangoToolDirect(identity, connectionId, toolName) {
|
|
|
30078
31611
|
providerContractHash: MAIN_INTEGRATION_CONTRACT_HASH,
|
|
30079
31612
|
protocolVersion: null,
|
|
30080
31613
|
schemaSource: "live_tools_list",
|
|
30081
|
-
schemaHash:
|
|
31614
|
+
schemaHash: createHash13("sha256").update(JSON.stringify(projected)).digest("hex"),
|
|
30082
31615
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
30083
31616
|
};
|
|
30084
31617
|
}
|
|
@@ -30228,37 +31761,37 @@ function controlSecret() {
|
|
|
30228
31761
|
if (!secret2) throw new NangoControlError("Scheduled service connections are not configured.", 503);
|
|
30229
31762
|
return secret2;
|
|
30230
31763
|
}
|
|
30231
|
-
function
|
|
31764
|
+
function isRecord4(value) {
|
|
30232
31765
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
30233
31766
|
}
|
|
30234
31767
|
function unwrapData(value) {
|
|
30235
|
-
if (
|
|
31768
|
+
if (isRecord4(value) && isRecord4(value.data)) return value.data;
|
|
30236
31769
|
return value;
|
|
30237
31770
|
}
|
|
30238
|
-
function
|
|
31771
|
+
function cleanString3(value, max = 300) {
|
|
30239
31772
|
if (typeof value !== "string") return null;
|
|
30240
31773
|
const trimmed = value.trim();
|
|
30241
31774
|
return trimmed ? trimmed.slice(0, max) : null;
|
|
30242
31775
|
}
|
|
30243
31776
|
function firstString(record, keys, max = 300) {
|
|
30244
31777
|
for (const key of keys) {
|
|
30245
|
-
const value =
|
|
31778
|
+
const value = cleanString3(record[key], max);
|
|
30246
31779
|
if (value) return value;
|
|
30247
31780
|
}
|
|
30248
31781
|
return null;
|
|
30249
31782
|
}
|
|
30250
31783
|
function cleanTools(value) {
|
|
30251
31784
|
if (!Array.isArray(value)) return [];
|
|
30252
|
-
const tools = value.map((tool) =>
|
|
31785
|
+
const tools = value.map((tool) => cleanString3(tool, 200)).filter((tool) => !!tool);
|
|
30253
31786
|
return [...new Set(tools)].slice(0, 200);
|
|
30254
31787
|
}
|
|
30255
31788
|
function cleanStringArray(value, maxItems = 20, maxLength = 100) {
|
|
30256
31789
|
if (!Array.isArray(value)) return [];
|
|
30257
|
-
const items = value.map((item) =>
|
|
31790
|
+
const items = value.map((item) => cleanString3(item, maxLength)).filter((item) => !!item);
|
|
30258
31791
|
return [...new Set(items)].slice(0, maxItems);
|
|
30259
31792
|
}
|
|
30260
31793
|
function cleanHttpsUrl(value) {
|
|
30261
|
-
const candidate =
|
|
31794
|
+
const candidate = cleanString3(value, 2e3);
|
|
30262
31795
|
if (!candidate) return null;
|
|
30263
31796
|
try {
|
|
30264
31797
|
const url = new URL(candidate);
|
|
@@ -30270,7 +31803,7 @@ function cleanHttpsUrl(value) {
|
|
|
30270
31803
|
function arrayFromPayload(value, keys) {
|
|
30271
31804
|
const unwrapped = unwrapData(value);
|
|
30272
31805
|
if (Array.isArray(unwrapped)) return unwrapped;
|
|
30273
|
-
if (!
|
|
31806
|
+
if (!isRecord4(unwrapped)) return [];
|
|
30274
31807
|
for (const key of keys) {
|
|
30275
31808
|
if (Array.isArray(unwrapped[key])) return unwrapped[key];
|
|
30276
31809
|
}
|
|
@@ -30351,7 +31884,7 @@ function defaultControlErrorMessage(status) {
|
|
|
30351
31884
|
function safeControlErrorPayload(body, responseStatus) {
|
|
30352
31885
|
const status = controlErrorStatus(responseStatus);
|
|
30353
31886
|
const unwrapped = unwrapData(body);
|
|
30354
|
-
const record =
|
|
31887
|
+
const record = isRecord4(unwrapped) ? unwrapped : isRecord4(body) ? body : null;
|
|
30355
31888
|
const candidateCode = record ? firstString(record, ["code", "errorCode", "error_code"], 100) : null;
|
|
30356
31889
|
const normalizedCandidateCode = candidateCode ? CONTROL_ERROR_CODE_ALIASES.get(candidateCode) ?? candidateCode : null;
|
|
30357
31890
|
const code = normalizedCandidateCode && SAFE_CONTROL_ERROR_CODES.has(normalizedCandidateCode) ? normalizedCandidateCode : defaultControlErrorCode(status);
|
|
@@ -30400,7 +31933,7 @@ function sanitizeScheduleConnectionSelections(value) {
|
|
|
30400
31933
|
const selections = [];
|
|
30401
31934
|
const seen = /* @__PURE__ */ new Set();
|
|
30402
31935
|
for (const item of value) {
|
|
30403
|
-
if (!
|
|
31936
|
+
if (!isRecord4(item)) throw new ScheduleConnectionValidationError("Each service connection must be an object.");
|
|
30404
31937
|
const connectionId = firstString(item, ["connectionId", "connection_id"]);
|
|
30405
31938
|
const providerConfigKey = firstString(item, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id"]);
|
|
30406
31939
|
const allowedTools = cleanTools(item.allowedTools ?? item.allowed_tools ?? item.allowedCapabilities ?? item.allowed_capabilities);
|
|
@@ -30420,7 +31953,7 @@ async function getNangoCatalog() {
|
|
|
30420
31953
|
const rows = arrayFromPayload(body, ["providers", "catalog", "integrations", "services"]);
|
|
30421
31954
|
const result = [];
|
|
30422
31955
|
for (const row of rows) {
|
|
30423
|
-
if (!
|
|
31956
|
+
if (!isRecord4(row)) continue;
|
|
30424
31957
|
const providerConfigKey = firstString(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "id"]);
|
|
30425
31958
|
if (!providerConfigKey) continue;
|
|
30426
31959
|
const provider = firstString(row, ["provider"]);
|
|
@@ -30440,18 +31973,18 @@ async function getNangoCatalog() {
|
|
|
30440
31973
|
).filter((tool) => !disabledTools?.has(tool));
|
|
30441
31974
|
const requiredPermissionsByTool = {};
|
|
30442
31975
|
const rawRequiredPermissions = row.requiredPermissionsByTool ?? row.required_permissions_by_tool;
|
|
30443
|
-
if (
|
|
31976
|
+
if (isRecord4(rawRequiredPermissions)) {
|
|
30444
31977
|
for (const [tool, permissions] of Object.entries(rawRequiredPermissions).slice(0, 500)) {
|
|
30445
|
-
const cleanTool =
|
|
31978
|
+
const cleanTool = cleanString3(tool, 200);
|
|
30446
31979
|
if (!cleanTool || !safeDefaultAllowedTools.includes(cleanTool) && !actionTools.includes(cleanTool)) continue;
|
|
30447
31980
|
requiredPermissionsByTool[cleanTool] = cleanStringArray(permissions, 32, 200);
|
|
30448
31981
|
}
|
|
30449
31982
|
}
|
|
30450
31983
|
const requiredFeaturesByTool = {};
|
|
30451
31984
|
const rawRequiredFeatures = row.requiredFeaturesByTool ?? row.required_features_by_tool;
|
|
30452
|
-
if (
|
|
31985
|
+
if (isRecord4(rawRequiredFeatures)) {
|
|
30453
31986
|
for (const [tool, features] of Object.entries(rawRequiredFeatures).slice(0, 500)) {
|
|
30454
|
-
const cleanTool =
|
|
31987
|
+
const cleanTool = cleanString3(tool, 200);
|
|
30455
31988
|
if (!cleanTool) continue;
|
|
30456
31989
|
requiredFeaturesByTool[cleanTool] = cleanStringArray(features, 32, 200);
|
|
30457
31990
|
}
|
|
@@ -30576,7 +32109,7 @@ async function getNangoConnections(identity, options = {}) {
|
|
|
30576
32109
|
const rows = arrayFromPayload(body, ["connections"]);
|
|
30577
32110
|
const result = [];
|
|
30578
32111
|
for (const row of rows) {
|
|
30579
|
-
if (!
|
|
32112
|
+
if (!isRecord4(row)) continue;
|
|
30580
32113
|
const connectionId = firstString(row, ["connectionId", "connection_id", "id"]);
|
|
30581
32114
|
const providerConfigKey = firstString(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
|
|
30582
32115
|
if (!connectionId || !providerConfigKey) continue;
|
|
@@ -30595,8 +32128,8 @@ async function getNangoConnections(identity, options = {}) {
|
|
|
30595
32128
|
const rawToolCapabilities = row.toolCapabilities ?? row.tool_capabilities;
|
|
30596
32129
|
if (Array.isArray(rawToolCapabilities)) {
|
|
30597
32130
|
for (const value of rawToolCapabilities.slice(0, 500)) {
|
|
30598
|
-
if (!
|
|
30599
|
-
const name =
|
|
32131
|
+
if (!isRecord4(value)) continue;
|
|
32132
|
+
const name = cleanString3(value.name, 200);
|
|
30600
32133
|
const classification = value.classification;
|
|
30601
32134
|
const blockedValue = Object.prototype.hasOwnProperty.call(value, "blockedReason") ? value.blockedReason : value.blocked_reason;
|
|
30602
32135
|
const blockedReason = blockedValue === null ? null : blockedValue === "missing_permission" || blockedValue === "permission_policy_missing" || blockedValue === "permission_verification_unavailable" || blockedValue === "missing_app_feature" ? blockedValue : void 0;
|
|
@@ -30663,7 +32196,7 @@ async function deleteNangoConnection(identity, connectionId) {
|
|
|
30663
32196
|
body: JSON.stringify({ identity, connectionId })
|
|
30664
32197
|
});
|
|
30665
32198
|
const data = unwrapData(body);
|
|
30666
|
-
if (!
|
|
32199
|
+
if (!isRecord4(data) || data.ok !== true) {
|
|
30667
32200
|
throw new NangoControlError("The connection service returned an invalid deletion response.");
|
|
30668
32201
|
}
|
|
30669
32202
|
const remaining = Number(data.remainingConnections);
|
|
@@ -30671,7 +32204,7 @@ async function deleteNangoConnection(identity, connectionId) {
|
|
|
30671
32204
|
}
|
|
30672
32205
|
function sanitizeConnectSession(body) {
|
|
30673
32206
|
const data = unwrapData(body);
|
|
30674
|
-
if (!
|
|
32207
|
+
if (!isRecord4(data)) throw new NangoControlError("The connection service returned an invalid connect session.");
|
|
30675
32208
|
const connectLink = firstString(data, ["connectLink", "connect_link"], 2e3);
|
|
30676
32209
|
if (!connectLink) throw new NangoControlError("The connection service did not return a connect link.");
|
|
30677
32210
|
return {
|
|
@@ -30713,14 +32246,14 @@ async function createNangoReconnectSession(identity, connectionId) {
|
|
|
30713
32246
|
function sanitizeBindingRows(body, defaultScheduleActionId) {
|
|
30714
32247
|
const data = unwrapData(body);
|
|
30715
32248
|
const flattened = [];
|
|
30716
|
-
if (
|
|
32249
|
+
if (isRecord4(data) && isRecord4(data.bindings) && !Array.isArray(data.bindings)) {
|
|
30717
32250
|
for (const [scheduleActionId, value] of Object.entries(data.bindings)) {
|
|
30718
32251
|
if (Array.isArray(value)) value.forEach((row) => flattened.push({ row, scheduleActionId }));
|
|
30719
32252
|
}
|
|
30720
32253
|
} else {
|
|
30721
32254
|
const rows = arrayFromPayload(data, ["bindings", "connections"]);
|
|
30722
32255
|
for (const row of rows) {
|
|
30723
|
-
if (
|
|
32256
|
+
if (isRecord4(row) && Array.isArray(row.connections)) {
|
|
30724
32257
|
const scheduleActionId = firstString(row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || defaultScheduleActionId;
|
|
30725
32258
|
row.connections.forEach((connection) => flattened.push({ row: connection, scheduleActionId }));
|
|
30726
32259
|
} else {
|
|
@@ -30730,7 +32263,7 @@ function sanitizeBindingRows(body, defaultScheduleActionId) {
|
|
|
30730
32263
|
}
|
|
30731
32264
|
const result = [];
|
|
30732
32265
|
for (const item of flattened) {
|
|
30733
|
-
if (!
|
|
32266
|
+
if (!isRecord4(item.row)) continue;
|
|
30734
32267
|
const connectionId = firstString(item.row, ["connectionId", "connection_id"]);
|
|
30735
32268
|
const providerConfigKey = firstString(item.row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
|
|
30736
32269
|
if (!connectionId || !providerConfigKey) continue;
|
|
@@ -30798,11 +32331,11 @@ async function setScheduleConnectionActionsEnabled(identity, connectionId, enabl
|
|
|
30798
32331
|
body: JSON.stringify({ identity, connectionId, enabled })
|
|
30799
32332
|
});
|
|
30800
32333
|
const data = unwrapData(body);
|
|
30801
|
-
if (!
|
|
32334
|
+
if (!isRecord4(data) || !isRecord4(data.connection)) return enabled;
|
|
30802
32335
|
return data.connection.actionsEnabled === true;
|
|
30803
32336
|
}
|
|
30804
32337
|
async function callScheduleConnectionAction(identity, connectionId, input, tool, idempotencyKey2) {
|
|
30805
|
-
const requestId = `main-connected-action:${
|
|
32338
|
+
const requestId = `main-connected-action:${createHash14("sha256").update(identity).update("\0").update(idempotencyKey2?.trim() || randomUUID17()).digest("hex")}`;
|
|
30806
32339
|
if (mainOwnsIntegrations()) {
|
|
30807
32340
|
const selectedTool = tool?.trim();
|
|
30808
32341
|
if (!selectedTool) throw new NangoControlError("An action tool is required.", 400, "invalid_request", false);
|
|
@@ -30848,7 +32381,7 @@ async function callScheduleConnectionAction(identity, connectionId, input, tool,
|
|
|
30848
32381
|
body: JSON.stringify({ identity, connectionId, ...tool ? { tool } : {}, input })
|
|
30849
32382
|
});
|
|
30850
32383
|
const data = unwrapData(body);
|
|
30851
|
-
return
|
|
32384
|
+
return isRecord4(data) ? data.result ?? data : data;
|
|
30852
32385
|
}
|
|
30853
32386
|
async function callScheduleConnectionRead(identity, connectionId, tool, args, idempotencyKey2) {
|
|
30854
32387
|
if (mainOwnsIntegrations()) {
|
|
@@ -30871,7 +32404,7 @@ async function callScheduleConnectionRead(identity, connectionId, tool, args, id
|
|
|
30871
32404
|
body: JSON.stringify({ identity, connectionId, tool, args: args ?? {} })
|
|
30872
32405
|
});
|
|
30873
32406
|
const data = unwrapData(body);
|
|
30874
|
-
return
|
|
32407
|
+
return isRecord4(data) ? data.result ?? data : data;
|
|
30875
32408
|
}
|
|
30876
32409
|
async function testNangoConnection(identity, connectionId) {
|
|
30877
32410
|
if (!mainOwnsIntegrations()) {
|
|
@@ -30891,20 +32424,20 @@ async function testNangoConnection(identity, connectionId) {
|
|
|
30891
32424
|
}
|
|
30892
32425
|
}
|
|
30893
32426
|
function sanitizeToolSchema(value) {
|
|
30894
|
-
if (!
|
|
32427
|
+
if (!isRecord4(value) || value.type !== "object") return null;
|
|
30895
32428
|
try {
|
|
30896
32429
|
const serialized = JSON.stringify(value);
|
|
30897
32430
|
if (Buffer.byteLength(serialized, "utf8") > 256 * 1024) return null;
|
|
30898
32431
|
const cloned = JSON.parse(serialized);
|
|
30899
|
-
return
|
|
32432
|
+
return isRecord4(cloned) ? cloned : null;
|
|
30900
32433
|
} catch {
|
|
30901
32434
|
return null;
|
|
30902
32435
|
}
|
|
30903
32436
|
}
|
|
30904
32437
|
function sanitizeToolAnnotations(value) {
|
|
30905
|
-
if (!
|
|
32438
|
+
if (!isRecord4(value)) return void 0;
|
|
30906
32439
|
const annotations = {};
|
|
30907
|
-
const title =
|
|
32440
|
+
const title = cleanString3(value.title, 200);
|
|
30908
32441
|
if (title) annotations.title = title;
|
|
30909
32442
|
for (const key of ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"]) {
|
|
30910
32443
|
if (typeof value[key] === "boolean") annotations[key] = value[key];
|
|
@@ -30912,7 +32445,7 @@ function sanitizeToolAnnotations(value) {
|
|
|
30912
32445
|
return Object.keys(annotations).length > 0 ? annotations : void 0;
|
|
30913
32446
|
}
|
|
30914
32447
|
function sanitizeToolIconSource(value) {
|
|
30915
|
-
const candidate =
|
|
32448
|
+
const candidate = cleanString3(value, 1e4);
|
|
30916
32449
|
if (!candidate) return null;
|
|
30917
32450
|
if (/^data:image\/(?:png|jpeg|webp|gif|svg\+xml);base64,[a-z0-9+/=]+$/i.test(candidate)) return candidate;
|
|
30918
32451
|
return cleanHttpsUrl(candidate);
|
|
@@ -30921,10 +32454,10 @@ function sanitizeToolIcons(value) {
|
|
|
30921
32454
|
if (!Array.isArray(value)) return void 0;
|
|
30922
32455
|
const icons = [];
|
|
30923
32456
|
for (const item of value.slice(0, 16)) {
|
|
30924
|
-
if (!
|
|
32457
|
+
if (!isRecord4(item)) continue;
|
|
30925
32458
|
const src = sanitizeToolIconSource(item.src);
|
|
30926
32459
|
if (!src) continue;
|
|
30927
|
-
const mimeType =
|
|
32460
|
+
const mimeType = cleanString3(item.mimeType ?? item.mime_type, 100);
|
|
30928
32461
|
const sizes = cleanStringArray(item.sizes, 16, 32);
|
|
30929
32462
|
const theme = item.theme === "light" || item.theme === "dark" ? item.theme : void 0;
|
|
30930
32463
|
icons.push({
|
|
@@ -30938,13 +32471,13 @@ function sanitizeToolIcons(value) {
|
|
|
30938
32471
|
}
|
|
30939
32472
|
function canonicalJson2(value) {
|
|
30940
32473
|
if (Array.isArray(value)) return `[${value.map(canonicalJson2).join(",")}]`;
|
|
30941
|
-
if (
|
|
32474
|
+
if (isRecord4(value)) {
|
|
30942
32475
|
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(value[key])}`).join(",")}}`;
|
|
30943
32476
|
}
|
|
30944
32477
|
return JSON.stringify(value);
|
|
30945
32478
|
}
|
|
30946
32479
|
function projectedToolSchemaHash(tool) {
|
|
30947
|
-
return
|
|
32480
|
+
return createHash14("sha256").update(canonicalJson2(tool)).digest("hex");
|
|
30948
32481
|
}
|
|
30949
32482
|
async function describeNangoTool(identity, connectionId, tool, fresh) {
|
|
30950
32483
|
if (mainOwnsIntegrations()) {
|
|
@@ -30969,8 +32502,8 @@ async function describeNangoTool(identity, connectionId, tool, fresh) {
|
|
|
30969
32502
|
}
|
|
30970
32503
|
function sanitizeNangoToolDescription(body, tool) {
|
|
30971
32504
|
const data = unwrapData(body);
|
|
30972
|
-
const rawTool =
|
|
30973
|
-
if (!
|
|
32505
|
+
const rawTool = isRecord4(data) && isRecord4(data.tool) ? data.tool : data;
|
|
32506
|
+
if (!isRecord4(rawTool)) {
|
|
30974
32507
|
throw new NangoControlError(
|
|
30975
32508
|
"The connection service returned an invalid live tool description.",
|
|
30976
32509
|
502,
|
|
@@ -30983,13 +32516,13 @@ function sanitizeNangoToolDescription(body, tool) {
|
|
|
30983
32516
|
serializedBytes = Buffer.byteLength(JSON.stringify(rawTool), "utf8");
|
|
30984
32517
|
} catch {
|
|
30985
32518
|
}
|
|
30986
|
-
const name =
|
|
30987
|
-
const classification =
|
|
30988
|
-
const transport =
|
|
30989
|
-
const providerConfigKey =
|
|
30990
|
-
const schemaSource =
|
|
30991
|
-
const upstreamSchemaHash =
|
|
30992
|
-
const fetchedAt =
|
|
32519
|
+
const name = cleanString3(rawTool.name, 200);
|
|
32520
|
+
const classification = cleanString3(rawTool.classification, 20);
|
|
32521
|
+
const transport = cleanString3(rawTool.transport, 20);
|
|
32522
|
+
const providerConfigKey = cleanString3(rawTool.providerConfigKey ?? rawTool.provider_config_key, 200);
|
|
32523
|
+
const schemaSource = cleanString3(rawTool.schemaSource ?? rawTool.schema_source, 50);
|
|
32524
|
+
const upstreamSchemaHash = cleanString3(rawTool.schemaHash ?? rawTool.schema_hash, 200);
|
|
32525
|
+
const fetchedAt = cleanString3(rawTool.fetchedAt ?? rawTool.fetched_at, 100);
|
|
30993
32526
|
const inputSchema = sanitizeToolSchema(rawTool.inputSchema ?? rawTool.input_schema);
|
|
30994
32527
|
const outputSchemaValue = rawTool.outputSchema ?? rawTool.output_schema;
|
|
30995
32528
|
const outputSchema = outputSchemaValue === void 0 ? void 0 : sanitizeToolSchema(outputSchemaValue);
|
|
@@ -31003,11 +32536,11 @@ function sanitizeNangoToolDescription(body, tool) {
|
|
|
31003
32536
|
true
|
|
31004
32537
|
);
|
|
31005
32538
|
}
|
|
31006
|
-
const protocolVersion = rawTool.protocolVersion === null || rawTool.protocol_version === null ? null :
|
|
32539
|
+
const protocolVersion = rawTool.protocolVersion === null || rawTool.protocol_version === null ? null : cleanString3(rawTool.protocolVersion ?? rawTool.protocol_version, 100);
|
|
31007
32540
|
const executionValue = rawTool.execution;
|
|
31008
|
-
const taskSupport =
|
|
31009
|
-
const title =
|
|
31010
|
-
const description =
|
|
32541
|
+
const taskSupport = isRecord4(executionValue) && (executionValue.taskSupport === "forbidden" || executionValue.taskSupport === "optional" || executionValue.taskSupport === "required") ? executionValue.taskSupport : null;
|
|
32542
|
+
const title = cleanString3(rawTool.title, 200);
|
|
32543
|
+
const description = cleanString3(rawTool.description, 12e3);
|
|
31011
32544
|
const annotations = sanitizeToolAnnotations(rawTool.annotations);
|
|
31012
32545
|
const icons = sanitizeToolIcons(rawTool.icons);
|
|
31013
32546
|
const execution = taskSupport ? { taskSupport } : void 0;
|
|
@@ -31050,10 +32583,13 @@ function sanitizeNangoToolDescription(body, tool) {
|
|
|
31050
32583
|
};
|
|
31051
32584
|
}
|
|
31052
32585
|
async function callScheduleConnectionExportPage(identity, input) {
|
|
31053
|
-
if (mainOwnsIntegrations() && input.dataset === "search_console_performance") {
|
|
32586
|
+
if (mainOwnsIntegrations() && (input.dataset === "search_console_performance" || input.dataset === "slack_channel_messages")) {
|
|
31054
32587
|
try {
|
|
31055
32588
|
return await callMainOwnedExportPage(identity, input);
|
|
31056
32589
|
} catch (error) {
|
|
32590
|
+
if (error instanceof Error && /^slack_export_(?:channel_required|cursor_invalid|range_invalid)$/.test(error.message)) {
|
|
32591
|
+
throw new NangoControlError("The Slack channel export request or continuation is invalid.", 400, "invalid_request", false);
|
|
32592
|
+
}
|
|
31057
32593
|
throw asNangoControlError(error);
|
|
31058
32594
|
}
|
|
31059
32595
|
}
|
|
@@ -31062,11 +32598,11 @@ async function callScheduleConnectionExportPage(identity, input) {
|
|
|
31062
32598
|
body: JSON.stringify({ identity, ...input })
|
|
31063
32599
|
}, 9e4);
|
|
31064
32600
|
const data = unwrapData(body);
|
|
31065
|
-
if (!
|
|
32601
|
+
if (!isRecord4(data) || data.ok !== true) {
|
|
31066
32602
|
throw new NangoControlError("Connected-data export returned an invalid page response.");
|
|
31067
32603
|
}
|
|
31068
|
-
const providerConfigKey =
|
|
31069
|
-
const dataset =
|
|
32604
|
+
const providerConfigKey = cleanString3(data.providerConfigKey, 128);
|
|
32605
|
+
const dataset = cleanString3(data.dataset, 64);
|
|
31070
32606
|
if (!providerConfigKey || dataset === "auto" || !CONNECTED_DATA_DATASETS.includes(dataset)) {
|
|
31071
32607
|
throw new NangoControlError("Connected-data export returned invalid provider metadata.");
|
|
31072
32608
|
}
|
|
@@ -31076,7 +32612,7 @@ async function callScheduleConnectionExportPage(identity, input) {
|
|
|
31076
32612
|
if (data.nextCursor !== void 0 && data.nextCursor !== null && typeof data.nextCursor !== "string") {
|
|
31077
32613
|
throw new NangoControlError("Connected-data export returned an invalid continuation cursor.");
|
|
31078
32614
|
}
|
|
31079
|
-
const rawCounts =
|
|
32615
|
+
const rawCounts = isRecord4(data.counts) ? data.counts : {};
|
|
31080
32616
|
const numberOrZero = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
31081
32617
|
return {
|
|
31082
32618
|
providerConfigKey,
|
|
@@ -31094,11 +32630,11 @@ async function callScheduleConnectionExportPage(identity, input) {
|
|
|
31094
32630
|
};
|
|
31095
32631
|
}
|
|
31096
32632
|
function unwrapMcpToolJson(value) {
|
|
31097
|
-
if (!
|
|
31098
|
-
if (
|
|
32633
|
+
if (!isRecord4(value)) return value;
|
|
32634
|
+
if (isRecord4(value.structuredContent)) return value.structuredContent;
|
|
31099
32635
|
if (Array.isArray(value.content)) {
|
|
31100
|
-
const textPart = value.content.find((part) =>
|
|
31101
|
-
if (
|
|
32636
|
+
const textPart = value.content.find((part) => isRecord4(part) && part.type === "text" && typeof part.text === "string");
|
|
32637
|
+
if (isRecord4(textPart) && typeof textPart.text === "string") {
|
|
31102
32638
|
try {
|
|
31103
32639
|
return JSON.parse(textPart.text);
|
|
31104
32640
|
} catch {
|
|
@@ -31111,13 +32647,26 @@ function unwrapMcpToolJson(value) {
|
|
|
31111
32647
|
function rowsFromToolResult(value, keys) {
|
|
31112
32648
|
const unwrapped = unwrapMcpToolJson(value);
|
|
31113
32649
|
if (Array.isArray(unwrapped)) return unwrapped;
|
|
31114
|
-
if (!
|
|
32650
|
+
if (!isRecord4(unwrapped)) return [];
|
|
31115
32651
|
for (const key of keys) if (Array.isArray(unwrapped[key])) return unwrapped[key];
|
|
31116
32652
|
return [];
|
|
31117
32653
|
}
|
|
31118
32654
|
async function callMainOwnedExportPage(identity, input) {
|
|
31119
32655
|
const connection = await getOwnedServiceConnection(identity, input.connectionId);
|
|
31120
32656
|
if (!connection) throw new NangoControlError("The service connection was not found.", 404, "connection_not_found", false);
|
|
32657
|
+
if (connection.providerConfigKey === "slack" && input.dataset === "slack_channel_messages") {
|
|
32658
|
+
return exportSlackChannelPage(input, {
|
|
32659
|
+
callTool: async (args) => callNangoToolDirect({
|
|
32660
|
+
identity,
|
|
32661
|
+
connectionId: input.connectionId,
|
|
32662
|
+
tool: args.tool,
|
|
32663
|
+
input: args.input,
|
|
32664
|
+
classification: "read",
|
|
32665
|
+
requestId: args.requestId,
|
|
32666
|
+
operationKind: "export"
|
|
32667
|
+
})
|
|
32668
|
+
});
|
|
32669
|
+
}
|
|
31121
32670
|
if (connection.providerConfigKey !== "google-search-console" || input.dataset !== "search_console_performance") {
|
|
31122
32671
|
throw new NangoControlError("This dataset has not moved to main-MCP export execution yet.", 400, "invalid_request", false);
|
|
31123
32672
|
}
|
|
@@ -31135,8 +32684,8 @@ async function callMainOwnedExportPage(identity, input) {
|
|
|
31135
32684
|
operationKind: "export"
|
|
31136
32685
|
});
|
|
31137
32686
|
const sites = rowsFromToolResult(listed, ["siteEntry", "sites", "items"]).flatMap((value) => {
|
|
31138
|
-
const row =
|
|
31139
|
-
const siteUrl2 = row ?
|
|
32687
|
+
const row = isRecord4(value) ? value : null;
|
|
32688
|
+
const siteUrl2 = row ? cleanString3(row.siteUrl ?? row.site_url, 2e3) : null;
|
|
31140
32689
|
return siteUrl2 ? [siteUrl2] : [];
|
|
31141
32690
|
}).sort().slice(0, 1e4);
|
|
31142
32691
|
if (siteIndex >= sites.length) {
|
|
@@ -31173,7 +32722,7 @@ async function callMainOwnedExportPage(identity, input) {
|
|
|
31173
32722
|
});
|
|
31174
32723
|
const rows = rowsFromToolResult(performance2, ["rows"]).slice(0, input.pageSize);
|
|
31175
32724
|
const records = rows.map((value, index) => {
|
|
31176
|
-
const row =
|
|
32725
|
+
const row = isRecord4(value) ? value : {};
|
|
31177
32726
|
const keys = Array.isArray(row.keys) ? row.keys.map((item) => String(item ?? "")) : [];
|
|
31178
32727
|
const date = keys[0] || null;
|
|
31179
32728
|
return {
|
|
@@ -31250,34 +32799,34 @@ function controlSecret2() {
|
|
|
31250
32799
|
if (!secret2) throw new ResendControlError("Resend connections are not configured.", 503);
|
|
31251
32800
|
return secret2;
|
|
31252
32801
|
}
|
|
31253
|
-
function
|
|
32802
|
+
function isRecord5(value) {
|
|
31254
32803
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
31255
32804
|
}
|
|
31256
32805
|
function unwrapData2(value) {
|
|
31257
|
-
return
|
|
32806
|
+
return isRecord5(value) && isRecord5(value.data) ? value.data : value;
|
|
31258
32807
|
}
|
|
31259
|
-
function
|
|
32808
|
+
function cleanString4(value, max = 300) {
|
|
31260
32809
|
if (typeof value !== "string") return null;
|
|
31261
32810
|
const trimmed = value.trim();
|
|
31262
32811
|
return trimmed ? trimmed.slice(0, max) : null;
|
|
31263
32812
|
}
|
|
31264
32813
|
function firstString2(record, keys, max = 300) {
|
|
31265
32814
|
for (const key of keys) {
|
|
31266
|
-
const value =
|
|
32815
|
+
const value = cleanString4(record[key], max);
|
|
31267
32816
|
if (value) return value;
|
|
31268
32817
|
}
|
|
31269
32818
|
return null;
|
|
31270
32819
|
}
|
|
31271
32820
|
function cleanTools2(value) {
|
|
31272
32821
|
if (!Array.isArray(value)) return [];
|
|
31273
|
-
return [...new Set(value.map((tool) =>
|
|
32822
|
+
return [...new Set(value.map((tool) => cleanString4(tool, 200)).filter((tool) => !!tool))].slice(0, 200);
|
|
31274
32823
|
}
|
|
31275
32824
|
function cleanStringArray2(value, maxItems = 50, maxLength = 500) {
|
|
31276
32825
|
if (!Array.isArray(value)) return [];
|
|
31277
|
-
return [...new Set(value.map((item) =>
|
|
32826
|
+
return [...new Set(value.map((item) => cleanString4(item, maxLength)).filter((item) => !!item))].slice(0, maxItems);
|
|
31278
32827
|
}
|
|
31279
32828
|
function cleanHttpsUrl2(value) {
|
|
31280
|
-
const candidate =
|
|
32829
|
+
const candidate = cleanString4(value, 2e3);
|
|
31281
32830
|
if (!candidate) return null;
|
|
31282
32831
|
try {
|
|
31283
32832
|
const url = new URL(candidate);
|
|
@@ -31295,7 +32844,7 @@ function cleanAuthorizationUrl(value) {
|
|
|
31295
32844
|
function arrayFromPayload2(value, keys) {
|
|
31296
32845
|
const unwrapped = unwrapData2(value);
|
|
31297
32846
|
if (Array.isArray(unwrapped)) return unwrapped;
|
|
31298
|
-
if (!
|
|
32847
|
+
if (!isRecord5(unwrapped)) return [];
|
|
31299
32848
|
for (const key of keys) {
|
|
31300
32849
|
if (Array.isArray(unwrapped[key])) return unwrapped[key];
|
|
31301
32850
|
}
|
|
@@ -31322,7 +32871,7 @@ async function controlRequest2(path5, init, timeoutMs = 2e4) {
|
|
|
31322
32871
|
body = {};
|
|
31323
32872
|
}
|
|
31324
32873
|
if (!response.ok) {
|
|
31325
|
-
const upstream =
|
|
32874
|
+
const upstream = isRecord5(body) ? cleanString4(body.error, 300) : null;
|
|
31326
32875
|
throw new ResendControlError(upstream || `Resend connection control failed (${response.status}).`);
|
|
31327
32876
|
}
|
|
31328
32877
|
return body;
|
|
@@ -31332,7 +32881,7 @@ async function getResendCatalog() {
|
|
|
31332
32881
|
const rows = arrayFromPayload2(body, ["providers", "catalog", "integrations", "services"]);
|
|
31333
32882
|
const result = [];
|
|
31334
32883
|
for (const row of rows) {
|
|
31335
|
-
if (!
|
|
32884
|
+
if (!isRecord5(row)) continue;
|
|
31336
32885
|
const id = firstString2(row, ["providerConfigKey", "provider_config_key", "id"]);
|
|
31337
32886
|
if (id !== RESEND_PROVIDER_CONFIG_KEY) continue;
|
|
31338
32887
|
const safeDefaultAllowedTools = cleanTools2(
|
|
@@ -31368,7 +32917,7 @@ async function getResendConnections(identity) {
|
|
|
31368
32917
|
const rows = arrayFromPayload2(body, ["connections"]);
|
|
31369
32918
|
const result = [];
|
|
31370
32919
|
for (const row of rows) {
|
|
31371
|
-
if (!
|
|
32920
|
+
if (!isRecord5(row)) continue;
|
|
31372
32921
|
const connectionId = firstString2(row, ["connectionId", "connection_id", "id"]);
|
|
31373
32922
|
const providerConfigKey = firstString2(row, ["providerConfigKey", "provider_config_key", "provider"]) || RESEND_PROVIDER_CONFIG_KEY;
|
|
31374
32923
|
if (!connectionId || providerConfigKey !== RESEND_PROVIDER_CONFIG_KEY) continue;
|
|
@@ -31412,7 +32961,7 @@ async function getResendConnections(identity) {
|
|
|
31412
32961
|
}
|
|
31413
32962
|
function sanitizeConnectSession2(body) {
|
|
31414
32963
|
const data = unwrapData2(body);
|
|
31415
|
-
if (!
|
|
32964
|
+
if (!isRecord5(data)) throw new ResendControlError("The Resend connection service returned an invalid connect session.");
|
|
31416
32965
|
const connectLink = cleanAuthorizationUrl(data.authorizationUrl ?? data.authorization_url ?? data.connectLink ?? data.connect_link);
|
|
31417
32966
|
if (!connectLink) throw new ResendControlError("The Resend connection service did not return a trusted authorization link.");
|
|
31418
32967
|
return {
|
|
@@ -31453,7 +33002,7 @@ async function setResendActionsEnabled(identity, connectionId, enabled) {
|
|
|
31453
33002
|
body: JSON.stringify({ identity, connectionId, enabled })
|
|
31454
33003
|
});
|
|
31455
33004
|
const data = unwrapData2(body);
|
|
31456
|
-
if (!
|
|
33005
|
+
if (!isRecord5(data) || !isRecord5(data.connection)) return enabled;
|
|
31457
33006
|
return data.connection.actionsEnabled === true || data.connection.actions_enabled === true;
|
|
31458
33007
|
}
|
|
31459
33008
|
async function callResendRead(identity, connectionId, tool, args) {
|
|
@@ -31462,7 +33011,7 @@ async function callResendRead(identity, connectionId, tool, args) {
|
|
|
31462
33011
|
body: JSON.stringify({ identity, connectionId, tool, input: args ?? {} })
|
|
31463
33012
|
});
|
|
31464
33013
|
const data = unwrapData2(body);
|
|
31465
|
-
return
|
|
33014
|
+
return isRecord5(data) ? data.result ?? data : data;
|
|
31466
33015
|
}
|
|
31467
33016
|
async function callResendAction(identity, connectionId, tool, input) {
|
|
31468
33017
|
const body = await controlRequest2("/api/internal/resend/actions/call", {
|
|
@@ -31470,7 +33019,7 @@ async function callResendAction(identity, connectionId, tool, input) {
|
|
|
31470
33019
|
body: JSON.stringify({ identity, connectionId, tool, input })
|
|
31471
33020
|
});
|
|
31472
33021
|
const data = unwrapData2(body);
|
|
31473
|
-
return
|
|
33022
|
+
return isRecord5(data) ? data.result ?? data : data;
|
|
31474
33023
|
}
|
|
31475
33024
|
async function describeResendTool(identity, connectionId, tool) {
|
|
31476
33025
|
const body = await controlRequest2("/api/internal/resend/describe", {
|
|
@@ -31478,12 +33027,12 @@ async function describeResendTool(identity, connectionId, tool) {
|
|
|
31478
33027
|
body: JSON.stringify({ identity, connectionId, tool })
|
|
31479
33028
|
});
|
|
31480
33029
|
const data = unwrapData2(body);
|
|
31481
|
-
const rawTool =
|
|
31482
|
-
if (!
|
|
33030
|
+
const rawTool = isRecord5(data) && isRecord5(data.tool) ? data.tool : data;
|
|
33031
|
+
if (!isRecord5(rawTool)) throw new ResendControlError("Resend returned an invalid tool description.");
|
|
31483
33032
|
const name = firstString2(rawTool, ["name"], 200);
|
|
31484
33033
|
const classification = firstString2(rawTool, ["classification", "kind"], 20);
|
|
31485
33034
|
const inputSchema = rawTool.inputSchema ?? rawTool.input_schema;
|
|
31486
|
-
if (!name || name !== tool || classification !== "read" && classification !== "action" || !
|
|
33035
|
+
if (!name || name !== tool || classification !== "read" && classification !== "action" || !isRecord5(inputSchema)) {
|
|
31487
33036
|
throw new ResendControlError("Resend returned an invalid tool description.");
|
|
31488
33037
|
}
|
|
31489
33038
|
return {
|
|
@@ -31500,10 +33049,10 @@ async function callResendExportPage(identity, input) {
|
|
|
31500
33049
|
body: JSON.stringify({ identity, ...input })
|
|
31501
33050
|
}, 9e4);
|
|
31502
33051
|
const data = unwrapData2(body);
|
|
31503
|
-
if (!
|
|
33052
|
+
if (!isRecord5(data) || data.ok !== true || data.providerConfigKey !== RESEND_PROVIDER_CONFIG_KEY) {
|
|
31504
33053
|
throw new ResendControlError("Resend export returned an invalid page response.");
|
|
31505
33054
|
}
|
|
31506
|
-
const dataset =
|
|
33055
|
+
const dataset = cleanString4(data.dataset, 64);
|
|
31507
33056
|
if (!dataset || dataset === "auto" || !CONNECTED_DATA_DATASETS.includes(dataset)) {
|
|
31508
33057
|
throw new ResendControlError("Resend export returned invalid dataset metadata.");
|
|
31509
33058
|
}
|
|
@@ -31511,7 +33060,7 @@ async function callResendExportPage(identity, input) {
|
|
|
31511
33060
|
if (data.nextCursor !== void 0 && data.nextCursor !== null && typeof data.nextCursor !== "string") {
|
|
31512
33061
|
throw new ResendControlError("Resend export returned an invalid continuation cursor.");
|
|
31513
33062
|
}
|
|
31514
|
-
const rawCounts =
|
|
33063
|
+
const rawCounts = isRecord5(data.counts) ? data.counts : {};
|
|
31515
33064
|
const numberOrZero = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
31516
33065
|
return {
|
|
31517
33066
|
providerConfigKey: RESEND_PROVIDER_CONFIG_KEY,
|
|
@@ -31530,7 +33079,7 @@ async function callResendExportPage(identity, input) {
|
|
|
31530
33079
|
}
|
|
31531
33080
|
|
|
31532
33081
|
// src/api/scheduler-integration-auth.ts
|
|
31533
|
-
import { createHash as
|
|
33082
|
+
import { createHash as createHash15, createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
31534
33083
|
var MAX_CLOCK_SKEW_MS = 5 * 60 * 1e3;
|
|
31535
33084
|
var SchedulerIntegrationAuthError = class extends Error {
|
|
31536
33085
|
code;
|
|
@@ -31546,7 +33095,7 @@ function signingSecret() {
|
|
|
31546
33095
|
return secret2;
|
|
31547
33096
|
}
|
|
31548
33097
|
function schedulerIntegrationSignature(args) {
|
|
31549
|
-
const bodyHash =
|
|
33098
|
+
const bodyHash = createHash15("sha256").update(args.body).digest("hex");
|
|
31550
33099
|
return createHmac4("sha256", args.secret).update(`${args.method.toUpperCase()}
|
|
31551
33100
|
${args.path}
|
|
31552
33101
|
${args.timestamp}
|
|
@@ -31713,6 +33262,18 @@ async function createSignupStripeCustomer(email) {
|
|
|
31713
33262
|
}
|
|
31714
33263
|
app.use("*", async (c, next) => {
|
|
31715
33264
|
await next();
|
|
33265
|
+
if (c.res.status >= 500 && c.res.headers.get("content-type")?.includes("application/json")) {
|
|
33266
|
+
try {
|
|
33267
|
+
const payload = await c.res.clone().json();
|
|
33268
|
+
const headers = new Headers(c.res.headers);
|
|
33269
|
+
c.res = new Response(JSON.stringify(sanitizeOutboundDiagnostics(payload)), {
|
|
33270
|
+
status: c.res.status,
|
|
33271
|
+
statusText: c.res.statusText,
|
|
33272
|
+
headers
|
|
33273
|
+
});
|
|
33274
|
+
} catch {
|
|
33275
|
+
}
|
|
33276
|
+
}
|
|
31716
33277
|
c.header("Cache-Control", "no-store");
|
|
31717
33278
|
c.header("X-Content-Type-Options", "nosniff");
|
|
31718
33279
|
c.header("Referrer-Policy", "same-origin");
|
|
@@ -32114,6 +33675,16 @@ app.get("/memory/note", auth2, async (c) => {
|
|
|
32114
33675
|
return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load note" }, 502);
|
|
32115
33676
|
}
|
|
32116
33677
|
});
|
|
33678
|
+
app.get("/memory/universe", auth2, async (c) => {
|
|
33679
|
+
const user = c.get("user");
|
|
33680
|
+
const includeConnections = c.req.query("includeConnections") === "1";
|
|
33681
|
+
try {
|
|
33682
|
+
const graph = await dbUniverse(resolveMemoryIdentity(user), includeConnections);
|
|
33683
|
+
return c.json({ ok: true, ...graph });
|
|
33684
|
+
} catch (err) {
|
|
33685
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : "could not build universe" }, 502);
|
|
33686
|
+
}
|
|
33687
|
+
});
|
|
32117
33688
|
app.put("/memory/note", auth2, async (c) => {
|
|
32118
33689
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
32119
33690
|
if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
|
|
@@ -32855,7 +34426,18 @@ app.post("/schedule-connections/actions/export", auth2, requireIntegrationsTier,
|
|
|
32855
34426
|
const body = await c.req.json().catch(() => ({}));
|
|
32856
34427
|
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
32857
34428
|
if (!connectionId) return c.json({ ok: false, error: "connectionId is required." }, 400);
|
|
32858
|
-
const
|
|
34429
|
+
const channelId = typeof body.channelId === "string" ? body.channelId.trim() : "";
|
|
34430
|
+
if (body.channelId !== void 0 && !channelId) {
|
|
34431
|
+
return c.json({ ok: false, error: "channelId must be a non-empty Slack conversation ID." }, 400);
|
|
34432
|
+
}
|
|
34433
|
+
if (body.includeThreads !== void 0 && typeof body.includeThreads !== "boolean") {
|
|
34434
|
+
return c.json({ ok: false, error: "includeThreads must be a boolean when provided." }, 400);
|
|
34435
|
+
}
|
|
34436
|
+
if (body.allTime !== void 0 && typeof body.allTime !== "boolean") {
|
|
34437
|
+
return c.json({ ok: false, error: "allTime must be a boolean when provided." }, 400);
|
|
34438
|
+
}
|
|
34439
|
+
const rawRequestedDataset = typeof body.dataset === "string" ? body.dataset.trim() : "auto";
|
|
34440
|
+
const requestedDataset = rawRequestedDataset === "auto" && channelId ? "slack_channel_messages" : rawRequestedDataset;
|
|
32859
34441
|
const datasets = [...CONNECTED_DATA_DATASETS];
|
|
32860
34442
|
if (!datasets.includes(requestedDataset)) {
|
|
32861
34443
|
return c.json({ ok: false, error: `dataset must be one of: ${datasets.join(", ")}.` }, 400);
|
|
@@ -32878,21 +34460,37 @@ app.post("/schedule-connections/actions/export", auth2, requireIntegrationsTier,
|
|
|
32878
34460
|
if (body.continuation !== void 0 && !continuation) {
|
|
32879
34461
|
return c.json({ ok: false, error: "continuation must be the complete object returned by a prior export." }, 400);
|
|
32880
34462
|
}
|
|
34463
|
+
if (body.allTime === true && requestedDataset !== "slack_channel_messages") {
|
|
34464
|
+
return c.json({ ok: false, error: "allTime is supported only for Slack channel exports." }, 400);
|
|
34465
|
+
}
|
|
34466
|
+
if (body.allTime === true && (body.from !== void 0 || body.lastDays !== void 0 || continuation)) {
|
|
34467
|
+
return c.json({ ok: false, error: "Do not pass allTime with from, lastDays, or continuation." }, 400);
|
|
34468
|
+
}
|
|
34469
|
+
if (requestedDataset === "slack_channel_messages" && !channelId && !continuation) {
|
|
34470
|
+
return c.json({ ok: false, error: "channelId is required for a Slack channel export." }, 400);
|
|
34471
|
+
}
|
|
32881
34472
|
try {
|
|
32882
34473
|
const useResend = await isResendConnection(user.email, connectionId);
|
|
32883
34474
|
const resolved = resolveConnectedDataExportRequest({
|
|
32884
34475
|
requestedDataset,
|
|
32885
|
-
from: typeof body.from === "string" ? body.from : void 0,
|
|
34476
|
+
from: body.allTime === true ? "1970-01-01T00:00:00.000Z" : typeof body.from === "string" ? body.from : void 0,
|
|
32886
34477
|
to: typeof body.to === "string" ? body.to : void 0,
|
|
32887
34478
|
lastDays,
|
|
32888
34479
|
cursor: typeof body.cursor === "string" ? body.cursor : void 0,
|
|
32889
|
-
continuation
|
|
34480
|
+
continuation,
|
|
34481
|
+
scope: channelId ? {
|
|
34482
|
+
slack: {
|
|
34483
|
+
channelId,
|
|
34484
|
+
includeThreads: body.includeThreads !== false
|
|
34485
|
+
}
|
|
34486
|
+
} : void 0
|
|
32890
34487
|
});
|
|
32891
34488
|
const result = await collectConnectedDataExport({
|
|
32892
34489
|
ownerId: sha256Hex(user.api_key).slice(0, 24),
|
|
32893
34490
|
connectionId,
|
|
32894
34491
|
dataset: resolved.dataset,
|
|
32895
34492
|
range: resolved.range,
|
|
34493
|
+
...resolved.scope ? { scope: resolved.scope } : {},
|
|
32896
34494
|
maxItems,
|
|
32897
34495
|
forceArtifact: body.delivery === "artifact",
|
|
32898
34496
|
...resolved.cursor ? { cursor: resolved.cursor } : {},
|
|
@@ -33253,8 +34851,8 @@ function countPaaQuestions(result) {
|
|
|
33253
34851
|
if (typeof value.totalQuestions === "number") return value.totalQuestions;
|
|
33254
34852
|
return Array.isArray(value.flat) ? value.flat.length : 0;
|
|
33255
34853
|
}
|
|
33256
|
-
function paaCostForQuestionCount(
|
|
33257
|
-
return MC_COSTS.paa_base + Math.max(1,
|
|
34854
|
+
function paaCostForQuestionCount(questionCount2) {
|
|
34855
|
+
return MC_COSTS.paa_base + Math.max(1, questionCount2) * MC_COSTS.paa;
|
|
33258
34856
|
}
|
|
33259
34857
|
async function checkHarvestLimits(user, reuseLockId) {
|
|
33260
34858
|
if (reuseLockId && await reuseExistingConcurrencyGate(user.id, reuseLockId, 30 * 60)) return null;
|
|
@@ -33301,9 +34899,50 @@ app.post("/harvest", auth2, async (c) => {
|
|
|
33301
34899
|
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
33302
34900
|
};
|
|
33303
34901
|
const harvestCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount(options.maxQuestions);
|
|
33304
|
-
|
|
33305
|
-
if (
|
|
33306
|
-
|
|
34902
|
+
let jobId2;
|
|
34903
|
+
if (options.serpOnly) {
|
|
34904
|
+
const { ok: harvestOk, balance_mc: harvestBal } = await debitMc(
|
|
34905
|
+
user.id,
|
|
34906
|
+
harvestCost,
|
|
34907
|
+
LedgerOperation.SERP,
|
|
34908
|
+
options.query
|
|
34909
|
+
);
|
|
34910
|
+
if (!harvestOk) return c.json(insufficientBalanceResponse(harvestBal, harvestCost), 402);
|
|
34911
|
+
jobId2 = await createJob(user.id, options.query, { ...options, billingHoldMc: harvestCost }, body.callback_url);
|
|
34912
|
+
} else {
|
|
34913
|
+
jobId2 = randomUUID18();
|
|
34914
|
+
const billingDebitKey = `paa-harvest:${jobId2}:hold`;
|
|
34915
|
+
const description = `PAA harvest: ${options.query}`.slice(0, 500);
|
|
34916
|
+
const hold = await debitMcIdempotent(
|
|
34917
|
+
user.id,
|
|
34918
|
+
harvestCost,
|
|
34919
|
+
LedgerOperation.PAA,
|
|
34920
|
+
description,
|
|
34921
|
+
billingDebitKey
|
|
34922
|
+
);
|
|
34923
|
+
if (!hold.ok) return c.json(insufficientBalanceResponse(hold.balance_mc, harvestCost), 402);
|
|
34924
|
+
try {
|
|
34925
|
+
await createJobWithId(jobId2, user.id, options.query, {
|
|
34926
|
+
...options,
|
|
34927
|
+
executionOwner: "inngest",
|
|
34928
|
+
billingHoldMc: harvestCost,
|
|
34929
|
+
billingDebitKey
|
|
34930
|
+
}, body.callback_url);
|
|
34931
|
+
} catch (error) {
|
|
34932
|
+
await settleDebitMcIdempotent(
|
|
34933
|
+
user.id,
|
|
34934
|
+
billingDebitKey,
|
|
34935
|
+
0,
|
|
34936
|
+
LedgerOperation.PAA_REFUND,
|
|
34937
|
+
"PAA job creation failed",
|
|
34938
|
+
"paa_harvest"
|
|
34939
|
+
).catch(() => void 0);
|
|
34940
|
+
throw error;
|
|
34941
|
+
}
|
|
34942
|
+
await dispatchPaaHarvest(jobId2).catch((error) => {
|
|
34943
|
+
console.error("[harvest] durable PAA dispatch pending reconciliation:", error instanceof Error ? error.message : String(error));
|
|
34944
|
+
});
|
|
34945
|
+
}
|
|
33307
34946
|
if (process.env.CRON_SECRET) {
|
|
33308
34947
|
const url = new URL(c.req.url);
|
|
33309
34948
|
void fetch(`${url.origin}/cron/tick`, {
|
|
@@ -33378,6 +35017,10 @@ app.post("/harvest/sync", auth2, async (c) => {
|
|
|
33378
35017
|
}
|
|
33379
35018
|
return c.json({ job_id: jobId2, status: "done", result: sanitizeHarvestResult(result), attempts: sanitizeAttempts(attempts) });
|
|
33380
35019
|
} catch (err) {
|
|
35020
|
+
console.error("[harvest/sync] failed", {
|
|
35021
|
+
jobId: jobId2,
|
|
35022
|
+
error: err instanceof Error ? err.message : String(err)
|
|
35023
|
+
});
|
|
33381
35024
|
const problem = classifyHarvestProblem(err);
|
|
33382
35025
|
const response = harvestProblemResponse(problem);
|
|
33383
35026
|
const attempts = await listHarvestAttempts(jobId2, user.id);
|
|
@@ -34421,10 +36064,10 @@ app.get("/cron/tick", async (c) => {
|
|
|
34421
36064
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
34422
36065
|
return c.json({ error: "Unauthorized" }, 401);
|
|
34423
36066
|
}
|
|
34424
|
-
const { drainQueue } = await import("./worker-
|
|
36067
|
+
const { drainQueue } = await import("./worker-5RWLFSAJ.js");
|
|
34425
36068
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
34426
36069
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
34427
|
-
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation] = await Promise.all([
|
|
36070
|
+
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation] = await Promise.all([
|
|
34428
36071
|
drainQueue(budget),
|
|
34429
36072
|
runMonthlyRefreshSweep(),
|
|
34430
36073
|
reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 })),
|
|
@@ -34437,7 +36080,9 @@ app.get("/cron/tick", async (c) => {
|
|
|
34437
36080
|
cleanupExpiredDirectoryArtifacts().catch(() => ({ deleted: 0, store: "none" })),
|
|
34438
36081
|
cleanupExpiredSiteExtractArtifacts().catch(() => ({ deleted: 0, store: "none" })),
|
|
34439
36082
|
redispatchFundedSiteExtractJobs(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
|
|
34440
|
-
reconcileSiteExtractSettlements(25).catch(() => ({ checked: 0, settled: 0, failed: 0 }))
|
|
36083
|
+
reconcileSiteExtractSettlements(25).catch(() => ({ checked: 0, settled: 0, failed: 0 })),
|
|
36084
|
+
redispatchPendingPaaHarvests(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
|
|
36085
|
+
reconcilePaaHarvestSettlements(25).catch(() => ({ checked: 0, staleFailed: 0, settled: 0, failed: 0 }))
|
|
34441
36086
|
]);
|
|
34442
36087
|
return c.json({
|
|
34443
36088
|
drained: results.length,
|
|
@@ -34454,6 +36099,8 @@ app.get("/cron/tick", async (c) => {
|
|
|
34454
36099
|
siteExtractArtifactCleanup,
|
|
34455
36100
|
siteExtractRedispatch,
|
|
34456
36101
|
siteExtractReconciliation,
|
|
36102
|
+
paaHarvestRedispatch,
|
|
36103
|
+
paaHarvestReconciliation,
|
|
34457
36104
|
workflowDispatch: workflowDispatchResult
|
|
34458
36105
|
});
|
|
34459
36106
|
});
|
|
@@ -34470,8 +36117,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
34470
36117
|
finishExtractJob: finishExtractJob2,
|
|
34471
36118
|
terminalExtractJobStatus: terminalExtractJobStatus2,
|
|
34472
36119
|
extractJobLimitInfo: extractJobLimitInfo2
|
|
34473
|
-
} = await import("./site-extract-repository-
|
|
34474
|
-
const { assembleExtractArtifacts } = await import("./extract-bundle-
|
|
36120
|
+
} = await import("./site-extract-repository-6GX72U4N.js");
|
|
36121
|
+
const { assembleExtractArtifacts } = await import("./extract-bundle-SKXEDG4Z.js");
|
|
34475
36122
|
const existing = await getExtractJob2(jobId2);
|
|
34476
36123
|
if (!existing) return c.json({ error: "job not found" }, 404);
|
|
34477
36124
|
if (existing.status !== "failed") {
|
|
@@ -34498,7 +36145,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
34498
36145
|
if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
|
|
34499
36146
|
let settlement = "already_settled_or_refunded";
|
|
34500
36147
|
if (job.billedMc == null && job.userId != null) {
|
|
34501
|
-
const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
36148
|
+
const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-6GX72U4N.js");
|
|
34502
36149
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
34503
36150
|
const successful = await countSuccessfulPages2(jobId2);
|
|
34504
36151
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -34513,7 +36160,10 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
34513
36160
|
const bundle = stored.find((a) => a.contentType === "application/zip" || a.filename?.endsWith(".zip"));
|
|
34514
36161
|
return c.json({ ok: true, jobId: jobId2, artifacts: stored.length, settlement, bundleUrl: bundle?.url ?? null, bundleBytes: bundle?.bytes ?? null });
|
|
34515
36162
|
});
|
|
34516
|
-
app.on(["GET", "POST", "PUT"], "/api/inngest", serveInngest({
|
|
36163
|
+
app.on(["GET", "POST", "PUT"], "/api/inngest", serveInngest({
|
|
36164
|
+
client: inngest,
|
|
36165
|
+
functions: [siteAuditFn, siteExtractFn, directoryWorkflowFn, paaHarvestFn]
|
|
36166
|
+
}));
|
|
34517
36167
|
app.route("/admin/credits", adminCreditsApp);
|
|
34518
36168
|
app.route("/api/internal/site-architecture-auditor", siteAuditApp);
|
|
34519
36169
|
app.route("/api/internal/memory", internalMemoryApp);
|
|
@@ -34671,4 +36321,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
34671
36321
|
export {
|
|
34672
36322
|
app
|
|
34673
36323
|
};
|
|
34674
|
-
//# sourceMappingURL=server-
|
|
36324
|
+
//# sourceMappingURL=server-2JJPCZH4.js.map
|