@canonry/canonry 4.177.2 → 4.178.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/assets/assets/{AuditHistoryPanel-YZvGU1la.js → AuditHistoryPanel-BmKdyRrR.js} +1 -1
- package/assets/assets/{BacklinksPage-CB2uJWWy.js → BacklinksPage-DtqKPVLV.js} +1 -1
- package/assets/assets/{ChartPrimitives-BoYe2M-i.js → ChartPrimitives-B_LDVvTy.js} +1 -1
- package/assets/assets/{HistoryPage-D9IxxbQ9.js → HistoryPage-yyJsQiJQ.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-CxgnnPdp.js → MeasurementPropertyPage-hjW4iYtr.js} +1 -1
- package/assets/assets/ProjectPage-DZKpnPvq.js +9 -0
- package/assets/assets/{RunRow-2KxOta8F.js → RunRow-BZeBFb22.js} +1 -1
- package/assets/assets/{RunsPage-ya8vTkyd.js → RunsPage-tnXHsK_R.js} +1 -1
- package/assets/assets/{SettingsPage-lTy3o2bo.js → SettingsPage-Djj5k8vv.js} +1 -1
- package/assets/assets/{SiteHealthSection-Cyu7MIv2.js → SiteHealthSection-BkDncS11.js} +3 -3
- package/assets/assets/{TrafficPage-DjhKfw57.js → TrafficPage-nJggXVh-.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-Pwi-Dl9Q.js → TrafficSourceDetailPage-zxONbSn-.js} +1 -1
- package/assets/assets/{extract-error-message-I1FVWHLH.js → extract-error-message-NpQu2vBm.js} +1 -1
- package/assets/assets/{index-DQVTRT9J.css → index-D0GbiVhZ.css} +1 -1
- package/assets/assets/index-Dwwnkd_V.js +83 -0
- package/assets/assets/{react-sigma_core.esm.min-BX6mOEq5.js → react-sigma_core.esm.min-CxMdd8U8.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-QRE3DAAU.js → chunk-A7FB2BVW.js} +2 -2
- package/dist/{chunk-XFAPF2YX.js → chunk-O7J5WFVZ.js} +361 -100
- package/dist/{chunk-6XUWDSLN.js → chunk-SXKA4OXZ.js} +1356 -907
- package/dist/{chunk-PQT3ECM3.js → chunk-T4JH77O6.js} +107 -0
- package/dist/{chunk-GJE334DA.js → chunk-YIQMTOEZ.js} +3029 -2080
- package/dist/cli.js +87 -5
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-C57RIB37.js → intelligence-service-YISV327N.js} +2 -2
- package/dist/mcp.js +5 -380
- package/package.json +9 -9
- 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-SXKA4OXZ.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,6 +95,7 @@ import {
|
|
|
90
95
|
gscUrlInspections,
|
|
91
96
|
gtmConnections,
|
|
92
97
|
gtmRawSnapshots,
|
|
98
|
+
hashApiKey,
|
|
93
99
|
hashAttributes,
|
|
94
100
|
hashLodging,
|
|
95
101
|
hashPlaceDetails,
|
|
@@ -110,6 +116,7 @@ import {
|
|
|
110
116
|
migrate,
|
|
111
117
|
nextRunFromCron,
|
|
112
118
|
notifications,
|
|
119
|
+
parseCookieHeader,
|
|
113
120
|
parseJsonColumn,
|
|
114
121
|
probeLatestRelease,
|
|
115
122
|
projects,
|
|
@@ -122,15 +129,19 @@ import {
|
|
|
122
129
|
readInstalledVersion,
|
|
123
130
|
redactNotificationUrl,
|
|
124
131
|
refreshAccessToken,
|
|
132
|
+
registerOAuthAdminRoutes,
|
|
133
|
+
registerOAuthRoutes,
|
|
125
134
|
researchRunQueries,
|
|
126
135
|
researchRuns,
|
|
127
136
|
resolveDestination,
|
|
128
137
|
resolveMeasurementSitemapTarget,
|
|
129
138
|
resolveTrustProxy,
|
|
139
|
+
resolveUserSession,
|
|
130
140
|
resolveVercelSyncDeadlineMs,
|
|
131
141
|
resolveWebhookTarget,
|
|
132
142
|
runs,
|
|
133
143
|
schedules,
|
|
144
|
+
serializeUserSessionCookie,
|
|
134
145
|
siteAuditPages,
|
|
135
146
|
siteAuditSnapshots,
|
|
136
147
|
siteCrawlAttempts,
|
|
@@ -145,7 +156,7 @@ import {
|
|
|
145
156
|
siteCrawlSnapshots,
|
|
146
157
|
toAlertView,
|
|
147
158
|
usageCounters
|
|
148
|
-
} from "./chunk-
|
|
159
|
+
} from "./chunk-YIQMTOEZ.js";
|
|
149
160
|
import {
|
|
150
161
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
151
162
|
AGENT_PROVIDER_IDS,
|
|
@@ -249,6 +260,7 @@ import {
|
|
|
249
260
|
isAgentProviderId,
|
|
250
261
|
isBrowserProvider,
|
|
251
262
|
isGhostTelemetryEvent,
|
|
263
|
+
isReadOnlyKey,
|
|
252
264
|
isRetryableHttpError,
|
|
253
265
|
isSelfLink,
|
|
254
266
|
isVertexGroundingRedirect,
|
|
@@ -282,7 +294,7 @@ import {
|
|
|
282
294
|
validationError,
|
|
283
295
|
winnabilityClassLabel,
|
|
284
296
|
withRetry
|
|
285
|
-
} from "./chunk-
|
|
297
|
+
} from "./chunk-T4JH77O6.js";
|
|
286
298
|
|
|
287
299
|
// src/telemetry.ts
|
|
288
300
|
import crypto from "crypto";
|
|
@@ -849,9 +861,11 @@ function dailyMetricClauses(options) {
|
|
|
849
861
|
);
|
|
850
862
|
}
|
|
851
863
|
const filters = [
|
|
852
|
-
`segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'
|
|
853
|
-
"campaign.status != 'REMOVED'"
|
|
864
|
+
`segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'`
|
|
854
865
|
];
|
|
866
|
+
if (options.campaignIds === void 0) {
|
|
867
|
+
filters.push("campaign.status != 'REMOVED'");
|
|
868
|
+
}
|
|
855
869
|
if (options.campaignIds !== void 0) {
|
|
856
870
|
if (!Array.isArray(options.campaignIds) || options.campaignIds.length === 0) {
|
|
857
871
|
throw new GoogleAdsApiError("Campaign IDs must be a non-empty array when provided", 400);
|
|
@@ -878,6 +892,22 @@ function dailyMetricClauses(options) {
|
|
|
878
892
|
`LIMIT ${validatedLimit(options.limit)}`
|
|
879
893
|
];
|
|
880
894
|
}
|
|
895
|
+
function buildCampaignSpendRankingQuery(options) {
|
|
896
|
+
const start = parseIsoDate(options.startDate, "Start date");
|
|
897
|
+
const end = parseIsoDate(options.endDate, "End date");
|
|
898
|
+
if (start > end) {
|
|
899
|
+
throw new GoogleAdsApiError("Start date must be on or before end date", 400);
|
|
900
|
+
}
|
|
901
|
+
return selectQuery([
|
|
902
|
+
"campaign.id",
|
|
903
|
+
"campaign.status",
|
|
904
|
+
"metrics.cost_micros"
|
|
905
|
+
], "campaign", [
|
|
906
|
+
`WHERE segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'`,
|
|
907
|
+
"ORDER BY metrics.cost_micros DESC",
|
|
908
|
+
`LIMIT ${validatedLimit(options.limit)}`
|
|
909
|
+
]);
|
|
910
|
+
}
|
|
881
911
|
function buildCampaignsQuery(options = {}) {
|
|
882
912
|
const clauses = [];
|
|
883
913
|
if (!options.includeRemoved) clauses.push("WHERE campaign.status != 'REMOVED'");
|
|
@@ -1317,6 +1347,18 @@ var GoogleAdsClient = class {
|
|
|
1317
1347
|
}
|
|
1318
1348
|
};
|
|
1319
1349
|
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Campaigns ranked by spend over a window, removed ones included, so the
|
|
1352
|
+
* bounded daily-metrics query can be scoped to where the money actually went
|
|
1353
|
+
* rather than to the lowest campaign ids.
|
|
1354
|
+
*/
|
|
1355
|
+
async getCampaignSpendRanking(customerId, options) {
|
|
1356
|
+
return this.#searchStream(
|
|
1357
|
+
customerId,
|
|
1358
|
+
"campaign-spend-ranking",
|
|
1359
|
+
buildCampaignSpendRankingQuery(options)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1320
1362
|
async getDailyCampaignMetrics(customerId, options) {
|
|
1321
1363
|
return this.#searchStream(
|
|
1322
1364
|
customerId,
|
|
@@ -2816,19 +2858,21 @@ function campaignMetricDto(row) {
|
|
|
2816
2858
|
conversionValueMicros: conversionValueMicros(row.metrics)
|
|
2817
2859
|
};
|
|
2818
2860
|
}
|
|
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 };
|
|
2861
|
+
function defaultMetricsWindow(now, timeZone) {
|
|
2823
2862
|
const customerTimeZone = typeof timeZone === "string" && timeZone.trim() !== "" ? timeZone : "UTC";
|
|
2824
2863
|
const endDate = formatIsoDateInTimeZone(now.toISOString(), customerTimeZone);
|
|
2864
|
+
return { startDate: shiftIsoCalendarDate(endDate, -(GOOGLE_ADS_CAMPAIGN_METRICS_MAX_DAYS - 1)), endDate };
|
|
2865
|
+
}
|
|
2866
|
+
function defaultMetricsQuery(now, campaigns, timeZone, rankedCampaignIds) {
|
|
2867
|
+
const inventoryIds = campaigns.map((campaign) => campaign.id).sort((left, right) => left.localeCompare(right));
|
|
2868
|
+
const ranked = rankedCampaignIds ?? [];
|
|
2869
|
+
const ordered = ranked.length > 0 ? [...ranked, ...inventoryIds.filter((id) => !ranked.includes(id))] : inventoryIds;
|
|
2870
|
+
const selected = ordered.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS);
|
|
2871
|
+
if (selected.length === 0) return { query: null, inventoryTruncated: false };
|
|
2872
|
+
const { startDate, endDate } = defaultMetricsWindow(now, timeZone);
|
|
2825
2873
|
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
|
|
2874
|
+
query: googleAdsCampaignMetricsQuerySchema.parse({ campaignIds: selected, startDate, endDate }),
|
|
2875
|
+
inventoryTruncated: ordered.length > selected.length
|
|
2832
2876
|
};
|
|
2833
2877
|
}
|
|
2834
2878
|
function countRedactedProviderValues(value) {
|
|
@@ -3131,7 +3175,21 @@ var DefaultGoogleMarketingRuntime = class {
|
|
|
3131
3175
|
capturedAt
|
|
3132
3176
|
);
|
|
3133
3177
|
const effectiveGoalGraph = deriveGoogleAdsEffectiveGoalGraph(inventory);
|
|
3134
|
-
const
|
|
3178
|
+
const rankingWindow = defaultMetricsWindow(this.#now(), customerResult.data.timeZone);
|
|
3179
|
+
const ranking = await client.getCampaignSpendRanking(customerId, {
|
|
3180
|
+
...rankingWindow,
|
|
3181
|
+
limit: GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS + 1
|
|
3182
|
+
}).then((result) => ({
|
|
3183
|
+
ids: result.data.map((row) => String(row.campaign.id)),
|
|
3184
|
+
failed: false
|
|
3185
|
+
})).catch(() => ({ ids: [], failed: true }));
|
|
3186
|
+
const rankingTruncated = ranking.ids.length > GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS;
|
|
3187
|
+
const defaults = defaultMetricsQuery(
|
|
3188
|
+
this.#now(),
|
|
3189
|
+
inventory.campaigns,
|
|
3190
|
+
customerResult.data.timeZone,
|
|
3191
|
+
ranking.failed ? void 0 : ranking.ids.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS)
|
|
3192
|
+
);
|
|
3135
3193
|
const metricsQuery = suppliedMetricsQuery ?? defaults.query;
|
|
3136
3194
|
if (!metricsQuery) {
|
|
3137
3195
|
return {
|
|
@@ -3149,7 +3207,10 @@ var DefaultGoogleMarketingRuntime = class {
|
|
|
3149
3207
|
const response = {
|
|
3150
3208
|
query: metricsQuery,
|
|
3151
3209
|
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
|
|
3210
|
+
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
|
|
3211
|
+
// order that omits removed campaigns) or it reported more campaigns
|
|
3212
|
+
// than the cap. Both mean these totals are a subset of the account.
|
|
3213
|
+
suppliedMetricsQuery === void 0 && (ranking.failed || rankingTruncated),
|
|
3153
3214
|
fetchedAt: this.#now().toISOString()
|
|
3154
3215
|
};
|
|
3155
3216
|
const metricsPayload = {
|
|
@@ -3330,11 +3391,11 @@ function createGoogleMarketingRuntime(options) {
|
|
|
3330
3391
|
|
|
3331
3392
|
// src/server.ts
|
|
3332
3393
|
import { createRequire as createRequire4 } from "module";
|
|
3333
|
-
import
|
|
3394
|
+
import crypto27 from "crypto";
|
|
3334
3395
|
import fs7 from "fs";
|
|
3335
3396
|
import path8 from "path";
|
|
3336
3397
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3337
|
-
import { and as and19, eq as
|
|
3398
|
+
import { and as and19, eq as eq26 } from "drizzle-orm";
|
|
3338
3399
|
import Fastify from "fastify";
|
|
3339
3400
|
import os4 from "os";
|
|
3340
3401
|
|
|
@@ -11839,7 +11900,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
11839
11900
|
return result;
|
|
11840
11901
|
}
|
|
11841
11902
|
async function backfillInsightsCommand(project, opts) {
|
|
11842
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
11903
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-YISV327N.js");
|
|
11843
11904
|
const config = loadConfig();
|
|
11844
11905
|
const db = createClient(config.database);
|
|
11845
11906
|
migrate(db);
|
|
@@ -15185,10 +15246,159 @@ ${lines.join("\n")}
|
|
|
15185
15246
|
}
|
|
15186
15247
|
};
|
|
15187
15248
|
|
|
15188
|
-
// src/
|
|
15249
|
+
// src/mcp-http.ts
|
|
15250
|
+
import crypto26 from "crypto";
|
|
15189
15251
|
import { eq as eq22 } from "drizzle-orm";
|
|
15252
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15253
|
+
var SESSION_IDLE_MS = 10 * 60 * 1e3;
|
|
15254
|
+
var SWEEP_INTERVAL_MS = 60 * 1e3;
|
|
15255
|
+
function mintSessionKey(db, scopes, userId) {
|
|
15256
|
+
const raw = `cnry_${crypto26.randomBytes(24).toString("hex")}`;
|
|
15257
|
+
const id = crypto26.randomUUID();
|
|
15258
|
+
db.insert(apiKeys).values({
|
|
15259
|
+
id,
|
|
15260
|
+
name: `mcp-session:${userId}`,
|
|
15261
|
+
keyHash: hashApiKey(raw),
|
|
15262
|
+
keyPrefix: raw.slice(0, 9),
|
|
15263
|
+
scopes: [...scopes],
|
|
15264
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15265
|
+
}).run();
|
|
15266
|
+
return { id, raw };
|
|
15267
|
+
}
|
|
15268
|
+
function revokeSessionKey(db, id) {
|
|
15269
|
+
try {
|
|
15270
|
+
db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(apiKeys.id, id)).run();
|
|
15271
|
+
} catch {
|
|
15272
|
+
}
|
|
15273
|
+
}
|
|
15274
|
+
function mcpTransportPaths() {
|
|
15275
|
+
const paths = ["/mcp", "/mcp/readonly"];
|
|
15276
|
+
for (const toolkit of CANONRY_MCP_TOOLKIT_NAMES) {
|
|
15277
|
+
paths.push(`/mcp/x/${toolkit}`, `/mcp/x/${toolkit}/readonly`);
|
|
15278
|
+
}
|
|
15279
|
+
return paths;
|
|
15280
|
+
}
|
|
15281
|
+
function registerMcpHttpRoutes(scope, opts) {
|
|
15282
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
15283
|
+
const now = opts.now ?? (() => Date.now());
|
|
15284
|
+
async function dropSession(id) {
|
|
15285
|
+
const session = sessions.get(id);
|
|
15286
|
+
if (!session) return;
|
|
15287
|
+
sessions.delete(id);
|
|
15288
|
+
await session.close().catch(() => {
|
|
15289
|
+
});
|
|
15290
|
+
}
|
|
15291
|
+
const sweep = setInterval(() => {
|
|
15292
|
+
const cutoff = now() - SESSION_IDLE_MS;
|
|
15293
|
+
for (const [id, session] of sessions) {
|
|
15294
|
+
if (session.lastSeenAt < cutoff) void dropSession(id);
|
|
15295
|
+
}
|
|
15296
|
+
}, SWEEP_INTERVAL_MS);
|
|
15297
|
+
sweep.unref();
|
|
15298
|
+
if (opts.issuer) {
|
|
15299
|
+
scope.addHook("onSend", async (request, reply, payload) => {
|
|
15300
|
+
const isTransport = request.routeOptions.config?.transportEnvelope === true;
|
|
15301
|
+
if (isTransport && reply.statusCode === 401 && !reply.getHeader("WWW-Authenticate")) {
|
|
15302
|
+
const metadata = `${opts.issuer}/.well-known/oauth-protected-resource${request.routeOptions.url ?? ""}`;
|
|
15303
|
+
void reply.header("WWW-Authenticate", `Bearer resource_metadata="${metadata}"`);
|
|
15304
|
+
}
|
|
15305
|
+
return payload;
|
|
15306
|
+
});
|
|
15307
|
+
}
|
|
15308
|
+
scope.addHook("onClose", async () => {
|
|
15309
|
+
clearInterval(sweep);
|
|
15310
|
+
await Promise.all([...sessions.keys()].map((id) => dropSession(id)));
|
|
15311
|
+
});
|
|
15312
|
+
function callerBearer(request) {
|
|
15313
|
+
const header = request.headers.authorization;
|
|
15314
|
+
if (typeof header !== "string") return null;
|
|
15315
|
+
const trimmed = header.trim();
|
|
15316
|
+
const prefix = "bearer ";
|
|
15317
|
+
if (trimmed.length <= prefix.length) return null;
|
|
15318
|
+
if (trimmed.slice(0, prefix.length).toLowerCase() !== prefix) return null;
|
|
15319
|
+
return trimmed.slice(prefix.length).trim() || null;
|
|
15320
|
+
}
|
|
15321
|
+
async function openSession(request, keyId, segment) {
|
|
15322
|
+
const bearer = callerBearer(request);
|
|
15323
|
+
if (!bearer) return null;
|
|
15324
|
+
const scopes = request.principal?.scopes ?? request.apiKey?.scopes ?? [];
|
|
15325
|
+
let sessionKey = null;
|
|
15326
|
+
if (!request.apiKey && request.principal?.kind === "user") {
|
|
15327
|
+
sessionKey = mintSessionKey(opts.db, scopes, request.principal.id);
|
|
15328
|
+
}
|
|
15329
|
+
const client = new ApiClient(opts.selfApiUrl, sessionKey?.raw ?? bearer, { skipProbe: true });
|
|
15330
|
+
const server = createCanonryMcpServer({
|
|
15331
|
+
scope: segment.readOnly || isReadOnlyKey(scopes) ? "read-only" : "all",
|
|
15332
|
+
tiers: segment.tiers,
|
|
15333
|
+
clientFactory: () => client
|
|
15334
|
+
});
|
|
15335
|
+
const transport = new StreamableHTTPServerTransport({
|
|
15336
|
+
sessionIdGenerator: () => crypto26.randomUUID(),
|
|
15337
|
+
onsessioninitialized: (id) => {
|
|
15338
|
+
sessions.set(id, { transport, close, keyId, segmentId: segment.id, lastSeenAt: now() });
|
|
15339
|
+
}
|
|
15340
|
+
});
|
|
15341
|
+
async function close() {
|
|
15342
|
+
await transport.close();
|
|
15343
|
+
await server.close();
|
|
15344
|
+
if (sessionKey) revokeSessionKey(opts.db, sessionKey.id);
|
|
15345
|
+
}
|
|
15346
|
+
transport.onclose = () => {
|
|
15347
|
+
const id = transport.sessionId;
|
|
15348
|
+
if (id) sessions.delete(id);
|
|
15349
|
+
if (sessionKey) revokeSessionKey(opts.db, sessionKey.id);
|
|
15350
|
+
};
|
|
15351
|
+
await server.connect(transport);
|
|
15352
|
+
return { transport, close, keyId, segmentId: segment.id, lastSeenAt: now() };
|
|
15353
|
+
}
|
|
15354
|
+
async function handle(request, reply, segment) {
|
|
15355
|
+
const keyId = request.principal?.id ?? request.apiKey?.id;
|
|
15356
|
+
if (!keyId) {
|
|
15357
|
+
await reply.status(401).send({ error: { code: "AUTH_REQUIRED", message: "Authentication required." } });
|
|
15358
|
+
return;
|
|
15359
|
+
}
|
|
15360
|
+
const sessionId = request.headers["mcp-session-id"];
|
|
15361
|
+
const existing = typeof sessionId === "string" ? sessions.get(sessionId) : void 0;
|
|
15362
|
+
if (existing) {
|
|
15363
|
+
if (existing.keyId !== keyId || existing.segmentId !== segment.id) {
|
|
15364
|
+
await reply.status(404).send({ error: { code: "NOT_FOUND", message: "Unknown MCP session." } });
|
|
15365
|
+
return;
|
|
15366
|
+
}
|
|
15367
|
+
existing.lastSeenAt = now();
|
|
15368
|
+
await existing.transport.handleRequest(request.raw, reply.raw, request.body);
|
|
15369
|
+
return;
|
|
15370
|
+
}
|
|
15371
|
+
if (typeof sessionId === "string") {
|
|
15372
|
+
await reply.status(404).send({ error: { code: "NOT_FOUND", message: "Unknown MCP session." } });
|
|
15373
|
+
return;
|
|
15374
|
+
}
|
|
15375
|
+
const opened = await openSession(request, keyId, segment);
|
|
15376
|
+
if (!opened) {
|
|
15377
|
+
await reply.status(401).send({ error: { code: "AUTH_REQUIRED", message: "Authentication required." } });
|
|
15378
|
+
return;
|
|
15379
|
+
}
|
|
15380
|
+
await opened.transport.handleRequest(request.raw, reply.raw, request.body);
|
|
15381
|
+
}
|
|
15382
|
+
const config = { transportEnvelope: true };
|
|
15383
|
+
function mount(pathname, segment) {
|
|
15384
|
+
const run = (request, reply) => handle(request, reply, segment);
|
|
15385
|
+
scope.post(pathname, { config }, run);
|
|
15386
|
+
scope.get(pathname, { config }, run);
|
|
15387
|
+
scope.delete(pathname, { config }, run);
|
|
15388
|
+
}
|
|
15389
|
+
mount("/mcp", { id: "core", tiers: ["core"], readOnly: false });
|
|
15390
|
+
mount("/mcp/readonly", { id: "core:ro", tiers: ["core"], readOnly: true });
|
|
15391
|
+
for (const toolkit of CANONRY_MCP_TOOLKIT_NAMES) {
|
|
15392
|
+
const tiers = ["core", toolkit];
|
|
15393
|
+
mount(`/mcp/x/${toolkit}`, { id: toolkit, tiers, readOnly: false });
|
|
15394
|
+
mount(`/mcp/x/${toolkit}/readonly`, { id: `${toolkit}:ro`, tiers, readOnly: true });
|
|
15395
|
+
}
|
|
15396
|
+
}
|
|
15397
|
+
|
|
15398
|
+
// src/agent/agent-routes.ts
|
|
15399
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
15190
15400
|
function resolveProject(db, name) {
|
|
15191
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
15401
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq23(projects.name, name)).get();
|
|
15192
15402
|
if (!row) throw notFound("project", name);
|
|
15193
15403
|
return row;
|
|
15194
15404
|
}
|
|
@@ -15197,7 +15407,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
15197
15407
|
"/projects/:name/agent/transcript",
|
|
15198
15408
|
async (request) => {
|
|
15199
15409
|
const project = resolveProject(opts.db, request.params.name);
|
|
15200
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
15410
|
+
const row = opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, project.id)).get();
|
|
15201
15411
|
if (!row) {
|
|
15202
15412
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
15203
15413
|
}
|
|
@@ -15221,7 +15431,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
15221
15431
|
async (request) => {
|
|
15222
15432
|
const project = resolveProject(opts.db, request.params.name);
|
|
15223
15433
|
opts.sessionRegistry.reset(project.name);
|
|
15224
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15434
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(agentSessions.projectId, project.id)).run();
|
|
15225
15435
|
return { status: "reset" };
|
|
15226
15436
|
}
|
|
15227
15437
|
);
|
|
@@ -16164,20 +16374,20 @@ function clipText(value, length) {
|
|
|
16164
16374
|
}
|
|
16165
16375
|
|
|
16166
16376
|
// src/research-runner.ts
|
|
16167
|
-
import { and as and17, eq as
|
|
16377
|
+
import { and as and17, eq as eq24, inArray as inArray8, sql as sql10 } from "drizzle-orm";
|
|
16168
16378
|
var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
|
|
16169
16379
|
function finalResearchRunStatus(completed, failed) {
|
|
16170
16380
|
if (failed === 0) return ResearchRunStatuses.completed;
|
|
16171
16381
|
return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
|
|
16172
16382
|
}
|
|
16173
16383
|
async function executeResearchRun(db, registry, runId, projectId) {
|
|
16174
|
-
const run = db.select().from(researchRuns).where(and17(
|
|
16384
|
+
const run = db.select().from(researchRuns).where(and17(eq24(researchRuns.id, runId), eq24(researchRuns.projectId, projectId))).get();
|
|
16175
16385
|
if (!run || run.status !== ResearchRunStatuses.queued) return;
|
|
16176
|
-
const project = db.select().from(projects).where(
|
|
16386
|
+
const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
16177
16387
|
if (!project) return;
|
|
16178
16388
|
const provider = registry.get(run.provider);
|
|
16179
16389
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16180
|
-
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and17(
|
|
16390
|
+
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and17(eq24(researchRuns.id, runId), eq24(researchRuns.status, ResearchRunStatuses.queued))).run();
|
|
16181
16391
|
if (claim.changes !== 1) return;
|
|
16182
16392
|
let reserved = 0;
|
|
16183
16393
|
let dispatched = 0;
|
|
@@ -16201,8 +16411,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16201
16411
|
}
|
|
16202
16412
|
reserved = run.totalQueries;
|
|
16203
16413
|
reservation = { scope, period };
|
|
16204
|
-
const rows = db.select().from(researchRunQueries).where(
|
|
16205
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
16414
|
+
const rows = db.select().from(researchRunQueries).where(eq24(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
|
|
16415
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq24(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
16206
16416
|
const domains = effectiveDomains(project);
|
|
16207
16417
|
const brands = effectiveBrandNames(project);
|
|
16208
16418
|
const config = { ...provider.config, model: run.resolvedModel };
|
|
@@ -16210,7 +16420,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16210
16420
|
await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
|
|
16211
16421
|
try {
|
|
16212
16422
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16213
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and17(
|
|
16423
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and17(eq24(researchRunQueries.id, row.id), eq24(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
|
|
16214
16424
|
const raw = await gate.run(async () => {
|
|
16215
16425
|
dispatched++;
|
|
16216
16426
|
return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
|
|
@@ -16237,7 +16447,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16237
16447
|
citationState: determineCitationState(normalized, domains),
|
|
16238
16448
|
rawResponse: raw.rawResponse,
|
|
16239
16449
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16240
|
-
}).where(and17(
|
|
16450
|
+
}).where(and17(eq24(researchRunQueries.id, row.id), eq24(researchRunQueries.status, ResearchQueryStatuses.running))).run();
|
|
16241
16451
|
if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
|
|
16242
16452
|
} catch (error) {
|
|
16243
16453
|
try {
|
|
@@ -16261,18 +16471,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
16261
16471
|
}
|
|
16262
16472
|
}
|
|
16263
16473
|
function incrementResearchProgress(db, runId, column) {
|
|
16264
|
-
db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(
|
|
16474
|
+
db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(eq24(researchRuns.id, runId)).run();
|
|
16265
16475
|
}
|
|
16266
16476
|
function markResearchQueryFailed(db, runId, queryId, error) {
|
|
16267
16477
|
const message = describeError(error);
|
|
16268
|
-
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(
|
|
16478
|
+
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
16479
|
if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
|
|
16270
16480
|
}
|
|
16271
16481
|
function markUnfinishedResearchQueriesFailed(db, runId, error) {
|
|
16272
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(
|
|
16482
|
+
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
16483
|
}
|
|
16274
16484
|
function finalizeResearchRun(db, runId, fatalError) {
|
|
16275
|
-
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(
|
|
16485
|
+
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq24(researchRunQueries.researchRunId, runId)).all();
|
|
16276
16486
|
const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
|
|
16277
16487
|
const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
|
|
16278
16488
|
db.update(researchRuns).set({
|
|
@@ -16281,11 +16491,11 @@ function finalizeResearchRun(db, runId, fatalError) {
|
|
|
16281
16491
|
failedQueries: failed,
|
|
16282
16492
|
error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
|
|
16283
16493
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16284
|
-
}).where(
|
|
16494
|
+
}).where(eq24(researchRuns.id, runId)).run();
|
|
16285
16495
|
}
|
|
16286
16496
|
|
|
16287
16497
|
// src/google-marketing-sync.ts
|
|
16288
|
-
import { and as and18, eq as
|
|
16498
|
+
import { and as and18, eq as eq25, isNull as isNull3 } from "drizzle-orm";
|
|
16289
16499
|
function safeErrorMessage(error) {
|
|
16290
16500
|
const raw = describeError(error);
|
|
16291
16501
|
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 +16519,9 @@ function assertSnapshotContext(metadata, expected) {
|
|
|
16309
16519
|
}
|
|
16310
16520
|
function requireSyncRun(db, runId, projectId, kind) {
|
|
16311
16521
|
const run = db.select().from(runs).where(and18(
|
|
16312
|
-
|
|
16313
|
-
|
|
16314
|
-
|
|
16522
|
+
eq25(runs.id, runId),
|
|
16523
|
+
eq25(runs.projectId, projectId),
|
|
16524
|
+
eq25(runs.kind, kind)
|
|
16315
16525
|
)).get();
|
|
16316
16526
|
if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
|
|
16317
16527
|
if (run.status !== RunStatuses.queued) {
|
|
@@ -16325,18 +16535,18 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
|
|
|
16325
16535
|
error: serializeRunError({ message: safeErrorMessage(error) }),
|
|
16326
16536
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16327
16537
|
}).where(and18(
|
|
16328
|
-
|
|
16329
|
-
|
|
16330
|
-
|
|
16331
|
-
|
|
16538
|
+
eq25(runs.id, runId),
|
|
16539
|
+
eq25(runs.projectId, projectId),
|
|
16540
|
+
eq25(runs.kind, kind),
|
|
16541
|
+
eq25(runs.status, expectedStatus)
|
|
16332
16542
|
)).run();
|
|
16333
16543
|
}
|
|
16334
16544
|
function claimSyncRun(db, runId, projectId, kind, startedAt) {
|
|
16335
16545
|
const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and18(
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
|
|
16339
|
-
|
|
16546
|
+
eq25(runs.id, runId),
|
|
16547
|
+
eq25(runs.projectId, projectId),
|
|
16548
|
+
eq25(runs.kind, kind),
|
|
16549
|
+
eq25(runs.status, RunStatuses.queued)
|
|
16340
16550
|
)).run();
|
|
16341
16551
|
return claim.changes === 1;
|
|
16342
16552
|
}
|
|
@@ -16344,9 +16554,9 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
16344
16554
|
let claimed = false;
|
|
16345
16555
|
try {
|
|
16346
16556
|
requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
|
|
16347
|
-
const project = db.select().from(projects).where(
|
|
16557
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
16348
16558
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
16349
|
-
const connection = db.select().from(googleAdsConnections).where(
|
|
16559
|
+
const connection = db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, projectId)).get();
|
|
16350
16560
|
if (!connection?.selectedCustomerId) {
|
|
16351
16561
|
throw new Error("Google Ads needs a selected customer before it can sync.");
|
|
16352
16562
|
}
|
|
@@ -16398,10 +16608,10 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
16398
16608
|
finishedAt,
|
|
16399
16609
|
error: null
|
|
16400
16610
|
}).where(and18(
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16611
|
+
eq25(runs.id, runId),
|
|
16612
|
+
eq25(runs.projectId, projectId),
|
|
16613
|
+
eq25(runs.kind, RunKinds["google-ads-sync"]),
|
|
16614
|
+
eq25(runs.status, RunStatuses.running)
|
|
16405
16615
|
)).run();
|
|
16406
16616
|
if (terminalClaim.changes !== 1) {
|
|
16407
16617
|
throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
|
|
@@ -16419,11 +16629,11 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
16419
16629
|
lastMetricsSnapshotId: result.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
|
|
16420
16630
|
updatedAt: finishedAt
|
|
16421
16631
|
}).where(and18(
|
|
16422
|
-
|
|
16423
|
-
|
|
16424
|
-
|
|
16425
|
-
connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) :
|
|
16426
|
-
|
|
16632
|
+
eq25(googleAdsConnections.id, connection.id),
|
|
16633
|
+
eq25(googleAdsConnections.projectId, projectId),
|
|
16634
|
+
eq25(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
|
|
16635
|
+
connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) : eq25(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
|
|
16636
|
+
eq25(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
|
|
16427
16637
|
)).run();
|
|
16428
16638
|
if (connectionClaim.changes !== 1) {
|
|
16429
16639
|
throw new Error("Google Ads selection changed while its evidence was being collected.");
|
|
@@ -16450,9 +16660,9 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
16450
16660
|
let claimed = false;
|
|
16451
16661
|
try {
|
|
16452
16662
|
requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
|
|
16453
|
-
const project = db.select().from(projects).where(
|
|
16663
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
16454
16664
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
16455
|
-
const connection = db.select().from(gtmConnections).where(
|
|
16665
|
+
const connection = db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, projectId)).get();
|
|
16456
16666
|
if (!connection?.selectedAccountId || !connection.selectedContainerId) {
|
|
16457
16667
|
throw new Error("GTM needs a selected account and container before it can sync.");
|
|
16458
16668
|
}
|
|
@@ -16488,10 +16698,10 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
16488
16698
|
finishedAt,
|
|
16489
16699
|
error: null
|
|
16490
16700
|
}).where(and18(
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16701
|
+
eq25(runs.id, runId),
|
|
16702
|
+
eq25(runs.projectId, projectId),
|
|
16703
|
+
eq25(runs.kind, RunKinds["gtm-sync"]),
|
|
16704
|
+
eq25(runs.status, RunStatuses.running)
|
|
16495
16705
|
)).run();
|
|
16496
16706
|
if (terminalClaim.changes !== 1) {
|
|
16497
16707
|
throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
|
|
@@ -16506,12 +16716,12 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
16506
16716
|
lastSnapshotId: snapshot.metadata.id,
|
|
16507
16717
|
updatedAt: finishedAt
|
|
16508
16718
|
}).where(and18(
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) :
|
|
16514
|
-
|
|
16719
|
+
eq25(gtmConnections.id, connection.id),
|
|
16720
|
+
eq25(gtmConnections.projectId, projectId),
|
|
16721
|
+
eq25(gtmConnections.selectedAccountId, connection.selectedAccountId),
|
|
16722
|
+
eq25(gtmConnections.selectedContainerId, connection.selectedContainerId),
|
|
16723
|
+
connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) : eq25(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
|
|
16724
|
+
eq25(gtmConnections.selectionGeneration, connection.selectionGeneration)
|
|
16515
16725
|
)).run();
|
|
16516
16726
|
if (connectionClaim.changes !== 1) {
|
|
16517
16727
|
throw new Error("GTM selection changed while its evidence was being collected.");
|
|
@@ -16597,15 +16807,15 @@ function summarizeProviderConfig(config) {
|
|
|
16597
16807
|
quota: { ...config?.quota ?? DEFAULT_QUOTA }
|
|
16598
16808
|
};
|
|
16599
16809
|
}
|
|
16600
|
-
function
|
|
16601
|
-
return
|
|
16810
|
+
function hashApiKey2(key) {
|
|
16811
|
+
return crypto27.createHash("sha256").update(key).digest("hex");
|
|
16602
16812
|
}
|
|
16603
16813
|
var DASHBOARD_SCRYPT_KEYLEN = 64;
|
|
16604
16814
|
var DASHBOARD_SCRYPT_COST = 1 << 15;
|
|
16605
16815
|
var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
16606
16816
|
function hashDashboardPassword(password) {
|
|
16607
|
-
const salt =
|
|
16608
|
-
const derived =
|
|
16817
|
+
const salt = crypto27.randomBytes(16);
|
|
16818
|
+
const derived = crypto27.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
|
|
16609
16819
|
N: DASHBOARD_SCRYPT_COST,
|
|
16610
16820
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
16611
16821
|
});
|
|
@@ -16626,23 +16836,23 @@ function verifyDashboardPassword(password, storedHash) {
|
|
|
16626
16836
|
} catch {
|
|
16627
16837
|
return { ok: false, needsRehash: false };
|
|
16628
16838
|
}
|
|
16629
|
-
const derived =
|
|
16839
|
+
const derived = crypto27.scryptSync(password, salt, expected.length, {
|
|
16630
16840
|
N: DASHBOARD_SCRYPT_COST,
|
|
16631
16841
|
maxmem: DASHBOARD_SCRYPT_MAXMEM
|
|
16632
16842
|
});
|
|
16633
16843
|
if (derived.length !== expected.length)
|
|
16634
16844
|
return { ok: false, needsRehash: false };
|
|
16635
16845
|
return {
|
|
16636
|
-
ok:
|
|
16846
|
+
ok: crypto27.timingSafeEqual(derived, expected),
|
|
16637
16847
|
needsRehash: false
|
|
16638
16848
|
};
|
|
16639
16849
|
}
|
|
16640
16850
|
if (/^[a-f0-9]{64}$/i.test(storedHash)) {
|
|
16641
|
-
const candidate = Buffer.from(
|
|
16851
|
+
const candidate = Buffer.from(hashApiKey2(password), "hex");
|
|
16642
16852
|
const expected = Buffer.from(storedHash, "hex");
|
|
16643
16853
|
if (candidate.length !== expected.length)
|
|
16644
16854
|
return { ok: false, needsRehash: false };
|
|
16645
|
-
const ok =
|
|
16855
|
+
const ok = crypto27.timingSafeEqual(candidate, expected);
|
|
16646
16856
|
return { ok, needsRehash: ok };
|
|
16647
16857
|
}
|
|
16648
16858
|
return { ok: false, needsRehash: false };
|
|
@@ -16778,7 +16988,7 @@ function cloneGtmConfig(config) {
|
|
|
16778
16988
|
function createGoogleMarketingConfigCredentialStore(input) {
|
|
16779
16989
|
const persistConfigPatch = input.saveConfigPatch ?? saveConfigPatch;
|
|
16780
16990
|
const env = input.env ?? process.env;
|
|
16781
|
-
const randomUUID = input.randomUUID ??
|
|
16991
|
+
const randomUUID = input.randomUUID ?? crypto27.randomUUID;
|
|
16782
16992
|
const snapshot = () => ({
|
|
16783
16993
|
googleAds: cloneGoogleAdsConfig(input.config.googleAds),
|
|
16784
16994
|
gtm: cloneGtmConfig(input.config.gtm)
|
|
@@ -16970,7 +17180,7 @@ async function createServer(opts) {
|
|
|
16970
17180
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
16971
17181
|
async (ctx) => {
|
|
16972
17182
|
if (!sessionRegistry) return;
|
|
16973
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
17183
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq26(projects.id, ctx.projectId)).get();
|
|
16974
17184
|
if (!project) return;
|
|
16975
17185
|
let content;
|
|
16976
17186
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -17413,7 +17623,7 @@ async function createServer(opts) {
|
|
|
17413
17623
|
void (async () => {
|
|
17414
17624
|
try {
|
|
17415
17625
|
const report = await aeroClient.runDoctor({ project: projectName });
|
|
17416
|
-
const project = opts.db.select().from(projects).where(
|
|
17626
|
+
const project = opts.db.select().from(projects).where(eq26(projects.name, projectName)).get();
|
|
17417
17627
|
if (!project) {
|
|
17418
17628
|
app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
|
|
17419
17629
|
return;
|
|
@@ -17442,8 +17652,8 @@ async function createServer(opts) {
|
|
|
17442
17652
|
if (!probed) return;
|
|
17443
17653
|
const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
|
|
17444
17654
|
and19(
|
|
17445
|
-
|
|
17446
|
-
|
|
17655
|
+
eq26(ccReleaseSyncs.release, probed.release),
|
|
17656
|
+
eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
|
|
17447
17657
|
)
|
|
17448
17658
|
).limit(1).get();
|
|
17449
17659
|
if (alreadySynced) {
|
|
@@ -17624,7 +17834,7 @@ async function createServer(opts) {
|
|
|
17624
17834
|
return removed;
|
|
17625
17835
|
}
|
|
17626
17836
|
};
|
|
17627
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
17837
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto27.randomBytes(32).toString("hex");
|
|
17628
17838
|
const googleConnectionStore = {
|
|
17629
17839
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
17630
17840
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -17697,14 +17907,26 @@ async function createServer(opts) {
|
|
|
17697
17907
|
"Dashboard password gate resolved"
|
|
17698
17908
|
);
|
|
17699
17909
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
17910
|
+
const credentialChecker = createCredentialChecker({
|
|
17911
|
+
db: opts.db,
|
|
17912
|
+
trustProxyConfigured: trustProxy !== false
|
|
17913
|
+
});
|
|
17700
17914
|
const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
|
|
17915
|
+
const publicOrigin = (() => {
|
|
17916
|
+
if (!googlePublicUrl) return void 0;
|
|
17917
|
+
try {
|
|
17918
|
+
return new URL(googlePublicUrl).origin;
|
|
17919
|
+
} catch {
|
|
17920
|
+
return void 0;
|
|
17921
|
+
}
|
|
17922
|
+
})();
|
|
17701
17923
|
if (opts.config.apiKey) {
|
|
17702
|
-
const keyHash =
|
|
17703
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
17924
|
+
const keyHash = hashApiKey2(opts.config.apiKey);
|
|
17925
|
+
const existing = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, keyHash)).get();
|
|
17704
17926
|
if (!existing) {
|
|
17705
17927
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
17706
17928
|
opts.db.insert(apiKeys).values({
|
|
17707
|
-
id: `key_${
|
|
17929
|
+
id: `key_${crypto27.randomBytes(8).toString("hex")}`,
|
|
17708
17930
|
name: "default",
|
|
17709
17931
|
keyHash,
|
|
17710
17932
|
keyPrefix: prefix,
|
|
@@ -17728,7 +17950,7 @@ async function createServer(opts) {
|
|
|
17728
17950
|
};
|
|
17729
17951
|
const createSession = (apiKeyId) => {
|
|
17730
17952
|
pruneExpiredSessions();
|
|
17731
|
-
const sessionId =
|
|
17953
|
+
const sessionId = crypto27.randomBytes(32).toString("hex");
|
|
17732
17954
|
sessions.set(sessionId, {
|
|
17733
17955
|
apiKeyId,
|
|
17734
17956
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -17752,7 +17974,7 @@ async function createServer(opts) {
|
|
|
17752
17974
|
};
|
|
17753
17975
|
const getDefaultApiKey = () => {
|
|
17754
17976
|
if (!opts.config.apiKey) return void 0;
|
|
17755
|
-
return opts.db.select().from(apiKeys).where(
|
|
17977
|
+
return opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
|
|
17756
17978
|
};
|
|
17757
17979
|
const createPasswordSession = (reply) => {
|
|
17758
17980
|
const key = getDefaultApiKey();
|
|
@@ -17776,7 +17998,7 @@ async function createServer(opts) {
|
|
|
17776
17998
|
if (!header) return false;
|
|
17777
17999
|
const parts = header.split(" ");
|
|
17778
18000
|
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
17779
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
18001
|
+
const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
|
|
17780
18002
|
return Boolean(key && !key.revokedAt);
|
|
17781
18003
|
};
|
|
17782
18004
|
const namedAccountsInUse = () => anyUsersExist(opts.db);
|
|
@@ -17866,12 +18088,12 @@ async function createServer(opts) {
|
|
|
17866
18088
|
return reply.send({ authenticated: true });
|
|
17867
18089
|
}
|
|
17868
18090
|
if (apiKey) {
|
|
17869
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
18091
|
+
const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(apiKey))).get();
|
|
17870
18092
|
if (!key || key.revokedAt) {
|
|
17871
18093
|
const err2 = authInvalid();
|
|
17872
18094
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
17873
18095
|
}
|
|
17874
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
18096
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(apiKeys.id, key.id)).run();
|
|
17875
18097
|
const sessionId = createSession(key.id);
|
|
17876
18098
|
reply.header(
|
|
17877
18099
|
"set-cookie",
|
|
@@ -18004,7 +18226,11 @@ async function createServer(opts) {
|
|
|
18004
18226
|
vercelSyncDeadlineMs: resolveVercelSyncDeadlineMs(process.env),
|
|
18005
18227
|
// Local-only Aero agent routes. Registered here so they inherit api-routes'
|
|
18006
18228
|
// auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
|
|
18229
|
+
credentials: credentialChecker,
|
|
18230
|
+
oauthResourceUrl: publicOrigin ? `${publicOrigin}${`${basePath ?? "/"}api/v1/mcp`.replace("//", "/")}` : void 0,
|
|
18007
18231
|
registerAuthenticatedRoutes: async (scope) => {
|
|
18232
|
+
registerMcpHttpRoutes(scope, { selfApiUrl: opts.config.apiUrl, issuer: publicOrigin, db: opts.db });
|
|
18233
|
+
registerOAuthAdminRoutes(scope, { db: opts.db });
|
|
18008
18234
|
if (!sessionRegistry) return;
|
|
18009
18235
|
registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
|
|
18010
18236
|
},
|
|
@@ -18015,8 +18241,8 @@ async function createServer(opts) {
|
|
|
18015
18241
|
getAgentProviderSummary: () => buildAgentProvidersResponse(opts.config).providers,
|
|
18016
18242
|
getGoogleMarketingDoctorInput: (ctx) => {
|
|
18017
18243
|
if (!ctx.project) return null;
|
|
18018
|
-
const googleAds = opts.db.select().from(googleAdsConnections).where(
|
|
18019
|
-
const gtm = opts.db.select().from(gtmConnections).where(
|
|
18244
|
+
const googleAds = opts.db.select().from(googleAdsConnections).where(eq26(googleAdsConnections.projectId, ctx.project.id)).get();
|
|
18245
|
+
const gtm = opts.db.select().from(gtmConnections).where(eq26(gtmConnections.projectId, ctx.project.id)).get();
|
|
18020
18246
|
const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
|
|
18021
18247
|
const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
|
|
18022
18248
|
const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
|
|
@@ -18140,7 +18366,7 @@ async function createServer(opts) {
|
|
|
18140
18366
|
deps: {
|
|
18141
18367
|
enqueueAutoExtract: ({ projectId, release: r }) => {
|
|
18142
18368
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18143
|
-
const runId =
|
|
18369
|
+
const runId = crypto27.randomUUID();
|
|
18144
18370
|
opts.db.insert(runs).values({
|
|
18145
18371
|
id: runId,
|
|
18146
18372
|
projectId,
|
|
@@ -18265,7 +18491,7 @@ async function createServer(opts) {
|
|
|
18265
18491
|
...inspectOpts,
|
|
18266
18492
|
config: opts.config
|
|
18267
18493
|
}).then(() => {
|
|
18268
|
-
const finished = opts.db.select({ status: runs.status }).from(runs).where(
|
|
18494
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq26(runs.id, runId)).get();
|
|
18269
18495
|
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
18270
18496
|
return maybeRefreshGscCoverage(
|
|
18271
18497
|
opts.db,
|
|
@@ -18375,7 +18601,7 @@ async function createServer(opts) {
|
|
|
18375
18601
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18376
18602
|
opts.db.insert(auditLog).values(
|
|
18377
18603
|
targetProjectIds.map((projectId) => ({
|
|
18378
|
-
id:
|
|
18604
|
+
id: crypto27.randomUUID(),
|
|
18379
18605
|
projectId,
|
|
18380
18606
|
actor: "api",
|
|
18381
18607
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -18629,6 +18855,16 @@ async function createServer(opts) {
|
|
|
18629
18855
|
const error = notFound("API route", url);
|
|
18630
18856
|
return reply.status(error.statusCode).send(error.toJSON());
|
|
18631
18857
|
}
|
|
18858
|
+
const hasDotSegment = (candidate) => candidate.split("/").some((segment) => segment.startsWith("."));
|
|
18859
|
+
let decodedUrl = url;
|
|
18860
|
+
try {
|
|
18861
|
+
decodedUrl = decodeURIComponent(url);
|
|
18862
|
+
} catch {
|
|
18863
|
+
}
|
|
18864
|
+
if (hasDotSegment(url) || hasDotSegment(decodedUrl)) {
|
|
18865
|
+
const error = notFound("Route", url);
|
|
18866
|
+
return reply.status(error.statusCode).send(error.toJSON());
|
|
18867
|
+
}
|
|
18632
18868
|
if (basePath && !url.startsWith(basePath)) {
|
|
18633
18869
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
18634
18870
|
}
|
|
@@ -18649,13 +18885,38 @@ async function createServer(opts) {
|
|
|
18649
18885
|
...update ? { updateAvailable: update } : {}
|
|
18650
18886
|
};
|
|
18651
18887
|
};
|
|
18888
|
+
if (publicOrigin) {
|
|
18889
|
+
registerOAuthRoutes(app, {
|
|
18890
|
+
db: opts.db,
|
|
18891
|
+
issuer: publicOrigin,
|
|
18892
|
+
resourcePaths: mcpTransportPaths().map(
|
|
18893
|
+
(suffix) => `${basePath ?? "/"}api/v1${suffix}`.replace("//", "/")
|
|
18894
|
+
),
|
|
18895
|
+
resolveUser: (request) => {
|
|
18896
|
+
const cookies = parseCookieHeader(request.headers.cookie);
|
|
18897
|
+
const token = cookies[USER_SESSION_COOKIE_NAME];
|
|
18898
|
+
if (!token) return null;
|
|
18899
|
+
const resolved = resolveUserSession(opts.db, token);
|
|
18900
|
+
return resolved ? { id: resolved.user.id, name: resolved.user.name } : null;
|
|
18901
|
+
},
|
|
18902
|
+
credentials: credentialChecker,
|
|
18903
|
+
startSession: (userId) => {
|
|
18904
|
+
const token = createUserSession(opts.db, userId);
|
|
18905
|
+
return serializeUserSessionCookie({
|
|
18906
|
+
value: token,
|
|
18907
|
+
path: basePath ?? "/",
|
|
18908
|
+
secure: opts.config.publicUrl?.startsWith("https://") ?? false
|
|
18909
|
+
});
|
|
18910
|
+
}
|
|
18911
|
+
});
|
|
18912
|
+
}
|
|
18652
18913
|
app.get("/health", healthHandler);
|
|
18653
18914
|
if (basePath) {
|
|
18654
18915
|
app.get(`${basePath}health`, healthHandler);
|
|
18655
18916
|
}
|
|
18656
18917
|
checkLatestVersionForServer();
|
|
18657
18918
|
scheduler.start();
|
|
18658
|
-
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(
|
|
18919
|
+
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq26(researchRuns.status, ResearchRunStatuses.queued)).all()) {
|
|
18659
18920
|
dispatchResearchRun(run.id, run.projectId);
|
|
18660
18921
|
}
|
|
18661
18922
|
app.addHook("onClose", async () => {
|