@ainyc/canonry 1.38.0 → 1.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -243,11 +243,11 @@ function trackEvent(event, properties) {
243
243
 
244
244
  // src/server.ts
245
245
  import { createRequire as createRequire2 } from "module";
246
- import crypto22 from "crypto";
246
+ import crypto23 from "crypto";
247
247
  import fs5 from "fs";
248
248
  import path6 from "path";
249
249
  import { fileURLToPath } from "url";
250
- import { eq as eq22 } from "drizzle-orm";
250
+ import { eq as eq24 } from "drizzle-orm";
251
251
  import Fastify from "fastify";
252
252
 
253
253
  // ../contracts/src/config-schema.ts
@@ -1224,6 +1224,8 @@ __export(schema_exports, {
1224
1224
  gscCoverageSnapshots: () => gscCoverageSnapshots,
1225
1225
  gscSearchData: () => gscSearchData,
1226
1226
  gscUrlInspections: () => gscUrlInspections,
1227
+ healthSnapshots: () => healthSnapshots,
1228
+ insights: () => insights,
1227
1229
  keywords: () => keywords,
1228
1230
  notifications: () => notifications,
1229
1231
  projects: () => projects,
@@ -1362,6 +1364,7 @@ var googleConnections = sqliteTable("google_connections", {
1362
1364
  connectionType: text("connection_type").notNull(),
1363
1365
  propertyId: text("property_id"),
1364
1366
  sitemapUrl: text("sitemap_url"),
1367
+ // WARNING: Authentication material should be stored in config.yaml per CLAUDE.md
1365
1368
  accessToken: text("access_token"),
1366
1369
  refreshToken: text("refresh_token"),
1367
1370
  tokenExpiresAt: text("token_expires_at"),
@@ -1470,6 +1473,7 @@ var gaConnections = sqliteTable("ga_connections", {
1470
1473
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1471
1474
  propertyId: text("property_id").notNull(),
1472
1475
  clientEmail: text("client_email").notNull(),
1476
+ // WARNING: Authentication material should be stored in config.yaml per CLAUDE.md
1473
1477
  privateKey: text("private_key").notNull(),
1474
1478
  createdAt: text("created_at").notNull(),
1475
1479
  updatedAt: text("updated_at").notNull()
@@ -1528,6 +1532,39 @@ var usageCounters = sqliteTable("usage_counters", {
1528
1532
  uniqueIndex("idx_usage_scope_period_metric").on(table.scope, table.period, table.metric),
1529
1533
  index("idx_usage_scope_period").on(table.scope, table.period)
1530
1534
  ]);
1535
+ var insights = sqliteTable("insights", {
1536
+ id: text("id").primaryKey(),
1537
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1538
+ runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
1539
+ type: text("type").notNull(),
1540
+ severity: text("severity").notNull(),
1541
+ title: text("title").notNull(),
1542
+ keyword: text("keyword").notNull(),
1543
+ provider: text("provider").notNull(),
1544
+ recommendation: text("recommendation"),
1545
+ cause: text("cause"),
1546
+ dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false),
1547
+ createdAt: text("created_at").notNull()
1548
+ }, (table) => [
1549
+ index("idx_insights_project").on(table.projectId),
1550
+ index("idx_insights_run").on(table.runId),
1551
+ index("idx_insights_created").on(table.createdAt),
1552
+ index("idx_insights_keyword_provider").on(table.keyword, table.provider)
1553
+ ]);
1554
+ var healthSnapshots = sqliteTable("health_snapshots", {
1555
+ id: text("id").primaryKey(),
1556
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1557
+ runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
1558
+ overallCitedRate: text("overall_cited_rate").notNull(),
1559
+ totalPairs: integer("total_pairs").notNull(),
1560
+ citedPairs: integer("cited_pairs").notNull(),
1561
+ providerBreakdown: text("provider_breakdown").notNull().default("{}"),
1562
+ createdAt: text("created_at").notNull()
1563
+ }, (table) => [
1564
+ index("idx_health_snapshots_project").on(table.projectId),
1565
+ index("idx_health_snapshots_run").on(table.runId),
1566
+ index("idx_health_snapshots_created").on(table.createdAt)
1567
+ ]);
1531
1568
 
1532
1569
  // ../db/src/client.ts
1533
1570
  function createClient(databasePath) {
@@ -1692,6 +1729,7 @@ var MIGRATIONS = [
1692
1729
  // v5b: Backfill model from rawResponse JSON for existing snapshots
1693
1730
  `UPDATE query_snapshots SET model = json_extract(raw_response, '$.model') WHERE model IS NULL AND raw_response IS NOT NULL AND json_extract(raw_response, '$.model') IS NOT NULL`,
1694
1731
  // v6: Google Search Console integration — google_connections table (domain-scoped)
1732
+ // WARNING: access_token, refresh_token are authentication material; consider storing in config.yaml per CLAUDE.md
1695
1733
  `CREATE TABLE IF NOT EXISTS google_connections (
1696
1734
  id TEXT PRIMARY KEY,
1697
1735
  domain TEXT NOT NULL,
@@ -1807,6 +1845,7 @@ var MIGRATIONS = [
1807
1845
  `CREATE INDEX IF NOT EXISTS idx_bing_keyword_project ON bing_keyword_stats(project_id)`,
1808
1846
  `CREATE INDEX IF NOT EXISTS idx_bing_keyword_query ON bing_keyword_stats(query)`,
1809
1847
  // v13: Google Analytics 4 — ga_connections table (service account auth)
1848
+ // WARNING: private_key is authentication material; consider storing in config.yaml per CLAUDE.md
1810
1849
  `CREATE TABLE IF NOT EXISTS ga_connections (
1811
1850
  id TEXT PRIMARY KEY,
1812
1851
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
@@ -1876,7 +1915,45 @@ var MIGRATIONS = [
1876
1915
  `ALTER TABLE ga_ai_referrals ADD COLUMN source_dimension TEXT NOT NULL DEFAULT 'session'`,
1877
1916
  // Replace old unique index with one that includes source_dimension
1878
1917
  `DROP INDEX IF EXISTS idx_ga_ai_ref_unique`,
1879
- `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`
1918
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`,
1919
+ // v21: Add missing indexes for query_snapshots filtering
1920
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_citation_state ON query_snapshots(citation_state)`,
1921
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_provider_model ON query_snapshots(provider, model)`,
1922
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_location ON query_snapshots(location)`,
1923
+ // v22: Intelligence — insights table for regression/gain/opportunity tracking
1924
+ `CREATE TABLE IF NOT EXISTS insights (
1925
+ id TEXT PRIMARY KEY,
1926
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1927
+ type TEXT NOT NULL,
1928
+ severity TEXT NOT NULL,
1929
+ title TEXT NOT NULL,
1930
+ keyword TEXT NOT NULL,
1931
+ provider TEXT NOT NULL,
1932
+ recommendation TEXT,
1933
+ cause TEXT,
1934
+ dismissed INTEGER NOT NULL DEFAULT 0,
1935
+ created_at TEXT NOT NULL
1936
+ )`,
1937
+ `CREATE INDEX IF NOT EXISTS idx_insights_project ON insights(project_id)`,
1938
+ `CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at)`,
1939
+ `CREATE INDEX IF NOT EXISTS idx_insights_keyword_provider ON insights(keyword, provider)`,
1940
+ // v23: Intelligence — health_snapshots table for citation health over time
1941
+ `CREATE TABLE IF NOT EXISTS health_snapshots (
1942
+ id TEXT PRIMARY KEY,
1943
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1944
+ overall_cited_rate TEXT NOT NULL,
1945
+ total_pairs INTEGER NOT NULL,
1946
+ cited_pairs INTEGER NOT NULL,
1947
+ provider_breakdown TEXT NOT NULL DEFAULT '{}',
1948
+ created_at TEXT NOT NULL
1949
+ )`,
1950
+ `CREATE INDEX IF NOT EXISTS idx_health_snapshots_project ON health_snapshots(project_id)`,
1951
+ `CREATE INDEX IF NOT EXISTS idx_health_snapshots_created ON health_snapshots(created_at)`,
1952
+ // v24: Intelligence — add run_id to insights and health_snapshots for per-run correlation and idempotency
1953
+ `ALTER TABLE insights ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
1954
+ `CREATE INDEX IF NOT EXISTS idx_insights_run ON insights(run_id)`,
1955
+ `ALTER TABLE health_snapshots ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
1956
+ `CREATE INDEX IF NOT EXISTS idx_health_snapshots_run ON health_snapshots(run_id)`
1880
1957
  ];
1881
1958
  function isDuplicateColumnError(err) {
1882
1959
  if (!(err instanceof Error)) return false;
@@ -3813,6 +3890,82 @@ function buildCategoryCounts(counts) {
3813
3890
  return result;
3814
3891
  }
3815
3892
 
3893
+ // ../api-routes/src/intelligence.ts
3894
+ import { eq as eq11, desc as desc4, and as and2 } from "drizzle-orm";
3895
+ function mapInsightRow(r) {
3896
+ return {
3897
+ id: r.id,
3898
+ projectId: r.projectId,
3899
+ runId: r.runId ?? null,
3900
+ type: r.type,
3901
+ severity: r.severity,
3902
+ title: r.title,
3903
+ keyword: r.keyword,
3904
+ provider: r.provider,
3905
+ recommendation: parseJsonColumn(r.recommendation, void 0),
3906
+ cause: parseJsonColumn(r.cause, void 0),
3907
+ dismissed: r.dismissed,
3908
+ createdAt: r.createdAt
3909
+ };
3910
+ }
3911
+ function mapHealthRow(r) {
3912
+ return {
3913
+ id: r.id,
3914
+ projectId: r.projectId,
3915
+ runId: r.runId ?? null,
3916
+ overallCitedRate: Number(r.overallCitedRate),
3917
+ totalPairs: r.totalPairs,
3918
+ citedPairs: r.citedPairs,
3919
+ providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
3920
+ createdAt: r.createdAt
3921
+ };
3922
+ }
3923
+ async function intelligenceRoutes(app) {
3924
+ app.get("/projects/:name/insights", async (request, reply) => {
3925
+ const project = resolveProject(app.db, request.params.name);
3926
+ const conditions = [eq11(insights.projectId, project.id)];
3927
+ if (request.query.runId) {
3928
+ conditions.push(eq11(insights.runId, request.query.runId));
3929
+ }
3930
+ const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and2(...conditions)).orderBy(desc4(insights.createdAt)).all();
3931
+ const showDismissed = request.query.dismissed === "true";
3932
+ const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
3933
+ return reply.send(result);
3934
+ });
3935
+ app.get("/projects/:name/insights/:id", async (request, reply) => {
3936
+ const project = resolveProject(app.db, request.params.name);
3937
+ const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
3938
+ if (!row || row.projectId !== project.id) {
3939
+ throw notFound("Insight", request.params.id);
3940
+ }
3941
+ return reply.send(mapInsightRow(row));
3942
+ });
3943
+ app.post("/projects/:name/insights/:id/dismiss", async (request, reply) => {
3944
+ const project = resolveProject(app.db, request.params.name);
3945
+ const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
3946
+ if (!row || row.projectId !== project.id) {
3947
+ throw notFound("Insight", request.params.id);
3948
+ }
3949
+ app.db.update(insights).set({ dismissed: true }).where(eq11(insights.id, request.params.id)).run();
3950
+ return reply.send({ ok: true });
3951
+ });
3952
+ app.get("/projects/:name/health/latest", async (request, reply) => {
3953
+ const project = resolveProject(app.db, request.params.name);
3954
+ const row = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
3955
+ if (!row) {
3956
+ throw notFound("Health data for project", request.params.name);
3957
+ }
3958
+ return reply.send(mapHealthRow(row));
3959
+ });
3960
+ app.get("/projects/:name/health/history", async (request, reply) => {
3961
+ const project = resolveProject(app.db, request.params.name);
3962
+ const parsed = request.query.limit ? parseInt(request.query.limit, 10) : NaN;
3963
+ const limit = Number.isNaN(parsed) ? 30 : Math.min(Math.max(parsed, 1), 100);
3964
+ const rows = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(limit).all();
3965
+ return reply.send(rows.map(mapHealthRow));
3966
+ });
3967
+ }
3968
+
3816
3969
  // ../api-routes/src/openapi.ts
3817
3970
  var stringSchema = { type: "string" };
3818
3971
  var booleanSchema = { type: "boolean" };
@@ -5834,10 +5987,83 @@ var routeCatalog = [
5834
5987
  400: { description: "GA4 is not connected." },
5835
5988
  404: { description: "Project not found." }
5836
5989
  }
5990
+ },
5991
+ // Intelligence
5992
+ {
5993
+ method: "get",
5994
+ path: "/api/v1/projects/{name}/insights",
5995
+ summary: "List intelligence insights for a project",
5996
+ tags: ["intelligence"],
5997
+ parameters: [
5998
+ nameParameter,
5999
+ { name: "dismissed", in: "query", description: "Include dismissed insights (true/false).", schema: stringSchema },
6000
+ { name: "runId", in: "query", description: "Filter by run ID.", schema: stringSchema }
6001
+ ],
6002
+ responses: {
6003
+ 200: { description: "Insights returned." },
6004
+ 404: { description: "Project not found." }
6005
+ }
6006
+ },
6007
+ {
6008
+ method: "get",
6009
+ path: "/api/v1/projects/{name}/insights/{id}",
6010
+ summary: "Get a single insight",
6011
+ tags: ["intelligence"],
6012
+ parameters: [
6013
+ nameParameter,
6014
+ { name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
6015
+ ],
6016
+ responses: {
6017
+ 200: { description: "Insight returned." },
6018
+ 404: { description: "Insight not found." }
6019
+ }
6020
+ },
6021
+ {
6022
+ method: "post",
6023
+ path: "/api/v1/projects/{name}/insights/{id}/dismiss",
6024
+ summary: "Dismiss an insight",
6025
+ tags: ["intelligence"],
6026
+ parameters: [
6027
+ nameParameter,
6028
+ { name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
6029
+ ],
6030
+ responses: {
6031
+ 200: { description: "Insight dismissed." },
6032
+ 404: { description: "Insight not found." }
6033
+ }
6034
+ },
6035
+ {
6036
+ method: "get",
6037
+ path: "/api/v1/projects/{name}/health/latest",
6038
+ summary: "Get latest health snapshot",
6039
+ tags: ["intelligence"],
6040
+ parameters: [nameParameter],
6041
+ responses: {
6042
+ 200: { description: "Health snapshot returned." },
6043
+ 404: { description: "Project not found." }
6044
+ }
6045
+ },
6046
+ {
6047
+ method: "get",
6048
+ path: "/api/v1/projects/{name}/health/history",
6049
+ summary: "Get health trend over time",
6050
+ tags: ["intelligence"],
6051
+ parameters: [
6052
+ nameParameter,
6053
+ { name: "limit", in: "query", description: "Max results.", schema: stringSchema }
6054
+ ],
6055
+ responses: {
6056
+ 200: { description: "Health history returned." },
6057
+ 404: { description: "Project not found." }
6058
+ }
5837
6059
  }
5838
6060
  ];
5839
6061
  function buildOpenApiDocument(info = {}) {
6062
+ const BASE_PREFIX = "/api/v1";
6063
+ const prefix = info.routePrefix ?? BASE_PREFIX;
5840
6064
  const paths = routeCatalog.reduce((acc, route) => {
6065
+ const subpath = route.path.startsWith(BASE_PREFIX) ? route.path.slice(BASE_PREFIX.length) : route.path;
6066
+ const fullPath = prefix + subpath;
5841
6067
  const operation = {
5842
6068
  summary: route.summary,
5843
6069
  tags: route.tags,
@@ -5848,9 +6074,9 @@ function buildOpenApiDocument(info = {}) {
5848
6074
  if (route.parameters) operation.parameters = route.parameters;
5849
6075
  if (route.requestBody) operation.requestBody = route.requestBody;
5850
6076
  if (route.auth === false) operation.security = [];
5851
- const pathItem = acc[route.path] ?? {};
6077
+ const pathItem = acc[fullPath] ?? {};
5852
6078
  pathItem[route.method] = operation;
5853
- acc[route.path] = pathItem;
6079
+ acc[fullPath] = pathItem;
5854
6080
  return acc;
5855
6081
  }, {});
5856
6082
  return {
@@ -6080,7 +6306,7 @@ async function telemetryRoutes(app, opts) {
6080
6306
 
6081
6307
  // ../api-routes/src/schedules.ts
6082
6308
  import crypto11 from "crypto";
6083
- import { eq as eq11 } from "drizzle-orm";
6309
+ import { eq as eq12 } from "drizzle-orm";
6084
6310
  async function scheduleRoutes(app, opts) {
6085
6311
  app.put("/projects/:name/schedule", async (request, reply) => {
6086
6312
  const project = resolveProject(app.db, request.params.name);
@@ -6123,7 +6349,7 @@ async function scheduleRoutes(app, opts) {
6123
6349
  }
6124
6350
  const now = (/* @__PURE__ */ new Date()).toISOString();
6125
6351
  const enabledInt = enabled === false ? 0 : 1;
6126
- const existing = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6352
+ const existing = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6127
6353
  if (existing) {
6128
6354
  app.db.update(schedules).set({
6129
6355
  cronExpr,
@@ -6132,7 +6358,7 @@ async function scheduleRoutes(app, opts) {
6132
6358
  providers: JSON.stringify(providers),
6133
6359
  enabled: enabledInt,
6134
6360
  updatedAt: now
6135
- }).where(eq11(schedules.id, existing.id)).run();
6361
+ }).where(eq12(schedules.id, existing.id)).run();
6136
6362
  } else {
6137
6363
  app.db.insert(schedules).values({
6138
6364
  id: crypto11.randomUUID(),
@@ -6154,12 +6380,12 @@ async function scheduleRoutes(app, opts) {
6154
6380
  diff: { cronExpr, preset, timezone, providers }
6155
6381
  });
6156
6382
  opts.onScheduleUpdated?.("upsert", project.id);
6157
- const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6383
+ const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6158
6384
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
6159
6385
  });
6160
6386
  app.get("/projects/:name/schedule", async (request, reply) => {
6161
6387
  const project = resolveProject(app.db, request.params.name);
6162
- const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6388
+ const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6163
6389
  if (!schedule) {
6164
6390
  throw notFound("Schedule", request.params.name);
6165
6391
  }
@@ -6167,11 +6393,11 @@ async function scheduleRoutes(app, opts) {
6167
6393
  });
6168
6394
  app.delete("/projects/:name/schedule", async (request, reply) => {
6169
6395
  const project = resolveProject(app.db, request.params.name);
6170
- const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6396
+ const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6171
6397
  if (!schedule) {
6172
6398
  throw notFound("Schedule", request.params.name);
6173
6399
  }
6174
- app.db.delete(schedules).where(eq11(schedules.id, schedule.id)).run();
6400
+ app.db.delete(schedules).where(eq12(schedules.id, schedule.id)).run();
6175
6401
  writeAuditLog(app.db, {
6176
6402
  projectId: project.id,
6177
6403
  actor: "api",
@@ -6201,7 +6427,7 @@ function formatSchedule(row) {
6201
6427
 
6202
6428
  // ../api-routes/src/notifications.ts
6203
6429
  import crypto12 from "crypto";
6204
- import { eq as eq12 } from "drizzle-orm";
6430
+ import { eq as eq13 } from "drizzle-orm";
6205
6431
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed"];
6206
6432
  async function notificationRoutes(app) {
6207
6433
  app.get("/notifications/events", async (_request, reply) => {
@@ -6240,22 +6466,22 @@ async function notificationRoutes(app) {
6240
6466
  diff: { channel, ...redactNotificationUrl(url), events }
6241
6467
  });
6242
6468
  return reply.status(201).send({
6243
- ...formatNotification(app.db.select().from(notifications).where(eq12(notifications.id, id)).get()),
6469
+ ...formatNotification(app.db.select().from(notifications).where(eq13(notifications.id, id)).get()),
6244
6470
  webhookSecret
6245
6471
  });
6246
6472
  });
6247
6473
  app.get("/projects/:name/notifications", async (request, reply) => {
6248
6474
  const project = resolveProject(app.db, request.params.name);
6249
- const rows = app.db.select().from(notifications).where(eq12(notifications.projectId, project.id)).all();
6475
+ const rows = app.db.select().from(notifications).where(eq13(notifications.projectId, project.id)).all();
6250
6476
  return reply.send(rows.map(formatNotification));
6251
6477
  });
6252
6478
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
6253
6479
  const project = resolveProject(app.db, request.params.name);
6254
- const notification = app.db.select().from(notifications).where(eq12(notifications.id, request.params.id)).get();
6480
+ const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
6255
6481
  if (!notification || notification.projectId !== project.id) {
6256
6482
  throw notFound("Notification", request.params.id);
6257
6483
  }
6258
- app.db.delete(notifications).where(eq12(notifications.id, notification.id)).run();
6484
+ app.db.delete(notifications).where(eq13(notifications.id, notification.id)).run();
6259
6485
  writeAuditLog(app.db, {
6260
6486
  projectId: project.id,
6261
6487
  actor: "api",
@@ -6267,7 +6493,7 @@ async function notificationRoutes(app) {
6267
6493
  });
6268
6494
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
6269
6495
  const project = resolveProject(app.db, request.params.name);
6270
- const notification = app.db.select().from(notifications).where(eq12(notifications.id, request.params.id)).get();
6496
+ const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
6271
6497
  if (!notification || notification.projectId !== project.id) {
6272
6498
  throw notFound("Notification", request.params.id);
6273
6499
  }
@@ -6319,7 +6545,7 @@ function formatNotification(row) {
6319
6545
 
6320
6546
  // ../api-routes/src/google.ts
6321
6547
  import crypto14 from "crypto";
6322
- import { eq as eq13, and as and2, desc as desc4, sql as sql3 } from "drizzle-orm";
6548
+ import { eq as eq14, and as and3, desc as desc5, sql as sql3 } from "drizzle-orm";
6323
6549
 
6324
6550
  // ../integration-google/src/constants.ts
6325
6551
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -6351,7 +6577,56 @@ var GoogleApiError = class extends Error {
6351
6577
  };
6352
6578
 
6353
6579
  // ../integration-google/src/oauth.ts
6580
+ function validateClientId(clientId) {
6581
+ if (!clientId || typeof clientId !== "string" || clientId.trim().length === 0) {
6582
+ throw new GoogleAuthError("Client ID is required and must be a non-empty string");
6583
+ }
6584
+ }
6585
+ function validateClientSecret(clientSecret) {
6586
+ if (!clientSecret || typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
6587
+ throw new GoogleAuthError("Client secret is required and must be a non-empty string");
6588
+ }
6589
+ }
6590
+ function validateRedirectUri(redirectUri) {
6591
+ if (!redirectUri || typeof redirectUri !== "string" || redirectUri.trim().length === 0) {
6592
+ throw new GoogleAuthError("Redirect URI is required and must be a non-empty string");
6593
+ }
6594
+ try {
6595
+ const url = new URL(redirectUri);
6596
+ if (!url.protocol.startsWith("http")) {
6597
+ throw new GoogleAuthError("Redirect URI must be an HTTP or HTTPS URL");
6598
+ }
6599
+ } catch {
6600
+ throw new GoogleAuthError("Redirect URI must be a valid URL");
6601
+ }
6602
+ }
6603
+ function validateCode(code) {
6604
+ if (!code || typeof code !== "string" || code.trim().length === 0) {
6605
+ throw new GoogleAuthError("Authorization code is required and must be a non-empty string");
6606
+ }
6607
+ }
6608
+ function validateScopes(scopes) {
6609
+ if (!Array.isArray(scopes) || scopes.length === 0) {
6610
+ throw new GoogleAuthError("At least one scope is required");
6611
+ }
6612
+ for (const scope of scopes) {
6613
+ if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
6614
+ throw new GoogleAuthError("Scope must be a non-empty string");
6615
+ }
6616
+ }
6617
+ }
6618
+ function validateRefreshToken(refreshToken) {
6619
+ if (!refreshToken || typeof refreshToken !== "string" || refreshToken.trim().length === 0) {
6620
+ throw new GoogleAuthError("Refresh token is required and must be a non-empty string");
6621
+ }
6622
+ }
6354
6623
  function getAuthUrl(clientId, redirectUri, scopes, state) {
6624
+ validateClientId(clientId);
6625
+ validateRedirectUri(redirectUri);
6626
+ validateScopes(scopes);
6627
+ if (state && (typeof state !== "string" || state.trim().length === 0)) {
6628
+ throw new GoogleAuthError("State must be a non-empty string if provided");
6629
+ }
6355
6630
  const params = new URLSearchParams({
6356
6631
  client_id: clientId,
6357
6632
  redirect_uri: redirectUri,
@@ -6364,6 +6639,10 @@ function getAuthUrl(clientId, redirectUri, scopes, state) {
6364
6639
  return `${GOOGLE_AUTH_URL}?${params.toString()}`;
6365
6640
  }
6366
6641
  async function exchangeCode(clientId, clientSecret, code, redirectUri) {
6642
+ validateClientId(clientId);
6643
+ validateClientSecret(clientSecret);
6644
+ validateCode(code);
6645
+ validateRedirectUri(redirectUri);
6367
6646
  const res = await fetch(GOOGLE_TOKEN_URL, {
6368
6647
  method: "POST",
6369
6648
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -6383,6 +6662,9 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
6383
6662
  return await res.json();
6384
6663
  }
6385
6664
  async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
6665
+ validateClientId(clientId);
6666
+ validateClientSecret(clientSecret);
6667
+ validateRefreshToken(currentRefreshToken);
6386
6668
  const res = await fetch(GOOGLE_TOKEN_URL, {
6387
6669
  method: "POST",
6388
6670
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -6402,6 +6684,47 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
6402
6684
  }
6403
6685
 
6404
6686
  // ../integration-google/src/gsc-client.ts
6687
+ function validateAccessToken(accessToken) {
6688
+ if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
6689
+ throw new GoogleApiError("Access token is required and must be a non-empty string", 400);
6690
+ }
6691
+ }
6692
+ function validateSiteUrl(siteUrl) {
6693
+ if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
6694
+ throw new GoogleApiError("Site URL is required and must be a non-empty string", 400);
6695
+ }
6696
+ if (siteUrl.startsWith("sc-domain:")) {
6697
+ const domain = siteUrl.slice("sc-domain:".length);
6698
+ if (!domain) {
6699
+ throw new GoogleApiError("Site URL sc-domain must include a domain", 400);
6700
+ }
6701
+ if (!domain.includes(".")) {
6702
+ throw new GoogleApiError("Site URL sc-domain must be a valid domain", 400);
6703
+ }
6704
+ } else {
6705
+ try {
6706
+ const url = new URL(siteUrl);
6707
+ if (!url.protocol.startsWith("http")) {
6708
+ throw new GoogleApiError("Site URL must be an HTTP or HTTPS URL", 400);
6709
+ }
6710
+ } catch {
6711
+ throw new GoogleApiError("Site URL must be a valid URL", 400);
6712
+ }
6713
+ }
6714
+ }
6715
+ function validateUrl(urlParam) {
6716
+ if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
6717
+ throw new GoogleApiError("URL is required and must be a non-empty string", 400);
6718
+ }
6719
+ try {
6720
+ const url = new URL(urlParam);
6721
+ if (!url.protocol.startsWith("http")) {
6722
+ throw new GoogleApiError("URL must be an HTTP or HTTPS URL", 400);
6723
+ }
6724
+ } catch {
6725
+ throw new GoogleApiError("URL must be a valid URL", 400);
6726
+ }
6727
+ }
6405
6728
  function gscClientLog(level, action, ctx) {
6406
6729
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GscClient", action, ...ctx };
6407
6730
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -6437,6 +6760,7 @@ async function gscFetch(accessToken, url, opts) {
6437
6760
  return await res.json();
6438
6761
  }
6439
6762
  async function listSites(accessToken) {
6763
+ validateAccessToken(accessToken);
6440
6764
  const data = await gscFetch(
6441
6765
  accessToken,
6442
6766
  `${GSC_API_BASE}/sites`
@@ -6444,6 +6768,8 @@ async function listSites(accessToken) {
6444
6768
  return data.siteEntry ?? [];
6445
6769
  }
6446
6770
  async function listSitemaps(accessToken, siteUrl) {
6771
+ validateAccessToken(accessToken);
6772
+ validateSiteUrl(siteUrl);
6447
6773
  const encodedSiteUrl = encodeURIComponent(siteUrl);
6448
6774
  const data = await gscFetch(
6449
6775
  accessToken,
@@ -6452,6 +6778,8 @@ async function listSitemaps(accessToken, siteUrl) {
6452
6778
  return data.sitemap ?? [];
6453
6779
  }
6454
6780
  async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
6781
+ validateAccessToken(accessToken);
6782
+ validateSiteUrl(siteUrl);
6455
6783
  const allRows = [];
6456
6784
  let startRow = 0;
6457
6785
  const dimensions = opts.dimensions ?? ["query", "page", "country", "device", "date"];
@@ -6487,6 +6815,8 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
6487
6815
  return allRows;
6488
6816
  }
6489
6817
  async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
6818
+ validateAccessToken(accessToken);
6819
+ validateUrl(url);
6490
6820
  return gscFetch(
6491
6821
  accessToken,
6492
6822
  `${INDEXING_API_BASE}/urlNotifications:publish`,
@@ -6497,6 +6827,9 @@ async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
6497
6827
  );
6498
6828
  }
6499
6829
  async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
6830
+ validateAccessToken(accessToken);
6831
+ validateUrl(inspectionUrl);
6832
+ validateSiteUrl(siteUrl);
6500
6833
  return gscFetch(
6501
6834
  accessToken,
6502
6835
  URL_INSPECTION_API,
@@ -6532,12 +6865,46 @@ var GA4ApiError = class extends Error {
6532
6865
  };
6533
6866
 
6534
6867
  // ../integration-google-analytics/src/ga4-client.ts
6868
+ function validateClientEmail(clientEmail) {
6869
+ if (!clientEmail || typeof clientEmail !== "string" || clientEmail.trim().length === 0) {
6870
+ throw new GA4ApiError("Client email is required and must be a non-empty string", 400);
6871
+ }
6872
+ if (!clientEmail.includes("@")) {
6873
+ throw new GA4ApiError("Client email must be a valid email address", 400);
6874
+ }
6875
+ }
6876
+ function validatePrivateKey(privateKey) {
6877
+ if (!privateKey || typeof privateKey !== "string" || privateKey.trim().length === 0) {
6878
+ throw new GA4ApiError("Private key is required and must be a non-empty string", 400);
6879
+ }
6880
+ }
6881
+ function validatePropertyId(propertyId) {
6882
+ if (!propertyId || typeof propertyId !== "string" || propertyId.trim().length === 0) {
6883
+ throw new GA4ApiError("Property ID is required and must be a non-empty string", 400);
6884
+ }
6885
+ if (!/^\d+$/.test(propertyId)) {
6886
+ throw new GA4ApiError("Property ID must be a numeric string", 400);
6887
+ }
6888
+ }
6889
+ function validateAccessToken2(accessToken) {
6890
+ if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
6891
+ throw new GA4ApiError("Access token is required and must be a non-empty string", 400);
6892
+ }
6893
+ }
6894
+ function validateScope(scope) {
6895
+ if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
6896
+ throw new GA4ApiError("Scope is required and must be a non-empty string", 400);
6897
+ }
6898
+ }
6535
6899
  function ga4Log(level, action, ctx) {
6536
6900
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Client", action, ...ctx };
6537
6901
  const stream = level === "error" ? process.stderr : process.stdout;
6538
6902
  stream.write(JSON.stringify(entry) + "\n");
6539
6903
  }
6540
6904
  function createServiceAccountJwt(clientEmail, privateKey, scope) {
6905
+ validateClientEmail(clientEmail);
6906
+ validatePrivateKey(privateKey);
6907
+ validateScope(scope);
6541
6908
  const now = Math.floor(Date.now() / 1e3);
6542
6909
  const header = { alg: "RS256", typ: "JWT" };
6543
6910
  const payload = {
@@ -6664,6 +7031,8 @@ var AI_REFERRAL_SOURCE_FILTERS = [
6664
7031
  { matchType: "CONTAINS", value: "meta.ai" }
6665
7032
  ];
6666
7033
  async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
7034
+ validateAccessToken2(accessToken);
7035
+ validatePropertyId(propertyId);
6667
7036
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
6668
7037
  const endDate = /* @__PURE__ */ new Date();
6669
7038
  const startDate = /* @__PURE__ */ new Date();
@@ -6738,10 +7107,15 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
6738
7107
  return rows;
6739
7108
  }
6740
7109
  async function verifyConnection(clientEmail, privateKey, propertyId) {
7110
+ validateClientEmail(clientEmail);
7111
+ validatePrivateKey(privateKey);
7112
+ validatePropertyId(propertyId);
6741
7113
  const accessToken = await getAccessToken(clientEmail, privateKey);
6742
7114
  return verifyConnectionWithToken(accessToken, propertyId);
6743
7115
  }
6744
7116
  async function verifyConnectionWithToken(accessToken, propertyId) {
7117
+ validateAccessToken2(accessToken);
7118
+ validatePropertyId(propertyId);
6745
7119
  const endDate = /* @__PURE__ */ new Date();
6746
7120
  const startDate = /* @__PURE__ */ new Date();
6747
7121
  startDate.setDate(startDate.getDate() - 1);
@@ -6754,6 +7128,8 @@ async function verifyConnectionWithToken(accessToken, propertyId) {
6754
7128
  return true;
6755
7129
  }
6756
7130
  async function fetchAggregateSummary(accessToken, propertyId, days) {
7131
+ validateAccessToken2(accessToken);
7132
+ validatePropertyId(propertyId);
6757
7133
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
6758
7134
  const endDate = /* @__PURE__ */ new Date();
6759
7135
  const startDate = /* @__PURE__ */ new Date();
@@ -6793,6 +7169,8 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
6793
7169
  return summary;
6794
7170
  }
6795
7171
  async function fetchAiReferrals(accessToken, propertyId, days) {
7172
+ validateAccessToken2(accessToken);
7173
+ validatePropertyId(propertyId);
6796
7174
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
6797
7175
  const endDate = /* @__PURE__ */ new Date();
6798
7176
  const startDate = /* @__PURE__ */ new Date();
@@ -6959,13 +7337,13 @@ async function googleRoutes(app, opts) {
6959
7337
  const project = resolveProject(app.db, request.params.name);
6960
7338
  let redirectUri;
6961
7339
  if (publicUrl) {
6962
- redirectUri = publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
7340
+ redirectUri = publicUrl.replace(/\/$/, "") + (opts.routePrefix ?? "/api/v1") + "/google/callback";
6963
7341
  } else if (opts.publicUrl) {
6964
- redirectUri = opts.publicUrl.replace(/\/$/, "") + "/api/v1/google/callback";
7342
+ redirectUri = opts.publicUrl.replace(/\/$/, "") + (opts.routePrefix ?? "/api/v1") + "/google/callback";
6965
7343
  } else {
6966
7344
  const proto = request.headers["x-forwarded-proto"] ?? "http";
6967
7345
  const host = request.headers.host ?? "localhost:4100";
6968
- redirectUri = `${proto}://${host}/api/v1/projects/${encodeURIComponent(request.params.name)}/google/callback`;
7346
+ redirectUri = `${proto}://${host}${opts.routePrefix ?? "/api/v1"}/projects/${encodeURIComponent(request.params.name)}/google/callback`;
6969
7347
  }
6970
7348
  const scopes = type === "gsc" ? [GSC_SCOPE, INDEXING_SCOPE] : [GA4_SCOPE];
6971
7349
  const stateEncoded = buildSignedState(
@@ -7124,18 +7502,18 @@ async function googleRoutes(app, opts) {
7124
7502
  if (opts.onGscSyncRequested) {
7125
7503
  opts.onGscSyncRequested(runId, project.id, { days, full });
7126
7504
  }
7127
- const run = app.db.select().from(runs).where(eq13(runs.id, runId)).get();
7505
+ const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7128
7506
  return run;
7129
7507
  });
7130
7508
  app.get("/projects/:name/google/gsc/performance", async (request) => {
7131
7509
  const project = resolveProject(app.db, request.params.name);
7132
7510
  const { startDate, endDate, query, page, limit } = request.query;
7133
- const conditions = [eq13(gscSearchData.projectId, project.id)];
7511
+ const conditions = [eq14(gscSearchData.projectId, project.id)];
7134
7512
  if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
7135
7513
  if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
7136
7514
  if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
7137
7515
  if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
7138
- const rows = app.db.select().from(gscSearchData).where(and2(...conditions)).orderBy(desc4(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
7516
+ const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc5(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
7139
7517
  return rows.map((r) => ({
7140
7518
  date: r.date,
7141
7519
  query: r.query,
@@ -7211,9 +7589,9 @@ async function googleRoutes(app, opts) {
7211
7589
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
7212
7590
  const project = resolveProject(app.db, request.params.name);
7213
7591
  const { url, limit } = request.query;
7214
- const conditions = [eq13(gscUrlInspections.projectId, project.id)];
7215
- if (url) conditions.push(eq13(gscUrlInspections.url, url));
7216
- const rows = app.db.select().from(gscUrlInspections).where(and2(...conditions)).orderBy(desc4(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7592
+ const conditions = [eq14(gscUrlInspections.projectId, project.id)];
7593
+ if (url) conditions.push(eq14(gscUrlInspections.url, url));
7594
+ const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc5(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7217
7595
  return rows.map((r) => ({
7218
7596
  id: r.id,
7219
7597
  url: r.url,
@@ -7232,7 +7610,7 @@ async function googleRoutes(app, opts) {
7232
7610
  });
7233
7611
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
7234
7612
  const project = resolveProject(app.db, request.params.name);
7235
- const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
7613
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7236
7614
  const byUrl = /* @__PURE__ */ new Map();
7237
7615
  for (const row of allInspections) {
7238
7616
  const existing = byUrl.get(row.url);
@@ -7260,7 +7638,7 @@ async function googleRoutes(app, opts) {
7260
7638
  });
7261
7639
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
7262
7640
  const project = resolveProject(app.db, request.params.name);
7263
- const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
7641
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7264
7642
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
7265
7643
  const latestByUrl = /* @__PURE__ */ new Map();
7266
7644
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -7357,7 +7735,7 @@ async function googleRoutes(app, opts) {
7357
7735
  const project = resolveProject(app.db, request.params.name);
7358
7736
  const parsed = parseInt(request.query.limit ?? "90", 10);
7359
7737
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7360
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(desc4(gscCoverageSnapshots.date)).limit(limit).all();
7738
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq14(gscCoverageSnapshots.projectId, project.id)).orderBy(desc5(gscCoverageSnapshots.date)).limit(limit).all();
7361
7739
  return rows.map((r) => ({
7362
7740
  date: r.date,
7363
7741
  indexed: r.indexed,
@@ -7425,7 +7803,7 @@ async function googleRoutes(app, opts) {
7425
7803
  if (opts.onInspectSitemapRequested) {
7426
7804
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
7427
7805
  }
7428
- const run = app.db.select().from(runs).where(eq13(runs.id, runId)).get();
7806
+ const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7429
7807
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7430
7808
  });
7431
7809
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
@@ -7455,7 +7833,7 @@ async function googleRoutes(app, opts) {
7455
7833
  if (opts.onInspectSitemapRequested) {
7456
7834
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
7457
7835
  }
7458
- const run = app.db.select().from(runs).where(eq13(runs.id, runId)).get();
7836
+ const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7459
7837
  return run;
7460
7838
  });
7461
7839
  app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
@@ -7510,7 +7888,7 @@ async function googleRoutes(app, opts) {
7510
7888
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7511
7889
  let urlsToNotify = request.body?.urls ?? [];
7512
7890
  if (request.body?.allUnindexed) {
7513
- const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
7891
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7514
7892
  const latestByUrl = /* @__PURE__ */ new Map();
7515
7893
  for (const row of allInspections) {
7516
7894
  if (!latestByUrl.has(row.url)) {
@@ -7585,7 +7963,7 @@ async function googleRoutes(app, opts) {
7585
7963
 
7586
7964
  // ../api-routes/src/bing.ts
7587
7965
  import crypto15 from "crypto";
7588
- import { eq as eq14, and as and3, desc as desc5 } from "drizzle-orm";
7966
+ import { eq as eq15, and as and4, desc as desc6 } from "drizzle-orm";
7589
7967
 
7590
7968
  // ../integration-bing/src/constants.ts
7591
7969
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -7604,6 +7982,45 @@ var BingApiError = class extends Error {
7604
7982
  };
7605
7983
 
7606
7984
  // ../integration-bing/src/bing-client.ts
7985
+ function validateApiKey(apiKey) {
7986
+ if (!apiKey || typeof apiKey !== "string" || apiKey.trim().length === 0) {
7987
+ throw new BingApiError("API key is required and must be a non-empty string", 400);
7988
+ }
7989
+ }
7990
+ function validateSiteUrl2(siteUrl) {
7991
+ if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
7992
+ throw new BingApiError("Site URL is required and must be a non-empty string", 400);
7993
+ }
7994
+ try {
7995
+ const url = new URL(siteUrl);
7996
+ if (!url.protocol.startsWith("http")) {
7997
+ throw new BingApiError("Site URL must be an HTTP or HTTPS URL", 400);
7998
+ }
7999
+ } catch {
8000
+ throw new BingApiError("Site URL must be a valid URL", 400);
8001
+ }
8002
+ }
8003
+ function validateUrl2(urlParam) {
8004
+ if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
8005
+ throw new BingApiError("URL is required and must be a non-empty string", 400);
8006
+ }
8007
+ try {
8008
+ const url = new URL(urlParam);
8009
+ if (!url.protocol.startsWith("http")) {
8010
+ throw new BingApiError("URL must be an HTTP or HTTPS URL", 400);
8011
+ }
8012
+ } catch {
8013
+ throw new BingApiError("URL must be a valid URL", 400);
8014
+ }
8015
+ }
8016
+ function validateUrls(urls) {
8017
+ if (!Array.isArray(urls)) {
8018
+ throw new BingApiError("URLs must be an array", 400);
8019
+ }
8020
+ for (const url of urls) {
8021
+ validateUrl2(url);
8022
+ }
8023
+ }
7607
8024
  function bingClientLog(level, action, ctx) {
7608
8025
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "BingClient", action, ...ctx };
7609
8026
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -7652,21 +8069,31 @@ async function bingFetch(apiKey, endpoint, opts) {
7652
8069
  }
7653
8070
  }
7654
8071
  async function getSites(apiKey) {
8072
+ validateApiKey(apiKey);
7655
8073
  const data = await bingFetch(apiKey, "GetUserSites");
7656
8074
  return data ?? [];
7657
8075
  }
7658
8076
  async function getUrlInfo(apiKey, siteUrl, url) {
8077
+ validateApiKey(apiKey);
8078
+ validateSiteUrl2(siteUrl);
8079
+ validateUrl2(url);
7659
8080
  const encodedSite = encodeURIComponent(siteUrl);
7660
8081
  const encodedUrl = encodeURIComponent(url);
7661
8082
  return bingFetch(apiKey, `GetUrlInfo?siteUrl=${encodedSite}&url=${encodedUrl}`);
7662
8083
  }
7663
8084
  async function submitUrl(apiKey, siteUrl, url) {
8085
+ validateApiKey(apiKey);
8086
+ validateSiteUrl2(siteUrl);
8087
+ validateUrl2(url);
7664
8088
  await bingFetch(apiKey, "SubmitUrl", {
7665
8089
  method: "POST",
7666
8090
  body: { siteUrl, url }
7667
8091
  });
7668
8092
  }
7669
8093
  async function submitUrlBatch(apiKey, siteUrl, urls) {
8094
+ validateApiKey(apiKey);
8095
+ validateSiteUrl2(siteUrl);
8096
+ validateUrls(urls);
7670
8097
  for (let i = 0; i < urls.length; i += BING_SUBMIT_URL_BATCH_LIMIT) {
7671
8098
  const batch = urls.slice(i, i + BING_SUBMIT_URL_BATCH_LIMIT);
7672
8099
  await bingFetch(apiKey, "SubmitUrlbatch", {
@@ -7676,6 +8103,8 @@ async function submitUrlBatch(apiKey, siteUrl, urls) {
7676
8103
  }
7677
8104
  }
7678
8105
  async function getKeywordStats(apiKey, siteUrl) {
8106
+ validateApiKey(apiKey);
8107
+ validateSiteUrl2(siteUrl);
7679
8108
  const encodedSite = encodeURIComponent(siteUrl);
7680
8109
  const data = await bingFetch(apiKey, `GetQueryStats?siteUrl=${encodedSite}`);
7681
8110
  return data ?? [];
@@ -7816,7 +8245,7 @@ async function bingRoutes(app, opts) {
7816
8245
  const project = resolveProject(app.db, request.params.name);
7817
8246
  const conn = requireConnection(store, project.canonicalDomain, reply);
7818
8247
  if (!conn) return;
7819
- const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
8248
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
7820
8249
  const latestByUrl = /* @__PURE__ */ new Map();
7821
8250
  const definitiveByUrl = /* @__PURE__ */ new Map();
7822
8251
  for (const row of allInspections) {
@@ -7886,8 +8315,8 @@ async function bingRoutes(app, opts) {
7886
8315
  if (!store) return;
7887
8316
  const project = resolveProject(app.db, request.params.name);
7888
8317
  const { url, limit } = request.query;
7889
- const whereClause = url ? and3(eq14(bingUrlInspections.projectId, project.id), eq14(bingUrlInspections.url, url)) : eq14(bingUrlInspections.projectId, project.id);
7890
- const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc5(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
8318
+ const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
8319
+ const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc6(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
7891
8320
  return filtered.map((r) => ({
7892
8321
  id: r.id,
7893
8322
  url: r.url,
@@ -7983,7 +8412,7 @@ async function bingRoutes(app, opts) {
7983
8412
  }
7984
8413
  let urlsToSubmit = request.body?.urls ?? [];
7985
8414
  if (request.body?.allUnindexed) {
7986
- const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
8415
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
7987
8416
  const latestByUrl = /* @__PURE__ */ new Map();
7988
8417
  for (const row of allInspections) {
7989
8418
  if (!latestByUrl.has(row.url)) {
@@ -8076,14 +8505,14 @@ async function bingRoutes(app, opts) {
8076
8505
  import fs2 from "fs";
8077
8506
  import path2 from "path";
8078
8507
  import os2 from "os";
8079
- import { eq as eq15, and as and4 } from "drizzle-orm";
8508
+ import { eq as eq16, and as and5 } from "drizzle-orm";
8080
8509
  function getScreenshotDir() {
8081
8510
  return path2.join(os2.homedir(), ".canonry", "screenshots");
8082
8511
  }
8083
8512
  async function cdpRoutes(app, opts) {
8084
8513
  app.get("/screenshots/:snapshotId", async (request, reply) => {
8085
8514
  const { snapshotId } = request.params;
8086
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq15(querySnapshots.id, snapshotId)).get();
8515
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq16(querySnapshots.id, snapshotId)).get();
8087
8516
  if (!snapshot?.screenshotPath) {
8088
8517
  const err = notFound("Screenshot", snapshotId);
8089
8518
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8149,7 +8578,7 @@ async function cdpRoutes(app, opts) {
8149
8578
  async (request, reply) => {
8150
8579
  const project = resolveProject(app.db, request.params.name);
8151
8580
  const { runId } = request.params;
8152
- const run = app.db.select().from(runs).where(and4(eq15(runs.id, runId), eq15(runs.projectId, project.id))).get();
8581
+ const run = app.db.select().from(runs).where(and5(eq16(runs.id, runId), eq16(runs.projectId, project.id))).get();
8153
8582
  if (!run) {
8154
8583
  const err = notFound("Run", runId);
8155
8584
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8162,8 +8591,8 @@ async function cdpRoutes(app, opts) {
8162
8591
  citedDomains: querySnapshots.citedDomains,
8163
8592
  screenshotPath: querySnapshots.screenshotPath,
8164
8593
  rawResponse: querySnapshots.rawResponse
8165
- }).from(querySnapshots).where(eq15(querySnapshots.runId, runId)).all();
8166
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq15(keywords.projectId, project.id)).all();
8594
+ }).from(querySnapshots).where(eq16(querySnapshots.runId, runId)).all();
8595
+ const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq16(keywords.projectId, project.id)).all();
8167
8596
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
8168
8597
  const byKeyword = /* @__PURE__ */ new Map();
8169
8598
  for (const snap of snapshots) {
@@ -8231,7 +8660,7 @@ async function cdpRoutes(app, opts) {
8231
8660
  citationState: browser.citationState,
8232
8661
  citedDomains: JSON.parse(browser.citedDomains || "[]"),
8233
8662
  groundingSources: parseGroundingSources2(browser),
8234
- screenshotUrl: browser.screenshotPath ? `/api/v1/screenshots/${browser.id}` : void 0
8663
+ screenshotUrl: browser.screenshotPath ? `${opts.routePrefix ?? "/api/v1"}/screenshots/${browser.id}` : void 0
8235
8664
  } : null,
8236
8665
  agreement
8237
8666
  };
@@ -8246,7 +8675,7 @@ async function cdpRoutes(app, opts) {
8246
8675
 
8247
8676
  // ../api-routes/src/ga.ts
8248
8677
  import crypto16 from "crypto";
8249
- import { eq as eq16, desc as desc6, and as and5, sql as sql4 } from "drizzle-orm";
8678
+ import { eq as eq17, desc as desc7, and as and6, sql as sql4 } from "drizzle-orm";
8250
8679
  function gaLog(level, action, ctx) {
8251
8680
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
8252
8681
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -8403,9 +8832,9 @@ async function ga4Routes(app, opts) {
8403
8832
  if (!saConn && !oauthConn) {
8404
8833
  throw notFound("GA4 connection", project.name);
8405
8834
  }
8406
- app.db.delete(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).run();
8407
- app.db.delete(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).run();
8408
- app.db.delete(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).run();
8835
+ app.db.delete(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).run();
8836
+ app.db.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
8837
+ app.db.delete(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).run();
8409
8838
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
8410
8839
  opts.ga4CredentialStore?.deleteConnection(project.name);
8411
8840
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -8426,7 +8855,7 @@ async function ga4Routes(app, opts) {
8426
8855
  if (!connected) {
8427
8856
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
8428
8857
  }
8429
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
8858
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
8430
8859
  return {
8431
8860
  connected: true,
8432
8861
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -8459,8 +8888,8 @@ async function ga4Routes(app, opts) {
8459
8888
  const now = (/* @__PURE__ */ new Date()).toISOString();
8460
8889
  app.db.transaction((tx) => {
8461
8890
  tx.delete(gaTrafficSnapshots).where(
8462
- and5(
8463
- eq16(gaTrafficSnapshots.projectId, project.id),
8891
+ and6(
8892
+ eq17(gaTrafficSnapshots.projectId, project.id),
8464
8893
  sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8465
8894
  sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8466
8895
  )
@@ -8480,8 +8909,8 @@ async function ga4Routes(app, opts) {
8480
8909
  }
8481
8910
  }
8482
8911
  tx.delete(gaAiReferrals).where(
8483
- and5(
8484
- eq16(gaAiReferrals.projectId, project.id),
8912
+ and6(
8913
+ eq17(gaAiReferrals.projectId, project.id),
8485
8914
  sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
8486
8915
  sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
8487
8916
  )
@@ -8501,7 +8930,7 @@ async function ga4Routes(app, opts) {
8501
8930
  }).run();
8502
8931
  }
8503
8932
  }
8504
- tx.delete(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).run();
8933
+ tx.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
8505
8934
  tx.insert(gaTrafficSummaries).values({
8506
8935
  id: crypto16.randomUUID(),
8507
8936
  projectId: project.id,
@@ -8536,20 +8965,20 @@ async function ga4Routes(app, opts) {
8536
8965
  totalSessions: gaTrafficSummaries.totalSessions,
8537
8966
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
8538
8967
  totalUsers: gaTrafficSummaries.totalUsers
8539
- }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).get();
8968
+ }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
8540
8969
  const rows = app.db.select({
8541
8970
  landingPage: gaTrafficSnapshots.landingPage,
8542
8971
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8543
8972
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8544
8973
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8545
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8974
+ }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8546
8975
  const aiReferrals = app.db.select({
8547
8976
  source: gaAiReferrals.source,
8548
8977
  medium: gaAiReferrals.medium,
8549
8978
  sourceDimension: gaAiReferrals.sourceDimension,
8550
8979
  sessions: sql4`SUM(${gaAiReferrals.sessions})`,
8551
8980
  users: sql4`SUM(${gaAiReferrals.users})`
8552
- }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8981
+ }).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8553
8982
  const aiDeduped = app.db.select({
8554
8983
  sessions: sql4`SUM(max_sessions)`,
8555
8984
  users: sql4`SUM(max_users)`
@@ -8563,7 +8992,7 @@ async function ga4Routes(app, opts) {
8563
8992
  GROUP BY date, source, medium
8564
8993
  )`
8565
8994
  ).get();
8566
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
8995
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
8567
8996
  return {
8568
8997
  totalSessions: summary?.totalSessions ?? 0,
8569
8998
  totalOrganicSessions: summary?.totalOrganicSessions ?? 0,
@@ -8596,7 +9025,7 @@ async function ga4Routes(app, opts) {
8596
9025
  sourceDimension: gaAiReferrals.sourceDimension,
8597
9026
  sessions: gaAiReferrals.sessions,
8598
9027
  users: gaAiReferrals.users
8599
- }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
9028
+ }).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
8600
9029
  return rows;
8601
9030
  });
8602
9031
  app.get("/projects/:name/ga/session-history", async (request, _reply) => {
@@ -8607,7 +9036,7 @@ async function ga4Routes(app, opts) {
8607
9036
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8608
9037
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8609
9038
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8610
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
9039
+ }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8611
9040
  return rows.map((r) => ({
8612
9041
  date: r.date,
8613
9042
  sessions: r.sessions ?? 0,
@@ -8623,7 +9052,7 @@ async function ga4Routes(app, opts) {
8623
9052
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8624
9053
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8625
9054
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8626
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9055
+ }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
8627
9056
  return {
8628
9057
  pages: trafficPages.map((r) => ({
8629
9058
  landingPage: r.landingPage,
@@ -8758,6 +9187,36 @@ function parseSchemaPageEntry(entry) {
8758
9187
 
8759
9188
  // ../integration-wordpress/src/wordpress-client.ts
8760
9189
  import crypto17 from "crypto";
9190
+ function validateUsername(username) {
9191
+ if (!username || typeof username !== "string" || username.trim().length === 0) {
9192
+ throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
9193
+ }
9194
+ }
9195
+ function validateAppPassword(appPassword) {
9196
+ if (!appPassword || typeof appPassword !== "string" || appPassword.trim().length === 0) {
9197
+ throw new WordpressApiError("AUTH_INVALID", "Application password is required and must be a non-empty string", 400);
9198
+ }
9199
+ }
9200
+ function validateSiteUrl3(siteUrl) {
9201
+ if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
9202
+ throw new WordpressApiError("AUTH_INVALID", "Site URL is required and must be a non-empty string", 400);
9203
+ }
9204
+ try {
9205
+ const url = new URL(siteUrl);
9206
+ if (!url.protocol.startsWith("http")) {
9207
+ throw new WordpressApiError("AUTH_INVALID", "Site URL must be an HTTP or HTTPS URL", 400);
9208
+ }
9209
+ if (url.protocol !== "https:") {
9210
+ }
9211
+ } catch {
9212
+ throw new WordpressApiError("AUTH_INVALID", "Site URL must be a valid URL", 400);
9213
+ }
9214
+ }
9215
+ function validateConnection(connection, siteUrl) {
9216
+ validateUsername(connection.username);
9217
+ validateAppPassword(connection.appPassword);
9218
+ validateSiteUrl3(siteUrl);
9219
+ }
8761
9220
  var WP_REQUEST_TIMEOUT_MS = 3e4;
8762
9221
  var WP_FETCH_TEXT_TIMEOUT_MS = 15e3;
8763
9222
  var PAGE_FIELDS = "id,slug,status,link,modified,modified_gmt,title,content,meta";
@@ -8999,6 +9458,7 @@ function getWpStagingAdminUrl(url) {
8999
9458
  }
9000
9459
  async function verifyWordpressConnection(connection) {
9001
9460
  const site = resolveEnvironment({ ...connection, defaultEnv: "live" }, "live");
9461
+ validateConnection(connection, site.siteUrl);
9002
9462
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
9003
9463
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
9004
9464
  const homeHtml = await fetchText(site.siteUrl);
@@ -9013,6 +9473,7 @@ async function verifyWordpressConnection(connection) {
9013
9473
  }
9014
9474
  async function getSiteStatus(connection, env) {
9015
9475
  const site = resolveEnvironment(connection, env);
9476
+ validateConnection(connection, site.siteUrl);
9016
9477
  try {
9017
9478
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
9018
9479
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
@@ -10317,7 +10778,7 @@ async function apiRoutes(app, opts) {
10317
10778
  resolveSessionApiKeyId: opts.resolveSessionApiKeyId
10318
10779
  });
10319
10780
  }
10320
- await api.register(openApiRoutes, opts.openApiInfo ?? {});
10781
+ await api.register(openApiRoutes, { ...opts.openApiInfo, routePrefix: opts.routePrefix });
10321
10782
  await api.register(projectRoutes, {
10322
10783
  onProjectDeleted: opts.onProjectDeleted,
10323
10784
  validProviderNames: opts.providerAdapters?.map((a) => a.name)
@@ -10343,6 +10804,7 @@ async function apiRoutes(app, opts) {
10343
10804
  });
10344
10805
  await api.register(historyRoutes);
10345
10806
  await api.register(analyticsRoutes);
10807
+ await api.register(intelligenceRoutes);
10346
10808
  await api.register(settingsRoutes, {
10347
10809
  providerSummary: opts.providerSummary,
10348
10810
  providerAdapters: opts.providerAdapters,
@@ -10372,6 +10834,7 @@ async function apiRoutes(app, opts) {
10372
10834
  googleConnectionStore: opts.googleConnectionStore,
10373
10835
  googleStateSecret: opts.googleStateSecret,
10374
10836
  publicUrl: opts.publicUrl,
10837
+ routePrefix: opts.routePrefix,
10375
10838
  onGscSyncRequested: opts.onGscSyncRequested,
10376
10839
  onInspectSitemapRequested: opts.onInspectSitemapRequested
10377
10840
  });
@@ -10382,7 +10845,8 @@ async function apiRoutes(app, opts) {
10382
10845
  await api.register(cdpRoutes, {
10383
10846
  getCdpStatus: opts.getCdpStatus,
10384
10847
  onCdpScreenshot: opts.onCdpScreenshot,
10385
- onCdpConfigure: opts.onCdpConfigure
10848
+ onCdpConfigure: opts.onCdpConfigure,
10849
+ routePrefix: opts.routePrefix
10386
10850
  });
10387
10851
  await api.register(ga4Routes, {
10388
10852
  ga4CredentialStore: opts.ga4CredentialStore,
@@ -10406,7 +10870,7 @@ async function withRetry(fn, options = {}) {
10406
10870
  lastError = err;
10407
10871
  if (attempt < maxRetries) {
10408
10872
  const delay = initialDelay * Math.pow(2, attempt);
10409
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
10873
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
10410
10874
  await new Promise((resolve) => setTimeout(resolve, delay));
10411
10875
  }
10412
10876
  }
@@ -10740,7 +11204,7 @@ async function withRetry2(fn, options = {}) {
10740
11204
  lastError = err;
10741
11205
  if (attempt < maxRetries) {
10742
11206
  const delay = initialDelay * Math.pow(2, attempt);
10743
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
11207
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
10744
11208
  await new Promise((resolve) => setTimeout(resolve, delay));
10745
11209
  }
10746
11210
  }
@@ -11046,7 +11510,7 @@ async function withRetry3(fn, options = {}) {
11046
11510
  lastError = err;
11047
11511
  if (attempt < maxRetries) {
11048
11512
  const delay = initialDelay * Math.pow(2, attempt);
11049
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
11513
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
11050
11514
  await new Promise((resolve) => setTimeout(resolve, delay));
11051
11515
  }
11052
11516
  }
@@ -11351,7 +11815,7 @@ async function withRetry4(fn, options = {}) {
11351
11815
  lastError = err;
11352
11816
  if (attempt < maxRetries) {
11353
11817
  const delay = initialDelay * Math.pow(2, attempt);
11354
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
11818
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
11355
11819
  await new Promise((resolve) => setTimeout(resolve, delay));
11356
11820
  }
11357
11821
  }
@@ -12146,7 +12610,7 @@ async function withRetry5(fn, options = {}) {
12146
12610
  lastError = err;
12147
12611
  if (attempt < maxRetries) {
12148
12612
  const delay = initialDelay * Math.pow(2, attempt);
12149
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
12613
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
12150
12614
  await new Promise((resolve) => setTimeout(resolve, delay));
12151
12615
  }
12152
12616
  }
@@ -12540,7 +13004,7 @@ import crypto18 from "crypto";
12540
13004
  import fs4 from "fs";
12541
13005
  import path5 from "path";
12542
13006
  import os4 from "os";
12543
- import { and as and6, eq as eq17, inArray as inArray3, sql as sql5 } from "drizzle-orm";
13007
+ import { and as and7, eq as eq18, inArray as inArray3, sql as sql5 } from "drizzle-orm";
12544
13008
 
12545
13009
  // src/logger.ts
12546
13010
  var IS_TTY = process.stdout.isTTY === true;
@@ -12562,7 +13026,7 @@ function emit(entry) {
12562
13026
  }
12563
13027
  }
12564
13028
  function createLogger(module) {
12565
- function log8(level, action, ctx) {
13029
+ function log10(level, action, ctx) {
12566
13030
  const entry = {
12567
13031
  ts: (/* @__PURE__ */ new Date()).toISOString(),
12568
13032
  level,
@@ -12573,9 +13037,9 @@ function createLogger(module) {
12573
13037
  emit(entry);
12574
13038
  }
12575
13039
  return {
12576
- info: (action, ctx) => log8("info", action, ctx),
12577
- warn: (action, ctx) => log8("warn", action, ctx),
12578
- error: (action, ctx) => log8("error", action, ctx)
13040
+ info: (action, ctx) => log10("info", action, ctx),
13041
+ warn: (action, ctx) => log10("warn", action, ctx),
13042
+ error: (action, ctx) => log10("error", action, ctx)
12579
13043
  };
12580
13044
  }
12581
13045
 
@@ -12662,7 +13126,7 @@ var JobRunner = class {
12662
13126
  if (stale.length === 0) return;
12663
13127
  const now = (/* @__PURE__ */ new Date()).toISOString();
12664
13128
  for (const run of stale) {
12665
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq17(runs.id, run.id)).run();
13129
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq18(runs.id, run.id)).run();
12666
13130
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
12667
13131
  }
12668
13132
  }
@@ -12690,10 +13154,10 @@ var JobRunner = class {
12690
13154
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
12691
13155
  }
12692
13156
  if (existingRun.status === "queued") {
12693
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and6(eq17(runs.id, runId), eq17(runs.status, "queued"))).run();
13157
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
12694
13158
  }
12695
13159
  this.throwIfRunCancelled(runId);
12696
- const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
13160
+ const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
12697
13161
  if (!project) {
12698
13162
  throw new Error(`Project ${projectId} not found`);
12699
13163
  }
@@ -12713,8 +13177,8 @@ var JobRunner = class {
12713
13177
  throw new Error("No providers configured. Add at least one provider API key.");
12714
13178
  }
12715
13179
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
12716
- projectKeywords = this.db.select().from(keywords).where(eq17(keywords.projectId, projectId)).all();
12717
- const projectCompetitors = this.db.select().from(competitors).where(eq17(competitors.projectId, projectId)).all();
13180
+ projectKeywords = this.db.select().from(keywords).where(eq18(keywords.projectId, projectId)).all();
13181
+ const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
12718
13182
  const competitorDomains = projectCompetitors.map((c) => c.domain);
12719
13183
  const allDomains = effectiveDomains({
12720
13184
  canonicalDomain: project.canonicalDomain,
@@ -12730,7 +13194,7 @@ var JobRunner = class {
12730
13194
  const todayPeriod = getCurrentUsageDay();
12731
13195
  for (const p of activeProviders) {
12732
13196
  const providerScope = `${projectId}:${p.adapter.name}`;
12733
- const providerUsage = this.db.select().from(usageCounters).where(eq17(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
13197
+ const providerUsage = this.db.select().from(usageCounters).where(eq18(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
12734
13198
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
12735
13199
  if (providerUsage + queriesPerProvider > limit) {
12736
13200
  throw new Error(
@@ -12871,12 +13335,12 @@ var JobRunner = class {
12871
13335
  const someFailed = providerErrors.size > 0;
12872
13336
  if (allFailed) {
12873
13337
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
12874
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq17(runs.id, runId)).run();
13338
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
12875
13339
  } else if (someFailed) {
12876
13340
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
12877
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq17(runs.id, runId)).run();
13341
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
12878
13342
  } else {
12879
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(runs.id, runId)).run();
13343
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
12880
13344
  }
12881
13345
  this.flushProviderUsage(projectId, providerDispatchCounts);
12882
13346
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -12911,7 +13375,7 @@ var JobRunner = class {
12911
13375
  status: "failed",
12912
13376
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
12913
13377
  error: errorMessage
12914
- }).where(eq17(runs.id, runId)).run();
13378
+ }).where(eq18(runs.id, runId)).run();
12915
13379
  this.flushProviderUsage(projectId, providerDispatchCounts);
12916
13380
  trackEvent("run.completed", {
12917
13381
  status: "failed",
@@ -12954,7 +13418,7 @@ var JobRunner = class {
12954
13418
  status: runs.status,
12955
13419
  finishedAt: runs.finishedAt,
12956
13420
  error: runs.error
12957
- }).from(runs).where(eq17(runs.id, runId)).get();
13421
+ }).from(runs).where(eq18(runs.id, runId)).get();
12958
13422
  }
12959
13423
  isRunCancelled(runId) {
12960
13424
  return this.getRunState(runId)?.status === "cancelled";
@@ -12970,7 +13434,7 @@ var JobRunner = class {
12970
13434
  this.db.update(runs).set({
12971
13435
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
12972
13436
  error: currentRun.error ?? "Cancelled by user"
12973
- }).where(eq17(runs.id, runId)).run();
13437
+ }).where(eq18(runs.id, runId)).run();
12974
13438
  }
12975
13439
  trackEvent("run.completed", {
12976
13440
  status: "cancelled",
@@ -13142,7 +13606,7 @@ function matchesBrandKey(candidateKey, brandKeys) {
13142
13606
 
13143
13607
  // src/gsc-sync.ts
13144
13608
  import crypto19 from "crypto";
13145
- import { eq as eq18, and as and7, sql as sql6 } from "drizzle-orm";
13609
+ import { eq as eq19, and as and8, sql as sql6 } from "drizzle-orm";
13146
13610
  var log2 = createLogger("GscSync");
13147
13611
  function formatDate2(d) {
13148
13612
  return d.toISOString().split("T")[0];
@@ -13154,13 +13618,13 @@ function daysAgo(n) {
13154
13618
  }
13155
13619
  async function executeGscSync(db, runId, projectId, opts) {
13156
13620
  const now = (/* @__PURE__ */ new Date()).toISOString();
13157
- db.update(runs).set({ status: "running", startedAt: now }).where(eq18(runs.id, runId)).run();
13621
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
13158
13622
  try {
13159
13623
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13160
13624
  if (!googleClientId || !googleClientSecret) {
13161
13625
  throw new Error("Google OAuth is not configured in the local Canonry config");
13162
13626
  }
13163
- const project = db.select().from(projects).where(eq18(projects.id, projectId)).get();
13627
+ const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
13164
13628
  if (!project) {
13165
13629
  throw new Error(`Project not found: ${projectId}`);
13166
13630
  }
@@ -13194,8 +13658,8 @@ async function executeGscSync(db, runId, projectId, opts) {
13194
13658
  });
13195
13659
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
13196
13660
  db.delete(gscSearchData).where(
13197
- and7(
13198
- eq18(gscSearchData.projectId, projectId),
13661
+ and8(
13662
+ eq19(gscSearchData.projectId, projectId),
13199
13663
  sql6`${gscSearchData.date} >= ${startDate}`,
13200
13664
  sql6`${gscSearchData.date} <= ${endDate}`
13201
13665
  )
@@ -13262,7 +13726,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13262
13726
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
13263
13727
  }
13264
13728
  }
13265
- const allInspections = db.select().from(gscUrlInspections).where(eq18(gscUrlInspections.projectId, projectId)).all();
13729
+ const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
13266
13730
  const latestByUrl = /* @__PURE__ */ new Map();
13267
13731
  for (const row of allInspections) {
13268
13732
  const existing = latestByUrl.get(row.url);
@@ -13283,7 +13747,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13283
13747
  }
13284
13748
  }
13285
13749
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
13286
- db.delete(gscCoverageSnapshots).where(and7(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
13750
+ db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
13287
13751
  db.insert(gscCoverageSnapshots).values({
13288
13752
  id: crypto19.randomUUID(),
13289
13753
  projectId,
@@ -13294,11 +13758,11 @@ async function executeGscSync(db, runId, projectId, opts) {
13294
13758
  reasonBreakdown: JSON.stringify(reasonCounts),
13295
13759
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
13296
13760
  }).run();
13297
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
13761
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13298
13762
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
13299
13763
  } catch (err) {
13300
13764
  const errorMsg = err instanceof Error ? err.message : String(err);
13301
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
13765
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13302
13766
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
13303
13767
  throw err;
13304
13768
  }
@@ -13306,7 +13770,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13306
13770
 
13307
13771
  // src/gsc-inspect-sitemap.ts
13308
13772
  import crypto20 from "crypto";
13309
- import { eq as eq19, and as and8 } from "drizzle-orm";
13773
+ import { eq as eq20, and as and9 } from "drizzle-orm";
13310
13774
 
13311
13775
  // src/sitemap-parser.ts
13312
13776
  var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
@@ -13375,13 +13839,13 @@ async function parseSitemapRecursive(url, urls, depth) {
13375
13839
  var log3 = createLogger("InspectSitemap");
13376
13840
  async function executeInspectSitemap(db, runId, projectId, opts) {
13377
13841
  const now = (/* @__PURE__ */ new Date()).toISOString();
13378
- db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
13842
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
13379
13843
  try {
13380
13844
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13381
13845
  if (!googleClientId || !googleClientSecret) {
13382
13846
  throw new Error("Google OAuth is not configured in the local Canonry config");
13383
13847
  }
13384
- const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
13848
+ const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
13385
13849
  if (!project) {
13386
13850
  throw new Error(`Project not found: ${projectId}`);
13387
13851
  }
@@ -13449,7 +13913,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13449
13913
  await new Promise((r) => setTimeout(r, 1e3));
13450
13914
  }
13451
13915
  }
13452
- const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
13916
+ const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
13453
13917
  const latestByUrl = /* @__PURE__ */ new Map();
13454
13918
  for (const row of allInspections) {
13455
13919
  const existing = latestByUrl.get(row.url);
@@ -13470,7 +13934,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13470
13934
  }
13471
13935
  }
13472
13936
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13473
- db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
13937
+ db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
13474
13938
  db.insert(gscCoverageSnapshots).values({
13475
13939
  id: crypto20.randomUUID(),
13476
13940
  projectId,
@@ -13482,11 +13946,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13482
13946
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
13483
13947
  }).run();
13484
13948
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
13485
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13949
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
13486
13950
  log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
13487
13951
  } catch (err) {
13488
13952
  const errorMsg = err instanceof Error ? err.message : String(err);
13489
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13953
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
13490
13954
  log3.error("inspect.failed", { runId, projectId, error: errorMsg });
13491
13955
  throw err;
13492
13956
  }
@@ -13545,7 +14009,7 @@ var ProviderRegistry = class {
13545
14009
 
13546
14010
  // src/scheduler.ts
13547
14011
  import cron from "node-cron";
13548
- import { eq as eq20 } from "drizzle-orm";
14012
+ import { eq as eq21 } from "drizzle-orm";
13549
14013
  var log4 = createLogger("Scheduler");
13550
14014
  var Scheduler = class {
13551
14015
  db;
@@ -13557,7 +14021,7 @@ var Scheduler = class {
13557
14021
  }
13558
14022
  /** Load all enabled schedules from DB and register cron jobs. */
13559
14023
  start() {
13560
- const allSchedules = this.db.select().from(schedules).where(eq20(schedules.enabled, 1)).all();
14024
+ const allSchedules = this.db.select().from(schedules).where(eq21(schedules.enabled, 1)).all();
13561
14025
  for (const schedule of allSchedules) {
13562
14026
  const missedRunAt = schedule.nextRunAt;
13563
14027
  this.registerCronTask(schedule);
@@ -13582,7 +14046,7 @@ var Scheduler = class {
13582
14046
  this.stopTask(projectId, existing, "Stopped");
13583
14047
  this.tasks.delete(projectId);
13584
14048
  }
13585
- const schedule = this.db.select().from(schedules).where(eq20(schedules.projectId, projectId)).get();
14049
+ const schedule = this.db.select().from(schedules).where(eq21(schedules.projectId, projectId)).get();
13586
14050
  if (schedule && schedule.enabled === 1) {
13587
14051
  this.registerCronTask(schedule);
13588
14052
  }
@@ -13615,14 +14079,14 @@ var Scheduler = class {
13615
14079
  this.db.update(schedules).set({
13616
14080
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
13617
14081
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13618
- }).where(eq20(schedules.id, scheduleId)).run();
14082
+ }).where(eq21(schedules.id, scheduleId)).run();
13619
14083
  const label = schedule.preset ?? cronExpr;
13620
14084
  log4.info("cron.registered", { projectId, schedule: label, timezone });
13621
14085
  }
13622
14086
  triggerRun(scheduleId, projectId) {
13623
14087
  try {
13624
14088
  const now = (/* @__PURE__ */ new Date()).toISOString();
13625
- const currentSchedule = this.db.select().from(schedules).where(eq20(schedules.id, scheduleId)).get();
14089
+ const currentSchedule = this.db.select().from(schedules).where(eq21(schedules.id, scheduleId)).get();
13626
14090
  if (!currentSchedule || currentSchedule.enabled !== 1) {
13627
14091
  log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
13628
14092
  this.remove(projectId);
@@ -13630,7 +14094,7 @@ var Scheduler = class {
13630
14094
  }
13631
14095
  const task = this.tasks.get(projectId);
13632
14096
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
13633
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
14097
+ const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
13634
14098
  if (!project) {
13635
14099
  log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
13636
14100
  this.remove(projectId);
@@ -13647,7 +14111,7 @@ var Scheduler = class {
13647
14111
  this.db.update(schedules).set({
13648
14112
  nextRunAt,
13649
14113
  updatedAt: now
13650
- }).where(eq20(schedules.id, currentSchedule.id)).run();
14114
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
13651
14115
  return;
13652
14116
  }
13653
14117
  const runId = queueResult.runId;
@@ -13655,7 +14119,7 @@ var Scheduler = class {
13655
14119
  lastRunAt: now,
13656
14120
  nextRunAt,
13657
14121
  updatedAt: now
13658
- }).where(eq20(schedules.id, currentSchedule.id)).run();
14122
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
13659
14123
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
13660
14124
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13661
14125
  log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -13667,7 +14131,7 @@ var Scheduler = class {
13667
14131
  };
13668
14132
 
13669
14133
  // src/notifier.ts
13670
- import { eq as eq21, desc as desc7, and as and9, or as or2 } from "drizzle-orm";
14134
+ import { eq as eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
13671
14135
  import crypto21 from "crypto";
13672
14136
  var log5 = createLogger("Notifier");
13673
14137
  var Notifier = class {
@@ -13680,18 +14144,18 @@ var Notifier = class {
13680
14144
  /** Called after a run completes (success, partial, or failed). */
13681
14145
  async onRunCompleted(runId, projectId) {
13682
14146
  log5.info("run.completed", { runId, projectId });
13683
- const notifs = this.db.select().from(notifications).where(eq21(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14147
+ const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
13684
14148
  if (notifs.length === 0) {
13685
14149
  log5.info("notifications.none-enabled", { projectId });
13686
14150
  return;
13687
14151
  }
13688
14152
  log5.info("notifications.found", { projectId, count: notifs.length });
13689
- const run = this.db.select().from(runs).where(eq21(runs.id, runId)).get();
14153
+ const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
13690
14154
  if (!run) {
13691
14155
  log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
13692
14156
  return;
13693
14157
  }
13694
- const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
14158
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
13695
14159
  if (!project) {
13696
14160
  log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
13697
14161
  return;
@@ -13731,11 +14195,11 @@ var Notifier = class {
13731
14195
  }
13732
14196
  computeTransitions(runId, projectId) {
13733
14197
  const recentRuns = this.db.select().from(runs).where(
13734
- and9(
13735
- eq21(runs.projectId, projectId),
13736
- or2(eq21(runs.status, "completed"), eq21(runs.status, "partial"))
14198
+ and10(
14199
+ eq22(runs.projectId, projectId),
14200
+ or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
13737
14201
  )
13738
- ).orderBy(desc7(runs.createdAt)).limit(2).all();
14202
+ ).orderBy(desc8(runs.createdAt)).limit(2).all();
13739
14203
  if (recentRuns.length < 2) return [];
13740
14204
  const currentRunId = recentRuns[0].id;
13741
14205
  const previousRunId = recentRuns[1].id;
@@ -13745,12 +14209,12 @@ var Notifier = class {
13745
14209
  keyword: keywords.keyword,
13746
14210
  provider: querySnapshots.provider,
13747
14211
  citationState: querySnapshots.citationState
13748
- }).from(querySnapshots).leftJoin(keywords, eq21(querySnapshots.keywordId, keywords.id)).where(eq21(querySnapshots.runId, currentRunId)).all();
14212
+ }).from(querySnapshots).leftJoin(keywords, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
13749
14213
  const previousSnapshots = this.db.select({
13750
14214
  keywordId: querySnapshots.keywordId,
13751
14215
  provider: querySnapshots.provider,
13752
14216
  citationState: querySnapshots.citationState
13753
- }).from(querySnapshots).where(eq21(querySnapshots.runId, previousRunId)).all();
14217
+ }).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
13754
14218
  const prevMap = /* @__PURE__ */ new Map();
13755
14219
  for (const s of previousSnapshots) {
13756
14220
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -13820,6 +14284,399 @@ var Notifier = class {
13820
14284
  }
13821
14285
  };
13822
14286
 
14287
+ // src/intelligence-service.ts
14288
+ import { eq as eq23, desc as desc9, asc as asc2, and as and11, or as or3 } from "drizzle-orm";
14289
+
14290
+ // ../intelligence/src/regressions.ts
14291
+ function detectRegressions(currentRun, previousRun) {
14292
+ const regressions = [];
14293
+ const previousCited = /* @__PURE__ */ new Map();
14294
+ for (const snap of previousRun.snapshots) {
14295
+ if (snap.cited) {
14296
+ previousCited.set(`${snap.keyword}:${snap.provider}`, {
14297
+ citationUrl: snap.citationUrl,
14298
+ position: snap.position
14299
+ });
14300
+ }
14301
+ }
14302
+ for (const snap of currentRun.snapshots) {
14303
+ const key = `${snap.keyword}:${snap.provider}`;
14304
+ if (!snap.cited && previousCited.has(key)) {
14305
+ const prev = previousCited.get(key);
14306
+ regressions.push({
14307
+ keyword: snap.keyword,
14308
+ provider: snap.provider,
14309
+ previousCitationUrl: prev.citationUrl,
14310
+ previousPosition: prev.position,
14311
+ currentRunId: currentRun.runId,
14312
+ previousRunId: previousRun.runId
14313
+ });
14314
+ }
14315
+ }
14316
+ return regressions;
14317
+ }
14318
+
14319
+ // ../intelligence/src/gains.ts
14320
+ function detectGains(currentRun, previousRun) {
14321
+ const gains = [];
14322
+ const previousCited = /* @__PURE__ */ new Set();
14323
+ for (const snap of previousRun.snapshots) {
14324
+ if (snap.cited) {
14325
+ previousCited.add(`${snap.keyword}:${snap.provider}`);
14326
+ }
14327
+ }
14328
+ for (const snap of currentRun.snapshots) {
14329
+ const key = `${snap.keyword}:${snap.provider}`;
14330
+ if (snap.cited && !previousCited.has(key)) {
14331
+ gains.push({
14332
+ keyword: snap.keyword,
14333
+ provider: snap.provider,
14334
+ citationUrl: snap.citationUrl,
14335
+ position: snap.position,
14336
+ snippet: snap.snippet,
14337
+ runId: currentRun.runId
14338
+ });
14339
+ }
14340
+ }
14341
+ return gains;
14342
+ }
14343
+
14344
+ // ../intelligence/src/health.ts
14345
+ function computeHealth(run) {
14346
+ const providerStats = /* @__PURE__ */ new Map();
14347
+ let totalPairs = 0;
14348
+ let citedPairs = 0;
14349
+ for (const snap of run.snapshots) {
14350
+ totalPairs++;
14351
+ if (snap.cited) citedPairs++;
14352
+ const stats = providerStats.get(snap.provider) ?? { cited: 0, total: 0 };
14353
+ stats.total++;
14354
+ if (snap.cited) stats.cited++;
14355
+ providerStats.set(snap.provider, stats);
14356
+ }
14357
+ const providerBreakdown = {};
14358
+ for (const [provider, stats] of providerStats) {
14359
+ providerBreakdown[provider] = {
14360
+ citedRate: stats.total > 0 ? stats.cited / stats.total : 0,
14361
+ cited: stats.cited,
14362
+ total: stats.total
14363
+ };
14364
+ }
14365
+ return {
14366
+ overallCitedRate: totalPairs > 0 ? citedPairs / totalPairs : 0,
14367
+ totalPairs,
14368
+ citedPairs,
14369
+ providerBreakdown
14370
+ };
14371
+ }
14372
+ function computeHealthTrend(runs2) {
14373
+ if (runs2.length === 0) {
14374
+ return { current: 0, previous: 0, delta: 0 };
14375
+ }
14376
+ const current = computeHealth(runs2[runs2.length - 1]).overallCitedRate;
14377
+ if (runs2.length === 1) {
14378
+ return { current, previous: 0, delta: current };
14379
+ }
14380
+ const previous = computeHealth(runs2[runs2.length - 2]).overallCitedRate;
14381
+ return {
14382
+ current,
14383
+ previous,
14384
+ delta: current - previous
14385
+ };
14386
+ }
14387
+
14388
+ // ../intelligence/src/causes.ts
14389
+ function analyzeCause(regression, currentSnapshots) {
14390
+ const currentSnap = currentSnapshots.find(
14391
+ (s) => s.keyword === regression.keyword && s.provider === regression.provider && !s.cited && s.competitorDomain
14392
+ );
14393
+ if (currentSnap) {
14394
+ return {
14395
+ cause: "competitor_gain",
14396
+ competitorDomain: currentSnap.competitorDomain,
14397
+ details: `Competitor ${currentSnap.competitorDomain} now cited for "${regression.keyword}" on ${regression.provider}`
14398
+ };
14399
+ }
14400
+ return {
14401
+ cause: "unknown",
14402
+ details: `No specific cause identified for loss of "${regression.keyword}" on ${regression.provider}`
14403
+ };
14404
+ }
14405
+
14406
+ // ../intelligence/src/insights.ts
14407
+ import { randomUUID } from "crypto";
14408
+ function generateInsights(regressions, gains, health, causes) {
14409
+ const insights2 = [];
14410
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14411
+ for (const reg of regressions) {
14412
+ const key = `${reg.keyword}:${reg.provider}`;
14413
+ const cause = causes.get(key);
14414
+ insights2.push({
14415
+ id: `ins_${randomUUID().slice(0, 8)}`,
14416
+ type: "regression",
14417
+ severity: "high",
14418
+ title: `Lost ${reg.provider} citation for "${reg.keyword}"`,
14419
+ keyword: reg.keyword,
14420
+ provider: reg.provider,
14421
+ recommendation: {
14422
+ action: "audit",
14423
+ target: reg.previousCitationUrl,
14424
+ reason: `Page was previously cited at position ${reg.previousPosition ?? "unknown"}. Run aeo-audit to check for content or schema issues.`
14425
+ },
14426
+ cause,
14427
+ createdAt: now
14428
+ });
14429
+ }
14430
+ for (const gain of gains) {
14431
+ insights2.push({
14432
+ id: `ins_${randomUUID().slice(0, 8)}`,
14433
+ type: "gain",
14434
+ severity: "low",
14435
+ title: `New ${gain.provider} citation for "${gain.keyword}"`,
14436
+ keyword: gain.keyword,
14437
+ provider: gain.provider,
14438
+ recommendation: {
14439
+ action: "monitor",
14440
+ target: gain.citationUrl,
14441
+ reason: `New citation appeared at position ${gain.position ?? "unknown"}. Monitor to confirm it persists.`
14442
+ },
14443
+ createdAt: now
14444
+ });
14445
+ }
14446
+ return insights2;
14447
+ }
14448
+
14449
+ // ../intelligence/src/analyzer.ts
14450
+ function analyzeRuns(currentRun, previousRun, allRuns) {
14451
+ const regressions = detectRegressions(currentRun, previousRun);
14452
+ const gains = detectGains(currentRun, previousRun);
14453
+ const health = computeHealth(currentRun);
14454
+ const trend = allRuns ? computeHealthTrend(allRuns) : void 0;
14455
+ const causes = /* @__PURE__ */ new Map();
14456
+ for (const reg of regressions) {
14457
+ const cause = analyzeCause(reg, currentRun.snapshots);
14458
+ causes.set(`${reg.keyword}:${reg.provider}`, cause);
14459
+ }
14460
+ const insights2 = generateInsights(regressions, gains, health, causes);
14461
+ return {
14462
+ regressions,
14463
+ gains,
14464
+ health,
14465
+ trend,
14466
+ insights: insights2
14467
+ };
14468
+ }
14469
+
14470
+ // src/intelligence-service.ts
14471
+ import crypto22 from "crypto";
14472
+ var log6 = createLogger("IntelligenceService");
14473
+ var IntelligenceService = class {
14474
+ constructor(db) {
14475
+ this.db = db;
14476
+ }
14477
+ /**
14478
+ * Analyze a completed run and persist insights + health snapshot.
14479
+ * Idempotent: deletes prior results for the same runId before inserting.
14480
+ * Returns the analysis result for the coordinator to inspect (e.g. for webhook dispatch).
14481
+ */
14482
+ analyzeAndPersist(runId, projectId) {
14483
+ const recentRuns = this.db.select().from(runs).where(
14484
+ and11(
14485
+ eq23(runs.projectId, projectId),
14486
+ or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
14487
+ )
14488
+ ).orderBy(desc9(runs.createdAt)).limit(2).all();
14489
+ if (recentRuns.length === 0) {
14490
+ log6.info("intelligence.skip", { runId, reason: "no completed runs" });
14491
+ return null;
14492
+ }
14493
+ const currentRunRecord = recentRuns.find((r) => r.id === runId);
14494
+ if (!currentRunRecord) {
14495
+ log6.info("intelligence.skip", { runId, reason: "run not in recent completed list" });
14496
+ return null;
14497
+ }
14498
+ const currentRun = this.buildRunData(runId, projectId, currentRunRecord.finishedAt ?? currentRunRecord.createdAt);
14499
+ if (currentRun.snapshots.length === 0) {
14500
+ log6.info("intelligence.skip", { runId, reason: "no snapshots" });
14501
+ return null;
14502
+ }
14503
+ const previousRunRecord = recentRuns.find((r) => r.id !== runId);
14504
+ const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
14505
+ if (!previousRun) {
14506
+ const result2 = analyzeRuns(currentRun, currentRun);
14507
+ log6.info("intelligence.analyzed", {
14508
+ runId,
14509
+ regressions: 0,
14510
+ gains: 0,
14511
+ citedRate: result2.health.overallCitedRate,
14512
+ insights: 0
14513
+ });
14514
+ this.persistResult({ ...result2, insights: [], regressions: [], gains: [] }, runId, projectId);
14515
+ return result2;
14516
+ }
14517
+ const result = analyzeRuns(currentRun, previousRun);
14518
+ log6.info("intelligence.analyzed", {
14519
+ runId,
14520
+ regressions: result.regressions.length,
14521
+ gains: result.gains.length,
14522
+ citedRate: result.health.overallCitedRate,
14523
+ insights: result.insights.length
14524
+ });
14525
+ this.persistResult(result, runId, projectId);
14526
+ return result;
14527
+ }
14528
+ /**
14529
+ * Analyze a single run given an explicit previous run (or null for first run).
14530
+ * Used by backfill where we control the run ordering.
14531
+ */
14532
+ analyzeRunWithPrevious(runRecord, previousRunRecord) {
14533
+ const currentRun = this.buildRunData(runRecord.id, runRecord.projectId, runRecord.finishedAt ?? runRecord.createdAt);
14534
+ if (currentRun.snapshots.length === 0) {
14535
+ return null;
14536
+ }
14537
+ const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, previousRunRecord.projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
14538
+ if (!previousRun) {
14539
+ const result2 = analyzeRuns(currentRun, currentRun);
14540
+ this.persistResult({ ...result2, insights: [], regressions: [], gains: [] }, runRecord.id, runRecord.projectId);
14541
+ return result2;
14542
+ }
14543
+ const result = analyzeRuns(currentRun, previousRun);
14544
+ this.persistResult(result, runRecord.id, runRecord.projectId);
14545
+ return result;
14546
+ }
14547
+ /**
14548
+ * Backfill intelligence for all completed/partial runs of a project.
14549
+ * Processes runs in chronological order so each run compares against its predecessor.
14550
+ */
14551
+ backfill(projectName, opts, onProgress) {
14552
+ const project = this.db.select().from(projects).where(eq23(projects.name, projectName)).get();
14553
+ if (!project) {
14554
+ throw new Error(`Project "${projectName}" not found`);
14555
+ }
14556
+ const allRuns = this.db.select().from(runs).where(
14557
+ and11(
14558
+ eq23(runs.projectId, project.id),
14559
+ or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
14560
+ )
14561
+ ).orderBy(asc2(runs.finishedAt)).all();
14562
+ let startIdx = 0;
14563
+ let endIdx = allRuns.length;
14564
+ if (opts?.fromRunId) {
14565
+ const idx = allRuns.findIndex((r) => r.id === opts.fromRunId);
14566
+ if (idx === -1) throw new Error(`Run "${opts.fromRunId}" not found in project`);
14567
+ startIdx = idx;
14568
+ }
14569
+ if (opts?.toRunId) {
14570
+ const idx = allRuns.findIndex((r) => r.id === opts.toRunId);
14571
+ if (idx === -1) throw new Error(`Run "${opts.toRunId}" not found in project`);
14572
+ endIdx = idx + 1;
14573
+ }
14574
+ const targetRuns = allRuns.slice(startIdx, endIdx);
14575
+ let processed = 0;
14576
+ let skipped = 0;
14577
+ let totalInsights = 0;
14578
+ for (let i = 0; i < targetRuns.length; i++) {
14579
+ const run = targetRuns[i];
14580
+ const globalIdx = allRuns.indexOf(run);
14581
+ const previousRun = globalIdx > 0 ? allRuns[globalIdx - 1] : null;
14582
+ const result = this.analyzeRunWithPrevious(run, previousRun);
14583
+ if (result) {
14584
+ processed++;
14585
+ totalInsights += result.insights.length;
14586
+ onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: result.insights.length });
14587
+ } else {
14588
+ skipped++;
14589
+ onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: 0 });
14590
+ }
14591
+ }
14592
+ return { processed, skipped, totalInsights };
14593
+ }
14594
+ persistResult(result, runId, projectId) {
14595
+ const previouslyDismissed = /* @__PURE__ */ new Set();
14596
+ const existingInsights = this.db.select({ keyword: insights.keyword, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq23(insights.runId, runId)).all();
14597
+ for (const row of existingInsights) {
14598
+ if (row.dismissed) {
14599
+ previouslyDismissed.add(`${row.keyword}:${row.provider}:${row.type}`);
14600
+ }
14601
+ }
14602
+ this.db.transaction((tx) => {
14603
+ tx.delete(insights).where(eq23(insights.runId, runId)).run();
14604
+ tx.delete(healthSnapshots).where(eq23(healthSnapshots.runId, runId)).run();
14605
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14606
+ for (const insight of result.insights) {
14607
+ const wasDismissed = previouslyDismissed.has(`${insight.keyword}:${insight.provider}:${insight.type}`);
14608
+ tx.insert(insights).values({
14609
+ id: insight.id,
14610
+ projectId,
14611
+ runId,
14612
+ type: insight.type,
14613
+ severity: insight.severity,
14614
+ title: insight.title,
14615
+ keyword: insight.keyword,
14616
+ provider: insight.provider,
14617
+ recommendation: insight.recommendation ? JSON.stringify(insight.recommendation) : null,
14618
+ cause: insight.cause ? JSON.stringify(insight.cause) : null,
14619
+ dismissed: wasDismissed,
14620
+ createdAt: insight.createdAt
14621
+ }).run();
14622
+ }
14623
+ tx.insert(healthSnapshots).values({
14624
+ id: crypto22.randomUUID(),
14625
+ projectId,
14626
+ runId,
14627
+ overallCitedRate: String(result.health.overallCitedRate),
14628
+ totalPairs: result.health.totalPairs,
14629
+ citedPairs: result.health.citedPairs,
14630
+ providerBreakdown: JSON.stringify(result.health.providerBreakdown),
14631
+ createdAt: now
14632
+ }).run();
14633
+ });
14634
+ log6.info("intelligence.persisted", { runId, insights: result.insights.length });
14635
+ }
14636
+ buildRunData(runId, projectId, completedAt) {
14637
+ const rows = this.db.select({
14638
+ keyword: keywords.keyword,
14639
+ provider: querySnapshots.provider,
14640
+ citationState: querySnapshots.citationState,
14641
+ citedDomains: querySnapshots.citedDomains,
14642
+ competitorOverlap: querySnapshots.competitorOverlap
14643
+ }).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, runId)).all();
14644
+ const snapshots = rows.map((r) => {
14645
+ const domains = parseJsonColumn(r.citedDomains, []);
14646
+ const competitors2 = parseJsonColumn(r.competitorOverlap, []);
14647
+ return {
14648
+ keyword: r.keyword ?? "",
14649
+ provider: r.provider,
14650
+ cited: r.citationState === "cited",
14651
+ citationUrl: domains[0] ?? void 0,
14652
+ competitorDomain: competitors2[0] ?? void 0
14653
+ };
14654
+ });
14655
+ return { runId, projectId, completedAt, snapshots };
14656
+ }
14657
+ };
14658
+
14659
+ // src/run-coordinator.ts
14660
+ var log7 = createLogger("RunCoordinator");
14661
+ var RunCoordinator = class {
14662
+ constructor(notifier, intelligenceService) {
14663
+ this.notifier = notifier;
14664
+ this.intelligenceService = intelligenceService;
14665
+ }
14666
+ async onRunCompleted(runId, projectId) {
14667
+ try {
14668
+ this.intelligenceService.analyzeAndPersist(runId, projectId);
14669
+ } catch (err) {
14670
+ log7.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14671
+ }
14672
+ try {
14673
+ await this.notifier.onRunCompleted(runId, projectId);
14674
+ } catch (err) {
14675
+ log7.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14676
+ }
14677
+ }
14678
+ };
14679
+
13823
14680
  // src/snapshot-service.ts
13824
14681
  import { runAeoAudit } from "@ainyc/aeo-audit";
13825
14682
 
@@ -13940,7 +14797,7 @@ function formatAuditFactorScore(factor) {
13940
14797
  }
13941
14798
 
13942
14799
  // src/snapshot-service.ts
13943
- var log6 = createLogger("Snapshot");
14800
+ var log8 = createLogger("Snapshot");
13944
14801
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
13945
14802
  var SNAPSHOT_QUERY_COUNT = 6;
13946
14803
  var ProviderExecutionGate2 = class {
@@ -14083,7 +14940,7 @@ var SnapshotService = class {
14083
14940
  return mapAuditReport(report);
14084
14941
  } catch (err) {
14085
14942
  const message = err instanceof Error ? err.message : String(err);
14086
- log6.warn("audit.failed", { homepageUrl, error: message });
14943
+ log8.warn("audit.failed", { homepageUrl, error: message });
14087
14944
  return {
14088
14945
  url: homepageUrl,
14089
14946
  finalUrl: homepageUrl,
@@ -14113,7 +14970,7 @@ var SnapshotService = class {
14113
14970
  phrases: parsedPhrases
14114
14971
  };
14115
14972
  } catch (err) {
14116
- log6.warn("profile.generation-failed", {
14973
+ log8.warn("profile.generation-failed", {
14117
14974
  domain: ctx.domain,
14118
14975
  provider: ctx.analysisProvider.adapter.name,
14119
14976
  error: err instanceof Error ? err.message : String(err)
@@ -14255,7 +15112,7 @@ var SnapshotService = class {
14255
15112
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
14256
15113
  };
14257
15114
  } catch (err) {
14258
- log6.warn("response.analysis-failed", {
15115
+ log8.warn("response.analysis-failed", {
14259
15116
  provider: ctx.analysisProvider.adapter.name,
14260
15117
  error: err instanceof Error ? err.message : String(err)
14261
15118
  });
@@ -14540,7 +15397,7 @@ function clipText(value, length) {
14540
15397
  // src/server.ts
14541
15398
  var _require2 = createRequire2(import.meta.url);
14542
15399
  var { version: PKG_VERSION } = _require2("../package.json");
14543
- var log7 = createLogger("Server");
15400
+ var log9 = createLogger("Server");
14544
15401
  var DEFAULT_QUOTA = {
14545
15402
  maxConcurrency: 2,
14546
15403
  maxRequestsPerMinute: 10,
@@ -14571,7 +15428,7 @@ function summarizeProviderConfig(provider, config) {
14571
15428
  };
14572
15429
  }
14573
15430
  function hashApiKey(key) {
14574
- return crypto22.createHash("sha256").update(key).digest("hex");
15431
+ return crypto23.createHash("sha256").update(key).digest("hex");
14575
15432
  }
14576
15433
  function parseCookies2(header) {
14577
15434
  if (!header) return {};
@@ -14630,7 +15487,7 @@ async function createServer(opts) {
14630
15487
  quota: opts.config.geminiQuota
14631
15488
  };
14632
15489
  }
14633
- log7.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
15490
+ log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
14634
15491
  const p = providers[k];
14635
15492
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
14636
15493
  }) });
@@ -14666,7 +15523,9 @@ async function createServer(opts) {
14666
15523
  const jobRunner = new JobRunner(opts.db, registry);
14667
15524
  jobRunner.recoverStaleRuns();
14668
15525
  const notifier = new Notifier(opts.db, serverUrl);
14669
- jobRunner.onRunCompleted = (runId, projectId) => notifier.onRunCompleted(runId, projectId);
15526
+ const intelligenceService = new IntelligenceService(opts.db);
15527
+ const runCoordinator = new RunCoordinator(notifier, intelligenceService);
15528
+ jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
14670
15529
  const snapshotService = new SnapshotService(registry);
14671
15530
  const scheduler = new Scheduler(opts.db, {
14672
15531
  onRunCreated: (runId, projectId, providers2) => {
@@ -14743,7 +15602,7 @@ async function createServer(opts) {
14743
15602
  return removed;
14744
15603
  }
14745
15604
  };
14746
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
15605
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
14747
15606
  const googleConnectionStore = {
14748
15607
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
14749
15608
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -14789,11 +15648,11 @@ async function createServer(opts) {
14789
15648
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
14790
15649
  if (opts.config.apiKey) {
14791
15650
  const keyHash = hashApiKey(opts.config.apiKey);
14792
- const existing = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, keyHash)).get();
15651
+ const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
14793
15652
  if (!existing) {
14794
15653
  const prefix = opts.config.apiKey.slice(0, 12);
14795
15654
  opts.db.insert(apiKeys).values({
14796
- id: `key_${crypto22.randomBytes(8).toString("hex")}`,
15655
+ id: `key_${crypto23.randomBytes(8).toString("hex")}`,
14797
15656
  name: "default",
14798
15657
  keyHash,
14799
15658
  keyPrefix: prefix,
@@ -14817,7 +15676,7 @@ async function createServer(opts) {
14817
15676
  };
14818
15677
  const createSession = (apiKeyId) => {
14819
15678
  pruneExpiredSessions();
14820
- const sessionId = crypto22.randomBytes(32).toString("hex");
15679
+ const sessionId = crypto23.randomBytes(32).toString("hex");
14821
15680
  sessions.set(sessionId, {
14822
15681
  apiKeyId,
14823
15682
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -14841,7 +15700,7 @@ async function createServer(opts) {
14841
15700
  };
14842
15701
  const getDefaultApiKey = () => {
14843
15702
  if (!opts.config.apiKey) return void 0;
14844
- return opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
15703
+ return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
14845
15704
  };
14846
15705
  const createPasswordSession = (reply) => {
14847
15706
  const key = getDefaultApiKey();
@@ -14898,12 +15757,12 @@ async function createServer(opts) {
14898
15757
  return reply.send({ authenticated: true });
14899
15758
  }
14900
15759
  if (apiKey) {
14901
- const key = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(apiKey))).get();
15760
+ const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
14902
15761
  if (!key || key.revokedAt) {
14903
15762
  const err2 = authInvalid();
14904
15763
  return reply.status(err2.statusCode).send(err2.toJSON());
14905
15764
  }
14906
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(apiKeys.id, key.id)).run();
15765
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
14907
15766
  const sessionId = createSession(key.id);
14908
15767
  reply.header("set-cookie", serializeSessionCookie({
14909
15768
  name: SESSION_COOKIE_NAME,
@@ -15048,7 +15907,7 @@ async function createServer(opts) {
15048
15907
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
15049
15908
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
15050
15909
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
15051
- id: crypto22.randomUUID(),
15910
+ id: crypto23.randomUUID(),
15052
15911
  projectId,
15053
15912
  actor: "api",
15054
15913
  action: existing ? "provider.updated" : "provider.created",
@@ -15314,6 +16173,7 @@ export {
15314
16173
  notificationEventSchema,
15315
16174
  effectiveDomains,
15316
16175
  determineAnswerMentioned,
16176
+ IntelligenceService,
15317
16177
  setGoogleAuthConfig,
15318
16178
  formatAuditFactorScore,
15319
16179
  createServer