@canonry/canonry 4.177.3 → 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.
Files changed (29) hide show
  1. package/assets/assets/{AuditHistoryPanel-YZvGU1la.js → AuditHistoryPanel-BmKdyRrR.js} +1 -1
  2. package/assets/assets/{BacklinksPage-CB2uJWWy.js → BacklinksPage-DtqKPVLV.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-BoYe2M-i.js → ChartPrimitives-B_LDVvTy.js} +1 -1
  4. package/assets/assets/{HistoryPage-D9IxxbQ9.js → HistoryPage-yyJsQiJQ.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-CxgnnPdp.js → MeasurementPropertyPage-hjW4iYtr.js} +1 -1
  6. package/assets/assets/ProjectPage-DZKpnPvq.js +9 -0
  7. package/assets/assets/{RunRow-2KxOta8F.js → RunRow-BZeBFb22.js} +1 -1
  8. package/assets/assets/{RunsPage-ya8vTkyd.js → RunsPage-tnXHsK_R.js} +1 -1
  9. package/assets/assets/{SettingsPage-lTy3o2bo.js → SettingsPage-Djj5k8vv.js} +1 -1
  10. package/assets/assets/{SiteHealthSection-Cyu7MIv2.js → SiteHealthSection-BkDncS11.js} +3 -3
  11. package/assets/assets/{TrafficPage-DjhKfw57.js → TrafficPage-nJggXVh-.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-Pwi-Dl9Q.js → TrafficSourceDetailPage-zxONbSn-.js} +1 -1
  13. package/assets/assets/{extract-error-message-I1FVWHLH.js → extract-error-message-NpQu2vBm.js} +1 -1
  14. package/assets/assets/{index-DQVTRT9J.css → index-D0GbiVhZ.css} +1 -1
  15. package/assets/assets/index-Dwwnkd_V.js +83 -0
  16. package/assets/assets/{react-sigma_core.esm.min-BX6mOEq5.js → react-sigma_core.esm.min-CxMdd8U8.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-QRE3DAAU.js → chunk-A7FB2BVW.js} +2 -2
  19. package/dist/{chunk-7ABFIVKO.js → chunk-O7J5WFVZ.js} +361 -100
  20. package/dist/{chunk-6XUWDSLN.js → chunk-SXKA4OXZ.js} +1356 -907
  21. package/dist/{chunk-PQT3ECM3.js → chunk-T4JH77O6.js} +107 -0
  22. package/dist/{chunk-5EH666EO.js → chunk-YIQMTOEZ.js} +2970 -2054
  23. package/dist/cli.js +87 -5
  24. package/dist/index.js +4 -4
  25. package/dist/{intelligence-service-CHZGHFGS.js → intelligence-service-YISV327N.js} +2 -2
  26. package/dist/mcp.js +5 -380
  27. package/package.json +8 -8
  28. package/assets/assets/ProjectPage-CPmaXzhL.js +0 -9
  29. 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-6XUWDSLN.js";
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-5EH666EO.js";
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-PQT3ECM3.js";
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 defaultMetricsQuery(now, campaigns, timeZone) {
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
- campaignIds: selected,
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 defaults = defaultMetricsQuery(this.#now(), inventory.campaigns, customerResult.data.timeZone);
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 crypto26 from "crypto";
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 eq25 } from "drizzle-orm";
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-CHZGHFGS.js");
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/agent/agent-routes.ts
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(eq22(projects.name, name)).get();
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(eq22(agentSessions.projectId, project.id)).get();
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(eq22(agentSessions.projectId, project.id)).run();
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 eq23, inArray as inArray8, sql as sql10 } from "drizzle-orm";
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(eq23(researchRuns.id, runId), eq23(researchRuns.projectId, projectId))).get();
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(eq23(projects.id, projectId)).get();
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(eq23(researchRuns.id, runId), eq23(researchRuns.status, ResearchRunStatuses.queued))).run();
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(eq23(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
16205
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq23(competitors.projectId, projectId)).all().map((row) => row.domain);
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(eq23(researchRunQueries.id, row.id), eq23(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
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(eq23(researchRunQueries.id, row.id), eq23(researchRunQueries.status, ResearchQueryStatuses.running))).run();
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(eq23(researchRuns.id, runId)).run();
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(eq23(researchRunQueries.id, queryId), inArray8(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
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(eq23(researchRunQueries.researchRunId, runId), inArray8(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
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(eq23(researchRunQueries.researchRunId, runId)).all();
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(eq23(researchRuns.id, runId)).run();
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 eq24, isNull as isNull3 } from "drizzle-orm";
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
- eq24(runs.id, runId),
16313
- eq24(runs.projectId, projectId),
16314
- eq24(runs.kind, kind)
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
- eq24(runs.id, runId),
16329
- eq24(runs.projectId, projectId),
16330
- eq24(runs.kind, kind),
16331
- eq24(runs.status, expectedStatus)
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
- eq24(runs.id, runId),
16337
- eq24(runs.projectId, projectId),
16338
- eq24(runs.kind, kind),
16339
- eq24(runs.status, RunStatuses.queued)
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(eq24(projects.id, projectId)).get();
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(eq24(googleAdsConnections.projectId, projectId)).get();
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
- eq24(runs.id, runId),
16402
- eq24(runs.projectId, projectId),
16403
- eq24(runs.kind, RunKinds["google-ads-sync"]),
16404
- eq24(runs.status, RunStatuses.running)
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
- eq24(googleAdsConnections.id, connection.id),
16423
- eq24(googleAdsConnections.projectId, projectId),
16424
- eq24(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
16425
- connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) : eq24(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
16426
- eq24(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
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(eq24(projects.id, projectId)).get();
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(eq24(gtmConnections.projectId, projectId)).get();
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
- eq24(runs.id, runId),
16492
- eq24(runs.projectId, projectId),
16493
- eq24(runs.kind, RunKinds["gtm-sync"]),
16494
- eq24(runs.status, RunStatuses.running)
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
- eq24(gtmConnections.id, connection.id),
16510
- eq24(gtmConnections.projectId, projectId),
16511
- eq24(gtmConnections.selectedAccountId, connection.selectedAccountId),
16512
- eq24(gtmConnections.selectedContainerId, connection.selectedContainerId),
16513
- connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) : eq24(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
16514
- eq24(gtmConnections.selectionGeneration, connection.selectionGeneration)
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 hashApiKey(key) {
16601
- return crypto26.createHash("sha256").update(key).digest("hex");
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 = crypto26.randomBytes(16);
16608
- const derived = crypto26.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
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 = crypto26.scryptSync(password, salt, expected.length, {
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: crypto26.timingSafeEqual(derived, expected),
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(hashApiKey(password), "hex");
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 = crypto26.timingSafeEqual(candidate, expected);
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 ?? crypto26.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(eq25(projects.id, ctx.projectId)).get();
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(eq25(projects.name, projectName)).get();
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
- eq25(ccReleaseSyncs.release, probed.release),
17446
- eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
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 ?? crypto26.randomBytes(32).toString("hex");
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 = hashApiKey(opts.config.apiKey);
17703
- const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
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_${crypto26.randomBytes(8).toString("hex")}`,
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 = crypto26.randomBytes(32).toString("hex");
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(eq25(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
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(eq25(apiKeys.keyHash, hashApiKey(parts[1]))).get();
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(eq25(apiKeys.keyHash, hashApiKey(apiKey))).get();
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(eq25(apiKeys.id, key.id)).run();
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(eq25(googleAdsConnections.projectId, ctx.project.id)).get();
18019
- const gtm = opts.db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, ctx.project.id)).get();
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 = crypto26.randomUUID();
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(eq25(runs.id, runId)).get();
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: crypto26.randomUUID(),
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(eq25(researchRuns.status, ResearchRunStatuses.queued)).all()) {
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 () => {