@canonry/canonry 4.177.3 → 4.178.1
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/assets/assets/{AuditHistoryPanel-YZvGU1la.js → AuditHistoryPanel-BCtMrEMF.js} +1 -1
- package/assets/assets/{BacklinksPage-CB2uJWWy.js → BacklinksPage-CIo2hA0i.js} +1 -1
- package/assets/assets/{ChartPrimitives-BoYe2M-i.js → ChartPrimitives-B_LDVvTy.js} +1 -1
- package/assets/assets/{HistoryPage-D9IxxbQ9.js → HistoryPage-D24U8cUG.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-CxgnnPdp.js → MeasurementPropertyPage-BKDdv-jJ.js} +1 -1
- package/assets/assets/ProjectPage-DrIs_ZuR.js +9 -0
- package/assets/assets/{RunRow-2KxOta8F.js → RunRow-B39IK7hK.js} +1 -1
- package/assets/assets/{RunsPage-ya8vTkyd.js → RunsPage-R4W1kD0B.js} +1 -1
- package/assets/assets/{SettingsPage-lTy3o2bo.js → SettingsPage-BAOGyt1F.js} +1 -1
- package/assets/assets/{SiteHealthSection-Cyu7MIv2.js → SiteHealthSection-Bx7yVNMQ.js} +3 -3
- package/assets/assets/{TrafficPage-DjhKfw57.js → TrafficPage-DL8RvM2R.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-Pwi-Dl9Q.js → TrafficSourceDetailPage-COzjakZa.js} +1 -1
- package/assets/assets/{extract-error-message-I1FVWHLH.js → extract-error-message-B3TERCQ-.js} +1 -1
- package/assets/assets/{index-DQVTRT9J.css → index-D0GbiVhZ.css} +1 -1
- package/assets/assets/index-xtxQtZRo.js +83 -0
- package/assets/assets/{react-sigma_core.esm.min-BX6mOEq5.js → react-sigma_core.esm.min-CbXAujub.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-5EH666EO.js → chunk-JKKHGDDC.js} +3048 -2058
- package/dist/{chunk-PQT3ECM3.js → chunk-OYDIND7Q.js} +178 -1
- package/dist/{chunk-6XUWDSLN.js → chunk-UTW6QOGP.js} +1356 -907
- package/dist/{chunk-QRE3DAAU.js → chunk-VIVLYBFX.js} +2 -2
- package/dist/{chunk-7ABFIVKO.js → chunk-WINVGL5B.js} +458 -137
- package/dist/cli.js +87 -5
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-CHZGHFGS.js → intelligence-service-YSRPTRKP.js} +2 -2
- package/dist/mcp.js +5 -380
- package/package.json +10 -10
- package/assets/assets/ProjectPage-CPmaXzhL.js +0 -9
- package/assets/assets/index-BeLVKN3H.js +0 -83
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ApiClient,
|
|
3
|
+
CANONRY_MCP_TOOLKIT_NAMES,
|
|
3
4
|
CanonryMcpToolNames,
|
|
4
5
|
canonryMcpTools,
|
|
5
6
|
configExists,
|
|
7
|
+
createCanonryMcpServer,
|
|
6
8
|
getBundledSkillSnapshots,
|
|
7
9
|
getConfigPath,
|
|
8
10
|
isMachineFormat,
|
|
9
11
|
loadConfig,
|
|
10
12
|
loadConfigRaw,
|
|
11
13
|
saveConfigPatch
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-UTW6QOGP.js";
|
|
13
15
|
import {
|
|
14
16
|
CC_CACHE_DIR,
|
|
15
17
|
DUCKDB_SPEC,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
GSC_REPORTING_TIME_ZONE,
|
|
19
21
|
IntelligenceService,
|
|
20
22
|
PLUGIN_DIR,
|
|
23
|
+
USER_SESSION_COOKIE_NAME,
|
|
21
24
|
adsAdGroups,
|
|
22
25
|
adsAds,
|
|
23
26
|
adsCampaigns,
|
|
@@ -48,7 +51,9 @@ import {
|
|
|
48
51
|
countPopulatedGroups,
|
|
49
52
|
crawlerEventsHourly,
|
|
50
53
|
createClient,
|
|
54
|
+
createCredentialChecker,
|
|
51
55
|
createLogger,
|
|
56
|
+
createUserSession,
|
|
52
57
|
deliverWebhook,
|
|
53
58
|
determineCitationState,
|
|
54
59
|
discoverySessions,
|
|
@@ -90,9 +95,11 @@ import {
|
|
|
90
95
|
gscUrlInspections,
|
|
91
96
|
gtmConnections,
|
|
92
97
|
gtmRawSnapshots,
|
|
98
|
+
hashApiKey,
|
|
93
99
|
hashAttributes,
|
|
94
100
|
hashLodging,
|
|
95
101
|
hashPlaceDetails,
|
|
102
|
+
insightNotifyState,
|
|
96
103
|
insights,
|
|
97
104
|
inspectUrl,
|
|
98
105
|
installDuckdb,
|
|
@@ -110,6 +117,7 @@ import {
|
|
|
110
117
|
migrate,
|
|
111
118
|
nextRunFromCron,
|
|
112
119
|
notifications,
|
|
120
|
+
parseCookieHeader,
|
|
113
121
|
parseJsonColumn,
|
|
114
122
|
probeLatestRelease,
|
|
115
123
|
projects,
|
|
@@ -122,15 +130,19 @@ import {
|
|
|
122
130
|
readInstalledVersion,
|
|
123
131
|
redactNotificationUrl,
|
|
124
132
|
refreshAccessToken,
|
|
133
|
+
registerOAuthAdminRoutes,
|
|
134
|
+
registerOAuthRoutes,
|
|
125
135
|
researchRunQueries,
|
|
126
136
|
researchRuns,
|
|
127
137
|
resolveDestination,
|
|
128
138
|
resolveMeasurementSitemapTarget,
|
|
129
139
|
resolveTrustProxy,
|
|
140
|
+
resolveUserSession,
|
|
130
141
|
resolveVercelSyncDeadlineMs,
|
|
131
142
|
resolveWebhookTarget,
|
|
132
143
|
runs,
|
|
133
144
|
schedules,
|
|
145
|
+
serializeUserSessionCookie,
|
|
134
146
|
siteAuditPages,
|
|
135
147
|
siteAuditSnapshots,
|
|
136
148
|
siteCrawlAttempts,
|
|
@@ -145,7 +157,7 @@ import {
|
|
|
145
157
|
siteCrawlSnapshots,
|
|
146
158
|
toAlertView,
|
|
147
159
|
usageCounters
|
|
148
|
-
} from "./chunk-
|
|
160
|
+
} from "./chunk-JKKHGDDC.js";
|
|
149
161
|
import {
|
|
150
162
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
151
163
|
AGENT_PROVIDER_IDS,
|
|
@@ -208,6 +220,7 @@ import {
|
|
|
208
220
|
canonicalizeGtmAccountId,
|
|
209
221
|
canonicalizeGtmContainerId,
|
|
210
222
|
canonicalizeGtmResourceSelection,
|
|
223
|
+
classifyProviderErrorMessages,
|
|
211
224
|
classifyTemplateLinkEdge,
|
|
212
225
|
cloudflareQueueNameSchema,
|
|
213
226
|
computeBacklinkSummaryMetrics,
|
|
@@ -249,6 +262,7 @@ import {
|
|
|
249
262
|
isAgentProviderId,
|
|
250
263
|
isBrowserProvider,
|
|
251
264
|
isGhostTelemetryEvent,
|
|
265
|
+
isReadOnlyKey,
|
|
252
266
|
isRetryableHttpError,
|
|
253
267
|
isSelfLink,
|
|
254
268
|
isVertexGroundingRedirect,
|
|
@@ -282,7 +296,7 @@ import {
|
|
|
282
296
|
validationError,
|
|
283
297
|
winnabilityClassLabel,
|
|
284
298
|
withRetry
|
|
285
|
-
} from "./chunk-
|
|
299
|
+
} from "./chunk-OYDIND7Q.js";
|
|
286
300
|
|
|
287
301
|
// src/telemetry.ts
|
|
288
302
|
import crypto from "crypto";
|
|
@@ -849,9 +863,11 @@ function dailyMetricClauses(options) {
|
|
|
849
863
|
);
|
|
850
864
|
}
|
|
851
865
|
const filters = [
|
|
852
|
-
`segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'
|
|
853
|
-
"campaign.status != 'REMOVED'"
|
|
866
|
+
`segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'`
|
|
854
867
|
];
|
|
868
|
+
if (options.campaignIds === void 0) {
|
|
869
|
+
filters.push("campaign.status != 'REMOVED'");
|
|
870
|
+
}
|
|
855
871
|
if (options.campaignIds !== void 0) {
|
|
856
872
|
if (!Array.isArray(options.campaignIds) || options.campaignIds.length === 0) {
|
|
857
873
|
throw new GoogleAdsApiError("Campaign IDs must be a non-empty array when provided", 400);
|
|
@@ -878,6 +894,22 @@ function dailyMetricClauses(options) {
|
|
|
878
894
|
`LIMIT ${validatedLimit(options.limit)}`
|
|
879
895
|
];
|
|
880
896
|
}
|
|
897
|
+
function buildCampaignSpendRankingQuery(options) {
|
|
898
|
+
const start = parseIsoDate(options.startDate, "Start date");
|
|
899
|
+
const end = parseIsoDate(options.endDate, "End date");
|
|
900
|
+
if (start > end) {
|
|
901
|
+
throw new GoogleAdsApiError("Start date must be on or before end date", 400);
|
|
902
|
+
}
|
|
903
|
+
return selectQuery([
|
|
904
|
+
"campaign.id",
|
|
905
|
+
"campaign.status",
|
|
906
|
+
"metrics.cost_micros"
|
|
907
|
+
], "campaign", [
|
|
908
|
+
`WHERE segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'`,
|
|
909
|
+
"ORDER BY metrics.cost_micros DESC",
|
|
910
|
+
`LIMIT ${validatedLimit(options.limit)}`
|
|
911
|
+
]);
|
|
912
|
+
}
|
|
881
913
|
function buildCampaignsQuery(options = {}) {
|
|
882
914
|
const clauses = [];
|
|
883
915
|
if (!options.includeRemoved) clauses.push("WHERE campaign.status != 'REMOVED'");
|
|
@@ -1317,6 +1349,18 @@ var GoogleAdsClient = class {
|
|
|
1317
1349
|
}
|
|
1318
1350
|
};
|
|
1319
1351
|
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Campaigns ranked by spend over a window, removed ones included, so the
|
|
1354
|
+
* bounded daily-metrics query can be scoped to where the money actually went
|
|
1355
|
+
* rather than to the lowest campaign ids.
|
|
1356
|
+
*/
|
|
1357
|
+
async getCampaignSpendRanking(customerId, options) {
|
|
1358
|
+
return this.#searchStream(
|
|
1359
|
+
customerId,
|
|
1360
|
+
"campaign-spend-ranking",
|
|
1361
|
+
buildCampaignSpendRankingQuery(options)
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1320
1364
|
async getDailyCampaignMetrics(customerId, options) {
|
|
1321
1365
|
return this.#searchStream(
|
|
1322
1366
|
customerId,
|
|
@@ -2816,19 +2860,21 @@ function campaignMetricDto(row) {
|
|
|
2816
2860
|
conversionValueMicros: conversionValueMicros(row.metrics)
|
|
2817
2861
|
};
|
|
2818
2862
|
}
|
|
2819
|
-
function
|
|
2820
|
-
const campaignIds = campaigns.map((campaign) => campaign.id).sort((left, right) => left.localeCompare(right));
|
|
2821
|
-
const selected = campaignIds.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS);
|
|
2822
|
-
if (selected.length === 0) return { query: null, inventoryTruncated: false };
|
|
2863
|
+
function defaultMetricsWindow(now, timeZone) {
|
|
2823
2864
|
const customerTimeZone = typeof timeZone === "string" && timeZone.trim() !== "" ? timeZone : "UTC";
|
|
2824
2865
|
const endDate = formatIsoDateInTimeZone(now.toISOString(), customerTimeZone);
|
|
2866
|
+
return { startDate: shiftIsoCalendarDate(endDate, -(GOOGLE_ADS_CAMPAIGN_METRICS_MAX_DAYS - 1)), endDate };
|
|
2867
|
+
}
|
|
2868
|
+
function defaultMetricsQuery(now, campaigns, timeZone, rankedCampaignIds) {
|
|
2869
|
+
const inventoryIds = campaigns.map((campaign) => campaign.id).sort((left, right) => left.localeCompare(right));
|
|
2870
|
+
const ranked = rankedCampaignIds ?? [];
|
|
2871
|
+
const ordered = ranked.length > 0 ? [...ranked, ...inventoryIds.filter((id) => !ranked.includes(id))] : inventoryIds;
|
|
2872
|
+
const selected = ordered.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS);
|
|
2873
|
+
if (selected.length === 0) return { query: null, inventoryTruncated: false };
|
|
2874
|
+
const { startDate, endDate } = defaultMetricsWindow(now, timeZone);
|
|
2825
2875
|
return {
|
|
2826
|
-
query: googleAdsCampaignMetricsQuerySchema.parse({
|
|
2827
|
-
|
|
2828
|
-
startDate: shiftIsoCalendarDate(endDate, -(GOOGLE_ADS_CAMPAIGN_METRICS_MAX_DAYS - 1)),
|
|
2829
|
-
endDate
|
|
2830
|
-
}),
|
|
2831
|
-
inventoryTruncated: campaignIds.length > selected.length
|
|
2876
|
+
query: googleAdsCampaignMetricsQuerySchema.parse({ campaignIds: selected, startDate, endDate }),
|
|
2877
|
+
inventoryTruncated: ordered.length > selected.length
|
|
2832
2878
|
};
|
|
2833
2879
|
}
|
|
2834
2880
|
function countRedactedProviderValues(value) {
|
|
@@ -3131,7 +3177,21 @@ var DefaultGoogleMarketingRuntime = class {
|
|
|
3131
3177
|
capturedAt
|
|
3132
3178
|
);
|
|
3133
3179
|
const effectiveGoalGraph = deriveGoogleAdsEffectiveGoalGraph(inventory);
|
|
3134
|
-
const
|
|
3180
|
+
const rankingWindow = defaultMetricsWindow(this.#now(), customerResult.data.timeZone);
|
|
3181
|
+
const ranking = await client.getCampaignSpendRanking(customerId, {
|
|
3182
|
+
...rankingWindow,
|
|
3183
|
+
limit: GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS + 1
|
|
3184
|
+
}).then((result) => ({
|
|
3185
|
+
ids: result.data.map((row) => String(row.campaign.id)),
|
|
3186
|
+
failed: false
|
|
3187
|
+
})).catch(() => ({ ids: [], failed: true }));
|
|
3188
|
+
const rankingTruncated = ranking.ids.length > GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS;
|
|
3189
|
+
const defaults = defaultMetricsQuery(
|
|
3190
|
+
this.#now(),
|
|
3191
|
+
inventory.campaigns,
|
|
3192
|
+
customerResult.data.timeZone,
|
|
3193
|
+
ranking.failed ? void 0 : ranking.ids.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS)
|
|
3194
|
+
);
|
|
3135
3195
|
const metricsQuery = suppliedMetricsQuery ?? defaults.query;
|
|
3136
3196
|
if (!metricsQuery) {
|
|
3137
3197
|
return {
|
|
@@ -3149,7 +3209,10 @@ var DefaultGoogleMarketingRuntime = class {
|
|
|
3149
3209
|
const response = {
|
|
3150
3210
|
query: metricsQuery,
|
|
3151
3211
|
rows: metricRows.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS),
|
|
3152
|
-
truncated: metricRows.length > GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS || suppliedMetricsQuery === void 0 && defaults.inventoryTruncated
|
|
3212
|
+
truncated: metricRows.length > GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS || suppliedMetricsQuery === void 0 && defaults.inventoryTruncated || // Either the ranking could not be read (so selection fell back to an
|
|
3213
|
+
// order that omits removed campaigns) or it reported more campaigns
|
|
3214
|
+
// than the cap. Both mean these totals are a subset of the account.
|
|
3215
|
+
suppliedMetricsQuery === void 0 && (ranking.failed || rankingTruncated),
|
|
3153
3216
|
fetchedAt: this.#now().toISOString()
|
|
3154
3217
|
};
|
|
3155
3218
|
const metricsPayload = {
|
|
@@ -3330,11 +3393,11 @@ function createGoogleMarketingRuntime(options) {
|
|
|
3330
3393
|
|
|
3331
3394
|
// src/server.ts
|
|
3332
3395
|
import { createRequire as createRequire4 } from "module";
|
|
3333
|
-
import
|
|
3396
|
+
import crypto27 from "crypto";
|
|
3334
3397
|
import fs7 from "fs";
|
|
3335
3398
|
import path8 from "path";
|
|
3336
3399
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3337
|
-
import { and as and19, eq as
|
|
3400
|
+
import { and as and19, eq as eq26 } from "drizzle-orm";
|
|
3338
3401
|
import Fastify from "fastify";
|
|
3339
3402
|
import os4 from "os";
|
|
3340
3403
|
|
|
@@ -6349,40 +6412,7 @@ function classifyRunAbortReason(message) {
|
|
|
6349
6412
|
return void 0;
|
|
6350
6413
|
}
|
|
6351
6414
|
function classifyProviderErrors(errors) {
|
|
6352
|
-
|
|
6353
|
-
for (const message of errors.values()) {
|
|
6354
|
-
codes.add(classifyOneProviderError(message));
|
|
6355
|
-
}
|
|
6356
|
-
const priority = [
|
|
6357
|
-
"PROVIDER_AUTH",
|
|
6358
|
-
"RATE_LIMITED",
|
|
6359
|
-
"TIMEOUT",
|
|
6360
|
-
"NETWORK",
|
|
6361
|
-
"PARSE_ERROR",
|
|
6362
|
-
"UNKNOWN"
|
|
6363
|
-
];
|
|
6364
|
-
for (const code of priority) {
|
|
6365
|
-
if (codes.has(code)) return code;
|
|
6366
|
-
}
|
|
6367
|
-
return "UNKNOWN";
|
|
6368
|
-
}
|
|
6369
|
-
function classifyOneProviderError(message) {
|
|
6370
|
-
if (/\b401\b|\b403\b|unauthorized|forbidden|invalid[_ -]?api[_ -]?key|missing[_ -]?api[_ -]?key|authentication/i.test(message)) {
|
|
6371
|
-
return "PROVIDER_AUTH";
|
|
6372
|
-
}
|
|
6373
|
-
if (/\b429\b|rate[_ -]?limit|too many requests|quota[_ -]?exceeded/i.test(message)) {
|
|
6374
|
-
return "RATE_LIMITED";
|
|
6375
|
-
}
|
|
6376
|
-
if (/timeout|timed out|ETIMEDOUT/i.test(message)) {
|
|
6377
|
-
return "TIMEOUT";
|
|
6378
|
-
}
|
|
6379
|
-
if (/ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|network|fetch failed|socket hang up/i.test(message)) {
|
|
6380
|
-
return "NETWORK";
|
|
6381
|
-
}
|
|
6382
|
-
if (/parse|unexpected token|invalid json|malformed|JSON\.parse/i.test(message)) {
|
|
6383
|
-
return "PARSE_ERROR";
|
|
6384
|
-
}
|
|
6385
|
-
return "UNKNOWN";
|
|
6415
|
+
return classifyProviderErrorMessages(errors.values());
|
|
6386
6416
|
}
|
|
6387
6417
|
var JobRunner = class {
|
|
6388
6418
|
/**
|
|
@@ -10855,6 +10885,31 @@ function partitionDeadLinks(deadLinks) {
|
|
|
10855
10885
|
];
|
|
10856
10886
|
return { dead, unverified };
|
|
10857
10887
|
}
|
|
10888
|
+
function reportedTermination(summary) {
|
|
10889
|
+
const limits = summary.limits;
|
|
10890
|
+
if (limits) {
|
|
10891
|
+
if (limits.maxBytes && (summary.bytesRead ?? 0) >= limits.maxBytes) return "max-bytes";
|
|
10892
|
+
if (limits.maxDurationMs && (summary.elapsedMs ?? 0) >= limits.maxDurationMs) return "max-duration";
|
|
10893
|
+
if (limits.maxFetches && (summary.fetchesStarted ?? 0) >= limits.maxFetches) return "max-fetches";
|
|
10894
|
+
}
|
|
10895
|
+
return summary.terminationReason ?? "complete";
|
|
10896
|
+
}
|
|
10897
|
+
function crawlBudgetsFor(maxPages) {
|
|
10898
|
+
return {
|
|
10899
|
+
// 2x the ~745 KB/page measured on a media-heavy production site, so the
|
|
10900
|
+
// byte cap is headroom rather than the thing that stops the crawl.
|
|
10901
|
+
maxBytes: maxPages * 15e5,
|
|
10902
|
+
// ~5 pages/s at the engine's default concurrency of 5, doubled for slow
|
|
10903
|
+
// origins, and never below the engine's own 2-minute floor.
|
|
10904
|
+
maxDurationMs: Math.max(12e4, Math.ceil(maxPages / 5 * 1e3 * 2)),
|
|
10905
|
+
// Redirects and dead-link probes cost fetches without producing pages.
|
|
10906
|
+
maxFetches: Math.ceil(maxPages * 1.5),
|
|
10907
|
+
// Raised off the default of 10 because it is an ADMISSION limit: a sitemap
|
|
10908
|
+
// with many query-parameter variants trips it during seeding and latches a
|
|
10909
|
+
// soft reason that then masks whatever really stops the crawl.
|
|
10910
|
+
maxQueryVariants: Math.max(50, Math.ceil(maxPages / 10))
|
|
10911
|
+
};
|
|
10912
|
+
}
|
|
10858
10913
|
async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
10859
10914
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10860
10915
|
const claim = db.update(runs).set({ status: "running", startedAt }).where(and12(eq15(runs.id, runId), eq15(runs.projectId, projectId), eq15(runs.status, "queued"))).run();
|
|
@@ -11155,6 +11210,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
11155
11210
|
maxEdges,
|
|
11156
11211
|
maxDepth: opts.maxDepth ?? null
|
|
11157
11212
|
});
|
|
11213
|
+
const budgets = crawlBudgetsFor(maxPages);
|
|
11158
11214
|
const report = await runSiteCrawl(crawlRootUrl, {
|
|
11159
11215
|
mode: "summary",
|
|
11160
11216
|
sitemapUrl: opts.sitemapUrl,
|
|
@@ -11162,6 +11218,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
11162
11218
|
maxEdges,
|
|
11163
11219
|
maxDepth: opts.maxDepth,
|
|
11164
11220
|
checkDeadLinks: opts.checkDeadLinks ?? false,
|
|
11221
|
+
...budgets,
|
|
11165
11222
|
signal: opts.signal,
|
|
11166
11223
|
onEvent: persistEvent
|
|
11167
11224
|
});
|
|
@@ -11288,7 +11345,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
11288
11345
|
maxDepth: opts.maxDepth ?? null,
|
|
11289
11346
|
checkDeadLinks: opts.checkDeadLinks ?? false,
|
|
11290
11347
|
complete: crawlSummary.complete,
|
|
11291
|
-
termination: crawlSummary
|
|
11348
|
+
termination: reportedTermination(crawlSummary),
|
|
11292
11349
|
detailsAvailable: true,
|
|
11293
11350
|
pagesDiscovered: crawlSummary.pagesDiscovered,
|
|
11294
11351
|
pagesFetched: crawlSummary.pagesFetched,
|
|
@@ -11839,7 +11896,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
11839
11896
|
return result;
|
|
11840
11897
|
}
|
|
11841
11898
|
async function backfillInsightsCommand(project, opts) {
|
|
11842
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
11899
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-YSRPTRKP.js");
|
|
11843
11900
|
const config = loadConfig();
|
|
11844
11901
|
const db = createClient(config.database);
|
|
11845
11902
|
migrate(db);
|
|
@@ -12829,6 +12886,21 @@ async function refreshAllIntegrations(client, projectName) {
|
|
|
12829
12886
|
import { eq as eq18, desc as desc7, and as and15, inArray as inArray7, or as or2 } from "drizzle-orm";
|
|
12830
12887
|
import crypto21 from "crypto";
|
|
12831
12888
|
var log16 = createLogger("Notifier");
|
|
12889
|
+
var MATERIAL_MAGNITUDE_DELTA = 20;
|
|
12890
|
+
var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
12891
|
+
function severityRank(severity) {
|
|
12892
|
+
return SEVERITY_RANK[severity] ?? 0;
|
|
12893
|
+
}
|
|
12894
|
+
function insightFingerprint(insight) {
|
|
12895
|
+
return insight.title.replace(/\b\d+(?:\.\d+)?%/g, "N%");
|
|
12896
|
+
}
|
|
12897
|
+
function insightMagnitude(insight) {
|
|
12898
|
+
const match = /\b(\d+(?:\.\d+)?)%/.exec(insight.title);
|
|
12899
|
+
return match?.[1] === void 0 ? null : Math.round(Number(match[1]));
|
|
12900
|
+
}
|
|
12901
|
+
function insightNotifyKey(projectId, insight) {
|
|
12902
|
+
return `${projectId}:${insight.type}:${insight.query}:${insightFingerprint(insight)}`;
|
|
12903
|
+
}
|
|
12832
12904
|
var Notifier = class {
|
|
12833
12905
|
db;
|
|
12834
12906
|
serverUrl;
|
|
@@ -12984,11 +13056,59 @@ var Notifier = class {
|
|
|
12984
13056
|
log16.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
|
|
12985
13057
|
return event;
|
|
12986
13058
|
}
|
|
13059
|
+
/**
|
|
13060
|
+
* Has this finding already been said?
|
|
13061
|
+
*
|
|
13062
|
+
* The identity of a finding is its type, its subject and its wording with the
|
|
13063
|
+
* magnitude neutralised. So the same drop reported at 79% and at 80% over the
|
|
13064
|
+
* same window is one piece of news, while the same drop over an ADVANCED
|
|
13065
|
+
* window is a new one.
|
|
13066
|
+
*
|
|
13067
|
+
* Re-notifies on three things and nothing else:
|
|
13068
|
+
* - never sent before
|
|
13069
|
+
* - severity got worse (medium -> high -> critical)
|
|
13070
|
+
* - magnitude moved by more than MATERIAL_MAGNITUDE_DELTA points
|
|
13071
|
+
*
|
|
13072
|
+
* Deliberately NOT time-based. A daily repeat is exactly the behaviour this
|
|
13073
|
+
* exists to stop, and "re-alert after N days" is that behaviour with a delay.
|
|
13074
|
+
*/
|
|
13075
|
+
isNewInsightNews(projectId, insight) {
|
|
13076
|
+
const key = insightNotifyKey(projectId, insight);
|
|
13077
|
+
const previous = this.db.select().from(insightNotifyState).where(eq18(insightNotifyState.key, key)).get();
|
|
13078
|
+
if (!previous) return true;
|
|
13079
|
+
if (severityRank(insight.severity) > severityRank(previous.severity)) return true;
|
|
13080
|
+
const magnitude = insightMagnitude(insight);
|
|
13081
|
+
if (magnitude !== null && previous.magnitude !== null && previous.magnitude !== void 0) {
|
|
13082
|
+
if (Math.abs(magnitude - previous.magnitude) > MATERIAL_MAGNITUDE_DELTA) return true;
|
|
13083
|
+
}
|
|
13084
|
+
return false;
|
|
13085
|
+
}
|
|
13086
|
+
/** Record a delivered insight so it stops being news. */
|
|
13087
|
+
rememberInsightNotified(projectId, insight) {
|
|
13088
|
+
const key = insightNotifyKey(projectId, insight);
|
|
13089
|
+
const row = {
|
|
13090
|
+
key,
|
|
13091
|
+
projectId,
|
|
13092
|
+
type: insight.type,
|
|
13093
|
+
subject: insight.query,
|
|
13094
|
+
fingerprint: insightFingerprint(insight),
|
|
13095
|
+
severity: insight.severity,
|
|
13096
|
+
magnitude: insightMagnitude(insight),
|
|
13097
|
+
notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13098
|
+
};
|
|
13099
|
+
const existing = this.db.select().from(insightNotifyState).where(eq18(insightNotifyState.key, key)).get();
|
|
13100
|
+
if (existing) {
|
|
13101
|
+
this.db.update(insightNotifyState).set(row).where(eq18(insightNotifyState.key, key)).run();
|
|
13102
|
+
} else {
|
|
13103
|
+
this.db.insert(insightNotifyState).values(row).run();
|
|
13104
|
+
}
|
|
13105
|
+
}
|
|
12987
13106
|
/** Dispatch insight webhooks for critical/high severity insights after a run. */
|
|
12988
13107
|
async dispatchInsightWebhooks(runId, projectId, result) {
|
|
12989
13108
|
const insightEvents = [];
|
|
12990
|
-
const
|
|
12991
|
-
const
|
|
13109
|
+
const unsent = result.insights.filter((i) => this.isNewInsightNews(projectId, i));
|
|
13110
|
+
const criticalInsights = unsent.filter((i) => i.severity === "critical");
|
|
13111
|
+
const highInsights = unsent.filter((i) => i.severity === "high");
|
|
12992
13112
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
12993
13113
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
12994
13114
|
if (insightEvents.length === 0) return;
|
|
@@ -13022,6 +13142,7 @@ var Notifier = class {
|
|
|
13022
13142
|
dashboardUrl: `${this.serverUrl}/projects/${project.name}`
|
|
13023
13143
|
};
|
|
13024
13144
|
await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
|
|
13145
|
+
for (const insight of relevantInsights) this.rememberInsightNotified(projectId, insight);
|
|
13025
13146
|
}
|
|
13026
13147
|
}
|
|
13027
13148
|
}
|
|
@@ -15185,10 +15306,159 @@ ${lines.join("\n")}
|
|
|
15185
15306
|
}
|
|
15186
15307
|
};
|
|
15187
15308
|
|
|
15188
|
-
// src/
|
|
15309
|
+
// src/mcp-http.ts
|
|
15310
|
+
import crypto26 from "crypto";
|
|
15189
15311
|
import { eq as eq22 } from "drizzle-orm";
|
|
15312
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15313
|
+
var SESSION_IDLE_MS = 10 * 60 * 1e3;
|
|
15314
|
+
var SWEEP_INTERVAL_MS = 60 * 1e3;
|
|
15315
|
+
function mintSessionKey(db, scopes, userId) {
|
|
15316
|
+
const raw = `cnry_${crypto26.randomBytes(24).toString("hex")}`;
|
|
15317
|
+
const id = crypto26.randomUUID();
|
|
15318
|
+
db.insert(apiKeys).values({
|
|
15319
|
+
id,
|
|
15320
|
+
name: `mcp-session:${userId}`,
|
|
15321
|
+
keyHash: hashApiKey(raw),
|
|
15322
|
+
keyPrefix: raw.slice(0, 9),
|
|
15323
|
+
scopes: [...scopes],
|
|
15324
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15325
|
+
}).run();
|
|
15326
|
+
return { id, raw };
|
|
15327
|
+
}
|
|
15328
|
+
function revokeSessionKey(db, id) {
|
|
15329
|
+
try {
|
|
15330
|
+
db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(apiKeys.id, id)).run();
|
|
15331
|
+
} catch {
|
|
15332
|
+
}
|
|
15333
|
+
}
|
|
15334
|
+
function mcpTransportPaths() {
|
|
15335
|
+
const paths = ["/mcp", "/mcp/readonly"];
|
|
15336
|
+
for (const toolkit of CANONRY_MCP_TOOLKIT_NAMES) {
|
|
15337
|
+
paths.push(`/mcp/x/${toolkit}`, `/mcp/x/${toolkit}/readonly`);
|
|
15338
|
+
}
|
|
15339
|
+
return paths;
|
|
15340
|
+
}
|
|
15341
|
+
function registerMcpHttpRoutes(scope, opts) {
|
|
15342
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
15343
|
+
const now = opts.now ?? (() => Date.now());
|
|
15344
|
+
async function dropSession(id) {
|
|
15345
|
+
const session = sessions.get(id);
|
|
15346
|
+
if (!session) return;
|
|
15347
|
+
sessions.delete(id);
|
|
15348
|
+
await session.close().catch(() => {
|
|
15349
|
+
});
|
|
15350
|
+
}
|
|
15351
|
+
const sweep = setInterval(() => {
|
|
15352
|
+
const cutoff = now() - SESSION_IDLE_MS;
|
|
15353
|
+
for (const [id, session] of sessions) {
|
|
15354
|
+
if (session.lastSeenAt < cutoff) void dropSession(id);
|
|
15355
|
+
}
|
|
15356
|
+
}, SWEEP_INTERVAL_MS);
|
|
15357
|
+
sweep.unref();
|
|
15358
|
+
if (opts.issuer) {
|
|
15359
|
+
scope.addHook("onSend", async (request, reply, payload) => {
|
|
15360
|
+
const isTransport = request.routeOptions.config?.transportEnvelope === true;
|
|
15361
|
+
if (isTransport && reply.statusCode === 401 && !reply.getHeader("WWW-Authenticate")) {
|
|
15362
|
+
const metadata = `${opts.issuer}/.well-known/oauth-protected-resource${request.routeOptions.url ?? ""}`;
|
|
15363
|
+
void reply.header("WWW-Authenticate", `Bearer resource_metadata="${metadata}"`);
|
|
15364
|
+
}
|
|
15365
|
+
return payload;
|
|
15366
|
+
});
|
|
15367
|
+
}
|
|
15368
|
+
scope.addHook("onClose", async () => {
|
|
15369
|
+
clearInterval(sweep);
|
|
15370
|
+
await Promise.all([...sessions.keys()].map((id) => dropSession(id)));
|
|
15371
|
+
});
|
|
15372
|
+
function callerBearer(request) {
|
|
15373
|
+
const header = request.headers.authorization;
|
|
15374
|
+
if (typeof header !== "string") return null;
|
|
15375
|
+
const trimmed = header.trim();
|
|
15376
|
+
const prefix = "bearer ";
|
|
15377
|
+
if (trimmed.length <= prefix.length) return null;
|
|
15378
|
+
if (trimmed.slice(0, prefix.length).toLowerCase() !== prefix) return null;
|
|
15379
|
+
return trimmed.slice(prefix.length).trim() || null;
|
|
15380
|
+
}
|
|
15381
|
+
async function openSession(request, keyId, segment) {
|
|
15382
|
+
const bearer = callerBearer(request);
|
|
15383
|
+
if (!bearer) return null;
|
|
15384
|
+
const scopes = request.principal?.scopes ?? request.apiKey?.scopes ?? [];
|
|
15385
|
+
let sessionKey = null;
|
|
15386
|
+
if (!request.apiKey && request.principal?.kind === "user") {
|
|
15387
|
+
sessionKey = mintSessionKey(opts.db, scopes, request.principal.id);
|
|
15388
|
+
}
|
|
15389
|
+
const client = new ApiClient(opts.selfApiUrl, sessionKey?.raw ?? bearer, { skipProbe: true });
|
|
15390
|
+
const server = createCanonryMcpServer({
|
|
15391
|
+
scope: segment.readOnly || isReadOnlyKey(scopes) ? "read-only" : "all",
|
|
15392
|
+
tiers: segment.tiers,
|
|
15393
|
+
clientFactory: () => client
|
|
15394
|
+
});
|
|
15395
|
+
const transport = new StreamableHTTPServerTransport({
|
|
15396
|
+
sessionIdGenerator: () => crypto26.randomUUID(),
|
|
15397
|
+
onsessioninitialized: (id) => {
|
|
15398
|
+
sessions.set(id, { transport, close, keyId, segmentId: segment.id, lastSeenAt: now() });
|
|
15399
|
+
}
|
|
15400
|
+
});
|
|
15401
|
+
async function close() {
|
|
15402
|
+
await transport.close();
|
|
15403
|
+
await server.close();
|
|
15404
|
+
if (sessionKey) revokeSessionKey(opts.db, sessionKey.id);
|
|
15405
|
+
}
|
|
15406
|
+
transport.onclose = () => {
|
|
15407
|
+
const id = transport.sessionId;
|
|
15408
|
+
if (id) sessions.delete(id);
|
|
15409
|
+
if (sessionKey) revokeSessionKey(opts.db, sessionKey.id);
|
|
15410
|
+
};
|
|
15411
|
+
await server.connect(transport);
|
|
15412
|
+
return { transport, close, keyId, segmentId: segment.id, lastSeenAt: now() };
|
|
15413
|
+
}
|
|
15414
|
+
async function handle(request, reply, segment) {
|
|
15415
|
+
const keyId = request.principal?.id ?? request.apiKey?.id;
|
|
15416
|
+
if (!keyId) {
|
|
15417
|
+
await reply.status(401).send({ error: { code: "AUTH_REQUIRED", message: "Authentication required." } });
|
|
15418
|
+
return;
|
|
15419
|
+
}
|
|
15420
|
+
const sessionId = request.headers["mcp-session-id"];
|
|
15421
|
+
const existing = typeof sessionId === "string" ? sessions.get(sessionId) : void 0;
|
|
15422
|
+
if (existing) {
|
|
15423
|
+
if (existing.keyId !== keyId || existing.segmentId !== segment.id) {
|
|
15424
|
+
await reply.status(404).send({ error: { code: "NOT_FOUND", message: "Unknown MCP session." } });
|
|
15425
|
+
return;
|
|
15426
|
+
}
|
|
15427
|
+
existing.lastSeenAt = now();
|
|
15428
|
+
await existing.transport.handleRequest(request.raw, reply.raw, request.body);
|
|
15429
|
+
return;
|
|
15430
|
+
}
|
|
15431
|
+
if (typeof sessionId === "string") {
|
|
15432
|
+
await reply.status(404).send({ error: { code: "NOT_FOUND", message: "Unknown MCP session." } });
|
|
15433
|
+
return;
|
|
15434
|
+
}
|
|
15435
|
+
const opened = await openSession(request, keyId, segment);
|
|
15436
|
+
if (!opened) {
|
|
15437
|
+
await reply.status(401).send({ error: { code: "AUTH_REQUIRED", message: "Authentication required." } });
|
|
15438
|
+
return;
|
|
15439
|
+
}
|
|
15440
|
+
await opened.transport.handleRequest(request.raw, reply.raw, request.body);
|
|
15441
|
+
}
|
|
15442
|
+
const config = { transportEnvelope: true };
|
|
15443
|
+
function mount(pathname, segment) {
|
|
15444
|
+
const run = (request, reply) => handle(request, reply, segment);
|
|
15445
|
+
scope.post(pathname, { config }, run);
|
|
15446
|
+
scope.get(pathname, { config }, run);
|
|
15447
|
+
scope.delete(pathname, { config }, run);
|
|
15448
|
+
}
|
|
15449
|
+
mount("/mcp", { id: "core", tiers: ["core"], readOnly: false });
|
|
15450
|
+
mount("/mcp/readonly", { id: "core:ro", tiers: ["core"], readOnly: true });
|
|
15451
|
+
for (const toolkit of CANONRY_MCP_TOOLKIT_NAMES) {
|
|
15452
|
+
const tiers = ["core", toolkit];
|
|
15453
|
+
mount(`/mcp/x/${toolkit}`, { id: toolkit, tiers, readOnly: false });
|
|
15454
|
+
mount(`/mcp/x/${toolkit}/readonly`, { id: `${toolkit}:ro`, tiers, readOnly: true });
|
|
15455
|
+
}
|
|
15456
|
+
}
|
|
15457
|
+
|
|
15458
|
+
// src/agent/agent-routes.ts
|
|
15459
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
15190
15460
|
function resolveProject(db, name) {
|
|
15191
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
15461
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq23(projects.name, name)).get();
|
|
15192
15462
|
if (!row) throw notFound("project", name);
|
|
15193
15463
|
return row;
|
|
15194
15464
|
}
|
|
@@ -15197,7 +15467,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
15197
15467
|
"/projects/:name/agent/transcript",
|
|
15198
15468
|
async (request) => {
|
|
15199
15469
|
const project = resolveProject(opts.db, request.params.name);
|
|
15200
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
15470
|
+
const row = opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, project.id)).get();
|
|
15201
15471
|
if (!row) {
|
|
15202
15472
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
15203
15473
|
}
|
|
@@ -15221,7 +15491,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
15221
15491
|
async (request) => {
|
|
15222
15492
|
const project = resolveProject(opts.db, request.params.name);
|
|
15223
15493
|
opts.sessionRegistry.reset(project.name);
|
|
15224
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15494
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(agentSessions.projectId, project.id)).run();
|
|
15225
15495
|
return { status: "reset" };
|
|
15226
15496
|
}
|
|
15227
15497
|
);
|
|
@@ -16164,20 +16434,20 @@ function clipText(value, length) {
|
|
|
16164
16434
|
}
|
|
16165
16435
|
|
|
16166
16436
|
// src/research-runner.ts
|
|
16167
|
-
import { and as and17, eq as
|
|
16437
|
+
import { and as and17, eq as eq24, inArray as inArray8, sql as sql10 } from "drizzle-orm";
|
|
16168
16438
|
var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
|
|
16169
16439
|
function finalResearchRunStatus(completed, failed) {
|
|
16170
16440
|
if (failed === 0) return ResearchRunStatuses.completed;
|
|
16171
16441
|
return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
|
|
16172
16442
|
}
|
|
16173
16443
|
async function executeResearchRun(db, registry, runId, projectId) {
|
|
16174
|
-
const run = db.select().from(researchRuns).where(and17(
|
|
16444
|
+
const run = db.select().from(researchRuns).where(and17(eq24(researchRuns.id, runId), eq24(researchRuns.projectId, projectId))).get();
|
|
16175
16445
|
if (!run || run.status !== ResearchRunStatuses.queued) return;
|
|
16176
|
-
const project = db.select().from(projects).where(
|
|
16446
|
+
const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
16177
16447
|
if (!project) return;
|
|
16178
16448
|
const provider = registry.get(run.provider);
|
|
16179
16449
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16180
|
-
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and17(
|
|
16450
|
+
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and17(eq24(researchRuns.id, runId), eq24(researchRuns.status, ResearchRunStatuses.queued))).run();
|
|
16181
16451
|
if (claim.changes !== 1) return;
|
|
16182
16452
|
let reserved = 0;
|
|
16183
16453
|
let dispatched = 0;
|
|
@@ -16201,8 +16471,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16201
16471
|
}
|
|
16202
16472
|
reserved = run.totalQueries;
|
|
16203
16473
|
reservation = { scope, period };
|
|
16204
|
-
const rows = db.select().from(researchRunQueries).where(
|
|
16205
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
16474
|
+
const rows = db.select().from(researchRunQueries).where(eq24(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
|
|
16475
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq24(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
16206
16476
|
const domains = effectiveDomains(project);
|
|
16207
16477
|
const brands = effectiveBrandNames(project);
|
|
16208
16478
|
const config = { ...provider.config, model: run.resolvedModel };
|
|
@@ -16210,7 +16480,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16210
16480
|
await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
|
|
16211
16481
|
try {
|
|
16212
16482
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16213
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and17(
|
|
16483
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and17(eq24(researchRunQueries.id, row.id), eq24(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
|
|
16214
16484
|
const raw = await gate.run(async () => {
|
|
16215
16485
|
dispatched++;
|
|
16216
16486
|
return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
|
|
@@ -16237,7 +16507,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16237
16507
|
citationState: determineCitationState(normalized, domains),
|
|
16238
16508
|
rawResponse: raw.rawResponse,
|
|
16239
16509
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16240
|
-
}).where(and17(
|
|
16510
|
+
}).where(and17(eq24(researchRunQueries.id, row.id), eq24(researchRunQueries.status, ResearchQueryStatuses.running))).run();
|
|
16241
16511
|
if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
|
|
16242
16512
|
} catch (error) {
|
|
16243
16513
|
try {
|
|
@@ -16261,18 +16531,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16261
16531
|
}
|
|
16262
16532
|
}
|
|
16263
16533
|
function incrementResearchProgress(db, runId, column) {
|
|
16264
|
-
db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(
|
|
16534
|
+
db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(eq24(researchRuns.id, runId)).run();
|
|
16265
16535
|
}
|
|
16266
16536
|
function markResearchQueryFailed(db, runId, queryId, error) {
|
|
16267
16537
|
const message = describeError(error);
|
|
16268
|
-
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(
|
|
16538
|
+
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(eq24(researchRunQueries.id, queryId), inArray8(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
16269
16539
|
if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
|
|
16270
16540
|
}
|
|
16271
16541
|
function markUnfinishedResearchQueriesFailed(db, runId, error) {
|
|
16272
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(
|
|
16542
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(eq24(researchRunQueries.researchRunId, runId), inArray8(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
16273
16543
|
}
|
|
16274
16544
|
function finalizeResearchRun(db, runId, fatalError) {
|
|
16275
|
-
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(
|
|
16545
|
+
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq24(researchRunQueries.researchRunId, runId)).all();
|
|
16276
16546
|
const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
|
|
16277
16547
|
const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
|
|
16278
16548
|
db.update(researchRuns).set({
|
|
@@ -16281,11 +16551,11 @@ function finalizeResearchRun(db, runId, fatalError) {
|
|
|
16281
16551
|
failedQueries: failed,
|
|
16282
16552
|
error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
|
|
16283
16553
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16284
|
-
}).where(
|
|
16554
|
+
}).where(eq24(researchRuns.id, runId)).run();
|
|
16285
16555
|
}
|
|
16286
16556
|
|
|
16287
16557
|
// src/google-marketing-sync.ts
|
|
16288
|
-
import { and as and18, eq as
|
|
16558
|
+
import { and as and18, eq as eq25, isNull as isNull3 } from "drizzle-orm";
|
|
16289
16559
|
function safeErrorMessage(error) {
|
|
16290
16560
|
const raw = describeError(error);
|
|
16291
16561
|
return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
|
|
@@ -16309,9 +16579,9 @@ function assertSnapshotContext(metadata, expected) {
|
|
|
16309
16579
|
}
|
|
16310
16580
|
function requireSyncRun(db, runId, projectId, kind) {
|
|
16311
16581
|
const run = db.select().from(runs).where(and18(
|
|
16312
|
-
|
|
16313
|
-
|
|
16314
|
-
|
|
16582
|
+
eq25(runs.id, runId),
|
|
16583
|
+
eq25(runs.projectId, projectId),
|
|
16584
|
+
eq25(runs.kind, kind)
|
|
16315
16585
|
)).get();
|
|
16316
16586
|
if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
|
|
16317
16587
|
if (run.status !== RunStatuses.queued) {
|
|
@@ -16325,18 +16595,18 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
|
|
|
16325
16595
|
error: serializeRunError({ message: safeErrorMessage(error) }),
|
|
16326
16596
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16327
16597
|
}).where(and18(
|
|
16328
|
-
|
|
16329
|
-
|
|
16330
|
-
|
|
16331
|
-
|
|
16598
|
+
eq25(runs.id, runId),
|
|
16599
|
+
eq25(runs.projectId, projectId),
|
|
16600
|
+
eq25(runs.kind, kind),
|
|
16601
|
+
eq25(runs.status, expectedStatus)
|
|
16332
16602
|
)).run();
|
|
16333
16603
|
}
|
|
16334
16604
|
function claimSyncRun(db, runId, projectId, kind, startedAt) {
|
|
16335
16605
|
const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and18(
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
|
|
16339
|
-
|
|
16606
|
+
eq25(runs.id, runId),
|
|
16607
|
+
eq25(runs.projectId, projectId),
|
|
16608
|
+
eq25(runs.kind, kind),
|
|
16609
|
+
eq25(runs.status, RunStatuses.queued)
|
|
16340
16610
|
)).run();
|
|
16341
16611
|
return claim.changes === 1;
|
|
16342
16612
|
}
|
|
@@ -16344,9 +16614,9 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
16344
16614
|
let claimed = false;
|
|
16345
16615
|
try {
|
|
16346
16616
|
requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
|
|
16347
|
-
const project = db.select().from(projects).where(
|
|
16617
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
16348
16618
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
16349
|
-
const connection = db.select().from(googleAdsConnections).where(
|
|
16619
|
+
const connection = db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, projectId)).get();
|
|
16350
16620
|
if (!connection?.selectedCustomerId) {
|
|
16351
16621
|
throw new Error("Google Ads needs a selected customer before it can sync.");
|
|
16352
16622
|
}
|
|
@@ -16398,10 +16668,10 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
16398
16668
|
finishedAt,
|
|
16399
16669
|
error: null
|
|
16400
16670
|
}).where(and18(
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16671
|
+
eq25(runs.id, runId),
|
|
16672
|
+
eq25(runs.projectId, projectId),
|
|
16673
|
+
eq25(runs.kind, RunKinds["google-ads-sync"]),
|
|
16674
|
+
eq25(runs.status, RunStatuses.running)
|
|
16405
16675
|
)).run();
|
|
16406
16676
|
if (terminalClaim.changes !== 1) {
|
|
16407
16677
|
throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
|
|
@@ -16419,11 +16689,11 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
16419
16689
|
lastMetricsSnapshotId: result.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
|
|
16420
16690
|
updatedAt: finishedAt
|
|
16421
16691
|
}).where(and18(
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) :
|
|
16426
|
-
|
|
16692
|
+
eq25(googleAdsConnections.id, connection.id),
|
|
16693
|
+
eq25(googleAdsConnections.projectId, projectId),
|
|
16694
|
+
eq25(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
|
|
16695
|
+
connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) : eq25(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
|
|
16696
|
+
eq25(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
|
|
16427
16697
|
)).run();
|
|
16428
16698
|
if (connectionClaim.changes !== 1) {
|
|
16429
16699
|
throw new Error("Google Ads selection changed while its evidence was being collected.");
|
|
@@ -16450,9 +16720,9 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
16450
16720
|
let claimed = false;
|
|
16451
16721
|
try {
|
|
16452
16722
|
requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
|
|
16453
|
-
const project = db.select().from(projects).where(
|
|
16723
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
16454
16724
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
16455
|
-
const connection = db.select().from(gtmConnections).where(
|
|
16725
|
+
const connection = db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, projectId)).get();
|
|
16456
16726
|
if (!connection?.selectedAccountId || !connection.selectedContainerId) {
|
|
16457
16727
|
throw new Error("GTM needs a selected account and container before it can sync.");
|
|
16458
16728
|
}
|
|
@@ -16488,10 +16758,10 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
16488
16758
|
finishedAt,
|
|
16489
16759
|
error: null
|
|
16490
16760
|
}).where(and18(
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16761
|
+
eq25(runs.id, runId),
|
|
16762
|
+
eq25(runs.projectId, projectId),
|
|
16763
|
+
eq25(runs.kind, RunKinds["gtm-sync"]),
|
|
16764
|
+
eq25(runs.status, RunStatuses.running)
|
|
16495
16765
|
)).run();
|
|
16496
16766
|
if (terminalClaim.changes !== 1) {
|
|
16497
16767
|
throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
|
|
@@ -16506,12 +16776,12 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
16506
16776
|
lastSnapshotId: snapshot.metadata.id,
|
|
16507
16777
|
updatedAt: finishedAt
|
|
16508
16778
|
}).where(and18(
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) :
|
|
16514
|
-
|
|
16779
|
+
eq25(gtmConnections.id, connection.id),
|
|
16780
|
+
eq25(gtmConnections.projectId, projectId),
|
|
16781
|
+
eq25(gtmConnections.selectedAccountId, connection.selectedAccountId),
|
|
16782
|
+
eq25(gtmConnections.selectedContainerId, connection.selectedContainerId),
|
|
16783
|
+
connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) : eq25(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
|
|
16784
|
+
eq25(gtmConnections.selectionGeneration, connection.selectionGeneration)
|
|
16515
16785
|
)).run();
|
|
16516
16786
|
if (connectionClaim.changes !== 1) {
|
|
16517
16787
|
throw new Error("GTM selection changed while its evidence was being collected.");
|
|
@@ -16597,15 +16867,15 @@ function summarizeProviderConfig(config) {
|
|
|
16597
16867
|
quota: { ...config?.quota ?? DEFAULT_QUOTA }
|
|
16598
16868
|
};
|
|
16599
16869
|
}
|
|
16600
|
-
function
|
|
16601
|
-
return
|
|
16870
|
+
function hashApiKey2(key) {
|
|
16871
|
+
return crypto27.createHash("sha256").update(key).digest("hex");
|
|
16602
16872
|
}
|
|
16603
16873
|
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
16604
16874
|
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
16605
16875
|
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
16606
16876
|
function hashDashboardPassword(password) {
|
|
16607
|
-
const salt =
|
|
16608
|
-
const derived =
|
|
16877
|
+
const salt = crypto27.randomBytes(16);
|
|
16878
|
+
const derived = crypto27.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
|
|
16609
16879
|
N: DASHBOARD_SCRYPT_COST,
|
|
16610
16880
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
16611
16881
|
});
|
|
@@ -16626,23 +16896,23 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
16626
16896
|
} catch {
|
|
16627
16897
|
return { ok: false, needsRehash: false };
|
|
16628
16898
|
}
|
|
16629
|
-
const derived =
|
|
16899
|
+
const derived = crypto27.scryptSync(password, salt, expected.length, {
|
|
16630
16900
|
N: DASHBOARD_SCRYPT_COST,
|
|
16631
16901
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
16632
16902
|
});
|
|
16633
16903
|
if (derived.length !== expected.length)
|
|
16634
16904
|
return { ok: false, needsRehash: false };
|
|
16635
16905
|
return {
|
|
16636
|
-
ok:
|
|
16906
|
+
ok: crypto27.timingSafeEqual(derived, expected),
|
|
16637
16907
|
needsRehash: false
|
|
16638
16908
|
};
|
|
16639
16909
|
}
|
|
16640
16910
|
if (/^[a-f0-9]{64}$/i.test(storedHash)) {
|
|
16641
|
-
const candidate = Buffer.from(
|
|
16911
|
+
const candidate = Buffer.from(hashApiKey2(password), "hex");
|
|
16642
16912
|
const expected = Buffer.from(storedHash, "hex");
|
|
16643
16913
|
if (candidate.length !== expected.length)
|
|
16644
16914
|
return { ok: false, needsRehash: false };
|
|
16645
|
-
const ok =
|
|
16915
|
+
const ok = crypto27.timingSafeEqual(candidate, expected);
|
|
16646
16916
|
return { ok, needsRehash: ok };
|
|
16647
16917
|
}
|
|
16648
16918
|
return { ok: false, needsRehash: false };
|
|
@@ -16778,7 +17048,7 @@ function cloneGtmConfig(config) {
|
|
|
16778
17048
|
function createGoogleMarketingConfigCredentialStore(input) {
|
|
16779
17049
|
const persistConfigPatch = input.saveConfigPatch ?? saveConfigPatch;
|
|
16780
17050
|
const env = input.env ?? process.env;
|
|
16781
|
-
const randomUUID = input.randomUUID ??
|
|
17051
|
+
const randomUUID = input.randomUUID ?? crypto27.randomUUID;
|
|
16782
17052
|
const snapshot = () => ({
|
|
16783
17053
|
googleAds: cloneGoogleAdsConfig(input.config.googleAds),
|
|
16784
17054
|
gtm: cloneGtmConfig(input.config.gtm)
|
|
@@ -16970,7 +17240,7 @@ async function createServer(opts) {
|
|
|
16970
17240
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
16971
17241
|
async (ctx) => {
|
|
16972
17242
|
if (!sessionRegistry) return;
|
|
16973
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
17243
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq26(projects.id, ctx.projectId)).get();
|
|
16974
17244
|
if (!project) return;
|
|
16975
17245
|
let content;
|
|
16976
17246
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -17413,7 +17683,7 @@ async function createServer(opts) {
|
|
|
17413
17683
|
void (async () => {
|
|
17414
17684
|
try {
|
|
17415
17685
|
const report = await aeroClient.runDoctor({ project: projectName });
|
|
17416
|
-
const project = opts.db.select().from(projects).where(
|
|
17686
|
+
const project = opts.db.select().from(projects).where(eq26(projects.name, projectName)).get();
|
|
17417
17687
|
if (!project) {
|
|
17418
17688
|
app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
|
|
17419
17689
|
return;
|
|
@@ -17442,8 +17712,8 @@ async function createServer(opts) {
|
|
|
17442
17712
|
if (!probed) return;
|
|
17443
17713
|
const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
|
|
17444
17714
|
and19(
|
|
17445
|
-
|
|
17446
|
-
|
|
17715
|
+
eq26(ccReleaseSyncs.release, probed.release),
|
|
17716
|
+
eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
|
|
17447
17717
|
)
|
|
17448
17718
|
).limit(1).get();
|
|
17449
17719
|
if (alreadySynced) {
|
|
@@ -17624,7 +17894,7 @@ async function createServer(opts) {
|
|
|
17624
17894
|
return removed;
|
|
17625
17895
|
}
|
|
17626
17896
|
};
|
|
17627
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
17897
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto27.randomBytes(32).toString("hex");
|
|
17628
17898
|
const googleConnectionStore = {
|
|
17629
17899
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
17630
17900
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -17697,14 +17967,26 @@ async function createServer(opts) {
|
|
|
17697
17967
|
"Dashboard password gate resolved"
|
|
17698
17968
|
);
|
|
17699
17969
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
17970
|
+
const credentialChecker = createCredentialChecker({
|
|
17971
|
+
db: opts.db,
|
|
17972
|
+
trustProxyConfigured: trustProxy !== false
|
|
17973
|
+
});
|
|
17700
17974
|
const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
|
|
17975
|
+
const publicOrigin = (() => {
|
|
17976
|
+
if (!googlePublicUrl) return void 0;
|
|
17977
|
+
try {
|
|
17978
|
+
return new URL(googlePublicUrl).origin;
|
|
17979
|
+
} catch {
|
|
17980
|
+
return void 0;
|
|
17981
|
+
}
|
|
17982
|
+
})();
|
|
17701
17983
|
if (opts.config.apiKey) {
|
|
17702
|
-
const keyHash =
|
|
17703
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
17984
|
+
const keyHash = hashApiKey2(opts.config.apiKey);
|
|
17985
|
+
const existing = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, keyHash)).get();
|
|
17704
17986
|
if (!existing) {
|
|
17705
17987
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
17706
17988
|
opts.db.insert(apiKeys).values({
|
|
17707
|
-
id: `key_${
|
|
17989
|
+
id: `key_${crypto27.randomBytes(8).toString("hex")}`,
|
|
17708
17990
|
name: "default",
|
|
17709
17991
|
keyHash,
|
|
17710
17992
|
keyPrefix: prefix,
|
|
@@ -17728,7 +18010,7 @@ async function createServer(opts) {
|
|
|
17728
18010
|
};
|
|
17729
18011
|
const createSession = (apiKeyId) => {
|
|
17730
18012
|
pruneExpiredSessions();
|
|
17731
|
-
const sessionId =
|
|
18013
|
+
const sessionId = crypto27.randomBytes(32).toString("hex");
|
|
17732
18014
|
sessions.set(sessionId, {
|
|
17733
18015
|
apiKeyId,
|
|
17734
18016
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -17752,7 +18034,7 @@ async function createServer(opts) {
|
|
|
17752
18034
|
};
|
|
17753
18035
|
const getDefaultApiKey = () => {
|
|
17754
18036
|
if (!opts.config.apiKey) return void 0;
|
|
17755
|
-
return opts.db.select().from(apiKeys).where(
|
|
18037
|
+
return opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
|
|
17756
18038
|
};
|
|
17757
18039
|
const createPasswordSession = (reply) => {
|
|
17758
18040
|
const key = getDefaultApiKey();
|
|
@@ -17776,7 +18058,7 @@ async function createServer(opts) {
|
|
|
17776
18058
|
if (!header) return false;
|
|
17777
18059
|
const parts = header.split(" ");
|
|
17778
18060
|
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
17779
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
18061
|
+
const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
|
|
17780
18062
|
return Boolean(key && !key.revokedAt);
|
|
17781
18063
|
};
|
|
17782
18064
|
const namedAccountsInUse = () => anyUsersExist(opts.db);
|
|
@@ -17866,12 +18148,12 @@ async function createServer(opts) {
|
|
|
17866
18148
|
return reply.send({ authenticated: true });
|
|
17867
18149
|
}
|
|
17868
18150
|
if (apiKey) {
|
|
17869
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
18151
|
+
const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(apiKey))).get();
|
|
17870
18152
|
if (!key || key.revokedAt) {
|
|
17871
18153
|
const err2 = authInvalid();
|
|
17872
18154
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
17873
18155
|
}
|
|
17874
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
18156
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(apiKeys.id, key.id)).run();
|
|
17875
18157
|
const sessionId = createSession(key.id);
|
|
17876
18158
|
reply.header(
|
|
17877
18159
|
"set-cookie",
|
|
@@ -18004,7 +18286,11 @@ async function createServer(opts) {
|
|
|
18004
18286
|
vercelSyncDeadlineMs: resolveVercelSyncDeadlineMs(process.env),
|
|
18005
18287
|
// Local-only Aero agent routes. Registered here so they inherit api-routes'
|
|
18006
18288
|
// auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
|
|
18289
|
+
credentials: credentialChecker,
|
|
18290
|
+
oauthResourceUrl: publicOrigin ? `${publicOrigin}${`${basePath ?? "/"}api/v1/mcp`.replace("//", "/")}` : void 0,
|
|
18007
18291
|
registerAuthenticatedRoutes: async (scope) => {
|
|
18292
|
+
registerMcpHttpRoutes(scope, { selfApiUrl: opts.config.apiUrl, issuer: publicOrigin, db: opts.db });
|
|
18293
|
+
registerOAuthAdminRoutes(scope, { db: opts.db });
|
|
18008
18294
|
if (!sessionRegistry) return;
|
|
18009
18295
|
registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
|
|
18010
18296
|
},
|
|
@@ -18015,8 +18301,8 @@ async function createServer(opts) {
|
|
|
18015
18301
|
getAgentProviderSummary: () => buildAgentProvidersResponse(opts.config).providers,
|
|
18016
18302
|
getGoogleMarketingDoctorInput: (ctx) => {
|
|
18017
18303
|
if (!ctx.project) return null;
|
|
18018
|
-
const googleAds = opts.db.select().from(googleAdsConnections).where(
|
|
18019
|
-
const gtm = opts.db.select().from(gtmConnections).where(
|
|
18304
|
+
const googleAds = opts.db.select().from(googleAdsConnections).where(eq26(googleAdsConnections.projectId, ctx.project.id)).get();
|
|
18305
|
+
const gtm = opts.db.select().from(gtmConnections).where(eq26(gtmConnections.projectId, ctx.project.id)).get();
|
|
18020
18306
|
const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
|
|
18021
18307
|
const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
|
|
18022
18308
|
const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
|
|
@@ -18140,7 +18426,7 @@ async function createServer(opts) {
|
|
|
18140
18426
|
deps: {
|
|
18141
18427
|
enqueueAutoExtract: ({ projectId, release: r }) => {
|
|
18142
18428
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18143
|
-
const runId =
|
|
18429
|
+
const runId = crypto27.randomUUID();
|
|
18144
18430
|
opts.db.insert(runs).values({
|
|
18145
18431
|
id: runId,
|
|
18146
18432
|
projectId,
|
|
@@ -18265,7 +18551,7 @@ async function createServer(opts) {
|
|
|
18265
18551
|
...inspectOpts,
|
|
18266
18552
|
config: opts.config
|
|
18267
18553
|
}).then(() => {
|
|
18268
|
-
const finished = opts.db.select({ status: runs.status }).from(runs).where(
|
|
18554
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq26(runs.id, runId)).get();
|
|
18269
18555
|
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
18270
18556
|
return maybeRefreshGscCoverage(
|
|
18271
18557
|
opts.db,
|
|
@@ -18375,7 +18661,7 @@ async function createServer(opts) {
|
|
|
18375
18661
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18376
18662
|
opts.db.insert(auditLog).values(
|
|
18377
18663
|
targetProjectIds.map((projectId) => ({
|
|
18378
|
-
id:
|
|
18664
|
+
id: crypto27.randomUUID(),
|
|
18379
18665
|
projectId,
|
|
18380
18666
|
actor: "api",
|
|
18381
18667
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -18629,6 +18915,16 @@ async function createServer(opts) {
|
|
|
18629
18915
|
const error = notFound("API route", url);
|
|
18630
18916
|
return reply.status(error.statusCode).send(error.toJSON());
|
|
18631
18917
|
}
|
|
18918
|
+
const hasDotSegment = (candidate) => candidate.split("/").some((segment) => segment.startsWith("."));
|
|
18919
|
+
let decodedUrl = url;
|
|
18920
|
+
try {
|
|
18921
|
+
decodedUrl = decodeURIComponent(url);
|
|
18922
|
+
} catch {
|
|
18923
|
+
}
|
|
18924
|
+
if (hasDotSegment(url) || hasDotSegment(decodedUrl)) {
|
|
18925
|
+
const error = notFound("Route", url);
|
|
18926
|
+
return reply.status(error.statusCode).send(error.toJSON());
|
|
18927
|
+
}
|
|
18632
18928
|
if (basePath && !url.startsWith(basePath)) {
|
|
18633
18929
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
18634
18930
|
}
|
|
@@ -18649,13 +18945,38 @@ async function createServer(opts) {
|
|
|
18649
18945
|
...update ? { updateAvailable: update } : {}
|
|
18650
18946
|
};
|
|
18651
18947
|
};
|
|
18948
|
+
if (publicOrigin) {
|
|
18949
|
+
registerOAuthRoutes(app, {
|
|
18950
|
+
db: opts.db,
|
|
18951
|
+
issuer: publicOrigin,
|
|
18952
|
+
resourcePaths: mcpTransportPaths().map(
|
|
18953
|
+
(suffix) => `${basePath ?? "/"}api/v1${suffix}`.replace("//", "/")
|
|
18954
|
+
),
|
|
18955
|
+
resolveUser: (request) => {
|
|
18956
|
+
const cookies = parseCookieHeader(request.headers.cookie);
|
|
18957
|
+
const token = cookies[USER_SESSION_COOKIE_NAME];
|
|
18958
|
+
if (!token) return null;
|
|
18959
|
+
const resolved = resolveUserSession(opts.db, token);
|
|
18960
|
+
return resolved ? { id: resolved.user.id, name: resolved.user.name } : null;
|
|
18961
|
+
},
|
|
18962
|
+
credentials: credentialChecker,
|
|
18963
|
+
startSession: (userId) => {
|
|
18964
|
+
const token = createUserSession(opts.db, userId);
|
|
18965
|
+
return serializeUserSessionCookie({
|
|
18966
|
+
value: token,
|
|
18967
|
+
path: basePath ?? "/",
|
|
18968
|
+
secure: opts.config.publicUrl?.startsWith("https://") ?? false
|
|
18969
|
+
});
|
|
18970
|
+
}
|
|
18971
|
+
});
|
|
18972
|
+
}
|
|
18652
18973
|
app.get("/health", healthHandler);
|
|
18653
18974
|
if (basePath) {
|
|
18654
18975
|
app.get(`${basePath}health`, healthHandler);
|
|
18655
18976
|
}
|
|
18656
18977
|
checkLatestVersionForServer();
|
|
18657
18978
|
scheduler.start();
|
|
18658
|
-
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(
|
|
18979
|
+
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq26(researchRuns.status, ResearchRunStatuses.queued)).all()) {
|
|
18659
18980
|
dispatchResearchRun(run.id, run.projectId);
|
|
18660
18981
|
}
|
|
18661
18982
|
app.addHook("onClose", async () => {
|