@canonry/canonry 5.16.1 → 5.18.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 (33) hide show
  1. package/assets/assets/{AuditHistoryPanel-CnoGPJ5Y.js → AuditHistoryPanel-Di5c6xSa.js} +1 -1
  2. package/assets/assets/{BacklinksPage-DYe2CYfD.js → BacklinksPage-CZi3sZ-7.js} +1 -1
  3. package/assets/assets/{HistoryPage-CO-dzLAo.js → HistoryPage-BY7HiZWU.js} +1 -1
  4. package/assets/assets/MeasurementPropertyPage-D3AdBHpI.js +1 -0
  5. package/assets/assets/ProjectPage-NyUl1dkF.js +8 -0
  6. package/assets/assets/{RunRow-CzcaoK_3.js → RunRow-cRjiHQbt.js} +1 -1
  7. package/assets/assets/{RunsPage-DQWPHDoq.js → RunsPage-DZP_kqJs.js} +1 -1
  8. package/assets/assets/{SettingsPage-DMtQLrWP.js → SettingsPage-BqJXYCDc.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-2ljW02xK.js → SiteHealthSection-Brr0C56X.js} +3 -3
  10. package/assets/assets/{TrafficPage-NFu0LEdk.js → TrafficPage-k5NPSDur.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-DbBIzmbL.js → TrafficSourceDetailPage-BKNzWnxA.js} +1 -1
  12. package/assets/assets/{extract-error-message-pMGpUsIh.js → extract-error-message-Cnvelll0.js} +1 -1
  13. package/assets/assets/index-Cd_1GknE.js +86 -0
  14. package/assets/assets/{react-sigma_core.esm.min-CadBie52.js → react-sigma_core.esm.min-CthGjQKl.js} +1 -1
  15. package/assets/assets/v2-overview-adapter-q2o2MzmG.js +1 -0
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-L36TC6LB.js → chunk-77VHT3JD.js} +29 -3
  18. package/dist/{chunk-CDQDPUY4.js → chunk-HAABFHXB.js} +2 -2
  19. package/dist/{chunk-5QK5KHA7.js → chunk-HIFBIDOW.js} +485 -160
  20. package/dist/{chunk-ZGZSROSR.js → chunk-TMTO2JHU.js} +9 -2
  21. package/dist/{chunk-JO5HHEJV.js → chunk-X3GFCKUJ.js} +2 -2
  22. package/dist/{chunk-3YAPVVFN.js → chunk-Y5IDGH5B.js} +52 -17
  23. package/dist/cli.js +18 -8
  24. package/dist/{demo-server-QLQUVMKJ.js → demo-server-JBGYZKAI.js} +3 -3
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +5 -5
  27. package/dist/{intelligence-service-EAUKUVVM.js → intelligence-service-EORTVFDF.js} +2 -2
  28. package/dist/mcp.js +3 -3
  29. package/package.json +11 -11
  30. package/assets/assets/MeasurementPropertyPage-smsZFdd6.js +0 -1
  31. package/assets/assets/ProjectPage-jL3jB2TR.js +0 -8
  32. package/assets/assets/index-D9NReRHX.js +0 -86
  33. package/assets/assets/v2-overview-adapter-Oe8hHMAO.js +0 -1
@@ -17,12 +17,12 @@ import {
17
17
  resolveOperatorApiKeyIds,
18
18
  runWithUsageTags,
19
19
  saveConfigPatch
20
- } from "./chunk-ZGZSROSR.js";
20
+ } from "./chunk-TMTO2JHU.js";
21
21
  import {
22
22
  deleteSiteCrawlGraphLayout,
23
23
  persistSiteCrawlGraphLayout,
24
24
  prepareSiteCrawlGraphLayout
25
- } from "./chunk-CDQDPUY4.js";
25
+ } from "./chunk-HAABFHXB.js";
26
26
  import {
27
27
  PACKAGE_VERSION
28
28
  } from "./chunk-AOJU5LKX.js";
@@ -184,7 +184,7 @@ import {
184
184
  siteLivenessState,
185
185
  toAlertView,
186
186
  usageCounters
187
- } from "./chunk-3YAPVVFN.js";
187
+ } from "./chunk-Y5IDGH5B.js";
188
188
  import {
189
189
  AGENT_MEMORY_VALUE_MAX_BYTES,
190
190
  AGENT_NONE,
@@ -194,6 +194,7 @@ import {
194
194
  AdsAdGroupBillingEventTypes,
195
195
  AdsCampaignBiddingTypes,
196
196
  AgentProviderIds,
197
+ AppError,
197
198
  BacklinkSources,
198
199
  CANONRY_NPM_PACKAGE_URL,
199
200
  CITED_URL_CAPTURE_VERSION,
@@ -232,6 +233,7 @@ import {
232
233
  TrafficEventKinds,
233
234
  TrafficEvidenceKinds,
234
235
  TrafficSourceTypes,
236
+ UserRoles,
235
237
  VERTEX_AI_SEARCH_PROXY_DOMAIN,
236
238
  actionConfidenceLabel,
237
239
  adsAccountDtoSchema,
@@ -345,7 +347,7 @@ import {
345
347
  visibilityReportRequestSchema,
346
348
  winnabilityClassLabel,
347
349
  withRetry
348
- } from "./chunk-L36TC6LB.js";
350
+ } from "./chunk-77VHT3JD.js";
349
351
 
350
352
  // src/runtime-context.ts
351
353
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -3560,11 +3562,11 @@ function createGoogleMarketingRuntime(options) {
3560
3562
 
3561
3563
  // src/server.ts
3562
3564
  import { createRequire as createRequire3 } from "module";
3563
- import crypto26 from "crypto";
3565
+ import crypto27 from "crypto";
3564
3566
  import fs8 from "fs";
3565
3567
  import path8 from "path";
3566
3568
  import { fileURLToPath as fileURLToPath3 } from "url";
3567
- import { and as and19, eq as eq25 } from "drizzle-orm";
3569
+ import { and as and20, eq as eq26 } from "drizzle-orm";
3568
3570
  import Fastify from "fastify";
3569
3571
  import os4 from "os";
3570
3572
 
@@ -6063,6 +6065,11 @@ function resolveAgentProactiveEnabled(env, config) {
6063
6065
  }
6064
6066
  return config.agent?.mode !== "prompt-only";
6065
6067
  }
6068
+ function resolveAgentAllowViewers(env, config) {
6069
+ const raw = env.CANONRY_AGENT_ALLOW_VIEWERS?.trim();
6070
+ if (raw) return raw === "1" || raw.toLowerCase() === "true";
6071
+ return config.agent?.allowViewers === true;
6072
+ }
6066
6073
 
6067
6074
  // src/places-config.ts
6068
6075
  var DEFAULT_TIER = "atmosphere";
@@ -6491,7 +6498,7 @@ function nonBlank(value) {
6491
6498
  return trimmed ? trimmed : void 0;
6492
6499
  }
6493
6500
  function resolveBuildCommit(env = process.env) {
6494
- const embedded = true ? "c03ab4f56518250251bec6d577c20d1334377cec" : void 0;
6501
+ const embedded = true ? "508d40ec00ff12e4604267c10940212dd3b750b9" : void 0;
6495
6502
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
6496
6503
  }
6497
6504
  function resolveInstanceIdentity(env = process.env) {
@@ -12067,7 +12074,7 @@ function readStoredGroundingSources(rawResponse) {
12067
12074
  return result2;
12068
12075
  }
12069
12076
  async function backfillInsightsCommand(project, opts) {
12070
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-EAUKUVVM.js");
12077
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-EORTVFDF.js");
12071
12078
  const config = loadConfig();
12072
12079
  const db = createClient(config.database);
12073
12080
  migrate(db);
@@ -15130,7 +15137,7 @@ function ensureBuiltinsRegistered() {
15130
15137
  builtinsRegistered = true;
15131
15138
  }
15132
15139
  }
15133
- function loadAeroSystemPrompt(pkgDir) {
15140
+ function loadAeroSystemPrompt(pkgDir, opts = {}) {
15134
15141
  const skillDir = resolveAeroSkillDir(pkgDir);
15135
15142
  const skillBody = fs7.readFileSync(path7.join(skillDir, "SKILL.md"), "utf-8");
15136
15143
  const soulPath = path7.join(skillDir, "soul.md");
@@ -15139,7 +15146,7 @@ function loadAeroSystemPrompt(pkgDir) {
15139
15146
  ---
15140
15147
 
15141
15148
  ${skillBody}` : skillBody;
15142
- return appendSystemPromptExtras(base + AERO_RUNTIME_PROMPT);
15149
+ return opts.extras === false ? base + AERO_RUNTIME_PROMPT : appendSystemPromptExtras(base + AERO_RUNTIME_PROMPT);
15143
15150
  }
15144
15151
  function appendSystemPromptExtras(base, env = process.env) {
15145
15152
  const inline = env.AERO_SYSTEM_PROMPT_APPEND?.trim();
@@ -16200,16 +16207,223 @@ ${lines.join("\n")}
16200
16207
  }
16201
16208
  };
16202
16209
 
16203
- // src/mcp-http.ts
16210
+ // src/agent/viewer-sessions.ts
16204
16211
  import crypto25 from "crypto";
16205
- import { eq as eq21 } from "drizzle-orm";
16212
+ import { and as and17, eq as eq21, isNotNull, like as like2 } from "drizzle-orm";
16213
+ var AERO_VIEWER_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
16214
+ CanonryMcpToolNames.canonry_ads_account,
16215
+ CanonryMcpToolNames.canonry_ads_geo_search,
16216
+ CanonryMcpToolNames.canonry_ads_live_delivery,
16217
+ CanonryMcpToolNames.canonry_ads_conversion_pixels,
16218
+ CanonryMcpToolNames.canonry_ads_conversion_event_settings,
16219
+ CanonryMcpToolNames.canonry_google_ads_customers,
16220
+ CanonryMcpToolNames.canonry_gtm_accounts,
16221
+ CanonryMcpToolNames.canonry_gtm_containers,
16222
+ CanonryMcpToolNames.canonry_gtm_workspaces,
16223
+ // These can list every account the install's Google or provider login can
16224
+ // see, other clients included, or run live checks. Not a viewer's to read.
16225
+ CanonryMcpToolNames.canonry_settings_get,
16226
+ CanonryMcpToolNames.canonry_doctor,
16227
+ CanonryMcpToolNames.canonry_ga_properties,
16228
+ CanonryMcpToolNames.canonry_gbp_accounts,
16229
+ // Marked read, but embeds the harvested queries with the operator's Gemini key.
16230
+ CanonryMcpToolNames.canonry_discover_harvest,
16231
+ // Live Search Console call on the operator's Google login, which can also
16232
+ // refresh and store its token.
16233
+ CanonryMcpToolNames.canonry_gsc_sitemaps,
16234
+ // Live Common Crawl probe; free, but an outbound call a viewer has no need to make.
16235
+ CanonryMcpToolNames.canonry_backlinks_latest_release,
16236
+ // The whole install's audit log, not this project's.
16237
+ CanonryMcpToolNames.canonry_history_global
16238
+ ]);
16239
+ var OPERATOR_ONLY_TOOL_NAMES = new Set(
16240
+ canonryMcpTools.filter((tool) => tool.tier === "agent" || tool.requiresOperator).map((tool) => tool.name)
16241
+ );
16242
+ var VIEWER_AERO_TURN_LIMITS = { maxToolCalls: 12, timeoutMs: 12e4 };
16243
+ var VIEWER_AERO_MAX_PROMPT_CHARS = 4e3;
16244
+ var VIEWER_AERO_DAILY_TURN_LIMIT = 50;
16245
+ function viewerAeroDailyLimitExceeded(limit) {
16246
+ return new AppError(
16247
+ "QUOTA_EXCEEDED",
16248
+ `You have used today's ${limit} Aero questions for this project. Try again tomorrow or ask an administrator.`,
16249
+ 429,
16250
+ { metric: "aero.viewer.turns", limit }
16251
+ );
16252
+ }
16253
+ var VIEWER_AERO_KEY_PREFIX = "aero-viewer:";
16254
+ var VIEWER_AERO_PROMPT = "\n\nThis conversation is with a view-only account. You can read this project's data but cannot change anything, start or schedule sweeps, or spend on live provider reads. If asked to, say an administrator has to do it. You cannot see the operator's own Aero conversations or notes.";
16255
+ var MAX_VIEWER_MESSAGES = 40;
16256
+ function trimViewerTranscript(messages, max = MAX_VIEWER_MESSAGES) {
16257
+ if (messages.length <= max) return messages;
16258
+ for (let i = messages.length - max; i < messages.length; i++) {
16259
+ if (messages[i].role === "user") return messages.slice(i);
16260
+ }
16261
+ const lastUser = messages.map((message) => message.role).lastIndexOf("user");
16262
+ return lastUser > 0 ? messages.slice(lastUser) : messages;
16263
+ }
16264
+ var VIEWER_IDLE_MS = 12 * 60 * 60 * 1e3;
16265
+ function mintViewerAeroKey(db, userId, now = /* @__PURE__ */ new Date()) {
16266
+ const raw = `cnry_${crypto25.randomBytes(24).toString("hex")}`;
16267
+ const id = crypto25.randomUUID();
16268
+ db.insert(apiKeys).values({
16269
+ id,
16270
+ name: `${VIEWER_AERO_KEY_PREFIX}${userId}`,
16271
+ keyHash: hashApiKey(raw),
16272
+ keyPrefix: raw.slice(0, 9),
16273
+ scopes: ["read"],
16274
+ delegatedUserId: userId,
16275
+ createdAt: now.toISOString()
16276
+ }).run();
16277
+ return { id, raw };
16278
+ }
16279
+ function buildViewerAeroTools(client, projectName, managedSweeps = false) {
16280
+ const state = buildAeroStateTools({ client, projectName }, {
16281
+ scope: AeroToolScopes.readOnly,
16282
+ managedSweeps
16283
+ }).filter((tool) => !AERO_VIEWER_EXCLUDED_MCP_TOOLS.has(tool.name) && !OPERATOR_ONLY_TOOL_NAMES.has(tool.name));
16284
+ return [...state, ...buildSkillDocTools()];
16285
+ }
16286
+ var ViewerAeroSessions = class {
16287
+ constructor(opts) {
16288
+ this.opts = opts;
16289
+ this.now = opts.now ?? Date.now;
16290
+ this.revokeOrphanedKeys();
16291
+ }
16292
+ opts;
16293
+ sessions = /* @__PURE__ */ new Map();
16294
+ acquisitions = /* @__PURE__ */ new Set();
16295
+ /** Turns per viewer and project today, keyed with the UTC date. In memory: a restart forgives. */
16296
+ turnsToday = /* @__PURE__ */ new Map();
16297
+ now;
16298
+ key(projectName, userId) {
16299
+ return `${projectName}\0${userId}`;
16300
+ }
16301
+ /** Without tool `details`, which can be large and the bar never renders; the model's text copy stays. */
16302
+ transcript(projectName, userId) {
16303
+ this.evictIdle();
16304
+ const messages = this.sessions.get(this.key(projectName, userId))?.agent.state.messages ?? [];
16305
+ return withoutPersistedToolDetails(messages);
16306
+ }
16307
+ updatedAt(projectName, userId) {
16308
+ return this.sessions.get(this.key(projectName, userId))?.updatedAt ?? null;
16309
+ }
16310
+ isBusy(projectName, userId) {
16311
+ const key = this.key(projectName, userId);
16312
+ return this.acquisitions.has(key) || this.sessions.get(key)?.agent.state.isStreaming === true;
16313
+ }
16314
+ reset(projectName, userId) {
16315
+ const key = this.key(projectName, userId);
16316
+ if (this.isBusy(projectName, userId)) throw agentBusy(projectName);
16317
+ this.sessions.delete(key);
16318
+ }
16319
+ async acquireForTurn(project, userId, preferences = {}) {
16320
+ preferences.signal?.throwIfAborted();
16321
+ this.evictIdle();
16322
+ const key = this.key(project.name, userId);
16323
+ if (this.isBusy(project.name, userId)) throw agentBusy(project.name);
16324
+ const today = new Date(this.now()).toISOString().slice(0, 10);
16325
+ const used = this.turnsToday.get(key);
16326
+ const count = used?.date === today ? used.count : 0;
16327
+ if (count >= VIEWER_AERO_DAILY_TURN_LIMIT) throw viewerAeroDailyLimitExceeded(VIEWER_AERO_DAILY_TURN_LIMIT);
16328
+ const minted = mintViewerAeroKey(this.opts.db, userId, new Date(this.now()));
16329
+ this.acquisitions.add(key);
16330
+ let handedOff = false;
16331
+ try {
16332
+ const client = new ApiClient(this.opts.selfApiUrl, minted.raw, { skipProbe: true, surface: "aero" });
16333
+ const agent = this.sessions.get(key)?.agent ?? this.createAgent(project, client);
16334
+ agent.state.messages = trimViewerTranscript(agent.state.messages);
16335
+ const view = { client, projectName: project.name, basePath: this.opts.config.basePath, context: preferences.context };
16336
+ const evidence = preferences.context ? await readAeroViewEvidence(view) : void 0;
16337
+ preferences.signal?.throwIfAborted();
16338
+ agent.state.systemPrompt = loadAeroSystemPrompt(void 0, { extras: false }) + VIEWER_AERO_PROMPT + aeroViewPrompt(preferences.context);
16339
+ configureAeroRuntime(agent, [...buildViewerAeroTools(client, project.name, this.opts.managedSweeps), buildAeroViewTool(view, evidence)], VIEWER_AERO_TURN_LIMITS, true);
16340
+ this.sessions.set(key, { agent, lastUsedAt: this.now(), updatedAt: this.sessions.get(key)?.updatedAt ?? null });
16341
+ this.turnsToday.set(key, { date: today, count: count + 1 });
16342
+ handedOff = true;
16343
+ let released = false;
16344
+ return {
16345
+ agent,
16346
+ release: () => {
16347
+ if (released) return;
16348
+ released = true;
16349
+ this.acquisitions.delete(key);
16350
+ const session = this.sessions.get(key);
16351
+ if (session) {
16352
+ session.lastUsedAt = this.now();
16353
+ const previous = session.updatedAt ? Date.parse(session.updatedAt) : 0;
16354
+ session.updatedAt = new Date(Math.max(this.now(), previous + 1)).toISOString();
16355
+ }
16356
+ this.removeKey(minted.id);
16357
+ }
16358
+ };
16359
+ } finally {
16360
+ if (!handedOff) {
16361
+ this.acquisitions.delete(key);
16362
+ this.removeKey(minted.id);
16363
+ }
16364
+ }
16365
+ }
16366
+ createAgent(project, client) {
16367
+ const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config);
16368
+ return createAeroSession({
16369
+ projectName: project.name,
16370
+ client,
16371
+ config: this.opts.config,
16372
+ provider,
16373
+ modelId,
16374
+ systemPromptOverride: loadAeroSystemPrompt(void 0, { extras: false }) + VIEWER_AERO_PROMPT,
16375
+ toolScope: AeroToolScopes.readOnly,
16376
+ managedSweeps: this.opts.managedSweeps,
16377
+ db: this.opts.db,
16378
+ projectId: project.id
16379
+ });
16380
+ }
16381
+ /**
16382
+ * Delete the turn's key rather than revoke it: the key list shows revoked
16383
+ * keys too, and one per turn would bury the operator's own. Revoke only if
16384
+ * the delete fails, so the key is dead either way.
16385
+ */
16386
+ removeKey(id) {
16387
+ try {
16388
+ this.opts.db.delete(apiKeys).where(eq21(apiKeys.id, id)).run();
16389
+ } catch {
16390
+ try {
16391
+ this.opts.db.update(apiKeys).set({ revokedAt: new Date(this.now()).toISOString() }).where(eq21(apiKeys.id, id)).run();
16392
+ } catch {
16393
+ }
16394
+ }
16395
+ }
16396
+ /**
16397
+ * A key outlives its turn only if the process died mid-turn. Remove those on
16398
+ * start. Only delegated keys match: the key API never sets a delegate, so an
16399
+ * operator key that merely shares the name prefix is left alone.
16400
+ */
16401
+ revokeOrphanedKeys() {
16402
+ try {
16403
+ this.opts.db.delete(apiKeys).where(and17(like2(apiKeys.name, `${VIEWER_AERO_KEY_PREFIX}%`), isNotNull(apiKeys.delegatedUserId))).run();
16404
+ } catch {
16405
+ }
16406
+ }
16407
+ evictIdle() {
16408
+ const cutoff = this.now() - VIEWER_IDLE_MS;
16409
+ for (const [key, session] of this.sessions) {
16410
+ if (session.lastUsedAt < cutoff && !this.acquisitions.has(key) && !session.agent.state.isStreaming) {
16411
+ this.sessions.delete(key);
16412
+ }
16413
+ }
16414
+ }
16415
+ };
16416
+
16417
+ // src/mcp-http.ts
16418
+ import crypto26 from "crypto";
16419
+ import { eq as eq22 } from "drizzle-orm";
16206
16420
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
16207
16421
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
16208
16422
  var SESSION_IDLE_MS = 10 * 60 * 1e3;
16209
16423
  var SWEEP_INTERVAL_MS = 60 * 1e3;
16210
16424
  function mintSessionKey(db, scopes, userId) {
16211
- const raw = `cnry_${crypto25.randomBytes(24).toString("hex")}`;
16212
- const id = crypto25.randomUUID();
16425
+ const raw = `cnry_${crypto26.randomBytes(24).toString("hex")}`;
16426
+ const id = crypto26.randomUUID();
16213
16427
  db.insert(apiKeys).values({
16214
16428
  id,
16215
16429
  name: `mcp-session:${userId}`,
@@ -16223,7 +16437,7 @@ function mintSessionKey(db, scopes, userId) {
16223
16437
  }
16224
16438
  function revokeSessionKey(db, id) {
16225
16439
  try {
16226
- db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(apiKeys.id, id)).run();
16440
+ db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(apiKeys.id, id)).run();
16227
16441
  } catch {
16228
16442
  }
16229
16443
  }
@@ -16293,7 +16507,7 @@ function registerMcpHttpRoutes(scope, opts) {
16293
16507
  clientName: `canonry-mcp/${PACKAGE_VERSION}`,
16294
16508
  surface: "mcp-http",
16295
16509
  // Correlation only: this is never accepted as caller identity or authority.
16296
- actorSession: crypto25.randomUUID()
16510
+ actorSession: crypto26.randomUUID()
16297
16511
  });
16298
16512
  let server;
16299
16513
  try {
@@ -16310,7 +16524,7 @@ function registerMcpHttpRoutes(scope, opts) {
16310
16524
  throw error;
16311
16525
  }
16312
16526
  const transport = new StreamableHTTPServerTransport({
16313
- sessionIdGenerator: () => crypto25.randomUUID(),
16527
+ sessionIdGenerator: () => crypto26.randomUUID(),
16314
16528
  onsessioninitialized: (id) => {
16315
16529
  sessions.set(id, { transport, close, authorizationId, segmentId: segment.id, lastSeenAt: now() });
16316
16530
  }
@@ -16406,20 +16620,145 @@ function registerMcpHttpRoutes(scope, opts) {
16406
16620
  }
16407
16621
 
16408
16622
  // src/agent/agent-routes.ts
16409
- import { eq as eq22 } from "drizzle-orm";
16623
+ import { eq as eq23 } from "drizzle-orm";
16624
+ function viewerAeroCaller(request, opts) {
16625
+ if (!opts.viewerSessions) return null;
16626
+ const principal = request.principal;
16627
+ return principal?.kind === "user" && principal.role === UserRoles.viewer ? principal.id : null;
16628
+ }
16629
+ var VIEWER_HIDDEN_ASSISTANT_KEYS = ["api", "provider", "model", "responseId", "usage"];
16630
+ var VIEWER_ERROR_MESSAGE = "Aero could not finish this answer. Try again, or ask an administrator if it keeps happening.";
16631
+ function viewerRedact(_key, value) {
16632
+ if (!value || typeof value !== "object" || value.role !== "assistant") return value;
16633
+ const copy = { ...value };
16634
+ for (const key of VIEWER_HIDDEN_ASSISTANT_KEYS) delete copy[key];
16635
+ if (typeof copy.errorMessage === "string" && copy.errorMessage) copy.errorMessage = VIEWER_ERROR_MESSAGE;
16636
+ return copy;
16637
+ }
16638
+ function redactForViewer(value) {
16639
+ return JSON.parse(JSON.stringify(value, viewerRedact));
16640
+ }
16641
+ function viewerTranscript(opts, projectName, userId) {
16642
+ const sessions = opts.viewerSessions;
16643
+ return {
16644
+ conversationId: null,
16645
+ messages: redactForViewer(sessions.transcript(projectName, userId)),
16646
+ isStreaming: sessions.isBusy(projectName, userId),
16647
+ modelProvider: null,
16648
+ modelId: null,
16649
+ updatedAt: sessions.updatedAt(projectName, userId)
16650
+ };
16651
+ }
16410
16652
  function resolveProject(db, name) {
16411
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq22(projects.name, name)).get();
16653
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq23(projects.name, name)).get();
16412
16654
  if (!row) throw notFound("project", name);
16413
16655
  return row;
16414
16656
  }
16657
+ function promptMessage(prompt, context) {
16658
+ return {
16659
+ role: "user",
16660
+ content: prompt,
16661
+ timestamp: Date.now(),
16662
+ ...context ? { aeroContext: context } : {}
16663
+ };
16664
+ }
16665
+ async function streamAgentTurn(reply, agent, turn) {
16666
+ reply.raw.writeHead(200, {
16667
+ "Content-Type": "text/event-stream",
16668
+ "Cache-Control": "no-cache",
16669
+ "Connection": "keep-alive",
16670
+ "X-Accel-Buffering": "no"
16671
+ });
16672
+ const write = (payload) => {
16673
+ if (reply.raw.writableEnded) return;
16674
+ try {
16675
+ const frame = turn.viewer && payload.type === "error" ? { type: "error", message: VIEWER_ERROR_MESSAGE } : payload;
16676
+ reply.raw.write(`data: ${JSON.stringify(frame, turn.viewer ? viewerRedact : void 0)}
16677
+
16678
+ `);
16679
+ } catch {
16680
+ }
16681
+ };
16682
+ write({ type: "stream_open" });
16683
+ const unsubscribe = agent.subscribe((event) => {
16684
+ if (event.type === "tool_execution_start") {
16685
+ const labelled = { ...event, label: agent.state.tools.find((tool) => tool.name === event.toolName)?.label };
16686
+ write(labelled);
16687
+ } else write(event);
16688
+ });
16689
+ let failed = false;
16690
+ try {
16691
+ await agent.prompt(turn.batch);
16692
+ await agent.waitForIdle();
16693
+ } catch (err) {
16694
+ failed = true;
16695
+ write({ type: "error", message: describeError(err) });
16696
+ } finally {
16697
+ try {
16698
+ turn.save();
16699
+ } catch (err) {
16700
+ failed = true;
16701
+ write({ type: "error", message: describeError(err) });
16702
+ }
16703
+ const status = aeroTurnStatus(agent);
16704
+ write({ type: "aero_turn_status", status: status && failed ? { ...status, reason: "error" } : status });
16705
+ unsubscribe();
16706
+ turn.detach();
16707
+ write({ type: "stream_close" });
16708
+ if (!reply.raw.writableEnded) {
16709
+ reply.raw.end();
16710
+ }
16711
+ }
16712
+ }
16713
+ async function streamViewerTurn(opts, project, viewerId, body, reply) {
16714
+ const disconnected = new AbortController();
16715
+ const live = {};
16716
+ const onClose = () => {
16717
+ if (!reply.raw.writableEnded) {
16718
+ disconnected.abort();
16719
+ live.agent?.abort();
16720
+ }
16721
+ };
16722
+ reply.raw.once("close", onClose);
16723
+ let turn;
16724
+ try {
16725
+ turn = await opts.viewerSessions.acquireForTurn(project, viewerId, { context: body.context, signal: disconnected.signal });
16726
+ } catch (err) {
16727
+ reply.raw.off("close", onClose);
16728
+ if (disconnected.signal.aborted) return reply;
16729
+ throw err;
16730
+ }
16731
+ live.agent = turn.agent;
16732
+ if (disconnected.signal.aborted) {
16733
+ turn.release();
16734
+ return reply;
16735
+ }
16736
+ try {
16737
+ await streamAgentTurn(reply, turn.agent, {
16738
+ batch: promptMessage(body.prompt, body.context),
16739
+ save: () => {
16740
+ },
16741
+ detach: () => reply.raw.off("close", onClose),
16742
+ viewer: true
16743
+ });
16744
+ } finally {
16745
+ turn.release();
16746
+ }
16747
+ return reply;
16748
+ }
16415
16749
  function registerAgentRoutes(app, opts) {
16416
16750
  registerAgentConversationRoutes(app, { db: opts.db, runtime: opts.sessionRegistry });
16417
16751
  app.get(
16418
16752
  "/projects/:name/agent/transcript",
16419
16753
  async (request) => {
16754
+ const viewerId = viewerAeroCaller(request, opts);
16755
+ if (viewerId) {
16756
+ const project2 = resolveProject(opts.db, request.params.name);
16757
+ return viewerTranscript(opts, project2.name, viewerId);
16758
+ }
16420
16759
  requireInstanceAdministrator(request);
16421
16760
  const project = resolveProject(opts.db, request.params.name);
16422
- const row = opts.db.select().from(agentSessions).where(eq22(agentSessions.projectId, project.id)).get();
16761
+ const row = opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, project.id)).get();
16423
16762
  if (!row) {
16424
16763
  return { conversationId: null, messages: [], modelProvider: null, modelId: null, updatedAt: null };
16425
16764
  }
@@ -16444,23 +16783,46 @@ function registerAgentRoutes(app, opts) {
16444
16783
  );
16445
16784
  app.delete(
16446
16785
  "/projects/:name/agent/transcript",
16786
+ // For a viewer this writes nothing: it drops their own in-memory
16787
+ // conversation. Everyone else still meets the administrator gate below
16788
+ // before anything is written, so the grant widens nothing for them.
16789
+ { config: { readSemantic: true } },
16447
16790
  async (request) => {
16791
+ const viewerId = viewerAeroCaller(request, opts);
16792
+ if (viewerId) {
16793
+ const project2 = resolveProject(opts.db, request.params.name);
16794
+ opts.viewerSessions.reset(project2.name, viewerId);
16795
+ return { status: "reset" };
16796
+ }
16448
16797
  requireInstanceAdministrator(request);
16449
16798
  const project = resolveProject(opts.db, request.params.name);
16450
16799
  if (opts.sessionRegistry.isBusy(project.name)) throw agentBusy(project.name);
16451
16800
  opts.sessionRegistry.reset(project.name);
16452
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(agentSessions.projectId, project.id)).run();
16801
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(agentSessions.projectId, project.id)).run();
16453
16802
  return { status: "reset" };
16454
16803
  }
16455
16804
  );
16456
- app.post("/projects/:name/agent/prompt", async (request, reply) => {
16457
- requireInstanceAdministrator(request);
16805
+ app.post("/projects/:name/agent/prompt", {
16806
+ // A turn is a paid read: it calls the LLM provider. The marker only lets a
16807
+ // read-only principal reach this handler; the handler decides. A viewer is
16808
+ // served from their own lane when the install allows it, and every other
16809
+ // caller still meets `requireInstanceAdministrator`.
16810
+ config: { paidRead: true }
16811
+ }, async (request, reply) => {
16812
+ const viewerId = viewerAeroCaller(request, opts);
16813
+ if (!viewerId) requireInstanceAdministrator(request);
16458
16814
  const project = resolveProject(opts.db, request.params.name);
16459
16815
  const parsed = agentPromptRequestSchema.safeParse(request.body);
16460
16816
  if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => issue.message).join("; "));
16461
16817
  const body = parsed.data;
16818
+ if (viewerId) {
16819
+ if (body.prompt.length > VIEWER_AERO_MAX_PROMPT_CHARS) {
16820
+ throw validationError(`Keep questions under ${VIEWER_AERO_MAX_PROMPT_CHARS} characters.`);
16821
+ }
16822
+ return streamViewerTurn(opts, project, viewerId, body, reply);
16823
+ }
16462
16824
  if (body.conversationId !== void 0) {
16463
- const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(eq22(agentSessions.projectId, project.id)).get();
16825
+ const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(eq23(agentSessions.projectId, project.id)).get();
16464
16826
  if ((current?.id ?? null) !== body.conversationId) throw validationError("The active conversation changed. Reload the conversation before sending.");
16465
16827
  }
16466
16828
  const promptText = body.prompt;
@@ -16491,60 +16853,13 @@ function registerAgentRoutes(app, opts) {
16491
16853
  throw err;
16492
16854
  }
16493
16855
  if (disconnected.signal.aborted) return reply;
16494
- const acquiredAgent = agent;
16495
- reply.raw.writeHead(200, {
16496
- "Content-Type": "text/event-stream",
16497
- "Cache-Control": "no-cache",
16498
- "Connection": "keep-alive",
16499
- "X-Accel-Buffering": "no"
16856
+ const pending = opts.sessionRegistry.consumePending(project.name);
16857
+ const userMessage = promptMessage(promptText, body.context);
16858
+ await streamAgentTurn(reply, agent, {
16859
+ batch: pending.length > 0 ? [...pending, userMessage] : userMessage,
16860
+ save: () => opts.sessionRegistry.save(project.name),
16861
+ detach: () => reply.raw.off("close", onClose)
16500
16862
  });
16501
- const write = (payload) => {
16502
- if (reply.raw.writableEnded) return;
16503
- try {
16504
- reply.raw.write(`data: ${JSON.stringify(payload)}
16505
-
16506
- `);
16507
- } catch {
16508
- }
16509
- };
16510
- write({ type: "stream_open" });
16511
- const unsubscribe = agent.subscribe((event) => {
16512
- if (event.type === "tool_execution_start") {
16513
- const labelled = { ...event, label: acquiredAgent.state.tools.find((tool) => tool.name === event.toolName)?.label };
16514
- write(labelled);
16515
- } else write(event);
16516
- });
16517
- let failed = false;
16518
- try {
16519
- const pending = opts.sessionRegistry.consumePending(project.name);
16520
- const userMessage = {
16521
- role: "user",
16522
- content: promptText,
16523
- timestamp: Date.now(),
16524
- ...body.context ? { aeroContext: body.context } : {}
16525
- };
16526
- const batch = pending.length > 0 ? [...pending, userMessage] : userMessage;
16527
- await agent.prompt(batch);
16528
- await agent.waitForIdle();
16529
- } catch (err) {
16530
- failed = true;
16531
- write({ type: "error", message: describeError(err) });
16532
- } finally {
16533
- try {
16534
- opts.sessionRegistry.save(project.name);
16535
- } catch (err) {
16536
- failed = true;
16537
- write({ type: "error", message: describeError(err) });
16538
- }
16539
- const status = aeroTurnStatus(agent);
16540
- write({ type: "aero_turn_status", status: status && failed ? { ...status, reason: "error" } : status });
16541
- unsubscribe();
16542
- reply.raw.off("close", onClose);
16543
- write({ type: "stream_close" });
16544
- if (!reply.raw.writableEnded) {
16545
- reply.raw.end();
16546
- }
16547
- }
16548
16863
  return reply;
16549
16864
  });
16550
16865
  app.get(
@@ -17445,20 +17760,20 @@ function clipText(value, length) {
17445
17760
  }
17446
17761
 
17447
17762
  // src/research-runner.ts
17448
- import { and as and17, eq as eq23, inArray as inArray9, sql as sql9 } from "drizzle-orm";
17763
+ import { and as and18, eq as eq24, inArray as inArray9, sql as sql9 } from "drizzle-orm";
17449
17764
  var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
17450
17765
  function finalResearchRunStatus(completed, failed) {
17451
17766
  if (failed === 0) return ResearchRunStatuses.completed;
17452
17767
  return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
17453
17768
  }
17454
17769
  async function executeResearchRun(db, registry, runId, projectId) {
17455
- const run = db.select().from(researchRuns).where(and17(eq23(researchRuns.id, runId), eq23(researchRuns.projectId, projectId))).get();
17770
+ const run = db.select().from(researchRuns).where(and18(eq24(researchRuns.id, runId), eq24(researchRuns.projectId, projectId))).get();
17456
17771
  if (!run || run.status !== ResearchRunStatuses.queued) return;
17457
- const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
17772
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
17458
17773
  if (!project) return;
17459
17774
  const provider = registry.get(run.provider);
17460
17775
  const now = (/* @__PURE__ */ new Date()).toISOString();
17461
- const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and17(eq23(researchRuns.id, runId), eq23(researchRuns.status, ResearchRunStatuses.queued))).run();
17776
+ const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and18(eq24(researchRuns.id, runId), eq24(researchRuns.status, ResearchRunStatuses.queued))).run();
17462
17777
  if (claim.changes !== 1) return;
17463
17778
  let reserved = 0;
17464
17779
  let dispatched = 0;
@@ -17482,8 +17797,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
17482
17797
  }
17483
17798
  reserved = run.totalQueries;
17484
17799
  reservation = { scope, period };
17485
- const rows = db.select().from(researchRunQueries).where(eq23(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
17486
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq23(competitors.projectId, projectId)).all().map((row) => row.domain);
17800
+ const rows = db.select().from(researchRunQueries).where(eq24(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
17801
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq24(competitors.projectId, projectId)).all().map((row) => row.domain);
17487
17802
  const domains = effectiveDomains(project);
17488
17803
  const brands = effectiveBrandNames(project);
17489
17804
  const config = { ...provider.config, model: run.resolvedModel };
@@ -17491,7 +17806,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
17491
17806
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
17492
17807
  try {
17493
17808
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
17494
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and17(eq23(researchRunQueries.id, row.id), eq23(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
17809
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and18(eq24(researchRunQueries.id, row.id), eq24(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
17495
17810
  const raw = await gate.run(async () => {
17496
17811
  dispatched++;
17497
17812
  return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
@@ -17518,7 +17833,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
17518
17833
  citationState: determineCitationState(normalized, domains),
17519
17834
  rawResponse: raw.rawResponse,
17520
17835
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
17521
- }).where(and17(eq23(researchRunQueries.id, row.id), eq23(researchRunQueries.status, ResearchQueryStatuses.running))).run();
17836
+ }).where(and18(eq24(researchRunQueries.id, row.id), eq24(researchRunQueries.status, ResearchQueryStatuses.running))).run();
17522
17837
  if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
17523
17838
  } catch (error) {
17524
17839
  try {
@@ -17542,18 +17857,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
17542
17857
  }
17543
17858
  }
17544
17859
  function incrementResearchProgress(db, runId, column) {
17545
- db.update(researchRuns).set({ [column]: sql9`${researchRuns[column]} + 1` }).where(eq23(researchRuns.id, runId)).run();
17860
+ db.update(researchRuns).set({ [column]: sql9`${researchRuns[column]} + 1` }).where(eq24(researchRuns.id, runId)).run();
17546
17861
  }
17547
17862
  function markResearchQueryFailed(db, runId, queryId, error) {
17548
17863
  const message = describeError(error);
17549
- const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(eq23(researchRunQueries.id, queryId), inArray9(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
17864
+ const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and18(eq24(researchRunQueries.id, queryId), inArray9(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
17550
17865
  if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
17551
17866
  }
17552
17867
  function markUnfinishedResearchQueriesFailed(db, runId, error) {
17553
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(eq23(researchRunQueries.researchRunId, runId), inArray9(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
17868
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and18(eq24(researchRunQueries.researchRunId, runId), inArray9(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
17554
17869
  }
17555
17870
  function finalizeResearchRun(db, runId, fatalError) {
17556
- const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq23(researchRunQueries.researchRunId, runId)).all();
17871
+ const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq24(researchRunQueries.researchRunId, runId)).all();
17557
17872
  const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
17558
17873
  const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
17559
17874
  db.update(researchRuns).set({
@@ -17562,11 +17877,11 @@ function finalizeResearchRun(db, runId, fatalError) {
17562
17877
  failedQueries: failed,
17563
17878
  error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
17564
17879
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
17565
- }).where(eq23(researchRuns.id, runId)).run();
17880
+ }).where(eq24(researchRuns.id, runId)).run();
17566
17881
  }
17567
17882
 
17568
17883
  // src/google-marketing-sync.ts
17569
- import { and as and18, eq as eq24, isNull as isNull2 } from "drizzle-orm";
17884
+ import { and as and19, eq as eq25, isNull as isNull2 } from "drizzle-orm";
17570
17885
  function safeErrorMessage(error) {
17571
17886
  const raw = describeError(error);
17572
17887
  return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
@@ -17589,10 +17904,10 @@ function assertSnapshotContext(metadata2, expected) {
17589
17904
  }
17590
17905
  }
17591
17906
  function requireSyncRun(db, runId, projectId, kind) {
17592
- const run = db.select().from(runs).where(and18(
17593
- eq24(runs.id, runId),
17594
- eq24(runs.projectId, projectId),
17595
- eq24(runs.kind, kind)
17907
+ const run = db.select().from(runs).where(and19(
17908
+ eq25(runs.id, runId),
17909
+ eq25(runs.projectId, projectId),
17910
+ eq25(runs.kind, kind)
17596
17911
  )).get();
17597
17912
  if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
17598
17913
  if (run.status !== RunStatuses.queued) {
@@ -17605,19 +17920,19 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
17605
17920
  status: RunStatuses.failed,
17606
17921
  error: serializeRunError({ message: safeErrorMessage(error) }),
17607
17922
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
17608
- }).where(and18(
17609
- eq24(runs.id, runId),
17610
- eq24(runs.projectId, projectId),
17611
- eq24(runs.kind, kind),
17612
- eq24(runs.status, expectedStatus)
17923
+ }).where(and19(
17924
+ eq25(runs.id, runId),
17925
+ eq25(runs.projectId, projectId),
17926
+ eq25(runs.kind, kind),
17927
+ eq25(runs.status, expectedStatus)
17613
17928
  )).run();
17614
17929
  }
17615
17930
  function claimSyncRun(db, runId, projectId, kind, startedAt) {
17616
- const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and18(
17617
- eq24(runs.id, runId),
17618
- eq24(runs.projectId, projectId),
17619
- eq24(runs.kind, kind),
17620
- eq24(runs.status, RunStatuses.queued)
17931
+ const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and19(
17932
+ eq25(runs.id, runId),
17933
+ eq25(runs.projectId, projectId),
17934
+ eq25(runs.kind, kind),
17935
+ eq25(runs.status, RunStatuses.queued)
17621
17936
  )).run();
17622
17937
  return claim.changes === 1;
17623
17938
  }
@@ -17625,9 +17940,9 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
17625
17940
  let claimed = false;
17626
17941
  try {
17627
17942
  requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
17628
- const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
17943
+ const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
17629
17944
  if (!project) throw new Error(`Project ${projectId} was not found.`);
17630
- const connection = db.select().from(googleAdsConnections).where(eq24(googleAdsConnections.projectId, projectId)).get();
17945
+ const connection = db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, projectId)).get();
17631
17946
  if (!connection?.selectedCustomerId) {
17632
17947
  throw new Error("Google Ads needs a selected customer before it can sync.");
17633
17948
  }
@@ -17678,11 +17993,11 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
17678
17993
  status: RunStatuses.completed,
17679
17994
  finishedAt,
17680
17995
  error: null
17681
- }).where(and18(
17682
- eq24(runs.id, runId),
17683
- eq24(runs.projectId, projectId),
17684
- eq24(runs.kind, RunKinds["google-ads-sync"]),
17685
- eq24(runs.status, RunStatuses.running)
17996
+ }).where(and19(
17997
+ eq25(runs.id, runId),
17998
+ eq25(runs.projectId, projectId),
17999
+ eq25(runs.kind, RunKinds["google-ads-sync"]),
18000
+ eq25(runs.status, RunStatuses.running)
17686
18001
  )).run();
17687
18002
  if (terminalClaim.changes !== 1) {
17688
18003
  throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
@@ -17699,12 +18014,12 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
17699
18014
  lastMetricsSnapshotAt: result2.metrics?.metadata.capturedAt ?? connection.lastMetricsSnapshotAt,
17700
18015
  lastMetricsSnapshotId: result2.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
17701
18016
  updatedAt: finishedAt
17702
- }).where(and18(
17703
- eq24(googleAdsConnections.id, connection.id),
17704
- eq24(googleAdsConnections.projectId, projectId),
17705
- eq24(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
17706
- connection.selectedLoginCustomerId === null ? isNull2(googleAdsConnections.selectedLoginCustomerId) : eq24(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
17707
- eq24(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
18017
+ }).where(and19(
18018
+ eq25(googleAdsConnections.id, connection.id),
18019
+ eq25(googleAdsConnections.projectId, projectId),
18020
+ eq25(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
18021
+ connection.selectedLoginCustomerId === null ? isNull2(googleAdsConnections.selectedLoginCustomerId) : eq25(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
18022
+ eq25(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
17708
18023
  )).run();
17709
18024
  if (connectionClaim.changes !== 1) {
17710
18025
  throw new Error("Google Ads selection changed while its evidence was being collected.");
@@ -17731,9 +18046,9 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
17731
18046
  let claimed = false;
17732
18047
  try {
17733
18048
  requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
17734
- const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
18049
+ const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
17735
18050
  if (!project) throw new Error(`Project ${projectId} was not found.`);
17736
- const connection = db.select().from(gtmConnections).where(eq24(gtmConnections.projectId, projectId)).get();
18051
+ const connection = db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, projectId)).get();
17737
18052
  if (!connection?.selectedAccountId || !connection.selectedContainerId) {
17738
18053
  throw new Error("GTM needs a selected account and container before it can sync.");
17739
18054
  }
@@ -17768,11 +18083,11 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
17768
18083
  status: RunStatuses.completed,
17769
18084
  finishedAt,
17770
18085
  error: null
17771
- }).where(and18(
17772
- eq24(runs.id, runId),
17773
- eq24(runs.projectId, projectId),
17774
- eq24(runs.kind, RunKinds["gtm-sync"]),
17775
- eq24(runs.status, RunStatuses.running)
18086
+ }).where(and19(
18087
+ eq25(runs.id, runId),
18088
+ eq25(runs.projectId, projectId),
18089
+ eq25(runs.kind, RunKinds["gtm-sync"]),
18090
+ eq25(runs.status, RunStatuses.running)
17776
18091
  )).run();
17777
18092
  if (terminalClaim.changes !== 1) {
17778
18093
  throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
@@ -17786,13 +18101,13 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
17786
18101
  lastSnapshotAt: snapshot.metadata.capturedAt,
17787
18102
  lastSnapshotId: snapshot.metadata.id,
17788
18103
  updatedAt: finishedAt
17789
- }).where(and18(
17790
- eq24(gtmConnections.id, connection.id),
17791
- eq24(gtmConnections.projectId, projectId),
17792
- eq24(gtmConnections.selectedAccountId, connection.selectedAccountId),
17793
- eq24(gtmConnections.selectedContainerId, connection.selectedContainerId),
17794
- connection.selectedWorkspaceId === null ? isNull2(gtmConnections.selectedWorkspaceId) : eq24(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
17795
- eq24(gtmConnections.selectionGeneration, connection.selectionGeneration)
18104
+ }).where(and19(
18105
+ eq25(gtmConnections.id, connection.id),
18106
+ eq25(gtmConnections.projectId, projectId),
18107
+ eq25(gtmConnections.selectedAccountId, connection.selectedAccountId),
18108
+ eq25(gtmConnections.selectedContainerId, connection.selectedContainerId),
18109
+ connection.selectedWorkspaceId === null ? isNull2(gtmConnections.selectedWorkspaceId) : eq25(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
18110
+ eq25(gtmConnections.selectionGeneration, connection.selectionGeneration)
17796
18111
  )).run();
17797
18112
  if (connectionClaim.changes !== 1) {
17798
18113
  throw new Error("GTM selection changed while its evidence was being collected.");
@@ -17913,14 +18228,14 @@ function summarizeProviderConfig(config) {
17913
18228
  };
17914
18229
  }
17915
18230
  function hashApiKey2(key) {
17916
- return crypto26.createHash("sha256").update(key).digest("hex");
18231
+ return crypto27.createHash("sha256").update(key).digest("hex");
17917
18232
  }
17918
18233
  var DASHBOARD_SCRYPT_KEYLEN = 64;
17919
18234
  var DASHBOARD_SCRYPT_COST = 1 << 15;
17920
18235
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
17921
18236
  function hashDashboardPassword(password) {
17922
- const salt = crypto26.randomBytes(16);
17923
- const derived = crypto26.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
18237
+ const salt = crypto27.randomBytes(16);
18238
+ const derived = crypto27.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
17924
18239
  N: DASHBOARD_SCRYPT_COST,
17925
18240
  maxmem: DASHBOARD_SCRYPT_MAXMEM
17926
18241
  });
@@ -17941,14 +18256,14 @@ function verifyDashboardPassword(password, storedHash) {
17941
18256
  } catch {
17942
18257
  return { ok: false, needsRehash: false };
17943
18258
  }
17944
- const derived = crypto26.scryptSync(password, salt, expected.length, {
18259
+ const derived = crypto27.scryptSync(password, salt, expected.length, {
17945
18260
  N: DASHBOARD_SCRYPT_COST,
17946
18261
  maxmem: DASHBOARD_SCRYPT_MAXMEM
17947
18262
  });
17948
18263
  if (derived.length !== expected.length)
17949
18264
  return { ok: false, needsRehash: false };
17950
18265
  return {
17951
- ok: crypto26.timingSafeEqual(derived, expected),
18266
+ ok: crypto27.timingSafeEqual(derived, expected),
17952
18267
  needsRehash: false
17953
18268
  };
17954
18269
  }
@@ -17957,7 +18272,7 @@ function verifyDashboardPassword(password, storedHash) {
17957
18272
  const expected = Buffer.from(storedHash, "hex");
17958
18273
  if (candidate.length !== expected.length)
17959
18274
  return { ok: false, needsRehash: false };
17960
- const ok = crypto26.timingSafeEqual(candidate, expected);
18275
+ const ok = crypto27.timingSafeEqual(candidate, expected);
17961
18276
  return { ok, needsRehash: ok };
17962
18277
  }
17963
18278
  return { ok: false, needsRehash: false };
@@ -18093,7 +18408,7 @@ function cloneGtmConfig(config) {
18093
18408
  function createGoogleMarketingConfigCredentialStore(input) {
18094
18409
  const persistConfigPatch = input.saveConfigPatch ?? saveConfigPatch;
18095
18410
  const env = input.env ?? process.env;
18096
- const randomUUID2 = input.randomUUID ?? crypto26.randomUUID;
18411
+ const randomUUID2 = input.randomUUID ?? crypto27.randomUUID;
18097
18412
  const snapshot = () => ({
18098
18413
  googleAds: cloneGoogleAdsConfig(input.config.googleAds),
18099
18414
  gtm: cloneGtmConfig(input.config.gtm)
@@ -18196,7 +18511,7 @@ async function createServer(opts) {
18196
18511
  const trustProxy = resolveTrustProxy(process.env.CANONRY_TRUST_PROXY);
18197
18512
  const app = Fastify({
18198
18513
  loggerInstance: logger,
18199
- genReqId: () => crypto26.randomUUID(),
18514
+ genReqId: () => crypto27.randomUUID(),
18200
18515
  trustProxy
18201
18516
  });
18202
18517
  const registry = new ProviderRegistry();
@@ -18281,6 +18596,13 @@ async function createServer(opts) {
18281
18596
  proactive: agentProactive,
18282
18597
  managedSweeps: dashboardManagedRunKinds.includes(SchedulableRunKinds["answer-visibility"])
18283
18598
  }) : void 0;
18599
+ const agentAllowViewers = agentEnabled && resolveAgentAllowViewers(process.env, opts.config);
18600
+ const viewerAeroSessions = agentAllowViewers ? new ViewerAeroSessions({
18601
+ db: opts.db,
18602
+ config: opts.config,
18603
+ selfApiUrl: opts.config.apiUrl,
18604
+ managedSweeps: dashboardManagedRunKinds.includes(SchedulableRunKinds["answer-visibility"])
18605
+ }) : void 0;
18284
18606
  const runCoordinator = new RunCoordinator(
18285
18607
  opts.db,
18286
18608
  notifier,
@@ -18289,7 +18611,7 @@ async function createServer(opts) {
18289
18611
  async (ctx) => {
18290
18612
  if (!sessionRegistry) return;
18291
18613
  if (!agentProactive) return;
18292
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq25(projects.id, ctx.projectId)).get();
18614
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq26(projects.id, ctx.projectId)).get();
18293
18615
  if (!project) return;
18294
18616
  let content;
18295
18617
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -18746,7 +19068,7 @@ async function createServer(opts) {
18746
19068
  void (async () => {
18747
19069
  try {
18748
19070
  const report = await schedulerClient.runDoctor({ project: projectName });
18749
- const project = opts.db.select().from(projects).where(eq25(projects.name, projectName)).get();
19071
+ const project = opts.db.select().from(projects).where(eq26(projects.name, projectName)).get();
18750
19072
  if (!project) {
18751
19073
  app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
18752
19074
  return;
@@ -18774,9 +19096,9 @@ async function createServer(opts) {
18774
19096
  });
18775
19097
  if (!probed) return;
18776
19098
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
18777
- and19(
18778
- eq25(ccReleaseSyncs.release, probed.release),
18779
- eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
19099
+ and20(
19100
+ eq26(ccReleaseSyncs.release, probed.release),
19101
+ eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
18780
19102
  )
18781
19103
  ).limit(1).get();
18782
19104
  if (alreadySynced) {
@@ -18957,7 +19279,7 @@ async function createServer(opts) {
18957
19279
  return removed;
18958
19280
  }
18959
19281
  };
18960
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto26.randomBytes(32).toString("hex");
19282
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto27.randomBytes(32).toString("hex");
18961
19283
  const googleConnectionStore = {
18962
19284
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
18963
19285
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -19047,11 +19369,11 @@ async function createServer(opts) {
19047
19369
  })();
19048
19370
  if (opts.config.apiKey) {
19049
19371
  const keyHash = hashApiKey2(opts.config.apiKey);
19050
- const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
19372
+ const existing = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, keyHash)).get();
19051
19373
  if (!existing) {
19052
19374
  const prefix = opts.config.apiKey.slice(0, 12);
19053
19375
  opts.db.insert(apiKeys).values({
19054
- id: `key_${crypto26.randomBytes(8).toString("hex")}`,
19376
+ id: `key_${crypto27.randomBytes(8).toString("hex")}`,
19055
19377
  name: "default",
19056
19378
  keyHash,
19057
19379
  keyPrefix: prefix,
@@ -19075,7 +19397,7 @@ async function createServer(opts) {
19075
19397
  };
19076
19398
  const createSession = (apiKeyId) => {
19077
19399
  pruneExpiredSessions();
19078
- const sessionId = crypto26.randomBytes(32).toString("hex");
19400
+ const sessionId = crypto27.randomBytes(32).toString("hex");
19079
19401
  sessions.set(sessionId, {
19080
19402
  apiKeyId,
19081
19403
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -19099,7 +19421,7 @@ async function createServer(opts) {
19099
19421
  };
19100
19422
  const getDefaultApiKey = () => {
19101
19423
  if (!opts.config.apiKey) return void 0;
19102
- return opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
19424
+ return opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
19103
19425
  };
19104
19426
  const createPasswordSession = (reply) => {
19105
19427
  const key = getDefaultApiKey();
@@ -19123,7 +19445,7 @@ async function createServer(opts) {
19123
19445
  if (!header) return false;
19124
19446
  const parts = header.split(" ");
19125
19447
  if (parts.length !== 2 || parts[0] !== "Bearer") return false;
19126
- const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
19448
+ const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
19127
19449
  return Boolean(key && !key.revokedAt);
19128
19450
  };
19129
19451
  const namedAccountsInUse = () => anyUsersExist(opts.db);
@@ -19213,12 +19535,12 @@ async function createServer(opts) {
19213
19535
  return reply.send({ authenticated: true });
19214
19536
  }
19215
19537
  if (apiKey) {
19216
- const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey2(apiKey))).get();
19538
+ const key = opts.db.select().from(apiKeys).where(eq26(apiKeys.keyHash, hashApiKey2(apiKey))).get();
19217
19539
  if (!key || key.revokedAt) {
19218
19540
  const err2 = authInvalid();
19219
19541
  return reply.status(err2.statusCode).send(err2.toJSON());
19220
19542
  }
19221
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, key.id)).run();
19543
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(apiKeys.id, key.id)).run();
19222
19544
  const sessionId = createSession(key.id);
19223
19545
  reply.header(
19224
19546
  "set-cookie",
@@ -19371,7 +19693,7 @@ async function createServer(opts) {
19371
19693
  });
19372
19694
  registerOAuthAdminRoutes(scope, { db: opts.db });
19373
19695
  if (!sessionRegistry) return;
19374
- registerAgentRoutes(scope, { db: opts.db, sessionRegistry });
19696
+ registerAgentRoutes(scope, { db: opts.db, sessionRegistry, viewerSessions: viewerAeroSessions });
19375
19697
  },
19376
19698
  getGoogleAuthConfig: () => getGoogleAuthConfig(opts.config),
19377
19699
  getPlacesConfig: () => getPlacesConfig(opts.config),
@@ -19381,8 +19703,8 @@ async function createServer(opts) {
19381
19703
  getAgentPin: () => describeAgentPin(opts.config),
19382
19704
  getGoogleMarketingDoctorInput: (ctx) => {
19383
19705
  if (!ctx.project) return null;
19384
- const googleAds = opts.db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, ctx.project.id)).get();
19385
- const gtm = opts.db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, ctx.project.id)).get();
19706
+ const googleAds = opts.db.select().from(googleAdsConnections).where(eq26(googleAdsConnections.projectId, ctx.project.id)).get();
19707
+ const gtm = opts.db.select().from(gtmConnections).where(eq26(gtmConnections.projectId, ctx.project.id)).get();
19386
19708
  const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
19387
19709
  const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
19388
19710
  const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
@@ -19506,7 +19828,7 @@ async function createServer(opts) {
19506
19828
  deps: {
19507
19829
  enqueueAutoExtract: ({ projectId, release: r }) => {
19508
19830
  const now = (/* @__PURE__ */ new Date()).toISOString();
19509
- const runId = crypto26.randomUUID();
19831
+ const runId = crypto27.randomUUID();
19510
19832
  opts.db.insert(runs).values({
19511
19833
  id: runId,
19512
19834
  projectId,
@@ -19633,7 +19955,7 @@ async function createServer(opts) {
19633
19955
  ...inspectOpts,
19634
19956
  config: opts.config
19635
19957
  }).then(() => {
19636
- const finished = opts.db.select({ status: runs.status }).from(runs).where(eq25(runs.id, runId)).get();
19958
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq26(runs.id, runId)).get();
19637
19959
  if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
19638
19960
  return maybeRefreshGscCoverage(
19639
19961
  opts.db,
@@ -19751,7 +20073,7 @@ async function createServer(opts) {
19751
20073
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
19752
20074
  opts.db.insert(auditLog).values(
19753
20075
  targetProjectIds.map((projectId) => ({
19754
- id: crypto26.randomUUID(),
20076
+ id: crypto27.randomUUID(),
19755
20077
  projectId,
19756
20078
  actor: auditContext?.actor ?? "api",
19757
20079
  userAgent: auditContext?.userAgent ?? null,
@@ -19946,6 +20268,9 @@ async function createServer(opts) {
19946
20268
  if (Object.keys(dashboardConfig).length > 0) {
19947
20269
  clientConfig.dashboard = dashboardConfig;
19948
20270
  }
20271
+ if (agentAllowViewers) {
20272
+ clientConfig.agent = { allowViewers: true };
20273
+ }
19949
20274
  if (researchAllowViewers) {
19950
20275
  clientConfig.research = {
19951
20276
  allowViewers: true,
@@ -20106,7 +20431,7 @@ async function createServer(opts) {
20106
20431
  },
20107
20432
  notify: (projectId, result2) => notifier.onSiteLivenessChecked(projectId, result2)
20108
20433
  });
20109
- for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq25(researchRuns.status, ResearchRunStatuses.queued)).all()) {
20434
+ for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq26(researchRuns.status, ResearchRunStatuses.queued)).all()) {
20110
20435
  dispatchResearchRun(run.id, run.projectId);
20111
20436
  }
20112
20437
  runtimeStartupSettled = true;