@canonry/canonry 4.117.2 → 4.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +53 -1
  2. package/assets/assets/AuditHistoryPanel-Yh7VEe4P.js +1 -0
  3. package/assets/assets/{BacklinksPage-CschVQxC.js → BacklinksPage-BmO77kMs.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-D4782Ifx.js → ChartPrimitives-H0ICVpw_.js} +1 -1
  5. package/assets/assets/HistoryPage-DkLV_iRc.js +1 -0
  6. package/assets/assets/ProjectPage-R3odr_eA.js +7 -0
  7. package/assets/assets/{RunRow-ggNGlwdN.js → RunRow-BDH9agxQ.js} +1 -1
  8. package/assets/assets/RunsPage-D9YsEeT3.js +1 -0
  9. package/assets/assets/{SettingsPage-Cy9SlBJX.js → SettingsPage-o5as3Ixw.js} +1 -1
  10. package/assets/assets/{TrafficPage-1S2PzSHB.js → TrafficPage-BCK203aQ.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-CqXCoaod.js → TrafficSourceDetailPage-DsVMAZvS.js} +1 -1
  12. package/assets/assets/{arrow-left-kCK5M8Bp.js → arrow-left-B7ZR6VC9.js} +1 -1
  13. package/assets/assets/{extract-error-message-C82LAW6q.js → extract-error-message-DeYDUpew.js} +1 -1
  14. package/assets/assets/index-Bot6YiD6.css +1 -0
  15. package/assets/assets/index-QCFQv0xh.js +210 -0
  16. package/assets/assets/{trash-2-8Pe4v1T0.js → trash-2-xxHTqWbF.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-4PMMEFME.js → chunk-75UBTCKS.js} +499 -16
  19. package/dist/{chunk-BMPIV35C.js → chunk-CMZIPFNQ.js} +937 -104
  20. package/dist/{chunk-F2SRZJMY.js → chunk-HGNY5GL4.js} +384 -15
  21. package/dist/{chunk-EZME2J2G.js → chunk-KEZWLP4O.js} +1196 -1012
  22. package/dist/cli.js +319 -86
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-GNOE4J35.js → intelligence-service-FWPXMAVK.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +8 -8
  27. package/assets/assets/ProjectPage-BSGTpBUJ.js +0 -7
  28. package/assets/assets/RunsPage-Bfp0K6Ry.js +0 -1
  29. package/assets/assets/index-DrDNyUhv.css +0 -1
  30. package/assets/assets/index-i-t64RXw.js +0 -210
@@ -1,7 +1,12 @@
1
1
  import {
2
+ ADS_WRITE_SCOPE,
2
3
  AGENT_PROVIDER_IDS,
3
4
  AI_ENGINE_DOMAINS,
4
5
  AI_PROVIDER_INFRA_DOMAINS,
6
+ AdsEntityStatuses,
7
+ AdsEntityTypes,
8
+ AdsOperationKinds,
9
+ AdsOperationStates,
5
10
  AiReferralTrafficClasses,
6
11
  AppError,
7
12
  BacklinkSources,
@@ -38,19 +43,29 @@ import {
38
43
  __export,
39
44
  absolutizeProjectUrl,
40
45
  actionConfidenceLabel,
46
+ adsAdCreateRequestSchema,
47
+ adsAdGroupCreateRequestSchema,
48
+ adsAdGroupUpdateRequestSchema,
49
+ adsAdUpdateRequestSchema,
50
+ adsCampaignCreateRequestSchema,
41
51
  adsCampaignListResponseSchema,
52
+ adsCampaignUpdateRequestSchema,
42
53
  adsConnectRequestSchema,
43
54
  adsConnectionStatusDtoSchema,
44
55
  adsCpcMicros,
45
56
  adsCtr,
46
57
  adsDisconnectResponseSchema,
58
+ adsImageUploadRequestSchema,
47
59
  adsInsightLevelSchema,
48
60
  adsInsightsResponseSchema,
61
+ adsOperationResponseSchema,
62
+ adsPauseRequestSchema,
49
63
  adsSummaryDtoSchema,
50
64
  adsSyncResponseSchema,
51
65
  agentProvidersResponseDtoSchema,
52
66
  aggregateHarvestedQueries,
53
67
  aiReferralClassCounts,
68
+ alreadyExists,
54
69
  apiKeyDtoSchema,
55
70
  apiKeyListDtoSchema,
56
71
  applyHarvestSemanticNovelty,
@@ -173,6 +188,7 @@ import {
173
188
  gscSitemapListResponseDtoSchema,
174
189
  gscUrlInspectionDtoSchema,
175
190
  hasLocationLabel,
191
+ healthSnapshotDtoSchema,
176
192
  hostOf,
177
193
  indexingRequestResponseDtoSchema,
178
194
  internalError,
@@ -283,10 +299,10 @@ import {
283
299
  wordpressSchemaDeployResultDtoSchema,
284
300
  wordpressSchemaStatusResultDtoSchema,
285
301
  wordpressStatusDtoSchema
286
- } from "./chunk-EZME2J2G.js";
302
+ } from "./chunk-KEZWLP4O.js";
287
303
 
288
304
  // src/intelligence-service.ts
289
- import { eq as eq40, desc as desc18, asc as asc7, and as and31, ne as ne5, or as or6, inArray as inArray15, gte as gte8, lte as lte5 } from "drizzle-orm";
305
+ import { eq as eq40, desc as desc18, asc as asc8, and as and31, ne as ne5, or as or6, inArray as inArray15, gte as gte9, lte as lte5 } from "drizzle-orm";
290
306
 
291
307
  // ../db/src/client.ts
292
308
  import { mkdirSync } from "fs";
@@ -302,6 +318,7 @@ __export(schema_exports, {
302
318
  adsCampaigns: () => adsCampaigns,
303
319
  adsConnections: () => adsConnections,
304
320
  adsInsightsDaily: () => adsInsightsDaily,
321
+ adsOperations: () => adsOperations,
305
322
  agentMemory: () => agentMemory,
306
323
  agentSessions: () => agentSessions,
307
324
  agentToolEvents: () => agentToolEvents,
@@ -1444,11 +1461,33 @@ var adsConnections = sqliteTable("ads_connections", {
1444
1461
  }, (table) => [
1445
1462
  uniqueIndex("idx_ads_conn_project").on(table.projectId)
1446
1463
  ]);
1464
+ var adsOperations = sqliteTable("ads_operations", {
1465
+ id: text("id").primaryKey(),
1466
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1467
+ operationKey: text("operation_key").notNull(),
1468
+ requestHash: text("request_hash").notNull(),
1469
+ kind: text("kind").notNull(),
1470
+ state: text("state").notNull(),
1471
+ entityType: text("entity_type"),
1472
+ entityId: text("entity_id"),
1473
+ upstreamUpdatedAt: integer("upstream_updated_at"),
1474
+ errorCode: text("error_code"),
1475
+ errorMessage: text("error_message"),
1476
+ createdAt: text("created_at").notNull(),
1477
+ updatedAt: text("updated_at").notNull()
1478
+ }, (table) => [
1479
+ uniqueIndex("idx_ads_operations_project_key").on(table.projectId, table.operationKey),
1480
+ index("idx_ads_operations_project_created").on(table.projectId, table.createdAt),
1481
+ index("idx_ads_operations_project_state").on(table.projectId, table.state)
1482
+ ]);
1447
1483
  var adsCampaigns = sqliteTable("ads_campaigns", {
1448
1484
  id: text("id").primaryKey(),
1449
1485
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1450
1486
  name: text("name").notNull(),
1487
+ description: text("description"),
1451
1488
  status: text("status").notNull(),
1489
+ startTime: integer("start_time"),
1490
+ endTime: integer("end_time"),
1452
1491
  biddingType: text("bidding_type"),
1453
1492
  dailySpendLimitMicros: integer("daily_spend_limit_micros"),
1454
1493
  lifetimeSpendLimitMicros: integer("lifetime_spend_limit_micros"),
@@ -1465,6 +1504,7 @@ var adsAdGroups = sqliteTable("ads_ad_groups", {
1465
1504
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1466
1505
  campaignId: text("campaign_id").notNull().references(() => adsCampaigns.id, { onDelete: "cascade" }),
1467
1506
  name: text("name").notNull(),
1507
+ description: text("description"),
1468
1508
  status: text("status").notNull(),
1469
1509
  billingEventType: text("billing_event_type"),
1470
1510
  maxBidMicros: integer("max_bid_micros"),
@@ -3600,6 +3640,34 @@ var MIGRATION_VERSIONS = [
3600
3640
  run: (tx) => {
3601
3641
  relinkOrphanedSnapshotQueryIds(tx);
3602
3642
  }
3643
+ },
3644
+ {
3645
+ version: 99,
3646
+ name: "ads-operator-lifecycle",
3647
+ statements: [
3648
+ `ALTER TABLE ads_campaigns ADD COLUMN description TEXT`,
3649
+ `ALTER TABLE ads_campaigns ADD COLUMN start_time INTEGER`,
3650
+ `ALTER TABLE ads_campaigns ADD COLUMN end_time INTEGER`,
3651
+ `ALTER TABLE ads_ad_groups ADD COLUMN description TEXT`,
3652
+ `CREATE TABLE IF NOT EXISTS ads_operations (
3653
+ id TEXT PRIMARY KEY,
3654
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3655
+ operation_key TEXT NOT NULL,
3656
+ request_hash TEXT NOT NULL,
3657
+ kind TEXT NOT NULL,
3658
+ state TEXT NOT NULL,
3659
+ entity_type TEXT,
3660
+ entity_id TEXT,
3661
+ upstream_updated_at INTEGER,
3662
+ error_code TEXT,
3663
+ error_message TEXT,
3664
+ created_at TEXT NOT NULL,
3665
+ updated_at TEXT NOT NULL
3666
+ )`,
3667
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ads_operations_project_key ON ads_operations(project_id, operation_key)`,
3668
+ `CREATE INDEX IF NOT EXISTS idx_ads_operations_project_created ON ads_operations(project_id, created_at)`,
3669
+ `CREATE INDEX IF NOT EXISTS idx_ads_operations_project_state ON ads_operations(project_id, state)`
3670
+ ]
3603
3671
  }
3604
3672
  ];
3605
3673
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -5948,6 +6016,14 @@ import fs8 from "fs";
5948
6016
  import crypto2 from "crypto";
5949
6017
  import { eq } from "drizzle-orm";
5950
6018
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
6019
+ function isAdsWriteOnlyKey(scopes) {
6020
+ const writeGrants = scopes.filter((scope) => scope === "*" || scope === "write" || scope.endsWith(".write"));
6021
+ return writeGrants.length === 1 && writeGrants[0] === ADS_WRITE_SCOPE;
6022
+ }
6023
+ function isAdsWriteRoute(url) {
6024
+ const rest = projectRouteRest(url);
6025
+ return rest !== null && rest.startsWith("ads/");
6026
+ }
5951
6027
  function requireScope(request, scope) {
5952
6028
  const key = request.apiKey;
5953
6029
  if (!key) return;
@@ -6100,6 +6176,9 @@ async function authPlugin(app, opts = {}) {
6100
6176
  if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
6101
6177
  throw forbidden("This API key is read-only and cannot perform write operations.");
6102
6178
  }
6179
+ if (isAdsWriteOnlyKey(scopes) && WRITE_METHODS.has(request.method) && !isAdsWriteRoute(url)) {
6180
+ throw forbidden("This API key can only perform OpenAI Ads write operations.");
6181
+ }
6103
6182
  enforceEmbedProjectTabs(request, opts.embedProjectTabs);
6104
6183
  if (key.projectId) {
6105
6184
  const match = url.match(/\/projects\/([^/?#]+)/);
@@ -7994,7 +8073,7 @@ function normalizeCompetitorList2(domains) {
7994
8073
  }
7995
8074
 
7996
8075
  // ../api-routes/src/history.ts
7997
- import { and as and6, eq as eq10, desc as desc2, inArray as inArray3 } from "drizzle-orm";
8076
+ import { and as and6, asc as asc2, desc as desc2, eq as eq10, gte as gte2, inArray as inArray3 } from "drizzle-orm";
7998
8077
 
7999
8078
  // ../api-routes/src/notification-redaction.ts
8000
8079
  var REDACTED_URL = {
@@ -8040,12 +8119,16 @@ function redactNotificationDiff(value) {
8040
8119
  async function historyRoutes(app) {
8041
8120
  app.get("/projects/:name/history", async (request, reply) => {
8042
8121
  const project = resolveProject(app.db, request.params.name);
8043
- const rows = app.db.select().from(auditLog).where(eq10(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
8122
+ const filters = [eq10(auditLog.projectId, project.id)];
8123
+ addAuditHistoryFilters(filters, request.query);
8124
+ const rows = app.db.select().from(auditLog).where(and6(...filters)).orderBy(desc2(auditLog.createdAt)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
8044
8125
  return reply.send(rows.map(formatAuditEntry));
8045
8126
  });
8046
8127
  app.get("/history", async (request, reply) => {
8047
8128
  const scopedProjectId = request.apiKey?.projectId;
8048
- const rows = app.db.select().from(auditLog).where(scopedProjectId ? eq10(auditLog.projectId, scopedProjectId) : void 0).orderBy(desc2(auditLog.createdAt)).all();
8129
+ const filters = scopedProjectId ? [eq10(auditLog.projectId, scopedProjectId)] : [];
8130
+ addAuditHistoryFilters(filters, request.query);
8131
+ const rows = app.db.select().from(auditLog).where(filters.length > 0 ? and6(...filters) : void 0).orderBy(desc2(auditLog.createdAt)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
8049
8132
  return reply.send(rows.map(formatAuditEntry));
8050
8133
  });
8051
8134
  app.get("/projects/:name/snapshots", async (request, reply) => {
@@ -8101,7 +8184,8 @@ async function historyRoutes(app) {
8101
8184
  app.get("/projects/:name/timeline", async (request, reply) => {
8102
8185
  const project = resolveProject(app.db, request.params.name);
8103
8186
  const projectQueries = app.db.select().from(queries).where(eq10(queries.projectId, project.id)).all();
8104
- const projectRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all();
8187
+ const requestedLimit = parseOptionalPositiveInt(request.query.limit, 100);
8188
+ const projectRuns = requestedLimit == null ? app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(asc2(runs.createdAt)).all() : app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(desc2(runs.createdAt)).limit(requestedLimit).all().reverse();
8105
8189
  if (projectRuns.length === 0 || projectQueries.length === 0) {
8106
8190
  return reply.send([]);
8107
8191
  }
@@ -8251,29 +8335,23 @@ async function historyRoutes(app) {
8251
8335
  }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run2)).all();
8252
8336
  const map1 = /* @__PURE__ */ new Map();
8253
8337
  for (const s of snaps1) {
8254
- const resolved = {
8255
- ...s,
8256
- resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
8257
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
8258
- resolvedMentionState: resolveSnapshotMentionState(s, project)
8259
- };
8260
8338
  const existing = map1.get(s.queryId);
8261
- if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
8262
- map1.set(s.queryId, resolved);
8263
- }
8339
+ map1.set(s.queryId, {
8340
+ ...s,
8341
+ query: existing?.query ?? s.query,
8342
+ citationState: existing?.citationState === CitationStates.cited || s.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"],
8343
+ resolvedAnswerMentioned: (existing?.resolvedAnswerMentioned ?? false) || resolveSnapshotAnswerMentioned(s, project)
8344
+ });
8264
8345
  }
8265
8346
  const map2 = /* @__PURE__ */ new Map();
8266
8347
  for (const s of snaps2) {
8267
- const resolved = {
8268
- ...s,
8269
- resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
8270
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
8271
- resolvedMentionState: resolveSnapshotMentionState(s, project)
8272
- };
8273
8348
  const existing = map2.get(s.queryId);
8274
- if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
8275
- map2.set(s.queryId, resolved);
8276
- }
8349
+ map2.set(s.queryId, {
8350
+ ...s,
8351
+ query: existing?.query ?? s.query,
8352
+ citationState: existing?.citationState === CitationStates.cited || s.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"],
8353
+ resolvedAnswerMentioned: (existing?.resolvedAnswerMentioned ?? false) || resolveSnapshotAnswerMentioned(s, project)
8354
+ });
8277
8355
  }
8278
8356
  const allQueryIds = /* @__PURE__ */ new Set([...map1.keys(), ...map2.keys()]);
8279
8357
  const diff = [...allQueryIds].map((qId) => {
@@ -8287,10 +8365,10 @@ async function historyRoutes(app) {
8287
8365
  run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
8288
8366
  run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
8289
8367
  // Legacy aliases — same data as run{1,2}MentionState below.
8290
- run1VisibilityState: s1?.resolvedVisibilityState ?? null,
8291
- run2VisibilityState: s2?.resolvedVisibilityState ?? null,
8292
- run1MentionState: s1?.resolvedMentionState ?? null,
8293
- run2MentionState: s2?.resolvedMentionState ?? null,
8368
+ run1VisibilityState: s1 ? visibilityStateFromAnswerMentioned(s1.resolvedAnswerMentioned) : null,
8369
+ run2VisibilityState: s2 ? visibilityStateFromAnswerMentioned(s2.resolvedAnswerMentioned) : null,
8370
+ run1MentionState: s1 ? mentionStateFromAnswerMentioned(s1.resolvedAnswerMentioned) : null,
8371
+ run2MentionState: s2 ? mentionStateFromAnswerMentioned(s2.resolvedAnswerMentioned) : null,
8294
8372
  changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
8295
8373
  visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
8296
8374
  };
@@ -8307,9 +8385,28 @@ function formatAuditEntry(row) {
8307
8385
  entityType: row.entityType,
8308
8386
  entityId: row.entityId,
8309
8387
  diff: row.diff ? row.entityType === "notification" ? redactNotificationDiff(parseJsonColumn(row.diff, null)) : parseJsonColumn(row.diff, null) : null,
8388
+ userAgent: row.userAgent,
8389
+ actorSession: row.actorSession,
8310
8390
  createdAt: row.createdAt
8311
8391
  };
8312
8392
  }
8393
+ function addAuditHistoryFilters(filters, query) {
8394
+ if (query.since && !Number.isNaN(Date.parse(query.since))) filters.push(gte2(auditLog.createdAt, query.since));
8395
+ if (query.action) filters.push(eq10(auditLog.action, query.action));
8396
+ if (query.actor) filters.push(eq10(auditLog.actor, query.actor));
8397
+ if (query.entityType) filters.push(eq10(auditLog.entityType, query.entityType));
8398
+ }
8399
+ function parseBoundedInt(value, fallback, max) {
8400
+ const parsed = Number.parseInt(value ?? "", 10);
8401
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
8402
+ return Math.min(parsed, max);
8403
+ }
8404
+ function parseOptionalPositiveInt(value, max) {
8405
+ if (value == null) return void 0;
8406
+ const parsed = Number.parseInt(value, 10);
8407
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
8408
+ return Math.min(parsed, max);
8409
+ }
8313
8410
 
8314
8411
  // ../api-routes/src/analytics.ts
8315
8412
  import { and as and7, eq as eq11, desc as desc3, inArray as inArray4 } from "drizzle-orm";
@@ -8973,7 +9070,7 @@ async function intelligenceRoutes(app) {
8973
9070
  }
8974
9071
 
8975
9072
  // ../api-routes/src/report.ts
8976
- import { and as and12, desc as desc7, eq as eq16, gte as gte2, inArray as inArray7, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
9073
+ import { and as and12, desc as desc7, eq as eq16, gte as gte3, inArray as inArray7, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
8977
9074
 
8978
9075
  // ../api-routes/src/content.ts
8979
9076
  import crypto12 from "crypto";
@@ -9709,7 +9806,7 @@ function winnabilityClassRank(winnabilityClass) {
9709
9806
  }
9710
9807
 
9711
9808
  // ../api-routes/src/gsc-totals.ts
9712
- import { and as and11, asc as asc2, eq as eq15, sql as sql6 } from "drizzle-orm";
9809
+ import { and as and11, asc as asc3, eq as eq15, sql as sql6 } from "drizzle-orm";
9713
9810
  function readGscDailyTotals(db, projectId, startDate, endDate) {
9714
9811
  const rows = db.select({
9715
9812
  date: gscDailyTotals.date,
@@ -9722,7 +9819,7 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
9722
9819
  sql6`${gscDailyTotals.date} >= ${startDate}`,
9723
9820
  sql6`${gscDailyTotals.date} <= ${endDate}`
9724
9821
  )
9725
- ).orderBy(asc2(gscDailyTotals.date)).all();
9822
+ ).orderBy(asc3(gscDailyTotals.date)).all();
9726
9823
  return rows.map((r) => {
9727
9824
  const position = Number(r.position);
9728
9825
  return {
@@ -12189,7 +12286,7 @@ function buildGaSection(db, projectId, windowDays) {
12189
12286
  eq16(gaAiReferrals.sourceDimension, "session")
12190
12287
  ];
12191
12288
  if (snapshotStartDate && snapshotMaxDate) {
12192
- aiConditions.push(gte2(gaAiReferrals.date, snapshotStartDate));
12289
+ aiConditions.push(gte3(gaAiReferrals.date, snapshotStartDate));
12193
12290
  aiConditions.push(lte(gaAiReferrals.date, snapshotMaxDate));
12194
12291
  }
12195
12292
  const aiSessionRows = db.select({
@@ -12406,7 +12503,7 @@ function buildServerActivity(db, projectId, windowDays) {
12406
12503
  and12(
12407
12504
  eq16(crawlerEventsHourly.projectId, projectId),
12408
12505
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12409
- gte2(crawlerEventsHourly.tsHour, windowStartIso),
12506
+ gte3(crawlerEventsHourly.tsHour, windowStartIso),
12410
12507
  exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
12411
12508
  )
12412
12509
  ).get()?.total ?? 0
@@ -12416,7 +12513,7 @@ function buildServerActivity(db, projectId, windowDays) {
12416
12513
  and12(
12417
12514
  eq16(crawlerEventsHourly.projectId, projectId),
12418
12515
  ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12419
- gte2(crawlerEventsHourly.tsHour, windowStartIso),
12516
+ gte3(crawlerEventsHourly.tsHour, windowStartIso),
12420
12517
  exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
12421
12518
  )
12422
12519
  ).get()?.total ?? 0
@@ -12430,7 +12527,7 @@ function buildServerActivity(db, projectId, windowDays) {
12430
12527
  and12(
12431
12528
  eq16(aiReferralEventsHourly.projectId, projectId),
12432
12529
  nonSubresourceReferralPathCondition(),
12433
- gte2(aiReferralEventsHourly.tsHour, windowStartIso),
12530
+ gte3(aiReferralEventsHourly.tsHour, windowStartIso),
12434
12531
  exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
12435
12532
  )
12436
12533
  ).get();
@@ -12440,7 +12537,7 @@ function buildServerActivity(db, projectId, windowDays) {
12440
12537
  db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
12441
12538
  and12(
12442
12539
  eq16(aiUserFetchEventsHourly.projectId, projectId),
12443
- gte2(aiUserFetchEventsHourly.tsHour, windowStartIso),
12540
+ gte3(aiUserFetchEventsHourly.tsHour, windowStartIso),
12444
12541
  exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
12445
12542
  )
12446
12543
  ).get()?.total ?? 0
@@ -12460,7 +12557,7 @@ function buildServerActivity(db, projectId, windowDays) {
12460
12557
  }).from(crawlerEventsHourly).where(
12461
12558
  and12(
12462
12559
  eq16(crawlerEventsHourly.projectId, projectId),
12463
- gte2(crawlerEventsHourly.tsHour, headlineStart),
12560
+ gte3(crawlerEventsHourly.tsHour, headlineStart),
12464
12561
  lte(crawlerEventsHourly.tsHour, headlineEnd)
12465
12562
  )
12466
12563
  ).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
@@ -12471,7 +12568,7 @@ function buildServerActivity(db, projectId, windowDays) {
12471
12568
  and12(
12472
12569
  eq16(crawlerEventsHourly.projectId, projectId),
12473
12570
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12474
- gte2(crawlerEventsHourly.tsHour, priorStart),
12571
+ gte3(crawlerEventsHourly.tsHour, priorStart),
12475
12572
  lt(crawlerEventsHourly.tsHour, headlineStart)
12476
12573
  )
12477
12574
  ).groupBy(crawlerEventsHourly.operator).all();
@@ -12482,7 +12579,7 @@ function buildServerActivity(db, projectId, windowDays) {
12482
12579
  and12(
12483
12580
  eq16(aiReferralEventsHourly.projectId, projectId),
12484
12581
  nonSubresourceReferralPathCondition(),
12485
- gte2(aiReferralEventsHourly.tsHour, headlineStart),
12582
+ gte3(aiReferralEventsHourly.tsHour, headlineStart),
12486
12583
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12487
12584
  )
12488
12585
  ).groupBy(aiReferralEventsHourly.operator).all();
@@ -12492,7 +12589,7 @@ function buildServerActivity(db, projectId, windowDays) {
12492
12589
  }).from(aiUserFetchEventsHourly).where(
12493
12590
  and12(
12494
12591
  eq16(aiUserFetchEventsHourly.projectId, projectId),
12495
- gte2(aiUserFetchEventsHourly.tsHour, headlineStart),
12592
+ gte3(aiUserFetchEventsHourly.tsHour, headlineStart),
12496
12593
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
12497
12594
  )
12498
12595
  ).groupBy(aiUserFetchEventsHourly.operator).all();
@@ -12537,7 +12634,7 @@ function buildServerActivity(db, projectId, windowDays) {
12537
12634
  and12(
12538
12635
  eq16(crawlerEventsHourly.projectId, projectId),
12539
12636
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12540
- gte2(crawlerEventsHourly.tsHour, headlineStart),
12637
+ gte3(crawlerEventsHourly.tsHour, headlineStart),
12541
12638
  lte(crawlerEventsHourly.tsHour, headlineEnd)
12542
12639
  )
12543
12640
  ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
@@ -12554,7 +12651,7 @@ function buildServerActivity(db, projectId, windowDays) {
12554
12651
  and12(
12555
12652
  eq16(aiReferralEventsHourly.projectId, projectId),
12556
12653
  nonSubresourceReferralPathCondition(),
12557
- gte2(aiReferralEventsHourly.tsHour, headlineStart),
12654
+ gte3(aiReferralEventsHourly.tsHour, headlineStart),
12558
12655
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12559
12656
  )
12560
12657
  ).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
@@ -12571,7 +12668,7 @@ function buildServerActivity(db, projectId, windowDays) {
12571
12668
  and12(
12572
12669
  eq16(aiReferralEventsHourly.projectId, projectId),
12573
12670
  nonSubresourceReferralPathCondition(),
12574
- gte2(aiReferralEventsHourly.tsHour, headlineStart),
12671
+ gte3(aiReferralEventsHourly.tsHour, headlineStart),
12575
12672
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12576
12673
  )
12577
12674
  ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
@@ -12587,7 +12684,7 @@ function buildServerActivity(db, projectId, windowDays) {
12587
12684
  and12(
12588
12685
  eq16(crawlerEventsHourly.projectId, projectId),
12589
12686
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12590
- gte2(crawlerEventsHourly.tsHour, trendStart),
12687
+ gte3(crawlerEventsHourly.tsHour, trendStart),
12591
12688
  lte(crawlerEventsHourly.tsHour, headlineEnd)
12592
12689
  )
12593
12690
  ).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
@@ -12598,7 +12695,7 @@ function buildServerActivity(db, projectId, windowDays) {
12598
12695
  and12(
12599
12696
  eq16(aiReferralEventsHourly.projectId, projectId),
12600
12697
  nonSubresourceReferralPathCondition(),
12601
- gte2(aiReferralEventsHourly.tsHour, trendStart),
12698
+ gte3(aiReferralEventsHourly.tsHour, trendStart),
12602
12699
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12603
12700
  )
12604
12701
  ).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
@@ -12608,7 +12705,7 @@ function buildServerActivity(db, projectId, windowDays) {
12608
12705
  }).from(aiUserFetchEventsHourly).where(
12609
12706
  and12(
12610
12707
  eq16(aiUserFetchEventsHourly.projectId, projectId),
12611
- gte2(aiUserFetchEventsHourly.tsHour, trendStart),
12708
+ gte3(aiUserFetchEventsHourly.tsHour, trendStart),
12612
12709
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
12613
12710
  )
12614
12711
  ).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
@@ -14243,7 +14340,7 @@ async function visibilityStatsRoutes(app) {
14243
14340
  }
14244
14341
 
14245
14342
  // ../api-routes/src/results-export.ts
14246
- import { and as and15, asc as asc3, eq as eq19, gte as gte3, lte as lte2 } from "drizzle-orm";
14343
+ import { and as and15, asc as asc4, eq as eq19, gte as gte4, lte as lte2 } from "drizzle-orm";
14247
14344
  var CSV_COLUMNS = [
14248
14345
  "export_schema_version",
14249
14346
  "project_id",
@@ -14373,7 +14470,7 @@ async function resultsExportRoutes(app) {
14373
14470
  eq19(runs.projectId, project.id),
14374
14471
  eq19(runs.kind, RunKinds["answer-visibility"]),
14375
14472
  ...includeProbes ? [] : [notProbeRun()],
14376
- ...sinceIso ? [gte3(runs.createdAt, sinceIso)] : [],
14473
+ ...sinceIso ? [gte4(runs.createdAt, sinceIso)] : [],
14377
14474
  ...untilIso ? [lte2(runs.createdAt, untilIso)] : []
14378
14475
  ];
14379
14476
  const rows = app.db.select({
@@ -14398,7 +14495,7 @@ async function resultsExportRoutes(app) {
14398
14495
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
14399
14496
  answerText: querySnapshots.answerText,
14400
14497
  rawResponse: querySnapshots.rawResponse
14401
- }).from(querySnapshots).innerJoin(runs, eq19(querySnapshots.runId, runs.id)).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(and15(...conditions)).orderBy(asc3(runs.createdAt), asc3(runs.id), asc3(querySnapshots.createdAt), asc3(querySnapshots.id)).all();
14498
+ }).from(querySnapshots).innerJoin(runs, eq19(querySnapshots.runId, runs.id)).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(and15(...conditions)).orderBy(asc4(runs.createdAt), asc4(runs.id), asc4(querySnapshots.createdAt), asc4(querySnapshots.id)).all();
14402
14499
  const records = rows.map((row) => {
14403
14500
  const evidence = readEvidence(row.rawResponse);
14404
14501
  const citationState = row.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"];
@@ -15166,6 +15263,7 @@ var SCHEMA_TABLE = {
15166
15263
  AdsConnectionStatusDto: adsConnectionStatusDtoSchema,
15167
15264
  AdsDisconnectResponse: adsDisconnectResponseSchema,
15168
15265
  AdsInsightsResponse: adsInsightsResponseSchema,
15266
+ AdsOperationResponse: adsOperationResponseSchema,
15169
15267
  AdsSummaryDto: adsSummaryDtoSchema,
15170
15268
  AdsSyncResponse: adsSyncResponseSchema,
15171
15269
  ApiKeyDto: apiKeyDtoSchema,
@@ -15234,6 +15332,7 @@ var SCHEMA_TABLE = {
15234
15332
  GscSiteListResponseDto: gscSiteListResponseDtoSchema,
15235
15333
  GscSitemapListResponseDto: gscSitemapListResponseDtoSchema,
15236
15334
  GscUrlInspectionDto: gscUrlInspectionDtoSchema,
15335
+ HealthSnapshotDto: healthSnapshotDtoSchema,
15237
15336
  IndexingRequestResponseDto: indexingRequestResponseDtoSchema,
15238
15337
  KeywordDto: keywordDtoSchema,
15239
15338
  LatestProjectRunDto: latestProjectRunDtoSchema,
@@ -15347,6 +15446,17 @@ var booleanSchema = { type: "boolean" };
15347
15446
  var integerSchema = { type: "integer" };
15348
15447
  var objectSchema = { type: "object", additionalProperties: true };
15349
15448
  var stringArraySchema = { type: "array", items: stringSchema };
15449
+ var adsOperationKeySchema = { type: "string", minLength: 8, maxLength: 128 };
15450
+ var adsCreativeRequestSchema = {
15451
+ type: "object",
15452
+ required: ["title", "body", "targetUrl", "fileId"],
15453
+ properties: {
15454
+ title: { type: "string", minLength: 3, maxLength: 50 },
15455
+ body: { type: "string", minLength: 1, maxLength: 100 },
15456
+ targetUrl: { type: "string", format: "uri" },
15457
+ fileId: stringSchema
15458
+ }
15459
+ };
15350
15460
  var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4", "gbp"] };
15351
15461
  var locationSchema = {
15352
15462
  type: "object",
@@ -16291,7 +16401,15 @@ var routeCatalog = [
16291
16401
  path: "/api/v1/projects/{name}/history",
16292
16402
  summary: "Get project audit history",
16293
16403
  tags: ["history"],
16294
- parameters: [nameParameter],
16404
+ parameters: [
16405
+ nameParameter,
16406
+ limitQueryParameter,
16407
+ offsetQueryParameter,
16408
+ { name: "since", in: "query", description: "ISO 8601 lower bound.", schema: stringSchema },
16409
+ { name: "action", in: "query", description: "Exact audit action filter.", schema: stringSchema },
16410
+ { name: "actor", in: "query", description: "Exact actor filter.", schema: stringSchema },
16411
+ { name: "entityType", in: "query", description: "Exact entity type filter.", schema: stringSchema }
16412
+ ],
16295
16413
  responses: {
16296
16414
  200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
16297
16415
  }
@@ -16301,6 +16419,14 @@ var routeCatalog = [
16301
16419
  path: "/api/v1/history",
16302
16420
  summary: "Get global audit history",
16303
16421
  tags: ["history"],
16422
+ parameters: [
16423
+ limitQueryParameter,
16424
+ offsetQueryParameter,
16425
+ { name: "since", in: "query", description: "ISO 8601 lower bound.", schema: stringSchema },
16426
+ { name: "action", in: "query", description: "Exact audit action filter.", schema: stringSchema },
16427
+ { name: "actor", in: "query", description: "Exact actor filter.", schema: stringSchema },
16428
+ { name: "entityType", in: "query", description: "Exact entity type filter.", schema: stringSchema }
16429
+ ],
16304
16430
  responses: {
16305
16431
  200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
16306
16432
  }
@@ -16325,7 +16451,11 @@ var routeCatalog = [
16325
16451
  path: "/api/v1/projects/{name}/timeline",
16326
16452
  summary: "Get query timeline",
16327
16453
  tags: ["history"],
16328
- parameters: [nameParameter, locationQueryParameter],
16454
+ parameters: [
16455
+ nameParameter,
16456
+ locationQueryParameter,
16457
+ { name: "limit", in: "query", description: "Restrict each query timeline to snapshots from the most recent N project runs. Omit for full history.", schema: { ...integerSchema, minimum: 1, maximum: 100 } }
16458
+ ],
16329
16459
  responses: {
16330
16460
  // TODO: Add `ProjectTimelineDto` Zod schema in contracts.
16331
16461
  200: rawJsonResponse("Timeline returned.", looseObjectSchema)
@@ -17446,6 +17576,7 @@ var routeCatalog = [
17446
17576
  responses: {
17447
17577
  200: jsonResponse("Connected; key validated against the upstream ad account.", "AdsConnectionStatusDto"),
17448
17578
  400: errorResponse("Missing/invalid key or credential storage unavailable."),
17579
+ 403: errorResponse("The key lacks ads.write."),
17449
17580
  404: errorResponse("Project not found.")
17450
17581
  }
17451
17582
  },
@@ -17457,6 +17588,7 @@ var routeCatalog = [
17457
17588
  parameters: [nameParameter],
17458
17589
  responses: {
17459
17590
  200: jsonResponse("Disconnected (idempotent).", "AdsDisconnectResponse"),
17591
+ 403: errorResponse("The key lacks ads.write."),
17460
17592
  404: errorResponse("Project not found.")
17461
17593
  }
17462
17594
  },
@@ -17471,6 +17603,288 @@ var routeCatalog = [
17471
17603
  404: errorResponse("Project not found.")
17472
17604
  }
17473
17605
  },
17606
+ {
17607
+ method: "get",
17608
+ path: "/api/v1/projects/{name}/ads/operations/{operationKey}",
17609
+ summary: "Read a durable OpenAI Ads mutation receipt by operation key",
17610
+ tags: ["ads"],
17611
+ parameters: [
17612
+ nameParameter,
17613
+ { name: "operationKey", in: "path", required: true, description: "Caller-supplied idempotency key.", schema: adsOperationKeySchema }
17614
+ ],
17615
+ responses: {
17616
+ 200: jsonResponse("Operation receipt.", "AdsOperationResponse"),
17617
+ 404: errorResponse("Project or operation not found.")
17618
+ }
17619
+ },
17620
+ {
17621
+ method: "post",
17622
+ path: "/api/v1/projects/{name}/ads/files",
17623
+ summary: "Upload an OpenAI Ads image from a public HTTPS URL",
17624
+ tags: ["ads"],
17625
+ parameters: [nameParameter],
17626
+ requestBody: {
17627
+ required: true,
17628
+ content: { "application/json": { schema: {
17629
+ type: "object",
17630
+ required: ["operationKey", "imageUrl"],
17631
+ properties: { operationKey: adsOperationKeySchema, imageUrl: { type: "string", format: "uri" } }
17632
+ } } }
17633
+ },
17634
+ responses: {
17635
+ 200: jsonResponse("Upload receipt.", "AdsOperationResponse"),
17636
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17637
+ 403: errorResponse("The key lacks ads.write."),
17638
+ 404: errorResponse("Project not found."),
17639
+ 409: errorResponse("Operation key was already used for a different request."),
17640
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17641
+ }
17642
+ },
17643
+ {
17644
+ method: "post",
17645
+ path: "/api/v1/projects/{name}/ads/campaigns",
17646
+ summary: "Create a paused OpenAI Ads campaign",
17647
+ description: "The server always creates the campaign paused. Status is not accepted from the caller.",
17648
+ tags: ["ads"],
17649
+ parameters: [nameParameter],
17650
+ requestBody: {
17651
+ required: true,
17652
+ content: { "application/json": { schema: {
17653
+ type: "object",
17654
+ required: ["operationKey", "name", "lifetimeSpendLimitMicros", "locationIds"],
17655
+ properties: {
17656
+ operationKey: adsOperationKeySchema,
17657
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17658
+ description: stringSchema,
17659
+ startTime: integerSchema,
17660
+ endTime: integerSchema,
17661
+ lifetimeSpendLimitMicros: { type: "integer", minimum: 1e6 },
17662
+ locationIds: { type: "array", minItems: 1, maxItems: 100, items: stringSchema }
17663
+ }
17664
+ } } }
17665
+ },
17666
+ responses: {
17667
+ 200: jsonResponse("Paused campaign creation receipt.", "AdsOperationResponse"),
17668
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17669
+ 403: errorResponse("The key lacks ads.write."),
17670
+ 404: errorResponse("Project not found."),
17671
+ 409: errorResponse("Operation key was already used for a different request."),
17672
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17673
+ }
17674
+ },
17675
+ {
17676
+ method: "post",
17677
+ path: "/api/v1/projects/{name}/ads/ad-groups",
17678
+ summary: "Create a paused OpenAI Ads ad group",
17679
+ description: "The server fixes billing to impressions and always creates the ad group paused.",
17680
+ tags: ["ads"],
17681
+ parameters: [nameParameter],
17682
+ requestBody: {
17683
+ required: true,
17684
+ content: { "application/json": { schema: {
17685
+ type: "object",
17686
+ required: ["operationKey", "campaignId", "name", "contextHints", "maxBidMicros"],
17687
+ properties: {
17688
+ operationKey: adsOperationKeySchema,
17689
+ campaignId: stringSchema,
17690
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17691
+ description: stringSchema,
17692
+ contextHints: { type: "array", minItems: 1, maxItems: 100, items: stringSchema },
17693
+ maxBidMicros: { type: "integer", minimum: 1, maximum: 1e8 }
17694
+ }
17695
+ } } }
17696
+ },
17697
+ responses: {
17698
+ 200: jsonResponse("Paused ad-group creation receipt.", "AdsOperationResponse"),
17699
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17700
+ 403: errorResponse("The key lacks ads.write."),
17701
+ 404: errorResponse("Project not found."),
17702
+ 409: errorResponse("Operation key was already used for a different request."),
17703
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17704
+ }
17705
+ },
17706
+ {
17707
+ method: "post",
17708
+ path: "/api/v1/projects/{name}/ads/ads",
17709
+ summary: "Create a paused OpenAI Ads chat-card ad",
17710
+ description: "The server always creates the ad paused and fixes creative.type to chat_card.",
17711
+ tags: ["ads"],
17712
+ parameters: [nameParameter],
17713
+ requestBody: {
17714
+ required: true,
17715
+ content: { "application/json": { schema: {
17716
+ type: "object",
17717
+ required: ["operationKey", "adGroupId", "name", "creative"],
17718
+ properties: {
17719
+ operationKey: adsOperationKeySchema,
17720
+ adGroupId: stringSchema,
17721
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17722
+ creative: adsCreativeRequestSchema
17723
+ }
17724
+ } } }
17725
+ },
17726
+ responses: {
17727
+ 200: jsonResponse("Paused ad creation receipt.", "AdsOperationResponse"),
17728
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17729
+ 403: errorResponse("The key lacks ads.write."),
17730
+ 404: errorResponse("Project not found."),
17731
+ 409: errorResponse("Operation key was already used for a different request."),
17732
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17733
+ }
17734
+ },
17735
+ {
17736
+ method: "post",
17737
+ path: "/api/v1/projects/{name}/ads/campaigns/{id}",
17738
+ summary: "Update an OpenAI Ads campaign with optimistic concurrency",
17739
+ description: "The campaign must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
17740
+ tags: ["ads"],
17741
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Campaign ID.", schema: stringSchema }],
17742
+ requestBody: {
17743
+ required: true,
17744
+ content: { "application/json": { schema: {
17745
+ type: "object",
17746
+ required: ["operationKey", "expectedUpdatedAt"],
17747
+ properties: {
17748
+ operationKey: adsOperationKeySchema,
17749
+ expectedUpdatedAt: integerSchema,
17750
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17751
+ description: { type: "string", nullable: true },
17752
+ startTime: { type: "integer", nullable: true },
17753
+ endTime: { type: "integer", nullable: true },
17754
+ lifetimeSpendLimitMicros: { type: "integer", minimum: 1e6 },
17755
+ locationIds: { type: "array", minItems: 1, maxItems: 100, items: stringSchema }
17756
+ }
17757
+ } } }
17758
+ },
17759
+ responses: {
17760
+ 200: jsonResponse("Campaign update receipt.", "AdsOperationResponse"),
17761
+ 400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
17762
+ 403: errorResponse("The key lacks ads.write."),
17763
+ 404: errorResponse("Project not found."),
17764
+ 409: errorResponse("Operation key was already used for a different request."),
17765
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17766
+ }
17767
+ },
17768
+ {
17769
+ method: "post",
17770
+ path: "/api/v1/projects/{name}/ads/ad-groups/{id}",
17771
+ summary: "Update an OpenAI Ads ad group with optimistic concurrency",
17772
+ description: "The ad group must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
17773
+ tags: ["ads"],
17774
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad group ID.", schema: stringSchema }],
17775
+ requestBody: {
17776
+ required: true,
17777
+ content: { "application/json": { schema: {
17778
+ type: "object",
17779
+ required: ["operationKey", "expectedUpdatedAt"],
17780
+ properties: {
17781
+ operationKey: adsOperationKeySchema,
17782
+ expectedUpdatedAt: integerSchema,
17783
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17784
+ description: { type: "string", nullable: true },
17785
+ contextHints: { type: "array", minItems: 1, maxItems: 100, items: stringSchema },
17786
+ maxBidMicros: { type: "integer", minimum: 1, maximum: 1e8 }
17787
+ }
17788
+ } } }
17789
+ },
17790
+ responses: {
17791
+ 200: jsonResponse("Ad-group update receipt.", "AdsOperationResponse"),
17792
+ 400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
17793
+ 403: errorResponse("The key lacks ads.write."),
17794
+ 404: errorResponse("Project not found."),
17795
+ 409: errorResponse("Operation key was already used for a different request."),
17796
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17797
+ }
17798
+ },
17799
+ {
17800
+ method: "post",
17801
+ path: "/api/v1/projects/{name}/ads/ads/{id}",
17802
+ summary: "Update an OpenAI Ads chat-card ad with optimistic concurrency",
17803
+ description: "The ad must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
17804
+ tags: ["ads"],
17805
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad ID.", schema: stringSchema }],
17806
+ requestBody: {
17807
+ required: true,
17808
+ content: { "application/json": { schema: {
17809
+ type: "object",
17810
+ required: ["operationKey", "expectedUpdatedAt"],
17811
+ properties: {
17812
+ operationKey: adsOperationKeySchema,
17813
+ expectedUpdatedAt: integerSchema,
17814
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17815
+ creative: adsCreativeRequestSchema
17816
+ }
17817
+ } } }
17818
+ },
17819
+ responses: {
17820
+ 200: jsonResponse("Ad update receipt.", "AdsOperationResponse"),
17821
+ 400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
17822
+ 403: errorResponse("The key lacks ads.write."),
17823
+ 404: errorResponse("Project not found."),
17824
+ 409: errorResponse("Operation key was already used for a different request."),
17825
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17826
+ }
17827
+ },
17828
+ {
17829
+ method: "post",
17830
+ path: "/api/v1/projects/{name}/ads/campaigns/{id}/pause",
17831
+ summary: "Pause an OpenAI Ads campaign",
17832
+ tags: ["ads"],
17833
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Campaign ID.", schema: stringSchema }],
17834
+ requestBody: { required: true, content: { "application/json": { schema: {
17835
+ type: "object",
17836
+ required: ["operationKey"],
17837
+ properties: { operationKey: adsOperationKeySchema }
17838
+ } } } },
17839
+ responses: {
17840
+ 200: jsonResponse("Campaign pause receipt.", "AdsOperationResponse"),
17841
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17842
+ 403: errorResponse("The key lacks ads.write."),
17843
+ 404: errorResponse("Project not found."),
17844
+ 409: errorResponse("Operation key was already used for a different request."),
17845
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17846
+ }
17847
+ },
17848
+ {
17849
+ method: "post",
17850
+ path: "/api/v1/projects/{name}/ads/ad-groups/{id}/pause",
17851
+ summary: "Pause an OpenAI Ads ad group",
17852
+ tags: ["ads"],
17853
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad group ID.", schema: stringSchema }],
17854
+ requestBody: { required: true, content: { "application/json": { schema: {
17855
+ type: "object",
17856
+ required: ["operationKey"],
17857
+ properties: { operationKey: adsOperationKeySchema }
17858
+ } } } },
17859
+ responses: {
17860
+ 200: jsonResponse("Ad-group pause receipt.", "AdsOperationResponse"),
17861
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17862
+ 403: errorResponse("The key lacks ads.write."),
17863
+ 404: errorResponse("Project not found."),
17864
+ 409: errorResponse("Operation key was already used for a different request."),
17865
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17866
+ }
17867
+ },
17868
+ {
17869
+ method: "post",
17870
+ path: "/api/v1/projects/{name}/ads/ads/{id}/pause",
17871
+ summary: "Pause an OpenAI Ads ad",
17872
+ tags: ["ads"],
17873
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad ID.", schema: stringSchema }],
17874
+ requestBody: { required: true, content: { "application/json": { schema: {
17875
+ type: "object",
17876
+ required: ["operationKey"],
17877
+ properties: { operationKey: adsOperationKeySchema }
17878
+ } } } },
17879
+ responses: {
17880
+ 200: jsonResponse("Ad pause receipt.", "AdsOperationResponse"),
17881
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17882
+ 403: errorResponse("The key lacks ads.write."),
17883
+ 404: errorResponse("Project not found."),
17884
+ 409: errorResponse("Operation key was already used for a different request."),
17885
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17886
+ }
17887
+ },
17474
17888
  {
17475
17889
  method: "post",
17476
17890
  path: "/api/v1/projects/{name}/ads/sync",
@@ -17480,13 +17894,14 @@ var routeCatalog = [
17480
17894
  responses: {
17481
17895
  200: jsonResponse("Sync run queued.", "AdsSyncResponse"),
17482
17896
  400: errorResponse("No ads connection for this project."),
17897
+ 403: errorResponse("The key lacks ads.write."),
17483
17898
  404: errorResponse("Project not found.")
17484
17899
  }
17485
17900
  },
17486
17901
  {
17487
17902
  method: "get",
17488
17903
  path: "/api/v1/projects/{name}/ads/campaigns",
17489
- summary: "Synced campaign snapshots with nested ad groups (context hints) and ads",
17904
+ summary: "Synced campaign snapshots with lifecycle timestamps, targeting, nested ad groups, and ads",
17490
17905
  tags: ["ads"],
17491
17906
  parameters: [nameParameter],
17492
17907
  responses: {
@@ -18396,8 +18811,7 @@ var routeCatalog = [
18396
18811
  tags: ["intelligence"],
18397
18812
  parameters: [nameParameter],
18398
18813
  responses: {
18399
- // TODO: Add `HealthSnapshotDto` Zod schema in contracts.
18400
- 200: rawJsonResponse("Health snapshot or no-data sentinel returned.", looseObjectSchema),
18814
+ 200: jsonResponse("Health snapshot or no-data sentinel returned.", "HealthSnapshotDto"),
18401
18815
  404: errorResponse("Project not found.")
18402
18816
  }
18403
18817
  },
@@ -18408,11 +18822,10 @@ var routeCatalog = [
18408
18822
  tags: ["intelligence"],
18409
18823
  parameters: [
18410
18824
  nameParameter,
18411
- { name: "limit", in: "query", description: "Max results.", schema: stringSchema }
18825
+ { name: "limit", in: "query", description: "Max results.", schema: { ...integerSchema, minimum: 1, maximum: 100 } }
18412
18826
  ],
18413
18827
  responses: {
18414
- // TODO: Add `HealthSnapshotDto` Zod schema in contracts.
18415
- 200: rawJsonResponse("Health history returned.", { type: "array", items: looseObjectSchema }),
18828
+ 200: jsonArrayResponse("Health history returned.", "HealthSnapshotDto"),
18416
18829
  404: errorResponse("Project not found.")
18417
18830
  }
18418
18831
  },
@@ -19313,22 +19726,26 @@ var routeCatalog = [
19313
19726
  method: "get",
19314
19727
  path: "/api/v1/projects/{name}/technical-aeo",
19315
19728
  summary: "Get the Technical AEO scorecard for a project",
19316
- description: "Returns the latest completed/partial site-audit: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
19729
+ description: "Returns the latest completed/partial site-audit, or the historical audit selected by `runId`: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the audit immediately before it. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
19317
19730
  tags: ["technical-aeo"],
19318
- parameters: [nameParameter],
19731
+ parameters: [
19732
+ nameParameter,
19733
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema }
19734
+ ],
19319
19735
  responses: {
19320
19736
  200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
19321
- 404: errorResponse("Project not found.")
19737
+ 404: errorResponse("Project or site-audit run not found.")
19322
19738
  }
19323
19739
  },
19324
19740
  {
19325
19741
  method: "get",
19326
19742
  path: "/api/v1/projects/{name}/technical-aeo/pages",
19327
- summary: "List audited pages from the latest site-audit run",
19328
- description: "Returns the per-page breakdown of the latest completed/partial site-audit run (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
19743
+ summary: "List audited pages from a site-audit run",
19744
+ description: "Returns the per-page breakdown of the latest completed/partial site-audit run, or the historical audit selected by `runId` (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
19329
19745
  tags: ["technical-aeo"],
19330
19746
  parameters: [
19331
19747
  nameParameter,
19748
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema },
19332
19749
  { name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
19333
19750
  { name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
19334
19751
  limitQueryParameter,
@@ -19336,7 +19753,7 @@ var routeCatalog = [
19336
19753
  ],
19337
19754
  responses: {
19338
19755
  200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
19339
- 404: errorResponse("Project not found.")
19756
+ 404: errorResponse("Project or site-audit run not found.")
19340
19757
  }
19341
19758
  },
19342
19759
  {
@@ -23078,7 +23495,17 @@ async function googleRoutes(app, opts) {
23078
23495
 
23079
23496
  // ../api-routes/src/ads.ts
23080
23497
  import crypto20 from "crypto";
23081
- import { eq as eq25, and as and19, asc as asc4, gte as gte4, lte as lte3, inArray as inArray12 } from "drizzle-orm";
23498
+ import { eq as eq25, and as and19, asc as asc5, gte as gte5, lte as lte3, inArray as inArray12 } from "drizzle-orm";
23499
+ var AdsPausedPostconditionError = class extends Error {
23500
+ constructor(entityId) {
23501
+ super("OpenAI Ads API did not confirm the required paused state");
23502
+ this.entityId = entityId;
23503
+ this.name = "AdsPausedPostconditionError";
23504
+ }
23505
+ entityId;
23506
+ code = "ADS_PAUSED_POSTCONDITION_FAILED";
23507
+ status = 502;
23508
+ };
23082
23509
  function statusDto(row) {
23083
23510
  if (!row) return { connected: false };
23084
23511
  return {
@@ -23099,15 +23526,185 @@ function creativeDto(raw) {
23099
23526
  type: typeof c.type === "string" ? c.type : null,
23100
23527
  title: typeof c.title === "string" ? c.title : null,
23101
23528
  body: typeof c.body === "string" ? c.body : null,
23102
- targetUrl: typeof c.target_url === "string" ? c.target_url : null
23529
+ targetUrl: typeof c.target_url === "string" ? c.target_url : null,
23530
+ fileId: typeof c.file_id === "string" ? c.file_id : null
23531
+ };
23532
+ }
23533
+ function locationIdsDto(raw) {
23534
+ if (!raw || typeof raw !== "object") return [];
23535
+ const locations = raw.locations;
23536
+ if (!locations || typeof locations !== "object") return [];
23537
+ const include = locations.include;
23538
+ if (!Array.isArray(include)) return [];
23539
+ return include.flatMap((entry) => {
23540
+ if (!entry || typeof entry !== "object") return [];
23541
+ const id = entry.id;
23542
+ return typeof id === "string" && id.length > 0 ? [id] : [];
23543
+ });
23544
+ }
23545
+ function operationDto(row) {
23546
+ return {
23547
+ id: row.id,
23548
+ operationKey: row.operationKey,
23549
+ kind: row.kind,
23550
+ state: row.state,
23551
+ entityType: row.entityType,
23552
+ entityId: row.entityId,
23553
+ upstreamUpdatedAt: row.upstreamUpdatedAt,
23554
+ errorCode: row.errorCode,
23555
+ errorMessage: row.errorMessage,
23556
+ createdAt: row.createdAt,
23557
+ updatedAt: row.updatedAt
23103
23558
  };
23104
23559
  }
23560
+ function canonicalize(value) {
23561
+ if (Array.isArray(value)) return value.map(canonicalize);
23562
+ if (!value || typeof value !== "object") return value;
23563
+ const out = {};
23564
+ for (const key of Object.keys(value).sort()) {
23565
+ const child = value[key];
23566
+ if (child !== void 0) out[key] = canonicalize(child);
23567
+ }
23568
+ return out;
23569
+ }
23570
+ function requestHash(value) {
23571
+ return crypto20.createHash("sha256").update(JSON.stringify(canonicalize(value))).digest("hex");
23572
+ }
23573
+ function errorDetails(err) {
23574
+ const candidate = err;
23575
+ const status = typeof candidate.status === "number" ? candidate.status : void 0;
23576
+ const knownClientFailure = err instanceof AppError || status !== void 0 && status >= 400 && status < 500 && status !== 408 && status !== 429;
23577
+ const rawCode = typeof candidate.code === "string" ? candidate.code : err instanceof AppError ? err.code : "upstream_error";
23578
+ const code = rawCode.replace(/[^\w.:-]/g, "_").slice(0, 100) || "upstream_error";
23579
+ const message = err instanceof AppError ? err.message.slice(0, 500) : knownClientFailure ? "OpenAI Ads API rejected the operation" : "OpenAI Ads API outcome could not be confirmed";
23580
+ return {
23581
+ state: knownClientFailure ? AdsOperationStates.failed : AdsOperationStates.unknown,
23582
+ code,
23583
+ message,
23584
+ entityId: typeof candidate.entityId === "string" ? candidate.entityId.slice(0, 200) : void 0
23585
+ };
23586
+ }
23587
+ function resolveAdsOperator(opts, projectName) {
23588
+ const apiKey = opts.adsCredentialStore?.getConnection(projectName)?.apiKey;
23589
+ if (!apiKey) {
23590
+ throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
23591
+ }
23592
+ if (!opts.adsOperator) {
23593
+ throw validationError("Ads lifecycle operations are not configured for this deployment");
23594
+ }
23595
+ return { apiKey, operator: opts.adsOperator };
23596
+ }
23597
+ async function executeAdsOperation(app, request, input) {
23598
+ const hash = requestHash({ kind: input.kind, entityType: input.entityType, payload: input.payload });
23599
+ const existing = app.db.select().from(adsOperations).where(and19(
23600
+ eq25(adsOperations.projectId, input.projectId),
23601
+ eq25(adsOperations.operationKey, input.operationKey)
23602
+ )).get();
23603
+ if (existing) {
23604
+ if (existing.requestHash !== hash) {
23605
+ throw alreadyExists("Ads operation key", input.operationKey);
23606
+ }
23607
+ return { operation: operationDto(existing), replayed: true };
23608
+ }
23609
+ const id = crypto20.randomUUID();
23610
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
23611
+ app.db.insert(adsOperations).values({
23612
+ id,
23613
+ projectId: input.projectId,
23614
+ operationKey: input.operationKey,
23615
+ requestHash: hash,
23616
+ kind: input.kind,
23617
+ state: AdsOperationStates.pending,
23618
+ entityType: input.entityType,
23619
+ createdAt,
23620
+ updatedAt: createdAt
23621
+ }).run();
23622
+ let result;
23623
+ try {
23624
+ result = await input.run();
23625
+ if (input.expectedStatus !== void 0 && result.status !== input.expectedStatus) {
23626
+ if (input.remediateStatus) result = await input.remediateStatus(result);
23627
+ if (result.status !== input.expectedStatus) {
23628
+ throw new AdsPausedPostconditionError(result.id);
23629
+ }
23630
+ }
23631
+ const succeededResult = result;
23632
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
23633
+ app.db.transaction((tx) => {
23634
+ tx.update(adsOperations).set({
23635
+ state: AdsOperationStates.succeeded,
23636
+ entityId: succeededResult.id,
23637
+ upstreamUpdatedAt: succeededResult.updatedAt ?? null,
23638
+ updatedAt
23639
+ }).where(eq25(adsOperations.id, id)).run();
23640
+ writeAuditLog(tx, auditFromRequest(request, {
23641
+ projectId: input.projectId,
23642
+ actor: "api",
23643
+ action: `ads.${input.kind}.succeeded`,
23644
+ entityType: input.entityType,
23645
+ entityId: succeededResult.id,
23646
+ diff: { operationId: id, operationKey: input.operationKey }
23647
+ }));
23648
+ });
23649
+ const row = app.db.select().from(adsOperations).where(eq25(adsOperations.id, id)).get();
23650
+ return { operation: operationDto(row), replayed: false };
23651
+ } catch (err) {
23652
+ const failure = errorDetails(err);
23653
+ const entityId = failure.entityId ?? result?.id;
23654
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
23655
+ app.db.transaction((tx) => {
23656
+ tx.update(adsOperations).set({
23657
+ state: failure.state,
23658
+ entityId,
23659
+ errorCode: failure.code,
23660
+ errorMessage: failure.message,
23661
+ updatedAt
23662
+ }).where(eq25(adsOperations.id, id)).run();
23663
+ writeAuditLog(tx, auditFromRequest(request, {
23664
+ projectId: input.projectId,
23665
+ actor: "api",
23666
+ action: `ads.${input.kind}.${failure.state}`,
23667
+ entityType: input.entityType,
23668
+ entityId: entityId ?? id,
23669
+ diff: { operationId: id, operationKey: input.operationKey, errorCode: failure.code }
23670
+ }));
23671
+ });
23672
+ if (err instanceof AppError) throw err;
23673
+ throw providerError("OpenAI Ads API mutation failed", {
23674
+ operationId: id,
23675
+ operationKey: input.operationKey,
23676
+ state: failure.state,
23677
+ code: failure.code
23678
+ });
23679
+ }
23680
+ }
23681
+ function parseBody(schema, value) {
23682
+ const parsed = schema.safeParse(value);
23683
+ if (!parsed.success) throw validationError("Invalid ads operation request", { issues: parsed.error.issues });
23684
+ return parsed.data;
23685
+ }
23686
+ function assertExpectedUpdatedAt(entity, expected) {
23687
+ if (entity.updatedAt !== expected) {
23688
+ throw validationError("The upstream ad entity changed since it was reviewed", {
23689
+ expectedUpdatedAt: expected,
23690
+ actualUpdatedAt: entity.updatedAt
23691
+ });
23692
+ }
23693
+ }
23694
+ function assertPausedForUpdate(entity) {
23695
+ if (entity.status !== AdsEntityStatuses.paused) {
23696
+ throw validationError("Pause the upstream ad entity before updating it", {
23697
+ actualStatus: entity.status
23698
+ });
23699
+ }
23700
+ }
23105
23701
  async function adsRoutes(app, opts) {
23106
23702
  app.post(
23107
23703
  "/projects/:name/ads/connect",
23108
23704
  async (request) => {
23705
+ requireScope(request, ADS_WRITE_SCOPE);
23109
23706
  const project = resolveProject(app.db, request.params.name);
23110
- const parsed = adsConnectRequestSchema.safeParse(request.body ?? {});
23707
+ const parsed = adsConnectRequestSchema.safeParse(request.body);
23111
23708
  if (!parsed.success) throw validationError('"apiKey" is required');
23112
23709
  if (!opts.adsCredentialStore || !opts.verifyAdsAccount) {
23113
23710
  throw validationError("Ads credential storage is not configured for this deployment");
@@ -23165,6 +23762,7 @@ async function adsRoutes(app, opts) {
23165
23762
  }
23166
23763
  );
23167
23764
  app.delete("/projects/:name/ads/connection", async (request) => {
23765
+ requireScope(request, ADS_WRITE_SCOPE);
23168
23766
  const project = resolveProject(app.db, request.params.name);
23169
23767
  const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
23170
23768
  if (row) {
@@ -23188,7 +23786,221 @@ async function adsRoutes(app, opts) {
23188
23786
  const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
23189
23787
  return statusDto(row);
23190
23788
  });
23789
+ app.get(
23790
+ "/projects/:name/ads/operations/:operationKey",
23791
+ async (request) => {
23792
+ const project = resolveProject(app.db, request.params.name);
23793
+ const row = app.db.select().from(adsOperations).where(and19(
23794
+ eq25(adsOperations.projectId, project.id),
23795
+ eq25(adsOperations.operationKey, request.params.operationKey)
23796
+ )).get();
23797
+ if (!row) throw notFound("Ads operation", request.params.operationKey);
23798
+ const response = { operation: operationDto(row), replayed: true };
23799
+ return response;
23800
+ }
23801
+ );
23802
+ app.post("/projects/:name/ads/files", async (request) => {
23803
+ requireScope(request, ADS_WRITE_SCOPE);
23804
+ const project = resolveProject(app.db, request.params.name);
23805
+ const body = parseBody(adsImageUploadRequestSchema, request.body);
23806
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23807
+ return executeAdsOperation(app, request, {
23808
+ projectId: project.id,
23809
+ operationKey: body.operationKey,
23810
+ kind: AdsOperationKinds.image_upload,
23811
+ entityType: AdsEntityTypes.file,
23812
+ payload: { imageUrl: body.imageUrl },
23813
+ run: async () => {
23814
+ const result = await operator.uploadImage(apiKey, body.imageUrl);
23815
+ return { id: result.fileId };
23816
+ }
23817
+ });
23818
+ });
23819
+ app.post("/projects/:name/ads/campaigns", async (request) => {
23820
+ requireScope(request, ADS_WRITE_SCOPE);
23821
+ const project = resolveProject(app.db, request.params.name);
23822
+ const body = parseBody(adsCampaignCreateRequestSchema, request.body);
23823
+ const { operationKey, ...providerInput } = body;
23824
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23825
+ return executeAdsOperation(app, request, {
23826
+ projectId: project.id,
23827
+ operationKey,
23828
+ kind: AdsOperationKinds.campaign_create,
23829
+ entityType: AdsEntityTypes.campaign,
23830
+ payload: providerInput,
23831
+ expectedStatus: AdsEntityStatuses.paused,
23832
+ remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
23833
+ run: async () => operator.createCampaign(apiKey, providerInput)
23834
+ });
23835
+ });
23836
+ app.post("/projects/:name/ads/ad-groups", async (request) => {
23837
+ requireScope(request, ADS_WRITE_SCOPE);
23838
+ const project = resolveProject(app.db, request.params.name);
23839
+ const body = parseBody(adsAdGroupCreateRequestSchema, request.body);
23840
+ const { operationKey, ...providerInput } = body;
23841
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23842
+ return executeAdsOperation(app, request, {
23843
+ projectId: project.id,
23844
+ operationKey,
23845
+ kind: AdsOperationKinds.ad_group_create,
23846
+ entityType: AdsEntityTypes.ad_group,
23847
+ payload: providerInput,
23848
+ expectedStatus: AdsEntityStatuses.paused,
23849
+ remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
23850
+ run: async () => operator.createAdGroup(apiKey, providerInput)
23851
+ });
23852
+ });
23853
+ app.post("/projects/:name/ads/ads", async (request) => {
23854
+ requireScope(request, ADS_WRITE_SCOPE);
23855
+ const project = resolveProject(app.db, request.params.name);
23856
+ const body = parseBody(adsAdCreateRequestSchema, request.body);
23857
+ const { operationKey, ...providerInput } = body;
23858
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23859
+ return executeAdsOperation(app, request, {
23860
+ projectId: project.id,
23861
+ operationKey,
23862
+ kind: AdsOperationKinds.ad_create,
23863
+ entityType: AdsEntityTypes.ad,
23864
+ payload: providerInput,
23865
+ expectedStatus: AdsEntityStatuses.paused,
23866
+ remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
23867
+ run: async () => operator.createAd(apiKey, providerInput)
23868
+ });
23869
+ });
23870
+ app.post(
23871
+ "/projects/:name/ads/campaigns/:id",
23872
+ async (request) => {
23873
+ requireScope(request, ADS_WRITE_SCOPE);
23874
+ const project = resolveProject(app.db, request.params.name);
23875
+ const body = parseBody(adsCampaignUpdateRequestSchema, request.body);
23876
+ const { operationKey, expectedUpdatedAt, ...update } = body;
23877
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23878
+ return executeAdsOperation(app, request, {
23879
+ projectId: project.id,
23880
+ operationKey,
23881
+ kind: AdsOperationKinds.campaign_update,
23882
+ entityType: AdsEntityTypes.campaign,
23883
+ payload: { id: request.params.id, expectedUpdatedAt, update },
23884
+ expectedStatus: AdsEntityStatuses.paused,
23885
+ remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
23886
+ run: async () => {
23887
+ const current = await operator.getCampaign(apiKey, request.params.id);
23888
+ assertExpectedUpdatedAt(current, expectedUpdatedAt);
23889
+ assertPausedForUpdate(current);
23890
+ return operator.updateCampaign(apiKey, request.params.id, update);
23891
+ }
23892
+ });
23893
+ }
23894
+ );
23895
+ app.post(
23896
+ "/projects/:name/ads/ad-groups/:id",
23897
+ async (request) => {
23898
+ requireScope(request, ADS_WRITE_SCOPE);
23899
+ const project = resolveProject(app.db, request.params.name);
23900
+ const body = parseBody(adsAdGroupUpdateRequestSchema, request.body);
23901
+ const { operationKey, expectedUpdatedAt, ...update } = body;
23902
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23903
+ return executeAdsOperation(app, request, {
23904
+ projectId: project.id,
23905
+ operationKey,
23906
+ kind: AdsOperationKinds.ad_group_update,
23907
+ entityType: AdsEntityTypes.ad_group,
23908
+ payload: { id: request.params.id, expectedUpdatedAt, update },
23909
+ expectedStatus: AdsEntityStatuses.paused,
23910
+ remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
23911
+ run: async () => {
23912
+ const current = await operator.getAdGroup(apiKey, request.params.id);
23913
+ assertExpectedUpdatedAt(current, expectedUpdatedAt);
23914
+ assertPausedForUpdate(current);
23915
+ return operator.updateAdGroup(apiKey, request.params.id, update);
23916
+ }
23917
+ });
23918
+ }
23919
+ );
23920
+ app.post(
23921
+ "/projects/:name/ads/ads/:id",
23922
+ async (request) => {
23923
+ requireScope(request, ADS_WRITE_SCOPE);
23924
+ const project = resolveProject(app.db, request.params.name);
23925
+ const body = parseBody(adsAdUpdateRequestSchema, request.body);
23926
+ const { operationKey, expectedUpdatedAt, ...update } = body;
23927
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23928
+ return executeAdsOperation(app, request, {
23929
+ projectId: project.id,
23930
+ operationKey,
23931
+ kind: AdsOperationKinds.ad_update,
23932
+ entityType: AdsEntityTypes.ad,
23933
+ payload: { id: request.params.id, expectedUpdatedAt, update },
23934
+ expectedStatus: AdsEntityStatuses.paused,
23935
+ remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
23936
+ run: async () => {
23937
+ const current = await operator.getAd(apiKey, request.params.id);
23938
+ assertExpectedUpdatedAt(current, expectedUpdatedAt);
23939
+ assertPausedForUpdate(current);
23940
+ return operator.updateAd(apiKey, request.params.id, update);
23941
+ }
23942
+ });
23943
+ }
23944
+ );
23945
+ app.post(
23946
+ "/projects/:name/ads/campaigns/:id/pause",
23947
+ async (request) => {
23948
+ requireScope(request, ADS_WRITE_SCOPE);
23949
+ const project = resolveProject(app.db, request.params.name);
23950
+ const body = parseBody(adsPauseRequestSchema, request.body);
23951
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23952
+ return executeAdsOperation(app, request, {
23953
+ projectId: project.id,
23954
+ operationKey: body.operationKey,
23955
+ kind: AdsOperationKinds.campaign_pause,
23956
+ entityType: AdsEntityTypes.campaign,
23957
+ payload: { id: request.params.id },
23958
+ expectedStatus: AdsEntityStatuses.paused,
23959
+ remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
23960
+ run: async () => operator.pauseCampaign(apiKey, request.params.id)
23961
+ });
23962
+ }
23963
+ );
23964
+ app.post(
23965
+ "/projects/:name/ads/ad-groups/:id/pause",
23966
+ async (request) => {
23967
+ requireScope(request, ADS_WRITE_SCOPE);
23968
+ const project = resolveProject(app.db, request.params.name);
23969
+ const body = parseBody(adsPauseRequestSchema, request.body);
23970
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23971
+ return executeAdsOperation(app, request, {
23972
+ projectId: project.id,
23973
+ operationKey: body.operationKey,
23974
+ kind: AdsOperationKinds.ad_group_pause,
23975
+ entityType: AdsEntityTypes.ad_group,
23976
+ payload: { id: request.params.id },
23977
+ expectedStatus: AdsEntityStatuses.paused,
23978
+ remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
23979
+ run: async () => operator.pauseAdGroup(apiKey, request.params.id)
23980
+ });
23981
+ }
23982
+ );
23983
+ app.post(
23984
+ "/projects/:name/ads/ads/:id/pause",
23985
+ async (request) => {
23986
+ requireScope(request, ADS_WRITE_SCOPE);
23987
+ const project = resolveProject(app.db, request.params.name);
23988
+ const body = parseBody(adsPauseRequestSchema, request.body);
23989
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23990
+ return executeAdsOperation(app, request, {
23991
+ projectId: project.id,
23992
+ operationKey: body.operationKey,
23993
+ kind: AdsOperationKinds.ad_pause,
23994
+ entityType: AdsEntityTypes.ad,
23995
+ payload: { id: request.params.id },
23996
+ expectedStatus: AdsEntityStatuses.paused,
23997
+ remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
23998
+ run: async () => operator.pauseAd(apiKey, request.params.id)
23999
+ });
24000
+ }
24001
+ );
23191
24002
  app.post("/projects/:name/ads/sync", async (request) => {
24003
+ requireScope(request, ADS_WRITE_SCOPE);
23192
24004
  const project = resolveProject(app.db, request.params.name);
23193
24005
  const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
23194
24006
  if (!row) {
@@ -23229,7 +24041,9 @@ async function adsRoutes(app, opts) {
23229
24041
  name: ad.name,
23230
24042
  status: ad.status,
23231
24043
  reviewStatus: ad.reviewStatus,
23232
- creative: creativeDto(ad.creative)
24044
+ creative: creativeDto(ad.creative),
24045
+ upstreamUpdatedAt: ad.upstreamUpdatedAt,
24046
+ syncedAt: ad.syncedAt
23233
24047
  };
23234
24048
  const list = adsByGroup.get(ad.adGroupId) ?? [];
23235
24049
  list.push(dto);
@@ -23241,11 +24055,14 @@ async function adsRoutes(app, opts) {
23241
24055
  id: group.id,
23242
24056
  campaignId: group.campaignId,
23243
24057
  name: group.name,
24058
+ description: group.description,
23244
24059
  status: group.status,
23245
24060
  billingEventType: group.billingEventType,
23246
24061
  maxBidMicros: group.maxBidMicros,
23247
24062
  contextHints: group.contextHints,
23248
- ads: adsByGroup.get(group.id) ?? []
24063
+ ads: adsByGroup.get(group.id) ?? [],
24064
+ upstreamUpdatedAt: group.upstreamUpdatedAt,
24065
+ syncedAt: group.syncedAt
23249
24066
  };
23250
24067
  const list = groupsByCampaign.get(group.campaignId) ?? [];
23251
24068
  list.push(dto);
@@ -23254,11 +24071,17 @@ async function adsRoutes(app, opts) {
23254
24071
  const campaigns = campaignRows.map((campaign) => ({
23255
24072
  id: campaign.id,
23256
24073
  name: campaign.name,
24074
+ description: campaign.description,
23257
24075
  status: campaign.status,
24076
+ startTime: campaign.startTime,
24077
+ endTime: campaign.endTime,
23258
24078
  biddingType: campaign.biddingType,
23259
24079
  dailySpendLimitMicros: campaign.dailySpendLimitMicros,
23260
24080
  lifetimeSpendLimitMicros: campaign.lifetimeSpendLimitMicros,
23261
- adGroups: groupsByCampaign.get(campaign.id) ?? []
24081
+ locationIds: locationIdsDto(campaign.targeting),
24082
+ adGroups: groupsByCampaign.get(campaign.id) ?? [],
24083
+ upstreamUpdatedAt: campaign.upstreamUpdatedAt,
24084
+ syncedAt: campaign.syncedAt
23262
24085
  }));
23263
24086
  const response = { campaigns };
23264
24087
  return response;
@@ -23277,9 +24100,9 @@ async function adsRoutes(app, opts) {
23277
24100
  const conditions = [eq25(adsInsightsDaily.projectId, project.id)];
23278
24101
  if (parsedLevel) conditions.push(eq25(adsInsightsDaily.level, parsedLevel));
23279
24102
  if (entityId) conditions.push(eq25(adsInsightsDaily.entityId, entityId));
23280
- if (from) conditions.push(gte4(adsInsightsDaily.date, from));
24103
+ if (from) conditions.push(gte5(adsInsightsDaily.date, from));
23281
24104
  if (to) conditions.push(lte3(adsInsightsDaily.date, to));
23282
- const rows = app.db.select().from(adsInsightsDaily).where(and19(...conditions)).orderBy(asc4(adsInsightsDaily.date)).all();
24105
+ const rows = app.db.select().from(adsInsightsDaily).where(and19(...conditions)).orderBy(asc5(adsInsightsDaily.date)).all();
23283
24106
  const dtoRows = rows.map((row) => ({
23284
24107
  level: row.level,
23285
24108
  entityId: row.entityId,
@@ -26768,7 +27591,7 @@ async function wordpressRoutes(app, opts) {
26768
27591
 
26769
27592
  // ../api-routes/src/backlinks.ts
26770
27593
  import crypto24 from "crypto";
26771
- import { and as and24, asc as asc5, desc as desc14, eq as eq29, sql as sql11 } from "drizzle-orm";
27594
+ import { and as and24, asc as asc6, desc as desc14, eq as eq29, sql as sql11 } from "drizzle-orm";
26772
27595
 
26773
27596
  // ../integration-commoncrawl/src/constants.ts
26774
27597
  import os2 from "os";
@@ -27527,7 +28350,7 @@ async function backlinksRoutes(app, opts) {
27527
28350
  async (request, reply) => {
27528
28351
  const project = resolveProject(app.db, request.params.name);
27529
28352
  const source = parseSourceParam(request.query.source);
27530
- const rows = app.db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, project.id), eq29(backlinkSummaries.source, source))).orderBy(asc5(backlinkSummaries.queriedAt)).all();
28353
+ const rows = app.db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, project.id), eq29(backlinkSummaries.source, source))).orderBy(asc6(backlinkSummaries.queriedAt)).all();
27531
28354
  const response = rows.map((r) => ({
27532
28355
  release: r.release,
27533
28356
  totalLinkingDomains: r.totalLinkingDomains,
@@ -27583,7 +28406,7 @@ async function backlinksRoutes(app, opts) {
27583
28406
  // ../api-routes/src/traffic.ts
27584
28407
  import crypto26 from "crypto";
27585
28408
  import { Agent as UndiciAgent } from "undici";
27586
- import { and as and25, desc as desc15, eq as eq30, gte as gte5, lte as lte4, sql as sql12 } from "drizzle-orm";
28409
+ import { and as and25, desc as desc15, eq as eq30, gte as gte6, lte as lte4, sql as sql12 } from "drizzle-orm";
27587
28410
 
27588
28411
  // ../integration-cloud-run/src/auth.ts
27589
28412
  import crypto25 from "crypto";
@@ -31490,28 +32313,28 @@ async function runBackfillTask(options) {
31490
32313
  tx.delete(crawlerEventsHourly).where(
31491
32314
  and25(
31492
32315
  eq30(crawlerEventsHourly.sourceId, sourceRow.id),
31493
- gte5(crawlerEventsHourly.tsHour, windowStartIso),
32316
+ gte6(crawlerEventsHourly.tsHour, windowStartIso),
31494
32317
  lte4(crawlerEventsHourly.tsHour, windowEndIso)
31495
32318
  )
31496
32319
  ).run();
31497
32320
  tx.delete(aiUserFetchEventsHourly).where(
31498
32321
  and25(
31499
32322
  eq30(aiUserFetchEventsHourly.sourceId, sourceRow.id),
31500
- gte5(aiUserFetchEventsHourly.tsHour, windowStartIso),
32323
+ gte6(aiUserFetchEventsHourly.tsHour, windowStartIso),
31501
32324
  lte4(aiUserFetchEventsHourly.tsHour, windowEndIso)
31502
32325
  )
31503
32326
  ).run();
31504
32327
  tx.delete(aiReferralEventsHourly).where(
31505
32328
  and25(
31506
32329
  eq30(aiReferralEventsHourly.sourceId, sourceRow.id),
31507
- gte5(aiReferralEventsHourly.tsHour, windowStartIso),
32330
+ gte6(aiReferralEventsHourly.tsHour, windowStartIso),
31508
32331
  lte4(aiReferralEventsHourly.tsHour, windowEndIso)
31509
32332
  )
31510
32333
  ).run();
31511
32334
  tx.delete(rawEventSamples).where(
31512
32335
  and25(
31513
32336
  eq30(rawEventSamples.sourceId, sourceRow.id),
31514
- gte5(rawEventSamples.ts, windowStartIso),
32337
+ gte6(rawEventSamples.ts, windowStartIso),
31515
32338
  lte4(rawEventSamples.ts, windowEndIso)
31516
32339
  )
31517
32340
  ).run();
@@ -32625,7 +33448,7 @@ async function trafficRoutes(app, opts) {
32625
33448
  }).from(crawlerEventsHourly).where(
32626
33449
  and25(
32627
33450
  eq30(crawlerEventsHourly.sourceId, row.id),
32628
- gte5(crawlerEventsHourly.tsHour, since)
33451
+ gte6(crawlerEventsHourly.tsHour, since)
32629
33452
  )
32630
33453
  ).groupBy(crawlerEventsHourly.pathNormalized).all();
32631
33454
  const crawlerSegments = segmentCrawlerHits(
@@ -32635,19 +33458,19 @@ async function trafficRoutes(app, opts) {
32635
33458
  const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
32636
33459
  and25(
32637
33460
  eq30(aiUserFetchEventsHourly.sourceId, row.id),
32638
- gte5(aiUserFetchEventsHourly.tsHour, since)
33461
+ gte6(aiUserFetchEventsHourly.tsHour, since)
32639
33462
  )
32640
33463
  ).get();
32641
33464
  const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
32642
33465
  and25(
32643
33466
  eq30(aiReferralEventsHourly.sourceId, row.id),
32644
- gte5(aiReferralEventsHourly.tsHour, since)
33467
+ gte6(aiReferralEventsHourly.tsHour, since)
32645
33468
  )
32646
33469
  ).get();
32647
33470
  const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
32648
33471
  and25(
32649
33472
  eq30(rawEventSamples.sourceId, row.id),
32650
- gte5(rawEventSamples.ts, since)
33473
+ gte6(rawEventSamples.ts, since)
32651
33474
  )
32652
33475
  ).get();
32653
33476
  const latestRun = app.db.select().from(runs).where(
@@ -32786,7 +33609,7 @@ async function trafficRoutes(app, opts) {
32786
33609
  if (kind === "all" || kind === TrafficEventKinds.crawler) {
32787
33610
  const crawlerFilters = [
32788
33611
  eq30(crawlerEventsHourly.projectId, project.id),
32789
- gte5(crawlerEventsHourly.tsHour, sinceIso),
33612
+ gte6(crawlerEventsHourly.tsHour, sinceIso),
32790
33613
  lte4(crawlerEventsHourly.tsHour, untilIso)
32791
33614
  ];
32792
33615
  if (sourceIdParam) crawlerFilters.push(eq30(crawlerEventsHourly.sourceId, sourceIdParam));
@@ -32818,7 +33641,7 @@ async function trafficRoutes(app, opts) {
32818
33641
  if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
32819
33642
  const userFetchFilters = [
32820
33643
  eq30(aiUserFetchEventsHourly.projectId, project.id),
32821
- gte5(aiUserFetchEventsHourly.tsHour, sinceIso),
33644
+ gte6(aiUserFetchEventsHourly.tsHour, sinceIso),
32822
33645
  lte4(aiUserFetchEventsHourly.tsHour, untilIso)
32823
33646
  ];
32824
33647
  if (sourceIdParam) userFetchFilters.push(eq30(aiUserFetchEventsHourly.sourceId, sourceIdParam));
@@ -32843,7 +33666,7 @@ async function trafficRoutes(app, opts) {
32843
33666
  if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
32844
33667
  const aiFilters = [
32845
33668
  eq30(aiReferralEventsHourly.projectId, project.id),
32846
- gte5(aiReferralEventsHourly.tsHour, sinceIso),
33669
+ gte6(aiReferralEventsHourly.tsHour, sinceIso),
32847
33670
  lte4(aiReferralEventsHourly.tsHour, untilIso)
32848
33671
  ];
32849
33672
  if (sourceIdParam) aiFilters.push(eq30(aiReferralEventsHourly.sourceId, sourceIdParam));
@@ -34461,7 +35284,7 @@ var RUNTIME_STATE_CHECKS = [
34461
35284
  ];
34462
35285
 
34463
35286
  // ../api-routes/src/doctor/checks/traffic-source.ts
34464
- import { and as and28, eq as eq36, gte as gte6, ne as ne4, sql as sql13 } from "drizzle-orm";
35287
+ import { and as and28, eq as eq36, gte as gte7, ne as ne4, sql as sql13 } from "drizzle-orm";
34465
35288
  var RECENT_DATA_WARN_DAYS = 7;
34466
35289
  var RECENT_DATA_FAIL_DAYS = 30;
34467
35290
  function skippedNoProject5() {
@@ -34558,7 +35381,7 @@ var recentDataCheck = {
34558
35381
  ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
34559
35382
  and28(
34560
35383
  eq36(crawlerEventsHourly.projectId, ctx.project.id),
34561
- gte6(crawlerEventsHourly.tsHour, warnCutoff)
35384
+ gte7(crawlerEventsHourly.tsHour, warnCutoff)
34562
35385
  )
34563
35386
  ).get()?.total ?? 0
34564
35387
  );
@@ -34566,7 +35389,7 @@ var recentDataCheck = {
34566
35389
  ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
34567
35390
  and28(
34568
35391
  eq36(aiReferralEventsHourly.projectId, ctx.project.id),
34569
- gte6(aiReferralEventsHourly.tsHour, warnCutoff)
35392
+ gte7(aiReferralEventsHourly.tsHour, warnCutoff)
34570
35393
  )
34571
35394
  ).get()?.total ?? 0
34572
35395
  );
@@ -34582,7 +35405,7 @@ var recentDataCheck = {
34582
35405
  ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
34583
35406
  and28(
34584
35407
  eq36(crawlerEventsHourly.projectId, ctx.project.id),
34585
- gte6(crawlerEventsHourly.tsHour, failCutoff)
35408
+ gte7(crawlerEventsHourly.tsHour, failCutoff)
34586
35409
  )
34587
35410
  ).get()?.total ?? 0
34588
35411
  );
@@ -34590,7 +35413,7 @@ var recentDataCheck = {
34590
35413
  ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
34591
35414
  and28(
34592
35415
  eq36(aiReferralEventsHourly.projectId, ctx.project.id),
34593
- gte6(aiReferralEventsHourly.tsHour, failCutoff)
35416
+ gte7(aiReferralEventsHourly.tsHour, failCutoff)
34594
35417
  )
34595
35418
  ).get()?.total ?? 0
34596
35419
  );
@@ -35005,7 +35828,7 @@ async function doctorRoutes(app, opts) {
35005
35828
 
35006
35829
  // ../api-routes/src/discovery/routes.ts
35007
35830
  import crypto28 from "crypto";
35008
- import { and as and29, desc as desc16, eq as eq37, gte as gte7, inArray as inArray13, isNull, or as or5 } from "drizzle-orm";
35831
+ import { and as and29, desc as desc16, eq as eq37, gte as gte8, inArray as inArray13, isNull, or as or5 } from "drizzle-orm";
35009
35832
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
35010
35833
  async function discoveryRoutes(app, opts) {
35011
35834
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -35065,7 +35888,7 @@ async function discoveryRoutes(app, opts) {
35065
35888
  DiscoverySessionStatuses.seeding,
35066
35889
  DiscoverySessionStatuses.probing
35067
35890
  ]),
35068
- gte7(discoverySessions.createdAt, ageFloorIso)
35891
+ gte8(discoverySessions.createdAt, ageFloorIso)
35069
35892
  )).orderBy(desc16(discoverySessions.createdAt)).get();
35070
35893
  if (existing && existing.runId) {
35071
35894
  return { reused: true, sessionId: existing.id, runId: existing.runId };
@@ -35685,7 +36508,7 @@ function dedupeStrings(input) {
35685
36508
 
35686
36509
  // ../api-routes/src/technical-aeo.ts
35687
36510
  import crypto30 from "crypto";
35688
- import { and as and30, asc as asc6, count, desc as desc17, eq as eq39, inArray as inArray14 } from "drizzle-orm";
36511
+ import { and as and30, asc as asc7, count, desc as desc17, eq as eq39, inArray as inArray14, lt as lt2 } from "drizzle-orm";
35689
36512
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
35690
36513
  function emptyScore(projectName) {
35691
36514
  return {
@@ -35717,16 +36540,20 @@ function parsePositiveInt(value, fallback, max) {
35717
36540
  async function technicalAeoRoutes(app, opts) {
35718
36541
  app.get("/projects/:name/technical-aeo", async (request) => {
35719
36542
  const project = resolveProject(app.db, request.params.name);
35720
- const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(
36543
+ const baseFilters = [
35721
36544
  eq39(siteAuditSnapshots.projectId, project.id),
35722
36545
  eq39(runs.kind, RunKinds["site-audit"]),
35723
36546
  inArray14(runs.status, SURFACEABLE_STATUSES),
35724
36547
  notProbeRun()
35725
- )).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
35726
- const latest = rows[0];
35727
- if (!latest) return emptyScore(project.name);
36548
+ ];
36549
+ const targetFilters = request.query.runId ? [...baseFilters, eq39(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
36550
+ const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
36551
+ if (!latest) {
36552
+ if (request.query.runId) throw notFound("Site audit run", request.query.runId);
36553
+ return emptyScore(project.name);
36554
+ }
35728
36555
  const snap = latest.snap;
35729
- const previous = rows[1]?.snap ?? null;
36556
+ const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...baseFilters, lt2(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
35730
36557
  const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
35731
36558
  const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
35732
36559
  return {
@@ -35752,12 +36579,17 @@ async function technicalAeoRoutes(app, opts) {
35752
36579
  });
35753
36580
  app.get("/projects/:name/technical-aeo/pages", async (request) => {
35754
36581
  const project = resolveProject(app.db, request.params.name);
35755
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(
36582
+ const targetFilters = [
35756
36583
  eq39(siteAuditSnapshots.projectId, project.id),
35757
36584
  eq39(runs.kind, RunKinds["site-audit"]),
35758
36585
  inArray14(runs.status, SURFACEABLE_STATUSES),
35759
36586
  notProbeRun()
35760
- )).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
36587
+ ];
36588
+ if (request.query.runId) targetFilters.push(eq39(siteAuditSnapshots.runId, request.query.runId));
36589
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
36590
+ if (!latest && request.query.runId) {
36591
+ throw notFound("Site audit run", request.query.runId);
36592
+ }
35761
36593
  if (!latest) {
35762
36594
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
35763
36595
  }
@@ -35769,7 +36601,7 @@ async function technicalAeoRoutes(app, opts) {
35769
36601
  const total = totalRow?.value ?? 0;
35770
36602
  const limit = parsePositiveInt(request.query.limit, 100, 500);
35771
36603
  const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
35772
- const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc6(siteAuditPages.url) : asc6(siteAuditPages.overallScore);
36604
+ const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc7(siteAuditPages.url) : asc7(siteAuditPages.overallScore);
35773
36605
  const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
35774
36606
  const pages = rows.map((row) => ({
35775
36607
  url: row.url,
@@ -35944,7 +36776,8 @@ async function apiRoutes(app, opts) {
35944
36776
  await api.register(adsRoutes, {
35945
36777
  adsCredentialStore: opts.adsCredentialStore,
35946
36778
  verifyAdsAccount: opts.verifyAdsAccount,
35947
- onAdsSyncRequested: opts.onAdsSyncRequested
36779
+ onAdsSyncRequested: opts.onAdsSyncRequested,
36780
+ adsOperator: opts.adsOperator
35948
36781
  });
35949
36782
  await api.register(bingRoutes, {
35950
36783
  bingConnectionStore: opts.bingConnectionStore,
@@ -36492,7 +37325,7 @@ var IntelligenceService = class {
36492
37325
  const selected = this.db.select().from(gbpLocations).where(and31(
36493
37326
  eq40(gbpLocations.projectId, projectId),
36494
37327
  eq40(gbpLocations.selected, true),
36495
- gte8(gbpLocations.syncedAt, windowStart),
37328
+ gte9(gbpLocations.syncedAt, windowStart),
36496
37329
  lte5(gbpLocations.syncedAt, windowEnd)
36497
37330
  )).all();
36498
37331
  if (selected.length === 0) {
@@ -36706,7 +37539,7 @@ var IntelligenceService = class {
36706
37539
  // Backfill must not replay probe runs as if they were real sweeps.
36707
37540
  ne5(runs.trigger, RunTriggers.probe)
36708
37541
  )
36709
- ).orderBy(asc7(runs.finishedAt)).all();
37542
+ ).orderBy(asc8(runs.finishedAt)).all();
36710
37543
  let startIdx = 0;
36711
37544
  let endIdx = allRuns.length;
36712
37545
  if (opts?.fromRunId) {