@ainyc/canonry 1.37.1 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,
@@ -1528,6 +1530,39 @@ var usageCounters = sqliteTable("usage_counters", {
1528
1530
  uniqueIndex("idx_usage_scope_period_metric").on(table.scope, table.period, table.metric),
1529
1531
  index("idx_usage_scope_period").on(table.scope, table.period)
1530
1532
  ]);
1533
+ var insights = sqliteTable("insights", {
1534
+ id: text("id").primaryKey(),
1535
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1536
+ runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
1537
+ type: text("type").notNull(),
1538
+ severity: text("severity").notNull(),
1539
+ title: text("title").notNull(),
1540
+ keyword: text("keyword").notNull(),
1541
+ provider: text("provider").notNull(),
1542
+ recommendation: text("recommendation"),
1543
+ cause: text("cause"),
1544
+ dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false),
1545
+ createdAt: text("created_at").notNull()
1546
+ }, (table) => [
1547
+ index("idx_insights_project").on(table.projectId),
1548
+ index("idx_insights_run").on(table.runId),
1549
+ index("idx_insights_created").on(table.createdAt),
1550
+ index("idx_insights_keyword_provider").on(table.keyword, table.provider)
1551
+ ]);
1552
+ var healthSnapshots = sqliteTable("health_snapshots", {
1553
+ id: text("id").primaryKey(),
1554
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1555
+ runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
1556
+ overallCitedRate: text("overall_cited_rate").notNull(),
1557
+ totalPairs: integer("total_pairs").notNull(),
1558
+ citedPairs: integer("cited_pairs").notNull(),
1559
+ providerBreakdown: text("provider_breakdown").notNull().default("{}"),
1560
+ createdAt: text("created_at").notNull()
1561
+ }, (table) => [
1562
+ index("idx_health_snapshots_project").on(table.projectId),
1563
+ index("idx_health_snapshots_run").on(table.runId),
1564
+ index("idx_health_snapshots_created").on(table.createdAt)
1565
+ ]);
1531
1566
 
1532
1567
  // ../db/src/client.ts
1533
1568
  function createClient(databasePath) {
@@ -1692,6 +1727,7 @@ var MIGRATIONS = [
1692
1727
  // v5b: Backfill model from rawResponse JSON for existing snapshots
1693
1728
  `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
1729
  // v6: Google Search Console integration — google_connections table (domain-scoped)
1730
+ // WARNING: access_token, refresh_token are authentication material; consider storing in config.yaml per CLAUDE.md
1695
1731
  `CREATE TABLE IF NOT EXISTS google_connections (
1696
1732
  id TEXT PRIMARY KEY,
1697
1733
  domain TEXT NOT NULL,
@@ -1807,6 +1843,7 @@ var MIGRATIONS = [
1807
1843
  `CREATE INDEX IF NOT EXISTS idx_bing_keyword_project ON bing_keyword_stats(project_id)`,
1808
1844
  `CREATE INDEX IF NOT EXISTS idx_bing_keyword_query ON bing_keyword_stats(query)`,
1809
1845
  // v13: Google Analytics 4 — ga_connections table (service account auth)
1846
+ // WARNING: private_key is authentication material; consider storing in config.yaml per CLAUDE.md
1810
1847
  `CREATE TABLE IF NOT EXISTS ga_connections (
1811
1848
  id TEXT PRIMARY KEY,
1812
1849
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
@@ -1876,8 +1913,52 @@ var MIGRATIONS = [
1876
1913
  `ALTER TABLE ga_ai_referrals ADD COLUMN source_dimension TEXT NOT NULL DEFAULT 'session'`,
1877
1914
  // Replace old unique index with one that includes source_dimension
1878
1915
  `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)`
1916
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`,
1917
+ // v21: Add missing indexes for query_snapshots filtering
1918
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_citation_state ON query_snapshots(citation_state)`,
1919
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_provider_model ON query_snapshots(provider, model)`,
1920
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_location ON query_snapshots(location)`,
1921
+ // v22: Intelligence — insights table for regression/gain/opportunity tracking
1922
+ `CREATE TABLE IF NOT EXISTS insights (
1923
+ id TEXT PRIMARY KEY,
1924
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1925
+ type TEXT NOT NULL,
1926
+ severity TEXT NOT NULL,
1927
+ title TEXT NOT NULL,
1928
+ keyword TEXT NOT NULL,
1929
+ provider TEXT NOT NULL,
1930
+ recommendation TEXT,
1931
+ cause TEXT,
1932
+ dismissed INTEGER NOT NULL DEFAULT 0,
1933
+ created_at TEXT NOT NULL
1934
+ )`,
1935
+ `CREATE INDEX IF NOT EXISTS idx_insights_project ON insights(project_id)`,
1936
+ `CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at)`,
1937
+ `CREATE INDEX IF NOT EXISTS idx_insights_keyword_provider ON insights(keyword, provider)`,
1938
+ // v23: Intelligence — health_snapshots table for citation health over time
1939
+ `CREATE TABLE IF NOT EXISTS health_snapshots (
1940
+ id TEXT PRIMARY KEY,
1941
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1942
+ overall_cited_rate TEXT NOT NULL,
1943
+ total_pairs INTEGER NOT NULL,
1944
+ cited_pairs INTEGER NOT NULL,
1945
+ provider_breakdown TEXT NOT NULL DEFAULT '{}',
1946
+ created_at TEXT NOT NULL
1947
+ )`,
1948
+ `CREATE INDEX IF NOT EXISTS idx_health_snapshots_project ON health_snapshots(project_id)`,
1949
+ `CREATE INDEX IF NOT EXISTS idx_health_snapshots_created ON health_snapshots(created_at)`,
1950
+ // v24: Intelligence — add run_id to insights and health_snapshots for per-run correlation and idempotency
1951
+ `ALTER TABLE insights ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
1952
+ `CREATE INDEX IF NOT EXISTS idx_insights_run ON insights(run_id)`,
1953
+ `ALTER TABLE health_snapshots ADD COLUMN run_id TEXT REFERENCES runs(id) ON DELETE CASCADE`,
1954
+ `CREATE INDEX IF NOT EXISTS idx_health_snapshots_run ON health_snapshots(run_id)`
1880
1955
  ];
1956
+ function isDuplicateColumnError(err) {
1957
+ if (!(err instanceof Error)) return false;
1958
+ if (err.message.includes("duplicate column name")) return true;
1959
+ if (err.cause instanceof Error && err.cause.message.includes("duplicate column name")) return true;
1960
+ return false;
1961
+ }
1881
1962
  function migrate(db) {
1882
1963
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
1883
1964
  for (const statement of statements) {
@@ -1886,7 +1967,9 @@ function migrate(db) {
1886
1967
  for (const migration of MIGRATIONS) {
1887
1968
  try {
1888
1969
  db.run(sql.raw(migration));
1889
- } catch {
1970
+ } catch (err) {
1971
+ if (isDuplicateColumnError(err)) continue;
1972
+ throw err;
1890
1973
  }
1891
1974
  }
1892
1975
  }
@@ -3805,6 +3888,81 @@ function buildCategoryCounts(counts) {
3805
3888
  return result;
3806
3889
  }
3807
3890
 
3891
+ // ../api-routes/src/intelligence.ts
3892
+ import { eq as eq11, desc as desc4, and as and2 } from "drizzle-orm";
3893
+ function mapInsightRow(r) {
3894
+ return {
3895
+ id: r.id,
3896
+ projectId: r.projectId,
3897
+ runId: r.runId ?? null,
3898
+ type: r.type,
3899
+ severity: r.severity,
3900
+ title: r.title,
3901
+ keyword: r.keyword,
3902
+ provider: r.provider,
3903
+ recommendation: parseJsonColumn(r.recommendation, void 0),
3904
+ cause: parseJsonColumn(r.cause, void 0),
3905
+ dismissed: r.dismissed,
3906
+ createdAt: r.createdAt
3907
+ };
3908
+ }
3909
+ function mapHealthRow(r) {
3910
+ return {
3911
+ id: r.id,
3912
+ projectId: r.projectId,
3913
+ runId: r.runId ?? null,
3914
+ overallCitedRate: Number(r.overallCitedRate),
3915
+ totalPairs: r.totalPairs,
3916
+ citedPairs: r.citedPairs,
3917
+ providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
3918
+ createdAt: r.createdAt
3919
+ };
3920
+ }
3921
+ async function intelligenceRoutes(app) {
3922
+ app.get("/projects/:name/insights", async (request, reply) => {
3923
+ const project = resolveProject(app.db, request.params.name);
3924
+ const conditions = [eq11(insights.projectId, project.id)];
3925
+ if (request.query.runId) {
3926
+ conditions.push(eq11(insights.runId, request.query.runId));
3927
+ }
3928
+ const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and2(...conditions)).orderBy(desc4(insights.createdAt)).all();
3929
+ const showDismissed = request.query.dismissed === "true";
3930
+ const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
3931
+ return reply.send(result);
3932
+ });
3933
+ app.get("/projects/:name/insights/:id", async (request, reply) => {
3934
+ const project = resolveProject(app.db, request.params.name);
3935
+ const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
3936
+ if (!row || row.projectId !== project.id) {
3937
+ throw notFound("Insight", request.params.id);
3938
+ }
3939
+ return reply.send(mapInsightRow(row));
3940
+ });
3941
+ app.post("/projects/:name/insights/:id/dismiss", async (request, reply) => {
3942
+ const project = resolveProject(app.db, request.params.name);
3943
+ const row = app.db.select().from(insights).where(eq11(insights.id, request.params.id)).get();
3944
+ if (!row || row.projectId !== project.id) {
3945
+ throw notFound("Insight", request.params.id);
3946
+ }
3947
+ app.db.update(insights).set({ dismissed: true }).where(eq11(insights.id, request.params.id)).run();
3948
+ return reply.send({ ok: true });
3949
+ });
3950
+ app.get("/projects/:name/health/latest", async (request, reply) => {
3951
+ const project = resolveProject(app.db, request.params.name);
3952
+ const row = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
3953
+ if (!row) {
3954
+ throw notFound("Health data for project", request.params.name);
3955
+ }
3956
+ return reply.send(mapHealthRow(row));
3957
+ });
3958
+ app.get("/projects/:name/health/history", async (request, reply) => {
3959
+ const project = resolveProject(app.db, request.params.name);
3960
+ const limit = request.query.limit ? Math.min(Number(request.query.limit), 100) : 30;
3961
+ const rows = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(limit).all();
3962
+ return reply.send(rows.map(mapHealthRow));
3963
+ });
3964
+ }
3965
+
3808
3966
  // ../api-routes/src/openapi.ts
3809
3967
  var stringSchema = { type: "string" };
3810
3968
  var booleanSchema = { type: "boolean" };
@@ -5826,6 +5984,75 @@ var routeCatalog = [
5826
5984
  400: { description: "GA4 is not connected." },
5827
5985
  404: { description: "Project not found." }
5828
5986
  }
5987
+ },
5988
+ // Intelligence
5989
+ {
5990
+ method: "get",
5991
+ path: "/api/v1/projects/{name}/insights",
5992
+ summary: "List intelligence insights for a project",
5993
+ tags: ["intelligence"],
5994
+ parameters: [
5995
+ nameParameter,
5996
+ { name: "dismissed", in: "query", description: "Include dismissed insights (true/false).", schema: stringSchema },
5997
+ { name: "runId", in: "query", description: "Filter by run ID.", schema: stringSchema }
5998
+ ],
5999
+ responses: {
6000
+ 200: { description: "Insights returned." },
6001
+ 404: { description: "Project not found." }
6002
+ }
6003
+ },
6004
+ {
6005
+ method: "get",
6006
+ path: "/api/v1/projects/{name}/insights/{id}",
6007
+ summary: "Get a single insight",
6008
+ tags: ["intelligence"],
6009
+ parameters: [
6010
+ nameParameter,
6011
+ { name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
6012
+ ],
6013
+ responses: {
6014
+ 200: { description: "Insight returned." },
6015
+ 404: { description: "Insight not found." }
6016
+ }
6017
+ },
6018
+ {
6019
+ method: "post",
6020
+ path: "/api/v1/projects/{name}/insights/{id}/dismiss",
6021
+ summary: "Dismiss an insight",
6022
+ tags: ["intelligence"],
6023
+ parameters: [
6024
+ nameParameter,
6025
+ { name: "id", in: "path", required: true, description: "Insight ID.", schema: stringSchema }
6026
+ ],
6027
+ responses: {
6028
+ 200: { description: "Insight dismissed." },
6029
+ 404: { description: "Insight not found." }
6030
+ }
6031
+ },
6032
+ {
6033
+ method: "get",
6034
+ path: "/api/v1/projects/{name}/health/latest",
6035
+ summary: "Get latest health snapshot",
6036
+ tags: ["intelligence"],
6037
+ parameters: [nameParameter],
6038
+ responses: {
6039
+ 200: { description: "Health snapshot returned." },
6040
+ 404: { description: "Project not found." }
6041
+ }
6042
+ },
6043
+ {
6044
+ method: "get",
6045
+ path: "/api/v1/projects/{name}/health/history",
6046
+ summary: "Get health trend over time",
6047
+ tags: ["intelligence"],
6048
+ parameters: [
6049
+ nameParameter,
6050
+ { name: "limit", in: "query", description: "Max results.", schema: stringSchema }
6051
+ ],
6052
+ responses: {
6053
+ 200: { description: "Health history returned." },
6054
+ 404: { description: "Project not found." }
6055
+ }
5829
6056
  }
5830
6057
  ];
5831
6058
  function buildOpenApiDocument(info = {}) {
@@ -6072,7 +6299,7 @@ async function telemetryRoutes(app, opts) {
6072
6299
 
6073
6300
  // ../api-routes/src/schedules.ts
6074
6301
  import crypto11 from "crypto";
6075
- import { eq as eq11 } from "drizzle-orm";
6302
+ import { eq as eq12 } from "drizzle-orm";
6076
6303
  async function scheduleRoutes(app, opts) {
6077
6304
  app.put("/projects/:name/schedule", async (request, reply) => {
6078
6305
  const project = resolveProject(app.db, request.params.name);
@@ -6115,7 +6342,7 @@ async function scheduleRoutes(app, opts) {
6115
6342
  }
6116
6343
  const now = (/* @__PURE__ */ new Date()).toISOString();
6117
6344
  const enabledInt = enabled === false ? 0 : 1;
6118
- const existing = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6345
+ const existing = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6119
6346
  if (existing) {
6120
6347
  app.db.update(schedules).set({
6121
6348
  cronExpr,
@@ -6124,7 +6351,7 @@ async function scheduleRoutes(app, opts) {
6124
6351
  providers: JSON.stringify(providers),
6125
6352
  enabled: enabledInt,
6126
6353
  updatedAt: now
6127
- }).where(eq11(schedules.id, existing.id)).run();
6354
+ }).where(eq12(schedules.id, existing.id)).run();
6128
6355
  } else {
6129
6356
  app.db.insert(schedules).values({
6130
6357
  id: crypto11.randomUUID(),
@@ -6146,12 +6373,12 @@ async function scheduleRoutes(app, opts) {
6146
6373
  diff: { cronExpr, preset, timezone, providers }
6147
6374
  });
6148
6375
  opts.onScheduleUpdated?.("upsert", project.id);
6149
- const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6376
+ const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6150
6377
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
6151
6378
  });
6152
6379
  app.get("/projects/:name/schedule", async (request, reply) => {
6153
6380
  const project = resolveProject(app.db, request.params.name);
6154
- const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6381
+ const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6155
6382
  if (!schedule) {
6156
6383
  throw notFound("Schedule", request.params.name);
6157
6384
  }
@@ -6159,11 +6386,11 @@ async function scheduleRoutes(app, opts) {
6159
6386
  });
6160
6387
  app.delete("/projects/:name/schedule", async (request, reply) => {
6161
6388
  const project = resolveProject(app.db, request.params.name);
6162
- const schedule = app.db.select().from(schedules).where(eq11(schedules.projectId, project.id)).get();
6389
+ const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
6163
6390
  if (!schedule) {
6164
6391
  throw notFound("Schedule", request.params.name);
6165
6392
  }
6166
- app.db.delete(schedules).where(eq11(schedules.id, schedule.id)).run();
6393
+ app.db.delete(schedules).where(eq12(schedules.id, schedule.id)).run();
6167
6394
  writeAuditLog(app.db, {
6168
6395
  projectId: project.id,
6169
6396
  actor: "api",
@@ -6193,7 +6420,7 @@ function formatSchedule(row) {
6193
6420
 
6194
6421
  // ../api-routes/src/notifications.ts
6195
6422
  import crypto12 from "crypto";
6196
- import { eq as eq12 } from "drizzle-orm";
6423
+ import { eq as eq13 } from "drizzle-orm";
6197
6424
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed"];
6198
6425
  async function notificationRoutes(app) {
6199
6426
  app.get("/notifications/events", async (_request, reply) => {
@@ -6232,22 +6459,22 @@ async function notificationRoutes(app) {
6232
6459
  diff: { channel, ...redactNotificationUrl(url), events }
6233
6460
  });
6234
6461
  return reply.status(201).send({
6235
- ...formatNotification(app.db.select().from(notifications).where(eq12(notifications.id, id)).get()),
6462
+ ...formatNotification(app.db.select().from(notifications).where(eq13(notifications.id, id)).get()),
6236
6463
  webhookSecret
6237
6464
  });
6238
6465
  });
6239
6466
  app.get("/projects/:name/notifications", async (request, reply) => {
6240
6467
  const project = resolveProject(app.db, request.params.name);
6241
- const rows = app.db.select().from(notifications).where(eq12(notifications.projectId, project.id)).all();
6468
+ const rows = app.db.select().from(notifications).where(eq13(notifications.projectId, project.id)).all();
6242
6469
  return reply.send(rows.map(formatNotification));
6243
6470
  });
6244
6471
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
6245
6472
  const project = resolveProject(app.db, request.params.name);
6246
- const notification = app.db.select().from(notifications).where(eq12(notifications.id, request.params.id)).get();
6473
+ const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
6247
6474
  if (!notification || notification.projectId !== project.id) {
6248
6475
  throw notFound("Notification", request.params.id);
6249
6476
  }
6250
- app.db.delete(notifications).where(eq12(notifications.id, notification.id)).run();
6477
+ app.db.delete(notifications).where(eq13(notifications.id, notification.id)).run();
6251
6478
  writeAuditLog(app.db, {
6252
6479
  projectId: project.id,
6253
6480
  actor: "api",
@@ -6259,7 +6486,7 @@ async function notificationRoutes(app) {
6259
6486
  });
6260
6487
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
6261
6488
  const project = resolveProject(app.db, request.params.name);
6262
- const notification = app.db.select().from(notifications).where(eq12(notifications.id, request.params.id)).get();
6489
+ const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
6263
6490
  if (!notification || notification.projectId !== project.id) {
6264
6491
  throw notFound("Notification", request.params.id);
6265
6492
  }
@@ -6311,7 +6538,7 @@ function formatNotification(row) {
6311
6538
 
6312
6539
  // ../api-routes/src/google.ts
6313
6540
  import crypto14 from "crypto";
6314
- import { eq as eq13, and as and2, desc as desc4, sql as sql3 } from "drizzle-orm";
6541
+ import { eq as eq14, and as and3, desc as desc5, sql as sql3 } from "drizzle-orm";
6315
6542
 
6316
6543
  // ../integration-google/src/constants.ts
6317
6544
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -6343,7 +6570,56 @@ var GoogleApiError = class extends Error {
6343
6570
  };
6344
6571
 
6345
6572
  // ../integration-google/src/oauth.ts
6573
+ function validateClientId(clientId) {
6574
+ if (!clientId || typeof clientId !== "string" || clientId.trim().length === 0) {
6575
+ throw new GoogleAuthError("Client ID is required and must be a non-empty string");
6576
+ }
6577
+ }
6578
+ function validateClientSecret(clientSecret) {
6579
+ if (!clientSecret || typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
6580
+ throw new GoogleAuthError("Client secret is required and must be a non-empty string");
6581
+ }
6582
+ }
6583
+ function validateRedirectUri(redirectUri) {
6584
+ if (!redirectUri || typeof redirectUri !== "string" || redirectUri.trim().length === 0) {
6585
+ throw new GoogleAuthError("Redirect URI is required and must be a non-empty string");
6586
+ }
6587
+ try {
6588
+ const url = new URL(redirectUri);
6589
+ if (!url.protocol.startsWith("http")) {
6590
+ throw new GoogleAuthError("Redirect URI must be an HTTP or HTTPS URL");
6591
+ }
6592
+ } catch {
6593
+ throw new GoogleAuthError("Redirect URI must be a valid URL");
6594
+ }
6595
+ }
6596
+ function validateCode(code) {
6597
+ if (!code || typeof code !== "string" || code.trim().length === 0) {
6598
+ throw new GoogleAuthError("Authorization code is required and must be a non-empty string");
6599
+ }
6600
+ }
6601
+ function validateScopes(scopes) {
6602
+ if (!Array.isArray(scopes) || scopes.length === 0) {
6603
+ throw new GoogleAuthError("At least one scope is required");
6604
+ }
6605
+ for (const scope of scopes) {
6606
+ if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
6607
+ throw new GoogleAuthError("Scope must be a non-empty string");
6608
+ }
6609
+ }
6610
+ }
6611
+ function validateRefreshToken(refreshToken) {
6612
+ if (!refreshToken || typeof refreshToken !== "string" || refreshToken.trim().length === 0) {
6613
+ throw new GoogleAuthError("Refresh token is required and must be a non-empty string");
6614
+ }
6615
+ }
6346
6616
  function getAuthUrl(clientId, redirectUri, scopes, state) {
6617
+ validateClientId(clientId);
6618
+ validateRedirectUri(redirectUri);
6619
+ validateScopes(scopes);
6620
+ if (state && (typeof state !== "string" || state.trim().length === 0)) {
6621
+ throw new GoogleAuthError("State must be a non-empty string if provided");
6622
+ }
6347
6623
  const params = new URLSearchParams({
6348
6624
  client_id: clientId,
6349
6625
  redirect_uri: redirectUri,
@@ -6356,6 +6632,10 @@ function getAuthUrl(clientId, redirectUri, scopes, state) {
6356
6632
  return `${GOOGLE_AUTH_URL}?${params.toString()}`;
6357
6633
  }
6358
6634
  async function exchangeCode(clientId, clientSecret, code, redirectUri) {
6635
+ validateClientId(clientId);
6636
+ validateClientSecret(clientSecret);
6637
+ validateCode(code);
6638
+ validateRedirectUri(redirectUri);
6359
6639
  const res = await fetch(GOOGLE_TOKEN_URL, {
6360
6640
  method: "POST",
6361
6641
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -6375,6 +6655,9 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
6375
6655
  return await res.json();
6376
6656
  }
6377
6657
  async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
6658
+ validateClientId(clientId);
6659
+ validateClientSecret(clientSecret);
6660
+ validateRefreshToken(currentRefreshToken);
6378
6661
  const res = await fetch(GOOGLE_TOKEN_URL, {
6379
6662
  method: "POST",
6380
6663
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -6394,6 +6677,47 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
6394
6677
  }
6395
6678
 
6396
6679
  // ../integration-google/src/gsc-client.ts
6680
+ function validateAccessToken(accessToken) {
6681
+ if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
6682
+ throw new GoogleApiError("Access token is required and must be a non-empty string", 400);
6683
+ }
6684
+ }
6685
+ function validateSiteUrl(siteUrl) {
6686
+ if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
6687
+ throw new GoogleApiError("Site URL is required and must be a non-empty string", 400);
6688
+ }
6689
+ if (siteUrl.startsWith("sc-domain:")) {
6690
+ const domain = siteUrl.slice("sc-domain:".length);
6691
+ if (!domain) {
6692
+ throw new GoogleApiError("Site URL sc-domain must include a domain", 400);
6693
+ }
6694
+ if (!domain.includes(".")) {
6695
+ throw new GoogleApiError("Site URL sc-domain must be a valid domain", 400);
6696
+ }
6697
+ } else {
6698
+ try {
6699
+ const url = new URL(siteUrl);
6700
+ if (!url.protocol.startsWith("http")) {
6701
+ throw new GoogleApiError("Site URL must be an HTTP or HTTPS URL", 400);
6702
+ }
6703
+ } catch {
6704
+ throw new GoogleApiError("Site URL must be a valid URL", 400);
6705
+ }
6706
+ }
6707
+ }
6708
+ function validateUrl(urlParam) {
6709
+ if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
6710
+ throw new GoogleApiError("URL is required and must be a non-empty string", 400);
6711
+ }
6712
+ try {
6713
+ const url = new URL(urlParam);
6714
+ if (!url.protocol.startsWith("http")) {
6715
+ throw new GoogleApiError("URL must be an HTTP or HTTPS URL", 400);
6716
+ }
6717
+ } catch {
6718
+ throw new GoogleApiError("URL must be a valid URL", 400);
6719
+ }
6720
+ }
6397
6721
  function gscClientLog(level, action, ctx) {
6398
6722
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GscClient", action, ...ctx };
6399
6723
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -6429,6 +6753,7 @@ async function gscFetch(accessToken, url, opts) {
6429
6753
  return await res.json();
6430
6754
  }
6431
6755
  async function listSites(accessToken) {
6756
+ validateAccessToken(accessToken);
6432
6757
  const data = await gscFetch(
6433
6758
  accessToken,
6434
6759
  `${GSC_API_BASE}/sites`
@@ -6436,6 +6761,8 @@ async function listSites(accessToken) {
6436
6761
  return data.siteEntry ?? [];
6437
6762
  }
6438
6763
  async function listSitemaps(accessToken, siteUrl) {
6764
+ validateAccessToken(accessToken);
6765
+ validateSiteUrl(siteUrl);
6439
6766
  const encodedSiteUrl = encodeURIComponent(siteUrl);
6440
6767
  const data = await gscFetch(
6441
6768
  accessToken,
@@ -6444,6 +6771,8 @@ async function listSitemaps(accessToken, siteUrl) {
6444
6771
  return data.sitemap ?? [];
6445
6772
  }
6446
6773
  async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
6774
+ validateAccessToken(accessToken);
6775
+ validateSiteUrl(siteUrl);
6447
6776
  const allRows = [];
6448
6777
  let startRow = 0;
6449
6778
  const dimensions = opts.dimensions ?? ["query", "page", "country", "device", "date"];
@@ -6479,6 +6808,8 @@ async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
6479
6808
  return allRows;
6480
6809
  }
6481
6810
  async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
6811
+ validateAccessToken(accessToken);
6812
+ validateUrl(url);
6482
6813
  return gscFetch(
6483
6814
  accessToken,
6484
6815
  `${INDEXING_API_BASE}/urlNotifications:publish`,
@@ -6489,6 +6820,9 @@ async function publishUrlNotification(accessToken, url, type = "URL_UPDATED") {
6489
6820
  );
6490
6821
  }
6491
6822
  async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
6823
+ validateAccessToken(accessToken);
6824
+ validateUrl(inspectionUrl);
6825
+ validateSiteUrl(siteUrl);
6492
6826
  return gscFetch(
6493
6827
  accessToken,
6494
6828
  URL_INSPECTION_API,
@@ -6524,12 +6858,46 @@ var GA4ApiError = class extends Error {
6524
6858
  };
6525
6859
 
6526
6860
  // ../integration-google-analytics/src/ga4-client.ts
6861
+ function validateClientEmail(clientEmail) {
6862
+ if (!clientEmail || typeof clientEmail !== "string" || clientEmail.trim().length === 0) {
6863
+ throw new GA4ApiError("Client email is required and must be a non-empty string", 400);
6864
+ }
6865
+ if (!clientEmail.includes("@")) {
6866
+ throw new GA4ApiError("Client email must be a valid email address", 400);
6867
+ }
6868
+ }
6869
+ function validatePrivateKey(privateKey) {
6870
+ if (!privateKey || typeof privateKey !== "string" || privateKey.trim().length === 0) {
6871
+ throw new GA4ApiError("Private key is required and must be a non-empty string", 400);
6872
+ }
6873
+ }
6874
+ function validatePropertyId(propertyId) {
6875
+ if (!propertyId || typeof propertyId !== "string" || propertyId.trim().length === 0) {
6876
+ throw new GA4ApiError("Property ID is required and must be a non-empty string", 400);
6877
+ }
6878
+ if (!/^\d+$/.test(propertyId)) {
6879
+ throw new GA4ApiError("Property ID must be a numeric string", 400);
6880
+ }
6881
+ }
6882
+ function validateAccessToken2(accessToken) {
6883
+ if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) {
6884
+ throw new GA4ApiError("Access token is required and must be a non-empty string", 400);
6885
+ }
6886
+ }
6887
+ function validateScope(scope) {
6888
+ if (!scope || typeof scope !== "string" || scope.trim().length === 0) {
6889
+ throw new GA4ApiError("Scope is required and must be a non-empty string", 400);
6890
+ }
6891
+ }
6527
6892
  function ga4Log(level, action, ctx) {
6528
6893
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Client", action, ...ctx };
6529
6894
  const stream = level === "error" ? process.stderr : process.stdout;
6530
6895
  stream.write(JSON.stringify(entry) + "\n");
6531
6896
  }
6532
6897
  function createServiceAccountJwt(clientEmail, privateKey, scope) {
6898
+ validateClientEmail(clientEmail);
6899
+ validatePrivateKey(privateKey);
6900
+ validateScope(scope);
6533
6901
  const now = Math.floor(Date.now() / 1e3);
6534
6902
  const header = { alg: "RS256", typ: "JWT" };
6535
6903
  const payload = {
@@ -6656,6 +7024,8 @@ var AI_REFERRAL_SOURCE_FILTERS = [
6656
7024
  { matchType: "CONTAINS", value: "meta.ai" }
6657
7025
  ];
6658
7026
  async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
7027
+ validateAccessToken2(accessToken);
7028
+ validatePropertyId(propertyId);
6659
7029
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
6660
7030
  const endDate = /* @__PURE__ */ new Date();
6661
7031
  const startDate = /* @__PURE__ */ new Date();
@@ -6730,10 +7100,15 @@ async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
6730
7100
  return rows;
6731
7101
  }
6732
7102
  async function verifyConnection(clientEmail, privateKey, propertyId) {
7103
+ validateClientEmail(clientEmail);
7104
+ validatePrivateKey(privateKey);
7105
+ validatePropertyId(propertyId);
6733
7106
  const accessToken = await getAccessToken(clientEmail, privateKey);
6734
7107
  return verifyConnectionWithToken(accessToken, propertyId);
6735
7108
  }
6736
7109
  async function verifyConnectionWithToken(accessToken, propertyId) {
7110
+ validateAccessToken2(accessToken);
7111
+ validatePropertyId(propertyId);
6737
7112
  const endDate = /* @__PURE__ */ new Date();
6738
7113
  const startDate = /* @__PURE__ */ new Date();
6739
7114
  startDate.setDate(startDate.getDate() - 1);
@@ -6746,6 +7121,8 @@ async function verifyConnectionWithToken(accessToken, propertyId) {
6746
7121
  return true;
6747
7122
  }
6748
7123
  async function fetchAggregateSummary(accessToken, propertyId, days) {
7124
+ validateAccessToken2(accessToken);
7125
+ validatePropertyId(propertyId);
6749
7126
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
6750
7127
  const endDate = /* @__PURE__ */ new Date();
6751
7128
  const startDate = /* @__PURE__ */ new Date();
@@ -6785,6 +7162,8 @@ async function fetchAggregateSummary(accessToken, propertyId, days) {
6785
7162
  return summary;
6786
7163
  }
6787
7164
  async function fetchAiReferrals(accessToken, propertyId, days) {
7165
+ validateAccessToken2(accessToken);
7166
+ validatePropertyId(propertyId);
6788
7167
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
6789
7168
  const endDate = /* @__PURE__ */ new Date();
6790
7169
  const startDate = /* @__PURE__ */ new Date();
@@ -7116,18 +7495,18 @@ async function googleRoutes(app, opts) {
7116
7495
  if (opts.onGscSyncRequested) {
7117
7496
  opts.onGscSyncRequested(runId, project.id, { days, full });
7118
7497
  }
7119
- const run = app.db.select().from(runs).where(eq13(runs.id, runId)).get();
7498
+ const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7120
7499
  return run;
7121
7500
  });
7122
7501
  app.get("/projects/:name/google/gsc/performance", async (request) => {
7123
7502
  const project = resolveProject(app.db, request.params.name);
7124
7503
  const { startDate, endDate, query, page, limit } = request.query;
7125
- const conditions = [eq13(gscSearchData.projectId, project.id)];
7504
+ const conditions = [eq14(gscSearchData.projectId, project.id)];
7126
7505
  if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
7127
7506
  if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
7128
7507
  if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
7129
7508
  if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
7130
- const rows = app.db.select().from(gscSearchData).where(and2(...conditions)).orderBy(desc4(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
7509
+ const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc5(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
7131
7510
  return rows.map((r) => ({
7132
7511
  date: r.date,
7133
7512
  query: r.query,
@@ -7203,9 +7582,9 @@ async function googleRoutes(app, opts) {
7203
7582
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
7204
7583
  const project = resolveProject(app.db, request.params.name);
7205
7584
  const { url, limit } = request.query;
7206
- const conditions = [eq13(gscUrlInspections.projectId, project.id)];
7207
- if (url) conditions.push(eq13(gscUrlInspections.url, url));
7208
- const rows = app.db.select().from(gscUrlInspections).where(and2(...conditions)).orderBy(desc4(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7585
+ const conditions = [eq14(gscUrlInspections.projectId, project.id)];
7586
+ if (url) conditions.push(eq14(gscUrlInspections.url, url));
7587
+ const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc5(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7209
7588
  return rows.map((r) => ({
7210
7589
  id: r.id,
7211
7590
  url: r.url,
@@ -7224,7 +7603,7 @@ async function googleRoutes(app, opts) {
7224
7603
  });
7225
7604
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
7226
7605
  const project = resolveProject(app.db, request.params.name);
7227
- const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
7606
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7228
7607
  const byUrl = /* @__PURE__ */ new Map();
7229
7608
  for (const row of allInspections) {
7230
7609
  const existing = byUrl.get(row.url);
@@ -7252,7 +7631,7 @@ async function googleRoutes(app, opts) {
7252
7631
  });
7253
7632
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
7254
7633
  const project = resolveProject(app.db, request.params.name);
7255
- const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
7634
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7256
7635
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
7257
7636
  const latestByUrl = /* @__PURE__ */ new Map();
7258
7637
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -7349,7 +7728,7 @@ async function googleRoutes(app, opts) {
7349
7728
  const project = resolveProject(app.db, request.params.name);
7350
7729
  const parsed = parseInt(request.query.limit ?? "90", 10);
7351
7730
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7352
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(desc4(gscCoverageSnapshots.date)).limit(limit).all();
7731
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq14(gscCoverageSnapshots.projectId, project.id)).orderBy(desc5(gscCoverageSnapshots.date)).limit(limit).all();
7353
7732
  return rows.map((r) => ({
7354
7733
  date: r.date,
7355
7734
  indexed: r.indexed,
@@ -7417,7 +7796,7 @@ async function googleRoutes(app, opts) {
7417
7796
  if (opts.onInspectSitemapRequested) {
7418
7797
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
7419
7798
  }
7420
- const run = app.db.select().from(runs).where(eq13(runs.id, runId)).get();
7799
+ const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7421
7800
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7422
7801
  });
7423
7802
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
@@ -7447,7 +7826,7 @@ async function googleRoutes(app, opts) {
7447
7826
  if (opts.onInspectSitemapRequested) {
7448
7827
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
7449
7828
  }
7450
- const run = app.db.select().from(runs).where(eq13(runs.id, runId)).get();
7829
+ const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7451
7830
  return run;
7452
7831
  });
7453
7832
  app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
@@ -7502,7 +7881,7 @@ async function googleRoutes(app, opts) {
7502
7881
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7503
7882
  let urlsToNotify = request.body?.urls ?? [];
7504
7883
  if (request.body?.allUnindexed) {
7505
- const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
7884
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7506
7885
  const latestByUrl = /* @__PURE__ */ new Map();
7507
7886
  for (const row of allInspections) {
7508
7887
  if (!latestByUrl.has(row.url)) {
@@ -7577,7 +7956,7 @@ async function googleRoutes(app, opts) {
7577
7956
 
7578
7957
  // ../api-routes/src/bing.ts
7579
7958
  import crypto15 from "crypto";
7580
- import { eq as eq14, and as and3, desc as desc5 } from "drizzle-orm";
7959
+ import { eq as eq15, and as and4, desc as desc6 } from "drizzle-orm";
7581
7960
 
7582
7961
  // ../integration-bing/src/constants.ts
7583
7962
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -7596,6 +7975,45 @@ var BingApiError = class extends Error {
7596
7975
  };
7597
7976
 
7598
7977
  // ../integration-bing/src/bing-client.ts
7978
+ function validateApiKey(apiKey) {
7979
+ if (!apiKey || typeof apiKey !== "string" || apiKey.trim().length === 0) {
7980
+ throw new BingApiError("API key is required and must be a non-empty string", 400);
7981
+ }
7982
+ }
7983
+ function validateSiteUrl2(siteUrl) {
7984
+ if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
7985
+ throw new BingApiError("Site URL is required and must be a non-empty string", 400);
7986
+ }
7987
+ try {
7988
+ const url = new URL(siteUrl);
7989
+ if (!url.protocol.startsWith("http")) {
7990
+ throw new BingApiError("Site URL must be an HTTP or HTTPS URL", 400);
7991
+ }
7992
+ } catch {
7993
+ throw new BingApiError("Site URL must be a valid URL", 400);
7994
+ }
7995
+ }
7996
+ function validateUrl2(urlParam) {
7997
+ if (!urlParam || typeof urlParam !== "string" || urlParam.trim().length === 0) {
7998
+ throw new BingApiError("URL is required and must be a non-empty string", 400);
7999
+ }
8000
+ try {
8001
+ const url = new URL(urlParam);
8002
+ if (!url.protocol.startsWith("http")) {
8003
+ throw new BingApiError("URL must be an HTTP or HTTPS URL", 400);
8004
+ }
8005
+ } catch {
8006
+ throw new BingApiError("URL must be a valid URL", 400);
8007
+ }
8008
+ }
8009
+ function validateUrls(urls) {
8010
+ if (!Array.isArray(urls)) {
8011
+ throw new BingApiError("URLs must be an array", 400);
8012
+ }
8013
+ for (const url of urls) {
8014
+ validateUrl2(url);
8015
+ }
8016
+ }
7599
8017
  function bingClientLog(level, action, ctx) {
7600
8018
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "BingClient", action, ...ctx };
7601
8019
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -7644,21 +8062,31 @@ async function bingFetch(apiKey, endpoint, opts) {
7644
8062
  }
7645
8063
  }
7646
8064
  async function getSites(apiKey) {
8065
+ validateApiKey(apiKey);
7647
8066
  const data = await bingFetch(apiKey, "GetUserSites");
7648
8067
  return data ?? [];
7649
8068
  }
7650
8069
  async function getUrlInfo(apiKey, siteUrl, url) {
8070
+ validateApiKey(apiKey);
8071
+ validateSiteUrl2(siteUrl);
8072
+ validateUrl2(url);
7651
8073
  const encodedSite = encodeURIComponent(siteUrl);
7652
8074
  const encodedUrl = encodeURIComponent(url);
7653
8075
  return bingFetch(apiKey, `GetUrlInfo?siteUrl=${encodedSite}&url=${encodedUrl}`);
7654
8076
  }
7655
8077
  async function submitUrl(apiKey, siteUrl, url) {
8078
+ validateApiKey(apiKey);
8079
+ validateSiteUrl2(siteUrl);
8080
+ validateUrl2(url);
7656
8081
  await bingFetch(apiKey, "SubmitUrl", {
7657
8082
  method: "POST",
7658
8083
  body: { siteUrl, url }
7659
8084
  });
7660
8085
  }
7661
8086
  async function submitUrlBatch(apiKey, siteUrl, urls) {
8087
+ validateApiKey(apiKey);
8088
+ validateSiteUrl2(siteUrl);
8089
+ validateUrls(urls);
7662
8090
  for (let i = 0; i < urls.length; i += BING_SUBMIT_URL_BATCH_LIMIT) {
7663
8091
  const batch = urls.slice(i, i + BING_SUBMIT_URL_BATCH_LIMIT);
7664
8092
  await bingFetch(apiKey, "SubmitUrlbatch", {
@@ -7668,6 +8096,8 @@ async function submitUrlBatch(apiKey, siteUrl, urls) {
7668
8096
  }
7669
8097
  }
7670
8098
  async function getKeywordStats(apiKey, siteUrl) {
8099
+ validateApiKey(apiKey);
8100
+ validateSiteUrl2(siteUrl);
7671
8101
  const encodedSite = encodeURIComponent(siteUrl);
7672
8102
  const data = await bingFetch(apiKey, `GetQueryStats?siteUrl=${encodedSite}`);
7673
8103
  return data ?? [];
@@ -7808,7 +8238,7 @@ async function bingRoutes(app, opts) {
7808
8238
  const project = resolveProject(app.db, request.params.name);
7809
8239
  const conn = requireConnection(store, project.canonicalDomain, reply);
7810
8240
  if (!conn) return;
7811
- const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
8241
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
7812
8242
  const latestByUrl = /* @__PURE__ */ new Map();
7813
8243
  const definitiveByUrl = /* @__PURE__ */ new Map();
7814
8244
  for (const row of allInspections) {
@@ -7878,8 +8308,8 @@ async function bingRoutes(app, opts) {
7878
8308
  if (!store) return;
7879
8309
  const project = resolveProject(app.db, request.params.name);
7880
8310
  const { url, limit } = request.query;
7881
- const whereClause = url ? and3(eq14(bingUrlInspections.projectId, project.id), eq14(bingUrlInspections.url, url)) : eq14(bingUrlInspections.projectId, project.id);
7882
- 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();
8311
+ const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
8312
+ 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();
7883
8313
  return filtered.map((r) => ({
7884
8314
  id: r.id,
7885
8315
  url: r.url,
@@ -7975,7 +8405,7 @@ async function bingRoutes(app, opts) {
7975
8405
  }
7976
8406
  let urlsToSubmit = request.body?.urls ?? [];
7977
8407
  if (request.body?.allUnindexed) {
7978
- const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
8408
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
7979
8409
  const latestByUrl = /* @__PURE__ */ new Map();
7980
8410
  for (const row of allInspections) {
7981
8411
  if (!latestByUrl.has(row.url)) {
@@ -8068,14 +8498,14 @@ async function bingRoutes(app, opts) {
8068
8498
  import fs2 from "fs";
8069
8499
  import path2 from "path";
8070
8500
  import os2 from "os";
8071
- import { eq as eq15, and as and4 } from "drizzle-orm";
8501
+ import { eq as eq16, and as and5 } from "drizzle-orm";
8072
8502
  function getScreenshotDir() {
8073
8503
  return path2.join(os2.homedir(), ".canonry", "screenshots");
8074
8504
  }
8075
8505
  async function cdpRoutes(app, opts) {
8076
8506
  app.get("/screenshots/:snapshotId", async (request, reply) => {
8077
8507
  const { snapshotId } = request.params;
8078
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq15(querySnapshots.id, snapshotId)).get();
8508
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq16(querySnapshots.id, snapshotId)).get();
8079
8509
  if (!snapshot?.screenshotPath) {
8080
8510
  const err = notFound("Screenshot", snapshotId);
8081
8511
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8141,7 +8571,7 @@ async function cdpRoutes(app, opts) {
8141
8571
  async (request, reply) => {
8142
8572
  const project = resolveProject(app.db, request.params.name);
8143
8573
  const { runId } = request.params;
8144
- const run = app.db.select().from(runs).where(and4(eq15(runs.id, runId), eq15(runs.projectId, project.id))).get();
8574
+ const run = app.db.select().from(runs).where(and5(eq16(runs.id, runId), eq16(runs.projectId, project.id))).get();
8145
8575
  if (!run) {
8146
8576
  const err = notFound("Run", runId);
8147
8577
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8154,8 +8584,8 @@ async function cdpRoutes(app, opts) {
8154
8584
  citedDomains: querySnapshots.citedDomains,
8155
8585
  screenshotPath: querySnapshots.screenshotPath,
8156
8586
  rawResponse: querySnapshots.rawResponse
8157
- }).from(querySnapshots).where(eq15(querySnapshots.runId, runId)).all();
8158
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq15(keywords.projectId, project.id)).all();
8587
+ }).from(querySnapshots).where(eq16(querySnapshots.runId, runId)).all();
8588
+ const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq16(keywords.projectId, project.id)).all();
8159
8589
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
8160
8590
  const byKeyword = /* @__PURE__ */ new Map();
8161
8591
  for (const snap of snapshots) {
@@ -8238,7 +8668,7 @@ async function cdpRoutes(app, opts) {
8238
8668
 
8239
8669
  // ../api-routes/src/ga.ts
8240
8670
  import crypto16 from "crypto";
8241
- import { eq as eq16, desc as desc6, and as and5, sql as sql4 } from "drizzle-orm";
8671
+ import { eq as eq17, desc as desc7, and as and6, sql as sql4 } from "drizzle-orm";
8242
8672
  function gaLog(level, action, ctx) {
8243
8673
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
8244
8674
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -8395,9 +8825,9 @@ async function ga4Routes(app, opts) {
8395
8825
  if (!saConn && !oauthConn) {
8396
8826
  throw notFound("GA4 connection", project.name);
8397
8827
  }
8398
- app.db.delete(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).run();
8399
- app.db.delete(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).run();
8400
- app.db.delete(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).run();
8828
+ app.db.delete(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).run();
8829
+ app.db.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
8830
+ app.db.delete(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).run();
8401
8831
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
8402
8832
  opts.ga4CredentialStore?.deleteConnection(project.name);
8403
8833
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -8418,7 +8848,7 @@ async function ga4Routes(app, opts) {
8418
8848
  if (!connected) {
8419
8849
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
8420
8850
  }
8421
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
8851
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
8422
8852
  return {
8423
8853
  connected: true,
8424
8854
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -8451,8 +8881,8 @@ async function ga4Routes(app, opts) {
8451
8881
  const now = (/* @__PURE__ */ new Date()).toISOString();
8452
8882
  app.db.transaction((tx) => {
8453
8883
  tx.delete(gaTrafficSnapshots).where(
8454
- and5(
8455
- eq16(gaTrafficSnapshots.projectId, project.id),
8884
+ and6(
8885
+ eq17(gaTrafficSnapshots.projectId, project.id),
8456
8886
  sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8457
8887
  sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8458
8888
  )
@@ -8472,8 +8902,8 @@ async function ga4Routes(app, opts) {
8472
8902
  }
8473
8903
  }
8474
8904
  tx.delete(gaAiReferrals).where(
8475
- and5(
8476
- eq16(gaAiReferrals.projectId, project.id),
8905
+ and6(
8906
+ eq17(gaAiReferrals.projectId, project.id),
8477
8907
  sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
8478
8908
  sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
8479
8909
  )
@@ -8493,7 +8923,7 @@ async function ga4Routes(app, opts) {
8493
8923
  }).run();
8494
8924
  }
8495
8925
  }
8496
- tx.delete(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).run();
8926
+ tx.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
8497
8927
  tx.insert(gaTrafficSummaries).values({
8498
8928
  id: crypto16.randomUUID(),
8499
8929
  projectId: project.id,
@@ -8528,20 +8958,20 @@ async function ga4Routes(app, opts) {
8528
8958
  totalSessions: gaTrafficSummaries.totalSessions,
8529
8959
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
8530
8960
  totalUsers: gaTrafficSummaries.totalUsers
8531
- }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).get();
8961
+ }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
8532
8962
  const rows = app.db.select({
8533
8963
  landingPage: gaTrafficSnapshots.landingPage,
8534
8964
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8535
8965
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8536
8966
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8537
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8967
+ }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8538
8968
  const aiReferrals = app.db.select({
8539
8969
  source: gaAiReferrals.source,
8540
8970
  medium: gaAiReferrals.medium,
8541
8971
  sourceDimension: gaAiReferrals.sourceDimension,
8542
8972
  sessions: sql4`SUM(${gaAiReferrals.sessions})`,
8543
8973
  users: sql4`SUM(${gaAiReferrals.users})`
8544
- }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8974
+ }).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8545
8975
  const aiDeduped = app.db.select({
8546
8976
  sessions: sql4`SUM(max_sessions)`,
8547
8977
  users: sql4`SUM(max_users)`
@@ -8555,7 +8985,7 @@ async function ga4Routes(app, opts) {
8555
8985
  GROUP BY date, source, medium
8556
8986
  )`
8557
8987
  ).get();
8558
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
8988
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
8559
8989
  return {
8560
8990
  totalSessions: summary?.totalSessions ?? 0,
8561
8991
  totalOrganicSessions: summary?.totalOrganicSessions ?? 0,
@@ -8588,7 +9018,7 @@ async function ga4Routes(app, opts) {
8588
9018
  sourceDimension: gaAiReferrals.sourceDimension,
8589
9019
  sessions: gaAiReferrals.sessions,
8590
9020
  users: gaAiReferrals.users
8591
- }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
9021
+ }).from(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
8592
9022
  return rows;
8593
9023
  });
8594
9024
  app.get("/projects/:name/ga/session-history", async (request, _reply) => {
@@ -8599,7 +9029,7 @@ async function ga4Routes(app, opts) {
8599
9029
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8600
9030
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8601
9031
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8602
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
9032
+ }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8603
9033
  return rows.map((r) => ({
8604
9034
  date: r.date,
8605
9035
  sessions: r.sessions ?? 0,
@@ -8615,7 +9045,7 @@ async function ga4Routes(app, opts) {
8615
9045
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8616
9046
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8617
9047
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8618
- }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9048
+ }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
8619
9049
  return {
8620
9050
  pages: trafficPages.map((r) => ({
8621
9051
  landingPage: r.landingPage,
@@ -8750,6 +9180,36 @@ function parseSchemaPageEntry(entry) {
8750
9180
 
8751
9181
  // ../integration-wordpress/src/wordpress-client.ts
8752
9182
  import crypto17 from "crypto";
9183
+ function validateUsername(username) {
9184
+ if (!username || typeof username !== "string" || username.trim().length === 0) {
9185
+ throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
9186
+ }
9187
+ }
9188
+ function validateAppPassword(appPassword) {
9189
+ if (!appPassword || typeof appPassword !== "string" || appPassword.trim().length === 0) {
9190
+ throw new WordpressApiError("AUTH_INVALID", "Application password is required and must be a non-empty string", 400);
9191
+ }
9192
+ }
9193
+ function validateSiteUrl3(siteUrl) {
9194
+ if (!siteUrl || typeof siteUrl !== "string" || siteUrl.trim().length === 0) {
9195
+ throw new WordpressApiError("AUTH_INVALID", "Site URL is required and must be a non-empty string", 400);
9196
+ }
9197
+ try {
9198
+ const url = new URL(siteUrl);
9199
+ if (!url.protocol.startsWith("http")) {
9200
+ throw new WordpressApiError("AUTH_INVALID", "Site URL must be an HTTP or HTTPS URL", 400);
9201
+ }
9202
+ if (url.protocol !== "https:") {
9203
+ }
9204
+ } catch {
9205
+ throw new WordpressApiError("AUTH_INVALID", "Site URL must be a valid URL", 400);
9206
+ }
9207
+ }
9208
+ function validateConnection(connection, siteUrl) {
9209
+ validateUsername(connection.username);
9210
+ validateAppPassword(connection.appPassword);
9211
+ validateSiteUrl3(siteUrl);
9212
+ }
8753
9213
  var WP_REQUEST_TIMEOUT_MS = 3e4;
8754
9214
  var WP_FETCH_TEXT_TIMEOUT_MS = 15e3;
8755
9215
  var PAGE_FIELDS = "id,slug,status,link,modified,modified_gmt,title,content,meta";
@@ -8991,6 +9451,7 @@ function getWpStagingAdminUrl(url) {
8991
9451
  }
8992
9452
  async function verifyWordpressConnection(connection) {
8993
9453
  const site = resolveEnvironment({ ...connection, defaultEnv: "live" }, "live");
9454
+ validateConnection(connection, site.siteUrl);
8994
9455
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
8995
9456
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
8996
9457
  const homeHtml = await fetchText(site.siteUrl);
@@ -9005,6 +9466,7 @@ async function verifyWordpressConnection(connection) {
9005
9466
  }
9006
9467
  async function getSiteStatus(connection, env) {
9007
9468
  const site = resolveEnvironment(connection, env);
9469
+ validateConnection(connection, site.siteUrl);
9008
9470
  try {
9009
9471
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
9010
9472
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
@@ -10335,6 +10797,7 @@ async function apiRoutes(app, opts) {
10335
10797
  });
10336
10798
  await api.register(historyRoutes);
10337
10799
  await api.register(analyticsRoutes);
10800
+ await api.register(intelligenceRoutes);
10338
10801
  await api.register(settingsRoutes, {
10339
10802
  providerSummary: opts.providerSummary,
10340
10803
  providerAdapters: opts.providerAdapters,
@@ -10386,6 +10849,27 @@ async function apiRoutes(app, opts) {
10386
10849
 
10387
10850
  // ../provider-gemini/src/normalize.ts
10388
10851
  import { GoogleGenAI } from "@google/genai";
10852
+
10853
+ // ../provider-gemini/src/utils.ts
10854
+ async function withRetry(fn, options = {}) {
10855
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
10856
+ let lastError;
10857
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
10858
+ try {
10859
+ return await fn();
10860
+ } catch (err) {
10861
+ lastError = err;
10862
+ if (attempt < maxRetries) {
10863
+ const delay = initialDelay * Math.pow(2, attempt);
10864
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
10865
+ await new Promise((resolve) => setTimeout(resolve, delay));
10866
+ }
10867
+ }
10868
+ }
10869
+ throw lastError;
10870
+ }
10871
+
10872
+ // ../provider-gemini/src/normalize.ts
10389
10873
  var DEFAULT_MODEL = "gemini-3-flash";
10390
10874
  function isVertexConfig(config) {
10391
10875
  return !!config.vertexProject;
@@ -10434,10 +10918,12 @@ async function healthcheck(config) {
10434
10918
  try {
10435
10919
  const model = resolveModel(config);
10436
10920
  const client = createClient2(config);
10437
- const result = await client.models.generateContent({
10438
- model,
10439
- contents: 'Say "ok"'
10440
- });
10921
+ const result = await withRetry(
10922
+ () => client.models.generateContent({
10923
+ model,
10924
+ contents: 'Say "ok"'
10925
+ })
10926
+ );
10441
10927
  const text2 = result.text ?? "";
10442
10928
  const backend = isVertexConfig(config) ? "vertex ai" : "api key";
10443
10929
  return {
@@ -10459,22 +10945,29 @@ async function executeTrackedQuery(input) {
10459
10945
  const model = resolveModel(input.config);
10460
10946
  const prompt = buildPrompt(input.keyword, input.location);
10461
10947
  const client = createClient2(input.config);
10462
- const result = await client.models.generateContent({
10463
- model,
10464
- contents: prompt,
10465
- config: {
10466
- tools: [{ googleSearch: {} }]
10467
- }
10468
- });
10469
- const groundingSources = extractGroundingMetadata(result);
10470
- const searchQueries = extractSearchQueries(result);
10471
- return {
10472
- provider: "gemini",
10473
- rawResponse: responseToRecord(result),
10474
- model,
10475
- groundingSources,
10476
- searchQueries
10477
- };
10948
+ try {
10949
+ const result = await withRetry(
10950
+ () => client.models.generateContent({
10951
+ model,
10952
+ contents: prompt,
10953
+ config: {
10954
+ tools: [{ googleSearch: {} }]
10955
+ }
10956
+ })
10957
+ );
10958
+ const groundingSources = extractGroundingMetadata(result);
10959
+ const searchQueries = extractSearchQueries(result);
10960
+ return {
10961
+ provider: "gemini",
10962
+ rawResponse: responseToRecord(result),
10963
+ model,
10964
+ groundingSources,
10965
+ searchQueries
10966
+ };
10967
+ } catch (err) {
10968
+ const msg = err instanceof Error ? err.message : String(err);
10969
+ throw new Error(`[provider-gemini] ${msg}`);
10970
+ }
10478
10971
  }
10479
10972
  function normalizeResult(raw) {
10480
10973
  const answerText = extractAnswerText(raw.rawResponse);
@@ -10576,10 +11069,12 @@ function extractDomainFromUri(uri) {
10576
11069
  async function generateText(prompt, config) {
10577
11070
  const model = resolveModel(config);
10578
11071
  const client = createClient2(config);
10579
- const result = await client.models.generateContent({
10580
- model,
10581
- contents: prompt
10582
- });
11072
+ const result = await withRetry(
11073
+ () => client.models.generateContent({
11074
+ model,
11075
+ contents: prompt
11076
+ })
11077
+ );
10583
11078
  return result.text ?? "";
10584
11079
  }
10585
11080
  function responseToRecord(response) {
@@ -10688,6 +11183,27 @@ var geminiAdapter = {
10688
11183
 
10689
11184
  // ../provider-openai/src/normalize.ts
10690
11185
  import OpenAI from "openai";
11186
+
11187
+ // ../provider-openai/src/utils.ts
11188
+ async function withRetry2(fn, options = {}) {
11189
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
11190
+ let lastError;
11191
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
11192
+ try {
11193
+ return await fn();
11194
+ } catch (err) {
11195
+ lastError = err;
11196
+ if (attempt < maxRetries) {
11197
+ const delay = initialDelay * Math.pow(2, attempt);
11198
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
11199
+ await new Promise((resolve) => setTimeout(resolve, delay));
11200
+ }
11201
+ }
11202
+ }
11203
+ throw lastError;
11204
+ }
11205
+
11206
+ // ../provider-openai/src/normalize.ts
10691
11207
  var DEFAULT_MODEL2 = "gpt-5.4";
10692
11208
  function validateConfig2(config) {
10693
11209
  if (!config.apiKey || config.apiKey.length === 0) {
@@ -10705,10 +11221,12 @@ async function healthcheck2(config) {
10705
11221
  if (!validation.ok) return validation;
10706
11222
  try {
10707
11223
  const client = new OpenAI({ apiKey: config.apiKey });
10708
- const response = await client.responses.create({
10709
- model: config.model ?? DEFAULT_MODEL2,
10710
- input: 'Say "ok"'
10711
- });
11224
+ const response = await withRetry2(
11225
+ () => client.responses.create({
11226
+ model: config.model ?? DEFAULT_MODEL2,
11227
+ input: 'Say "ok"'
11228
+ })
11229
+ );
10712
11230
  const text2 = extractResponseText(response);
10713
11231
  return {
10714
11232
  ok: text2.length > 0,
@@ -10738,21 +11256,28 @@ async function executeTrackedQuery2(input) {
10738
11256
  ...input.location.timezone ? { timezone: input.location.timezone } : {}
10739
11257
  };
10740
11258
  }
10741
- const response = await client.responses.create({
10742
- model,
10743
- tools: [webSearchTool],
10744
- tool_choice: "required",
10745
- input: buildPrompt2(input.keyword)
10746
- });
10747
- const groundingSources = extractGroundingSources(response);
10748
- const searchQueries = extractSearchQueries2(response);
10749
- return {
10750
- provider: "openai",
10751
- rawResponse: responseToRecord2(response),
10752
- model,
10753
- groundingSources,
10754
- searchQueries
10755
- };
11259
+ try {
11260
+ const response = await withRetry2(
11261
+ () => client.responses.create({
11262
+ model,
11263
+ tools: [webSearchTool],
11264
+ tool_choice: "required",
11265
+ input: buildPrompt2(input.keyword)
11266
+ })
11267
+ );
11268
+ const groundingSources = extractGroundingSources(response);
11269
+ const searchQueries = extractSearchQueries2(response);
11270
+ return {
11271
+ provider: "openai",
11272
+ rawResponse: responseToRecord2(response),
11273
+ model,
11274
+ groundingSources,
11275
+ searchQueries
11276
+ };
11277
+ } catch (err) {
11278
+ const msg = err instanceof Error ? err.message : String(err);
11279
+ throw new Error(`[provider-openai] ${msg}`);
11280
+ }
10756
11281
  }
10757
11282
  function normalizeResult2(raw) {
10758
11283
  const answerText = extractAnswerTextFromRaw(raw.rawResponse);
@@ -10863,10 +11388,12 @@ function extractDomainFromUri2(uri) {
10863
11388
  async function generateText2(prompt, config) {
10864
11389
  const model = config.model ?? DEFAULT_MODEL2;
10865
11390
  const client = new OpenAI({ apiKey: config.apiKey });
10866
- const response = await client.responses.create({
10867
- model,
10868
- input: prompt
10869
- });
11391
+ const response = await withRetry2(
11392
+ () => client.responses.create({
11393
+ model,
11394
+ input: prompt
11395
+ })
11396
+ );
10870
11397
  return extractResponseText(response);
10871
11398
  }
10872
11399
  function responseToRecord2(response) {
@@ -10962,6 +11489,27 @@ var openaiAdapter = {
10962
11489
 
10963
11490
  // ../provider-claude/src/normalize.ts
10964
11491
  import Anthropic from "@anthropic-ai/sdk";
11492
+
11493
+ // ../provider-claude/src/utils.ts
11494
+ async function withRetry3(fn, options = {}) {
11495
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
11496
+ let lastError;
11497
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
11498
+ try {
11499
+ return await fn();
11500
+ } catch (err) {
11501
+ lastError = err;
11502
+ if (attempt < maxRetries) {
11503
+ const delay = initialDelay * Math.pow(2, attempt);
11504
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
11505
+ await new Promise((resolve) => setTimeout(resolve, delay));
11506
+ }
11507
+ }
11508
+ }
11509
+ throw lastError;
11510
+ }
11511
+
11512
+ // ../provider-claude/src/normalize.ts
10965
11513
  var DEFAULT_MODEL3 = "claude-sonnet-4-6";
10966
11514
  var VALIDATION_PATTERN = /^claude-/;
10967
11515
  function resolveModel2(config) {
@@ -10992,11 +11540,13 @@ async function healthcheck3(config) {
10992
11540
  try {
10993
11541
  const model = resolveModel2(config);
10994
11542
  const client = new Anthropic({ apiKey: config.apiKey });
10995
- const response = await client.messages.create({
10996
- model,
10997
- max_tokens: 32,
10998
- messages: [{ role: "user", content: 'Say "ok"' }]
10999
- });
11543
+ const response = await withRetry3(
11544
+ () => client.messages.create({
11545
+ model,
11546
+ max_tokens: 32,
11547
+ messages: [{ role: "user", content: 'Say "ok"' }]
11548
+ })
11549
+ );
11000
11550
  const text2 = extractTextFromResponse(response);
11001
11551
  return {
11002
11552
  ok: text2.length > 0,
@@ -11030,21 +11580,28 @@ async function executeTrackedQuery3(input) {
11030
11580
  ...input.location.timezone ? { timezone: input.location.timezone } : {}
11031
11581
  };
11032
11582
  }
11033
- const response = await client.messages.create({
11034
- model,
11035
- max_tokens: 4096,
11036
- tools: [webSearchTool],
11037
- messages: [{ role: "user", content: input.keyword }]
11038
- });
11039
- const groundingSources = extractGroundingSources2(response);
11040
- const searchQueries = extractSearchQueries3(response);
11041
- return {
11042
- provider: "claude",
11043
- rawResponse: responseToRecord3(response),
11044
- model,
11045
- groundingSources,
11046
- searchQueries
11047
- };
11583
+ try {
11584
+ const response = await withRetry3(
11585
+ () => client.messages.create({
11586
+ model,
11587
+ max_tokens: 4096,
11588
+ tools: [webSearchTool],
11589
+ messages: [{ role: "user", content: input.keyword }]
11590
+ })
11591
+ );
11592
+ const groundingSources = extractGroundingSources2(response);
11593
+ const searchQueries = extractSearchQueries3(response);
11594
+ return {
11595
+ provider: "claude",
11596
+ rawResponse: responseToRecord3(response),
11597
+ model,
11598
+ groundingSources,
11599
+ searchQueries
11600
+ };
11601
+ } catch (err) {
11602
+ const msg = err instanceof Error ? err.message : String(err);
11603
+ throw new Error(`[provider-claude] ${msg}`);
11604
+ }
11048
11605
  }
11049
11606
  function normalizeResult3(raw) {
11050
11607
  const answerText = extractAnswerTextFromRaw2(raw.rawResponse);
@@ -11138,11 +11695,13 @@ function extractDomainFromUri3(uri) {
11138
11695
  async function generateText3(prompt, config) {
11139
11696
  const model = resolveModel2(config);
11140
11697
  const client = new Anthropic({ apiKey: config.apiKey });
11141
- const response = await client.messages.create({
11142
- model,
11143
- max_tokens: 2048,
11144
- messages: [{ role: "user", content: prompt }]
11145
- });
11698
+ const response = await withRetry3(
11699
+ () => client.messages.create({
11700
+ model,
11701
+ max_tokens: 2048,
11702
+ messages: [{ role: "user", content: prompt }]
11703
+ })
11704
+ );
11146
11705
  return extractTextFromResponse(response);
11147
11706
  }
11148
11707
  function responseToRecord3(response) {
@@ -11235,6 +11794,27 @@ var claudeAdapter = {
11235
11794
 
11236
11795
  // ../provider-local/src/normalize.ts
11237
11796
  import OpenAI2 from "openai";
11797
+
11798
+ // ../provider-local/src/utils.ts
11799
+ async function withRetry4(fn, options = {}) {
11800
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
11801
+ let lastError;
11802
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
11803
+ try {
11804
+ return await fn();
11805
+ } catch (err) {
11806
+ lastError = err;
11807
+ if (attempt < maxRetries) {
11808
+ const delay = initialDelay * Math.pow(2, attempt);
11809
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
11810
+ await new Promise((resolve) => setTimeout(resolve, delay));
11811
+ }
11812
+ }
11813
+ }
11814
+ throw lastError;
11815
+ }
11816
+
11817
+ // ../provider-local/src/normalize.ts
11238
11818
  var DEFAULT_MODEL4 = "llama3";
11239
11819
  function validateConfig4(config) {
11240
11820
  if (!config.baseUrl || config.baseUrl.length === 0) {
@@ -11255,16 +11835,19 @@ async function healthcheck4(config) {
11255
11835
  baseURL: config.baseUrl,
11256
11836
  apiKey: config.apiKey || "not-needed"
11257
11837
  });
11258
- const models = await client.models.list();
11259
- const modelList = [];
11260
- for await (const m of models) {
11261
- modelList.push(m.id);
11262
- if (modelList.length >= 5) break;
11263
- }
11838
+ const models = await withRetry4(async () => {
11839
+ const list = await client.models.list();
11840
+ const items = [];
11841
+ for await (const m of list) {
11842
+ items.push(m.id);
11843
+ if (items.length >= 5) break;
11844
+ }
11845
+ return items;
11846
+ });
11264
11847
  return {
11265
11848
  ok: true,
11266
11849
  provider: "local",
11267
- message: `connected, ${modelList.length} model(s) available`,
11850
+ message: `connected, ${models.length} model(s) available`,
11268
11851
  model: config.model ?? DEFAULT_MODEL4
11269
11852
  };
11270
11853
  } catch (err) {
@@ -11282,26 +11865,33 @@ async function executeTrackedQuery4(input) {
11282
11865
  baseURL: input.config.baseUrl,
11283
11866
  apiKey: input.config.apiKey || "not-needed"
11284
11867
  });
11285
- const response = await client.chat.completions.create({
11286
- model,
11287
- messages: [
11288
- {
11289
- role: "system",
11290
- content: "You are a helpful assistant. Provide comprehensive, factual answers. When mentioning websites or services, include their domain names."
11291
- },
11292
- {
11293
- role: "user",
11294
- content: buildPrompt3(input.keyword, input.location)
11295
- }
11296
- ]
11297
- });
11298
- return {
11299
- provider: "local",
11300
- rawResponse: responseToRecord4(response),
11301
- model,
11302
- groundingSources: [],
11303
- searchQueries: []
11304
- };
11868
+ try {
11869
+ const response = await withRetry4(
11870
+ () => client.chat.completions.create({
11871
+ model,
11872
+ messages: [
11873
+ {
11874
+ role: "system",
11875
+ content: "You are a helpful assistant. Provide comprehensive, factual answers. When mentioning websites or services, include their domain names."
11876
+ },
11877
+ {
11878
+ role: "user",
11879
+ content: buildPrompt3(input.keyword, input.location)
11880
+ }
11881
+ ]
11882
+ })
11883
+ );
11884
+ return {
11885
+ provider: "local",
11886
+ rawResponse: responseToRecord4(response),
11887
+ model,
11888
+ groundingSources: [],
11889
+ searchQueries: []
11890
+ };
11891
+ } catch (err) {
11892
+ const msg = err instanceof Error ? err.message : String(err);
11893
+ throw new Error(`[provider-local] ${msg}`);
11894
+ }
11305
11895
  }
11306
11896
  function normalizeResult4(raw) {
11307
11897
  const answerText = extractAnswerText2(raw.rawResponse);
@@ -11333,10 +11923,12 @@ async function generateText4(prompt, config) {
11333
11923
  baseURL: config.baseUrl,
11334
11924
  apiKey: config.apiKey || "not-needed"
11335
11925
  });
11336
- const response = await client.chat.completions.create({
11337
- model,
11338
- messages: [{ role: "user", content: prompt }]
11339
- });
11926
+ const response = await withRetry4(
11927
+ () => client.chat.completions.create({
11928
+ model,
11929
+ messages: [{ role: "user", content: prompt }]
11930
+ })
11931
+ );
11340
11932
  return response.choices[0]?.message?.content ?? "";
11341
11933
  }
11342
11934
  function extractDomainMentions(text2) {
@@ -11952,35 +12544,40 @@ var cdpChatgptAdapter = {
11952
12544
  async executeTrackedQuery(input, config) {
11953
12545
  const conn = getConnection(config);
11954
12546
  const target = chatgptTarget;
11955
- const client = await conn.prepareForQuery(target);
11956
- await target.submitQuery(client, input.keyword);
11957
- await target.waitForResponse(client);
11958
- const answerText = await target.extractAnswer(client);
11959
- const groundingSources = await target.extractCitations(client);
11960
- const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11961
- const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
11962
- let capturedScreenshotPath;
11963
12547
  try {
11964
- capturedScreenshotPath = await captureElementScreenshot(
11965
- client,
11966
- target.responseSelector,
11967
- screenshotPath
11968
- );
11969
- } catch {
11970
- }
11971
- return {
11972
- provider: "cdp:chatgpt",
11973
- rawResponse: {
11974
- answerText,
12548
+ const client = await conn.prepareForQuery(target);
12549
+ await target.submitQuery(client, input.keyword);
12550
+ await target.waitForResponse(client);
12551
+ const answerText = await target.extractAnswer(client);
12552
+ const groundingSources = await target.extractCitations(client);
12553
+ const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
12554
+ const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
12555
+ let capturedScreenshotPath;
12556
+ try {
12557
+ capturedScreenshotPath = await captureElementScreenshot(
12558
+ client,
12559
+ target.responseSelector,
12560
+ screenshotPath
12561
+ );
12562
+ } catch {
12563
+ }
12564
+ return {
12565
+ provider: "cdp:chatgpt",
12566
+ rawResponse: {
12567
+ answerText,
12568
+ groundingSources,
12569
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
12570
+ targetUrl: target.newConversationUrl
12571
+ },
12572
+ model: "chatgpt-web",
11975
12573
  groundingSources,
11976
- extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
11977
- targetUrl: target.newConversationUrl
11978
- },
11979
- model: "chatgpt-web",
11980
- groundingSources,
11981
- searchQueries: [input.keyword],
11982
- screenshotPath: capturedScreenshotPath
11983
- };
12574
+ searchQueries: [input.keyword],
12575
+ screenshotPath: capturedScreenshotPath
12576
+ };
12577
+ } catch (err) {
12578
+ const msg = err instanceof Error ? err.message : String(err);
12579
+ throw new Error(`[provider-cdp] ${msg}`);
12580
+ }
11984
12581
  },
11985
12582
  normalizeResult(raw) {
11986
12583
  return normalizeResult5(raw);
@@ -11992,6 +12589,27 @@ var cdpChatgptAdapter = {
11992
12589
 
11993
12590
  // ../provider-perplexity/src/normalize.ts
11994
12591
  import OpenAI3 from "openai";
12592
+
12593
+ // ../provider-perplexity/src/utils.ts
12594
+ async function withRetry5(fn, options = {}) {
12595
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
12596
+ let lastError;
12597
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
12598
+ try {
12599
+ return await fn();
12600
+ } catch (err) {
12601
+ lastError = err;
12602
+ if (attempt < maxRetries) {
12603
+ const delay = initialDelay * Math.pow(2, attempt);
12604
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
12605
+ await new Promise((resolve) => setTimeout(resolve, delay));
12606
+ }
12607
+ }
12608
+ }
12609
+ throw lastError;
12610
+ }
12611
+
12612
+ // ../provider-perplexity/src/normalize.ts
11995
12613
  var DEFAULT_MODEL5 = "sonar";
11996
12614
  var BASE_URL = "https://api.perplexity.ai";
11997
12615
  function validateConfig5(config) {
@@ -12010,10 +12628,12 @@ async function healthcheck5(config) {
12010
12628
  if (!validation.ok) return validation;
12011
12629
  try {
12012
12630
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
12013
- const response = await client.chat.completions.create({
12014
- model: config.model ?? DEFAULT_MODEL5,
12015
- messages: [{ role: "user", content: 'Say "ok"' }]
12016
- });
12631
+ const response = await withRetry5(
12632
+ () => client.chat.completions.create({
12633
+ model: config.model ?? DEFAULT_MODEL5,
12634
+ messages: [{ role: "user", content: 'Say "ok"' }]
12635
+ })
12636
+ );
12017
12637
  const text2 = response.choices[0]?.message?.content ?? "";
12018
12638
  return {
12019
12639
  ok: text2.length > 0,
@@ -12034,25 +12654,30 @@ async function executeTrackedQuery5(input) {
12034
12654
  const model = input.config.model ?? DEFAULT_MODEL5;
12035
12655
  const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
12036
12656
  const prompt = buildPrompt4(input.keyword, input.location);
12037
- const response = await client.chat.completions.create({
12038
- model,
12039
- messages: [
12040
- { role: "user", content: prompt }
12041
- ]
12042
- });
12043
- const rawResponse = responseToRecord5(response);
12044
- const citations = extractCitations(rawResponse);
12045
- const groundingSources = citations.map((url) => ({
12046
- uri: url,
12047
- title: ""
12048
- }));
12049
- return {
12050
- provider: "perplexity",
12051
- rawResponse,
12052
- model,
12053
- groundingSources,
12054
- searchQueries: [input.keyword]
12055
- };
12657
+ try {
12658
+ const response = await withRetry5(
12659
+ () => client.chat.completions.create({
12660
+ model,
12661
+ messages: [{ role: "user", content: prompt }]
12662
+ })
12663
+ );
12664
+ const rawResponse = responseToRecord5(response);
12665
+ const citations = extractCitations(rawResponse);
12666
+ const groundingSources = citations.map((url) => ({
12667
+ uri: url,
12668
+ title: ""
12669
+ }));
12670
+ return {
12671
+ provider: "perplexity",
12672
+ rawResponse,
12673
+ model,
12674
+ groundingSources,
12675
+ searchQueries: [input.keyword]
12676
+ };
12677
+ } catch (err) {
12678
+ const msg = err instanceof Error ? err.message : String(err);
12679
+ throw new Error(`[provider-perplexity] ${msg}`);
12680
+ }
12056
12681
  }
12057
12682
  function normalizeResult6(raw) {
12058
12683
  const answerText = extractAnswerText3(raw.rawResponse);
@@ -12112,10 +12737,12 @@ function extractDomainFromUri4(uri) {
12112
12737
  async function generateText5(prompt, config) {
12113
12738
  const model = config.model ?? DEFAULT_MODEL5;
12114
12739
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
12115
- const response = await client.chat.completions.create({
12116
- model,
12117
- messages: [{ role: "user", content: prompt }]
12118
- });
12740
+ const response = await withRetry5(
12741
+ () => client.chat.completions.create({
12742
+ model,
12743
+ messages: [{ role: "user", content: prompt }]
12744
+ })
12745
+ );
12119
12746
  return response.choices[0]?.message?.content ?? "";
12120
12747
  }
12121
12748
  function responseToRecord5(response) {
@@ -12368,7 +12995,7 @@ import crypto18 from "crypto";
12368
12995
  import fs4 from "fs";
12369
12996
  import path5 from "path";
12370
12997
  import os4 from "os";
12371
- import { and as and6, eq as eq17, inArray as inArray3, sql as sql5 } from "drizzle-orm";
12998
+ import { and as and7, eq as eq18, inArray as inArray3, sql as sql5 } from "drizzle-orm";
12372
12999
 
12373
13000
  // src/logger.ts
12374
13001
  var IS_TTY = process.stdout.isTTY === true;
@@ -12390,7 +13017,7 @@ function emit(entry) {
12390
13017
  }
12391
13018
  }
12392
13019
  function createLogger(module) {
12393
- function log8(level, action, ctx) {
13020
+ function log10(level, action, ctx) {
12394
13021
  const entry = {
12395
13022
  ts: (/* @__PURE__ */ new Date()).toISOString(),
12396
13023
  level,
@@ -12401,9 +13028,9 @@ function createLogger(module) {
12401
13028
  emit(entry);
12402
13029
  }
12403
13030
  return {
12404
- info: (action, ctx) => log8("info", action, ctx),
12405
- warn: (action, ctx) => log8("warn", action, ctx),
12406
- error: (action, ctx) => log8("error", action, ctx)
13031
+ info: (action, ctx) => log10("info", action, ctx),
13032
+ warn: (action, ctx) => log10("warn", action, ctx),
13033
+ error: (action, ctx) => log10("error", action, ctx)
12407
13034
  };
12408
13035
  }
12409
13036
 
@@ -12490,7 +13117,7 @@ var JobRunner = class {
12490
13117
  if (stale.length === 0) return;
12491
13118
  const now = (/* @__PURE__ */ new Date()).toISOString();
12492
13119
  for (const run of stale) {
12493
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq17(runs.id, run.id)).run();
13120
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq18(runs.id, run.id)).run();
12494
13121
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
12495
13122
  }
12496
13123
  }
@@ -12518,10 +13145,10 @@ var JobRunner = class {
12518
13145
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
12519
13146
  }
12520
13147
  if (existingRun.status === "queued") {
12521
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and6(eq17(runs.id, runId), eq17(runs.status, "queued"))).run();
13148
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and7(eq18(runs.id, runId), eq18(runs.status, "queued"))).run();
12522
13149
  }
12523
13150
  this.throwIfRunCancelled(runId);
12524
- const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
13151
+ const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
12525
13152
  if (!project) {
12526
13153
  throw new Error(`Project ${projectId} not found`);
12527
13154
  }
@@ -12541,8 +13168,8 @@ var JobRunner = class {
12541
13168
  throw new Error("No providers configured. Add at least one provider API key.");
12542
13169
  }
12543
13170
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
12544
- projectKeywords = this.db.select().from(keywords).where(eq17(keywords.projectId, projectId)).all();
12545
- const projectCompetitors = this.db.select().from(competitors).where(eq17(competitors.projectId, projectId)).all();
13171
+ projectKeywords = this.db.select().from(keywords).where(eq18(keywords.projectId, projectId)).all();
13172
+ const projectCompetitors = this.db.select().from(competitors).where(eq18(competitors.projectId, projectId)).all();
12546
13173
  const competitorDomains = projectCompetitors.map((c) => c.domain);
12547
13174
  const allDomains = effectiveDomains({
12548
13175
  canonicalDomain: project.canonicalDomain,
@@ -12558,7 +13185,7 @@ var JobRunner = class {
12558
13185
  const todayPeriod = getCurrentUsageDay();
12559
13186
  for (const p of activeProviders) {
12560
13187
  const providerScope = `${projectId}:${p.adapter.name}`;
12561
- 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);
13188
+ 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);
12562
13189
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
12563
13190
  if (providerUsage + queriesPerProvider > limit) {
12564
13191
  throw new Error(
@@ -12699,12 +13326,12 @@ var JobRunner = class {
12699
13326
  const someFailed = providerErrors.size > 0;
12700
13327
  if (allFailed) {
12701
13328
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
12702
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq17(runs.id, runId)).run();
13329
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
12703
13330
  } else if (someFailed) {
12704
13331
  const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
12705
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq17(runs.id, runId)).run();
13332
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq18(runs.id, runId)).run();
12706
13333
  } else {
12707
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(runs.id, runId)).run();
13334
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
12708
13335
  }
12709
13336
  this.flushProviderUsage(projectId, providerDispatchCounts);
12710
13337
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -12739,7 +13366,7 @@ var JobRunner = class {
12739
13366
  status: "failed",
12740
13367
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
12741
13368
  error: errorMessage
12742
- }).where(eq17(runs.id, runId)).run();
13369
+ }).where(eq18(runs.id, runId)).run();
12743
13370
  this.flushProviderUsage(projectId, providerDispatchCounts);
12744
13371
  trackEvent("run.completed", {
12745
13372
  status: "failed",
@@ -12782,7 +13409,7 @@ var JobRunner = class {
12782
13409
  status: runs.status,
12783
13410
  finishedAt: runs.finishedAt,
12784
13411
  error: runs.error
12785
- }).from(runs).where(eq17(runs.id, runId)).get();
13412
+ }).from(runs).where(eq18(runs.id, runId)).get();
12786
13413
  }
12787
13414
  isRunCancelled(runId) {
12788
13415
  return this.getRunState(runId)?.status === "cancelled";
@@ -12798,7 +13425,7 @@ var JobRunner = class {
12798
13425
  this.db.update(runs).set({
12799
13426
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
12800
13427
  error: currentRun.error ?? "Cancelled by user"
12801
- }).where(eq17(runs.id, runId)).run();
13428
+ }).where(eq18(runs.id, runId)).run();
12802
13429
  }
12803
13430
  trackEvent("run.completed", {
12804
13431
  status: "cancelled",
@@ -12970,7 +13597,7 @@ function matchesBrandKey(candidateKey, brandKeys) {
12970
13597
 
12971
13598
  // src/gsc-sync.ts
12972
13599
  import crypto19 from "crypto";
12973
- import { eq as eq18, and as and7, sql as sql6 } from "drizzle-orm";
13600
+ import { eq as eq19, and as and8, sql as sql6 } from "drizzle-orm";
12974
13601
  var log2 = createLogger("GscSync");
12975
13602
  function formatDate2(d) {
12976
13603
  return d.toISOString().split("T")[0];
@@ -12982,13 +13609,13 @@ function daysAgo(n) {
12982
13609
  }
12983
13610
  async function executeGscSync(db, runId, projectId, opts) {
12984
13611
  const now = (/* @__PURE__ */ new Date()).toISOString();
12985
- db.update(runs).set({ status: "running", startedAt: now }).where(eq18(runs.id, runId)).run();
13612
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
12986
13613
  try {
12987
13614
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
12988
13615
  if (!googleClientId || !googleClientSecret) {
12989
13616
  throw new Error("Google OAuth is not configured in the local Canonry config");
12990
13617
  }
12991
- const project = db.select().from(projects).where(eq18(projects.id, projectId)).get();
13618
+ const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
12992
13619
  if (!project) {
12993
13620
  throw new Error(`Project not found: ${projectId}`);
12994
13621
  }
@@ -13022,8 +13649,8 @@ async function executeGscSync(db, runId, projectId, opts) {
13022
13649
  });
13023
13650
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
13024
13651
  db.delete(gscSearchData).where(
13025
- and7(
13026
- eq18(gscSearchData.projectId, projectId),
13652
+ and8(
13653
+ eq19(gscSearchData.projectId, projectId),
13027
13654
  sql6`${gscSearchData.date} >= ${startDate}`,
13028
13655
  sql6`${gscSearchData.date} <= ${endDate}`
13029
13656
  )
@@ -13090,7 +13717,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13090
13717
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
13091
13718
  }
13092
13719
  }
13093
- const allInspections = db.select().from(gscUrlInspections).where(eq18(gscUrlInspections.projectId, projectId)).all();
13720
+ const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
13094
13721
  const latestByUrl = /* @__PURE__ */ new Map();
13095
13722
  for (const row of allInspections) {
13096
13723
  const existing = latestByUrl.get(row.url);
@@ -13111,7 +13738,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13111
13738
  }
13112
13739
  }
13113
13740
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
13114
- db.delete(gscCoverageSnapshots).where(and7(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
13741
+ db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
13115
13742
  db.insert(gscCoverageSnapshots).values({
13116
13743
  id: crypto19.randomUUID(),
13117
13744
  projectId,
@@ -13122,11 +13749,11 @@ async function executeGscSync(db, runId, projectId, opts) {
13122
13749
  reasonBreakdown: JSON.stringify(reasonCounts),
13123
13750
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
13124
13751
  }).run();
13125
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
13752
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13126
13753
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
13127
13754
  } catch (err) {
13128
13755
  const errorMsg = err instanceof Error ? err.message : String(err);
13129
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
13756
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13130
13757
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
13131
13758
  throw err;
13132
13759
  }
@@ -13134,7 +13761,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13134
13761
 
13135
13762
  // src/gsc-inspect-sitemap.ts
13136
13763
  import crypto20 from "crypto";
13137
- import { eq as eq19, and as and8 } from "drizzle-orm";
13764
+ import { eq as eq20, and as and9 } from "drizzle-orm";
13138
13765
 
13139
13766
  // src/sitemap-parser.ts
13140
13767
  var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
@@ -13203,13 +13830,13 @@ async function parseSitemapRecursive(url, urls, depth) {
13203
13830
  var log3 = createLogger("InspectSitemap");
13204
13831
  async function executeInspectSitemap(db, runId, projectId, opts) {
13205
13832
  const now = (/* @__PURE__ */ new Date()).toISOString();
13206
- db.update(runs).set({ status: "running", startedAt: now }).where(eq19(runs.id, runId)).run();
13833
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
13207
13834
  try {
13208
13835
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13209
13836
  if (!googleClientId || !googleClientSecret) {
13210
13837
  throw new Error("Google OAuth is not configured in the local Canonry config");
13211
13838
  }
13212
- const project = db.select().from(projects).where(eq19(projects.id, projectId)).get();
13839
+ const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
13213
13840
  if (!project) {
13214
13841
  throw new Error(`Project not found: ${projectId}`);
13215
13842
  }
@@ -13277,7 +13904,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13277
13904
  await new Promise((r) => setTimeout(r, 1e3));
13278
13905
  }
13279
13906
  }
13280
- const allInspections = db.select().from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).all();
13907
+ const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
13281
13908
  const latestByUrl = /* @__PURE__ */ new Map();
13282
13909
  for (const row of allInspections) {
13283
13910
  const existing = latestByUrl.get(row.url);
@@ -13298,7 +13925,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13298
13925
  }
13299
13926
  }
13300
13927
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13301
- db.delete(gscCoverageSnapshots).where(and8(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
13928
+ db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
13302
13929
  db.insert(gscCoverageSnapshots).values({
13303
13930
  id: crypto20.randomUUID(),
13304
13931
  projectId,
@@ -13310,11 +13937,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13310
13937
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
13311
13938
  }).run();
13312
13939
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
13313
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13940
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
13314
13941
  log3.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
13315
13942
  } catch (err) {
13316
13943
  const errorMsg = err instanceof Error ? err.message : String(err);
13317
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13944
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
13318
13945
  log3.error("inspect.failed", { runId, projectId, error: errorMsg });
13319
13946
  throw err;
13320
13947
  }
@@ -13373,7 +14000,7 @@ var ProviderRegistry = class {
13373
14000
 
13374
14001
  // src/scheduler.ts
13375
14002
  import cron from "node-cron";
13376
- import { eq as eq20 } from "drizzle-orm";
14003
+ import { eq as eq21 } from "drizzle-orm";
13377
14004
  var log4 = createLogger("Scheduler");
13378
14005
  var Scheduler = class {
13379
14006
  db;
@@ -13385,7 +14012,7 @@ var Scheduler = class {
13385
14012
  }
13386
14013
  /** Load all enabled schedules from DB and register cron jobs. */
13387
14014
  start() {
13388
- const allSchedules = this.db.select().from(schedules).where(eq20(schedules.enabled, 1)).all();
14015
+ const allSchedules = this.db.select().from(schedules).where(eq21(schedules.enabled, 1)).all();
13389
14016
  for (const schedule of allSchedules) {
13390
14017
  const missedRunAt = schedule.nextRunAt;
13391
14018
  this.registerCronTask(schedule);
@@ -13410,7 +14037,7 @@ var Scheduler = class {
13410
14037
  this.stopTask(projectId, existing, "Stopped");
13411
14038
  this.tasks.delete(projectId);
13412
14039
  }
13413
- const schedule = this.db.select().from(schedules).where(eq20(schedules.projectId, projectId)).get();
14040
+ const schedule = this.db.select().from(schedules).where(eq21(schedules.projectId, projectId)).get();
13414
14041
  if (schedule && schedule.enabled === 1) {
13415
14042
  this.registerCronTask(schedule);
13416
14043
  }
@@ -13443,14 +14070,14 @@ var Scheduler = class {
13443
14070
  this.db.update(schedules).set({
13444
14071
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
13445
14072
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13446
- }).where(eq20(schedules.id, scheduleId)).run();
14073
+ }).where(eq21(schedules.id, scheduleId)).run();
13447
14074
  const label = schedule.preset ?? cronExpr;
13448
14075
  log4.info("cron.registered", { projectId, schedule: label, timezone });
13449
14076
  }
13450
14077
  triggerRun(scheduleId, projectId) {
13451
14078
  try {
13452
14079
  const now = (/* @__PURE__ */ new Date()).toISOString();
13453
- const currentSchedule = this.db.select().from(schedules).where(eq20(schedules.id, scheduleId)).get();
14080
+ const currentSchedule = this.db.select().from(schedules).where(eq21(schedules.id, scheduleId)).get();
13454
14081
  if (!currentSchedule || currentSchedule.enabled !== 1) {
13455
14082
  log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
13456
14083
  this.remove(projectId);
@@ -13458,7 +14085,7 @@ var Scheduler = class {
13458
14085
  }
13459
14086
  const task = this.tasks.get(projectId);
13460
14087
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
13461
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
14088
+ const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
13462
14089
  if (!project) {
13463
14090
  log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
13464
14091
  this.remove(projectId);
@@ -13475,7 +14102,7 @@ var Scheduler = class {
13475
14102
  this.db.update(schedules).set({
13476
14103
  nextRunAt,
13477
14104
  updatedAt: now
13478
- }).where(eq20(schedules.id, currentSchedule.id)).run();
14105
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
13479
14106
  return;
13480
14107
  }
13481
14108
  const runId = queueResult.runId;
@@ -13483,7 +14110,7 @@ var Scheduler = class {
13483
14110
  lastRunAt: now,
13484
14111
  nextRunAt,
13485
14112
  updatedAt: now
13486
- }).where(eq20(schedules.id, currentSchedule.id)).run();
14113
+ }).where(eq21(schedules.id, currentSchedule.id)).run();
13487
14114
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
13488
14115
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13489
14116
  log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -13495,7 +14122,7 @@ var Scheduler = class {
13495
14122
  };
13496
14123
 
13497
14124
  // src/notifier.ts
13498
- import { eq as eq21, desc as desc7, and as and9, or as or2 } from "drizzle-orm";
14125
+ import { eq as eq22, desc as desc8, and as and10, or as or2 } from "drizzle-orm";
13499
14126
  import crypto21 from "crypto";
13500
14127
  var log5 = createLogger("Notifier");
13501
14128
  var Notifier = class {
@@ -13508,18 +14135,18 @@ var Notifier = class {
13508
14135
  /** Called after a run completes (success, partial, or failed). */
13509
14136
  async onRunCompleted(runId, projectId) {
13510
14137
  log5.info("run.completed", { runId, projectId });
13511
- const notifs = this.db.select().from(notifications).where(eq21(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14138
+ const notifs = this.db.select().from(notifications).where(eq22(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
13512
14139
  if (notifs.length === 0) {
13513
14140
  log5.info("notifications.none-enabled", { projectId });
13514
14141
  return;
13515
14142
  }
13516
14143
  log5.info("notifications.found", { projectId, count: notifs.length });
13517
- const run = this.db.select().from(runs).where(eq21(runs.id, runId)).get();
14144
+ const run = this.db.select().from(runs).where(eq22(runs.id, runId)).get();
13518
14145
  if (!run) {
13519
14146
  log5.error("run.not-found", { runId, msg: "skipping notification dispatch" });
13520
14147
  return;
13521
14148
  }
13522
- const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
14149
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
13523
14150
  if (!project) {
13524
14151
  log5.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
13525
14152
  return;
@@ -13559,11 +14186,11 @@ var Notifier = class {
13559
14186
  }
13560
14187
  computeTransitions(runId, projectId) {
13561
14188
  const recentRuns = this.db.select().from(runs).where(
13562
- and9(
13563
- eq21(runs.projectId, projectId),
13564
- or2(eq21(runs.status, "completed"), eq21(runs.status, "partial"))
14189
+ and10(
14190
+ eq22(runs.projectId, projectId),
14191
+ or2(eq22(runs.status, "completed"), eq22(runs.status, "partial"))
13565
14192
  )
13566
- ).orderBy(desc7(runs.createdAt)).limit(2).all();
14193
+ ).orderBy(desc8(runs.createdAt)).limit(2).all();
13567
14194
  if (recentRuns.length < 2) return [];
13568
14195
  const currentRunId = recentRuns[0].id;
13569
14196
  const previousRunId = recentRuns[1].id;
@@ -13573,12 +14200,12 @@ var Notifier = class {
13573
14200
  keyword: keywords.keyword,
13574
14201
  provider: querySnapshots.provider,
13575
14202
  citationState: querySnapshots.citationState
13576
- }).from(querySnapshots).leftJoin(keywords, eq21(querySnapshots.keywordId, keywords.id)).where(eq21(querySnapshots.runId, currentRunId)).all();
14203
+ }).from(querySnapshots).leftJoin(keywords, eq22(querySnapshots.keywordId, keywords.id)).where(eq22(querySnapshots.runId, currentRunId)).all();
13577
14204
  const previousSnapshots = this.db.select({
13578
14205
  keywordId: querySnapshots.keywordId,
13579
14206
  provider: querySnapshots.provider,
13580
14207
  citationState: querySnapshots.citationState
13581
- }).from(querySnapshots).where(eq21(querySnapshots.runId, previousRunId)).all();
14208
+ }).from(querySnapshots).where(eq22(querySnapshots.runId, previousRunId)).all();
13582
14209
  const prevMap = /* @__PURE__ */ new Map();
13583
14210
  for (const s of previousSnapshots) {
13584
14211
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -13648,6 +14275,382 @@ var Notifier = class {
13648
14275
  }
13649
14276
  };
13650
14277
 
14278
+ // src/intelligence-service.ts
14279
+ import { eq as eq23, desc as desc9, asc as asc2, and as and11, or as or3 } from "drizzle-orm";
14280
+
14281
+ // ../intelligence/src/regressions.ts
14282
+ function detectRegressions(currentRun, previousRun) {
14283
+ const regressions = [];
14284
+ const previousCited = /* @__PURE__ */ new Map();
14285
+ for (const snap of previousRun.snapshots) {
14286
+ if (snap.cited) {
14287
+ previousCited.set(`${snap.keyword}:${snap.provider}`, {
14288
+ citationUrl: snap.citationUrl,
14289
+ position: snap.position
14290
+ });
14291
+ }
14292
+ }
14293
+ for (const snap of currentRun.snapshots) {
14294
+ const key = `${snap.keyword}:${snap.provider}`;
14295
+ if (!snap.cited && previousCited.has(key)) {
14296
+ const prev = previousCited.get(key);
14297
+ regressions.push({
14298
+ keyword: snap.keyword,
14299
+ provider: snap.provider,
14300
+ previousCitationUrl: prev.citationUrl,
14301
+ previousPosition: prev.position,
14302
+ currentRunId: currentRun.runId,
14303
+ previousRunId: previousRun.runId
14304
+ });
14305
+ }
14306
+ }
14307
+ return regressions;
14308
+ }
14309
+
14310
+ // ../intelligence/src/gains.ts
14311
+ function detectGains(currentRun, previousRun) {
14312
+ const gains = [];
14313
+ const previousCited = /* @__PURE__ */ new Set();
14314
+ for (const snap of previousRun.snapshots) {
14315
+ if (snap.cited) {
14316
+ previousCited.add(`${snap.keyword}:${snap.provider}`);
14317
+ }
14318
+ }
14319
+ for (const snap of currentRun.snapshots) {
14320
+ const key = `${snap.keyword}:${snap.provider}`;
14321
+ if (snap.cited && !previousCited.has(key)) {
14322
+ gains.push({
14323
+ keyword: snap.keyword,
14324
+ provider: snap.provider,
14325
+ citationUrl: snap.citationUrl,
14326
+ position: snap.position,
14327
+ snippet: snap.snippet,
14328
+ runId: currentRun.runId
14329
+ });
14330
+ }
14331
+ }
14332
+ return gains;
14333
+ }
14334
+
14335
+ // ../intelligence/src/health.ts
14336
+ function computeHealth(run) {
14337
+ const providerStats = /* @__PURE__ */ new Map();
14338
+ let totalPairs = 0;
14339
+ let citedPairs = 0;
14340
+ for (const snap of run.snapshots) {
14341
+ totalPairs++;
14342
+ if (snap.cited) citedPairs++;
14343
+ const stats = providerStats.get(snap.provider) ?? { cited: 0, total: 0 };
14344
+ stats.total++;
14345
+ if (snap.cited) stats.cited++;
14346
+ providerStats.set(snap.provider, stats);
14347
+ }
14348
+ const providerBreakdown = {};
14349
+ for (const [provider, stats] of providerStats) {
14350
+ providerBreakdown[provider] = {
14351
+ citedRate: stats.total > 0 ? stats.cited / stats.total : 0,
14352
+ cited: stats.cited,
14353
+ total: stats.total
14354
+ };
14355
+ }
14356
+ return {
14357
+ overallCitedRate: totalPairs > 0 ? citedPairs / totalPairs : 0,
14358
+ totalPairs,
14359
+ citedPairs,
14360
+ providerBreakdown
14361
+ };
14362
+ }
14363
+ function computeHealthTrend(runs2) {
14364
+ if (runs2.length === 0) {
14365
+ return { current: 0, previous: 0, delta: 0 };
14366
+ }
14367
+ const current = computeHealth(runs2[runs2.length - 1]).overallCitedRate;
14368
+ if (runs2.length === 1) {
14369
+ return { current, previous: 0, delta: current };
14370
+ }
14371
+ const previous = computeHealth(runs2[runs2.length - 2]).overallCitedRate;
14372
+ return {
14373
+ current,
14374
+ previous,
14375
+ delta: current - previous
14376
+ };
14377
+ }
14378
+
14379
+ // ../intelligence/src/causes.ts
14380
+ function analyzeCause(regression, currentSnapshots) {
14381
+ const currentSnap = currentSnapshots.find(
14382
+ (s) => s.keyword === regression.keyword && s.provider === regression.provider && !s.cited && s.competitorDomain
14383
+ );
14384
+ if (currentSnap) {
14385
+ return {
14386
+ cause: "competitor_gain",
14387
+ competitorDomain: currentSnap.competitorDomain,
14388
+ details: `Competitor ${currentSnap.competitorDomain} now cited for "${regression.keyword}" on ${regression.provider}`
14389
+ };
14390
+ }
14391
+ return {
14392
+ cause: "unknown",
14393
+ details: `No specific cause identified for loss of "${regression.keyword}" on ${regression.provider}`
14394
+ };
14395
+ }
14396
+
14397
+ // ../intelligence/src/insights.ts
14398
+ import { randomUUID } from "crypto";
14399
+ function generateInsights(regressions, gains, health, causes) {
14400
+ const insights2 = [];
14401
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14402
+ for (const reg of regressions) {
14403
+ const key = `${reg.keyword}:${reg.provider}`;
14404
+ const cause = causes.get(key);
14405
+ insights2.push({
14406
+ id: `ins_${randomUUID().slice(0, 8)}`,
14407
+ type: "regression",
14408
+ severity: "high",
14409
+ title: `Lost ${reg.provider} citation for "${reg.keyword}"`,
14410
+ keyword: reg.keyword,
14411
+ provider: reg.provider,
14412
+ recommendation: {
14413
+ action: "audit",
14414
+ target: reg.previousCitationUrl,
14415
+ reason: `Page was previously cited at position ${reg.previousPosition ?? "unknown"}. Run aeo-audit to check for content or schema issues.`
14416
+ },
14417
+ cause,
14418
+ createdAt: now
14419
+ });
14420
+ }
14421
+ for (const gain of gains) {
14422
+ insights2.push({
14423
+ id: `ins_${randomUUID().slice(0, 8)}`,
14424
+ type: "gain",
14425
+ severity: "low",
14426
+ title: `New ${gain.provider} citation for "${gain.keyword}"`,
14427
+ keyword: gain.keyword,
14428
+ provider: gain.provider,
14429
+ recommendation: {
14430
+ action: "monitor",
14431
+ target: gain.citationUrl,
14432
+ reason: `New citation appeared at position ${gain.position ?? "unknown"}. Monitor to confirm it persists.`
14433
+ },
14434
+ createdAt: now
14435
+ });
14436
+ }
14437
+ return insights2;
14438
+ }
14439
+
14440
+ // ../intelligence/src/analyzer.ts
14441
+ function analyzeRuns(currentRun, previousRun, allRuns) {
14442
+ const regressions = detectRegressions(currentRun, previousRun);
14443
+ const gains = detectGains(currentRun, previousRun);
14444
+ const health = computeHealth(currentRun);
14445
+ const trend = allRuns ? computeHealthTrend(allRuns) : void 0;
14446
+ const causes = /* @__PURE__ */ new Map();
14447
+ for (const reg of regressions) {
14448
+ const cause = analyzeCause(reg, currentRun.snapshots);
14449
+ causes.set(`${reg.keyword}:${reg.provider}`, cause);
14450
+ }
14451
+ const insights2 = generateInsights(regressions, gains, health, causes);
14452
+ return {
14453
+ regressions,
14454
+ gains,
14455
+ health,
14456
+ trend,
14457
+ insights: insights2
14458
+ };
14459
+ }
14460
+
14461
+ // src/intelligence-service.ts
14462
+ import crypto22 from "crypto";
14463
+ var log6 = createLogger("IntelligenceService");
14464
+ var IntelligenceService = class {
14465
+ constructor(db) {
14466
+ this.db = db;
14467
+ }
14468
+ /**
14469
+ * Analyze a completed run and persist insights + health snapshot.
14470
+ * Idempotent: deletes prior results for the same runId before inserting.
14471
+ * Returns the analysis result for the coordinator to inspect (e.g. for webhook dispatch).
14472
+ */
14473
+ analyzeAndPersist(runId, projectId) {
14474
+ const recentRuns = this.db.select().from(runs).where(
14475
+ and11(
14476
+ eq23(runs.projectId, projectId),
14477
+ or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
14478
+ )
14479
+ ).orderBy(desc9(runs.createdAt)).limit(2).all();
14480
+ if (recentRuns.length === 0) {
14481
+ log6.info("intelligence.skip", { runId, reason: "no completed runs" });
14482
+ return null;
14483
+ }
14484
+ const currentRunRecord = recentRuns.find((r) => r.id === runId);
14485
+ if (!currentRunRecord) {
14486
+ log6.info("intelligence.skip", { runId, reason: "run not in recent completed list" });
14487
+ return null;
14488
+ }
14489
+ const currentRun = this.buildRunData(runId, projectId, currentRunRecord.finishedAt ?? currentRunRecord.createdAt);
14490
+ if (currentRun.snapshots.length === 0) {
14491
+ log6.info("intelligence.skip", { runId, reason: "no snapshots" });
14492
+ return null;
14493
+ }
14494
+ const previousRunRecord = recentRuns.find((r) => r.id !== runId);
14495
+ const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
14496
+ const result = previousRun ? analyzeRuns(currentRun, previousRun) : analyzeRuns(currentRun, { ...currentRun, snapshots: [] });
14497
+ log6.info("intelligence.analyzed", {
14498
+ runId,
14499
+ regressions: result.regressions.length,
14500
+ gains: result.gains.length,
14501
+ citedRate: result.health.overallCitedRate,
14502
+ insights: result.insights.length
14503
+ });
14504
+ this.persistResult(result, runId, projectId);
14505
+ return result;
14506
+ }
14507
+ /**
14508
+ * Analyze a single run given an explicit previous run (or null for first run).
14509
+ * Used by backfill where we control the run ordering.
14510
+ */
14511
+ analyzeRunWithPrevious(runRecord, previousRunRecord) {
14512
+ const currentRun = this.buildRunData(runRecord.id, runRecord.projectId, runRecord.finishedAt ?? runRecord.createdAt);
14513
+ if (currentRun.snapshots.length === 0) {
14514
+ return null;
14515
+ }
14516
+ const previousRun = previousRunRecord ? this.buildRunData(previousRunRecord.id, previousRunRecord.projectId, previousRunRecord.finishedAt ?? previousRunRecord.createdAt) : null;
14517
+ const result = previousRun ? analyzeRuns(currentRun, previousRun) : analyzeRuns(currentRun, { ...currentRun, snapshots: [] });
14518
+ this.persistResult(result, runRecord.id, runRecord.projectId);
14519
+ return result;
14520
+ }
14521
+ /**
14522
+ * Backfill intelligence for all completed/partial runs of a project.
14523
+ * Processes runs in chronological order so each run compares against its predecessor.
14524
+ */
14525
+ backfill(projectName, opts, onProgress) {
14526
+ const project = this.db.select().from(projects).where(eq23(projects.name, projectName)).get();
14527
+ if (!project) {
14528
+ throw new Error(`Project "${projectName}" not found`);
14529
+ }
14530
+ const allRuns = this.db.select().from(runs).where(
14531
+ and11(
14532
+ eq23(runs.projectId, project.id),
14533
+ or3(eq23(runs.status, "completed"), eq23(runs.status, "partial"))
14534
+ )
14535
+ ).orderBy(asc2(runs.finishedAt)).all();
14536
+ let startIdx = 0;
14537
+ let endIdx = allRuns.length;
14538
+ if (opts?.fromRunId) {
14539
+ const idx = allRuns.findIndex((r) => r.id === opts.fromRunId);
14540
+ if (idx === -1) throw new Error(`Run "${opts.fromRunId}" not found in project`);
14541
+ startIdx = idx;
14542
+ }
14543
+ if (opts?.toRunId) {
14544
+ const idx = allRuns.findIndex((r) => r.id === opts.toRunId);
14545
+ if (idx === -1) throw new Error(`Run "${opts.toRunId}" not found in project`);
14546
+ endIdx = idx + 1;
14547
+ }
14548
+ const targetRuns = allRuns.slice(startIdx, endIdx);
14549
+ let processed = 0;
14550
+ let skipped = 0;
14551
+ let totalInsights = 0;
14552
+ for (let i = 0; i < targetRuns.length; i++) {
14553
+ const run = targetRuns[i];
14554
+ const globalIdx = allRuns.indexOf(run);
14555
+ const previousRun = globalIdx > 0 ? allRuns[globalIdx - 1] : null;
14556
+ const result = this.analyzeRunWithPrevious(run, previousRun);
14557
+ if (result) {
14558
+ processed++;
14559
+ totalInsights += result.insights.length;
14560
+ onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: result.insights.length });
14561
+ } else {
14562
+ skipped++;
14563
+ onProgress?.({ runId: run.id, index: i + 1, total: targetRuns.length, insights: 0 });
14564
+ }
14565
+ }
14566
+ return { processed, skipped, totalInsights };
14567
+ }
14568
+ persistResult(result, runId, projectId) {
14569
+ const previouslyDismissed = /* @__PURE__ */ new Set();
14570
+ 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();
14571
+ for (const row of existingInsights) {
14572
+ if (row.dismissed) {
14573
+ previouslyDismissed.add(`${row.keyword}:${row.provider}:${row.type}`);
14574
+ }
14575
+ }
14576
+ this.db.transaction((tx) => {
14577
+ tx.delete(insights).where(eq23(insights.runId, runId)).run();
14578
+ tx.delete(healthSnapshots).where(eq23(healthSnapshots.runId, runId)).run();
14579
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14580
+ for (const insight of result.insights) {
14581
+ const wasDismissed = previouslyDismissed.has(`${insight.keyword}:${insight.provider}:${insight.type}`);
14582
+ tx.insert(insights).values({
14583
+ id: insight.id,
14584
+ projectId,
14585
+ runId,
14586
+ type: insight.type,
14587
+ severity: insight.severity,
14588
+ title: insight.title,
14589
+ keyword: insight.keyword,
14590
+ provider: insight.provider,
14591
+ recommendation: insight.recommendation ? JSON.stringify(insight.recommendation) : null,
14592
+ cause: insight.cause ? JSON.stringify(insight.cause) : null,
14593
+ dismissed: wasDismissed,
14594
+ createdAt: insight.createdAt
14595
+ }).run();
14596
+ }
14597
+ tx.insert(healthSnapshots).values({
14598
+ id: crypto22.randomUUID(),
14599
+ projectId,
14600
+ runId,
14601
+ overallCitedRate: String(result.health.overallCitedRate),
14602
+ totalPairs: result.health.totalPairs,
14603
+ citedPairs: result.health.citedPairs,
14604
+ providerBreakdown: JSON.stringify(result.health.providerBreakdown),
14605
+ createdAt: now
14606
+ }).run();
14607
+ });
14608
+ log6.info("intelligence.persisted", { runId, insights: result.insights.length });
14609
+ }
14610
+ buildRunData(runId, projectId, completedAt) {
14611
+ const rows = this.db.select({
14612
+ keyword: keywords.keyword,
14613
+ provider: querySnapshots.provider,
14614
+ citationState: querySnapshots.citationState,
14615
+ citedDomains: querySnapshots.citedDomains,
14616
+ competitorOverlap: querySnapshots.competitorOverlap
14617
+ }).from(querySnapshots).leftJoin(keywords, eq23(querySnapshots.keywordId, keywords.id)).where(eq23(querySnapshots.runId, runId)).all();
14618
+ const snapshots = rows.map((r) => {
14619
+ const domains = parseJsonColumn(r.citedDomains, []);
14620
+ const competitors2 = parseJsonColumn(r.competitorOverlap, []);
14621
+ return {
14622
+ keyword: r.keyword ?? "",
14623
+ provider: r.provider,
14624
+ cited: r.citationState === "cited",
14625
+ citationUrl: domains[0] ?? void 0,
14626
+ competitorDomain: competitors2[0] ?? void 0
14627
+ };
14628
+ });
14629
+ return { runId, projectId, completedAt, snapshots };
14630
+ }
14631
+ };
14632
+
14633
+ // src/run-coordinator.ts
14634
+ var log7 = createLogger("RunCoordinator");
14635
+ var RunCoordinator = class {
14636
+ constructor(notifier, intelligenceService) {
14637
+ this.notifier = notifier;
14638
+ this.intelligenceService = intelligenceService;
14639
+ }
14640
+ async onRunCompleted(runId, projectId) {
14641
+ try {
14642
+ this.intelligenceService.analyzeAndPersist(runId, projectId);
14643
+ } catch (err) {
14644
+ log7.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14645
+ }
14646
+ try {
14647
+ await this.notifier.onRunCompleted(runId, projectId);
14648
+ } catch (err) {
14649
+ log7.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
14650
+ }
14651
+ }
14652
+ };
14653
+
13651
14654
  // src/snapshot-service.ts
13652
14655
  import { runAeoAudit } from "@ainyc/aeo-audit";
13653
14656
 
@@ -13768,7 +14771,7 @@ function formatAuditFactorScore(factor) {
13768
14771
  }
13769
14772
 
13770
14773
  // src/snapshot-service.ts
13771
- var log6 = createLogger("Snapshot");
14774
+ var log8 = createLogger("Snapshot");
13772
14775
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
13773
14776
  var SNAPSHOT_QUERY_COUNT = 6;
13774
14777
  var ProviderExecutionGate2 = class {
@@ -13911,7 +14914,7 @@ var SnapshotService = class {
13911
14914
  return mapAuditReport(report);
13912
14915
  } catch (err) {
13913
14916
  const message = err instanceof Error ? err.message : String(err);
13914
- log6.warn("audit.failed", { homepageUrl, error: message });
14917
+ log8.warn("audit.failed", { homepageUrl, error: message });
13915
14918
  return {
13916
14919
  url: homepageUrl,
13917
14920
  finalUrl: homepageUrl,
@@ -13941,7 +14944,7 @@ var SnapshotService = class {
13941
14944
  phrases: parsedPhrases
13942
14945
  };
13943
14946
  } catch (err) {
13944
- log6.warn("profile.generation-failed", {
14947
+ log8.warn("profile.generation-failed", {
13945
14948
  domain: ctx.domain,
13946
14949
  provider: ctx.analysisProvider.adapter.name,
13947
14950
  error: err instanceof Error ? err.message : String(err)
@@ -14083,7 +15086,7 @@ var SnapshotService = class {
14083
15086
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
14084
15087
  };
14085
15088
  } catch (err) {
14086
- log6.warn("response.analysis-failed", {
15089
+ log8.warn("response.analysis-failed", {
14087
15090
  provider: ctx.analysisProvider.adapter.name,
14088
15091
  error: err instanceof Error ? err.message : String(err)
14089
15092
  });
@@ -14368,7 +15371,7 @@ function clipText(value, length) {
14368
15371
  // src/server.ts
14369
15372
  var _require2 = createRequire2(import.meta.url);
14370
15373
  var { version: PKG_VERSION } = _require2("../package.json");
14371
- var log7 = createLogger("Server");
15374
+ var log9 = createLogger("Server");
14372
15375
  var DEFAULT_QUOTA = {
14373
15376
  maxConcurrency: 2,
14374
15377
  maxRequestsPerMinute: 10,
@@ -14399,7 +15402,7 @@ function summarizeProviderConfig(provider, config) {
14399
15402
  };
14400
15403
  }
14401
15404
  function hashApiKey(key) {
14402
- return crypto22.createHash("sha256").update(key).digest("hex");
15405
+ return crypto23.createHash("sha256").update(key).digest("hex");
14403
15406
  }
14404
15407
  function parseCookies2(header) {
14405
15408
  if (!header) return {};
@@ -14458,7 +15461,7 @@ async function createServer(opts) {
14458
15461
  quota: opts.config.geminiQuota
14459
15462
  };
14460
15463
  }
14461
- log7.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
15464
+ log9.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
14462
15465
  const p = providers[k];
14463
15466
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
14464
15467
  }) });
@@ -14494,7 +15497,9 @@ async function createServer(opts) {
14494
15497
  const jobRunner = new JobRunner(opts.db, registry);
14495
15498
  jobRunner.recoverStaleRuns();
14496
15499
  const notifier = new Notifier(opts.db, serverUrl);
14497
- jobRunner.onRunCompleted = (runId, projectId) => notifier.onRunCompleted(runId, projectId);
15500
+ const intelligenceService = new IntelligenceService(opts.db);
15501
+ const runCoordinator = new RunCoordinator(notifier, intelligenceService);
15502
+ jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
14498
15503
  const snapshotService = new SnapshotService(registry);
14499
15504
  const scheduler = new Scheduler(opts.db, {
14500
15505
  onRunCreated: (runId, projectId, providers2) => {
@@ -14571,7 +15576,7 @@ async function createServer(opts) {
14571
15576
  return removed;
14572
15577
  }
14573
15578
  };
14574
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
15579
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
14575
15580
  const googleConnectionStore = {
14576
15581
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
14577
15582
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -14617,11 +15622,11 @@ async function createServer(opts) {
14617
15622
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
14618
15623
  if (opts.config.apiKey) {
14619
15624
  const keyHash = hashApiKey(opts.config.apiKey);
14620
- const existing = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, keyHash)).get();
15625
+ const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
14621
15626
  if (!existing) {
14622
15627
  const prefix = opts.config.apiKey.slice(0, 12);
14623
15628
  opts.db.insert(apiKeys).values({
14624
- id: `key_${crypto22.randomBytes(8).toString("hex")}`,
15629
+ id: `key_${crypto23.randomBytes(8).toString("hex")}`,
14625
15630
  name: "default",
14626
15631
  keyHash,
14627
15632
  keyPrefix: prefix,
@@ -14645,7 +15650,7 @@ async function createServer(opts) {
14645
15650
  };
14646
15651
  const createSession = (apiKeyId) => {
14647
15652
  pruneExpiredSessions();
14648
- const sessionId = crypto22.randomBytes(32).toString("hex");
15653
+ const sessionId = crypto23.randomBytes(32).toString("hex");
14649
15654
  sessions.set(sessionId, {
14650
15655
  apiKeyId,
14651
15656
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -14669,7 +15674,7 @@ async function createServer(opts) {
14669
15674
  };
14670
15675
  const getDefaultApiKey = () => {
14671
15676
  if (!opts.config.apiKey) return void 0;
14672
- return opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
15677
+ return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
14673
15678
  };
14674
15679
  const createPasswordSession = (reply) => {
14675
15680
  const key = getDefaultApiKey();
@@ -14726,12 +15731,12 @@ async function createServer(opts) {
14726
15731
  return reply.send({ authenticated: true });
14727
15732
  }
14728
15733
  if (apiKey) {
14729
- const key = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(apiKey))).get();
15734
+ const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
14730
15735
  if (!key || key.revokedAt) {
14731
15736
  const err2 = authInvalid();
14732
15737
  return reply.status(err2.statusCode).send(err2.toJSON());
14733
15738
  }
14734
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(apiKeys.id, key.id)).run();
15739
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
14735
15740
  const sessionId = createSession(key.id);
14736
15741
  reply.header("set-cookie", serializeSessionCookie({
14737
15742
  name: SESSION_COOKIE_NAME,
@@ -14876,7 +15881,7 @@ async function createServer(opts) {
14876
15881
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
14877
15882
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
14878
15883
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
14879
- id: crypto22.randomUUID(),
15884
+ id: crypto23.randomUUID(),
14880
15885
  projectId,
14881
15886
  actor: "api",
14882
15887
  action: existing ? "provider.updated" : "provider.created",
@@ -15142,6 +16147,7 @@ export {
15142
16147
  notificationEventSchema,
15143
16148
  effectiveDomains,
15144
16149
  determineAnswerMentioned,
16150
+ IntelligenceService,
15145
16151
  setGoogleAuthConfig,
15146
16152
  formatAuditFactorScore,
15147
16153
  createServer