@ainyc/canonry 4.51.0 → 4.51.2

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.
@@ -2,10 +2,11 @@ import {
2
2
  ApiClient,
3
3
  canonryMcpTools,
4
4
  configExists,
5
+ getConfigPath,
5
6
  loadConfig,
6
7
  loadConfigRaw,
7
8
  saveConfigPatch
8
- } from "./chunk-GGXU5VKI.js";
9
+ } from "./chunk-HMZKIOLG.js";
9
10
  import {
10
11
  DEFAULT_RUN_HISTORY_LIMIT,
11
12
  IntelligenceService,
@@ -82,7 +83,7 @@ import {
82
83
  smoothedRunDelta,
83
84
  trafficSources,
84
85
  usageCounters
85
- } from "./chunk-DLDLDWH4.js";
86
+ } from "./chunk-QZ5XSM6C.js";
86
87
  import {
87
88
  AGENT_MEMORY_VALUE_MAX_BYTES,
88
89
  AGENT_PROVIDER_IDS,
@@ -247,6 +248,7 @@ import {
247
248
  runInProgress,
248
249
  runNotCancellable,
249
250
  runTriggerRequestSchema,
251
+ runtimeStateMissing,
250
252
  schedulableRunKindSchema,
251
253
  scheduleDtoSchema,
252
254
  scheduleUpsertRequestSchema,
@@ -282,7 +284,7 @@ import {
282
284
  wordpressSchemaDeployResultDtoSchema,
283
285
  wordpressSchemaStatusResultDtoSchema,
284
286
  wordpressStatusDtoSchema
285
- } from "./chunk-2ARCCG5E.js";
287
+ } from "./chunk-FYGBW3SM.js";
286
288
 
287
289
  // src/telemetry.ts
288
290
  import crypto from "crypto";
@@ -571,12 +573,15 @@ function checkLatestVersionForServer(opts) {
571
573
  // src/server.ts
572
574
  import { createRequire as createRequire4 } from "module";
573
575
  import crypto35 from "crypto";
574
- import fs13 from "fs";
576
+ import fs15 from "fs";
575
577
  import path15 from "path";
576
578
  import { fileURLToPath as fileURLToPath2 } from "url";
577
579
  import { eq as eq42 } from "drizzle-orm";
578
580
  import Fastify from "fastify";
579
581
 
582
+ // ../api-routes/src/index.ts
583
+ import fs8 from "fs";
584
+
580
585
  // ../api-routes/src/auth.ts
581
586
  import crypto2 from "crypto";
582
587
  import { eq } from "drizzle-orm";
@@ -669,9 +674,22 @@ function writeAuditLog(db, entry) {
669
674
  entityType: entry.entityType,
670
675
  entityId: entry.entityId ?? null,
671
676
  diff: entry.diff != null ? JSON.stringify(entry.diff) : null,
677
+ userAgent: entry.userAgent ?? null,
678
+ actorSession: entry.actorSession ?? null,
672
679
  createdAt: now
673
680
  }).run();
674
681
  }
682
+ function auditFromRequest(request, entry) {
683
+ const ua = request.headers["user-agent"];
684
+ const userAgent = Array.isArray(ua) ? ua.join(", ") : ua ?? null;
685
+ const sess = request.headers["x-canonry-actor-session"];
686
+ const actorSession = Array.isArray(sess) ? sess.join(", ") : sess ?? null;
687
+ return {
688
+ ...entry,
689
+ userAgent: entry.userAgent ?? userAgent,
690
+ actorSession: entry.actorSession ?? actorSession
691
+ };
692
+ }
675
693
  function resolveSnapshotMentionResult(snapshot, project) {
676
694
  if (snapshot.answerText) {
677
695
  const domains = effectiveDomains({
@@ -1023,6 +1041,12 @@ function aliasArraysEqual(a, b) {
1023
1041
  // ../api-routes/src/queries.ts
1024
1042
  import crypto5 from "crypto";
1025
1043
  import { eq as eq4, inArray, sql as sql3 } from "drizzle-orm";
1044
+ function preserveSnapshotQueryText(tx, projectId, queryIds) {
1045
+ const candidates = queryIds && queryIds.length > 0 ? tx.select({ id: queries.id, text: queries.query }).from(queries).where(inArray(queries.id, queryIds)).all() : tx.select({ id: queries.id, text: queries.query }).from(queries).where(eq4(queries.projectId, projectId)).all();
1046
+ for (const q of candidates) {
1047
+ tx.update(querySnapshots).set({ queryText: q.text }).where(eq4(querySnapshots.queryId, q.id)).run();
1048
+ }
1049
+ }
1026
1050
  async function queryRoutes(app, opts) {
1027
1051
  app.get("/projects/:name/queries", async (request, reply) => {
1028
1052
  const project = resolveProject(app.db, request.params.name);
@@ -1037,6 +1061,7 @@ async function queryRoutes(app, opts) {
1037
1061
  }
1038
1062
  const now = (/* @__PURE__ */ new Date()).toISOString();
1039
1063
  app.db.transaction((tx) => {
1064
+ preserveSnapshotQueryText(tx, project.id);
1040
1065
  tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
1041
1066
  for (const q of body.queries) {
1042
1067
  tx.insert(queries).values({
@@ -1047,13 +1072,13 @@ async function queryRoutes(app, opts) {
1047
1072
  createdAt: now
1048
1073
  }).run();
1049
1074
  }
1050
- writeAuditLog(tx, {
1075
+ writeAuditLog(tx, auditFromRequest(request, {
1051
1076
  projectId: project.id,
1052
1077
  actor: "api",
1053
1078
  action: "queries.replaced",
1054
1079
  entityType: "query",
1055
1080
  diff: { queries: body.queries }
1056
- });
1081
+ }));
1057
1082
  });
1058
1083
  const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
1059
1084
  return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
@@ -1099,16 +1124,17 @@ async function queryRoutes(app, opts) {
1099
1124
  const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
1100
1125
  if (idsToDelete.length > 0) {
1101
1126
  app.db.transaction((tx) => {
1127
+ preserveSnapshotQueryText(tx, project.id, idsToDelete);
1102
1128
  for (const id of idsToDelete) {
1103
1129
  tx.delete(queries).where(eq4(queries.id, id)).run();
1104
1130
  }
1105
- writeAuditLog(tx, {
1131
+ writeAuditLog(tx, auditFromRequest(request, {
1106
1132
  projectId: project.id,
1107
1133
  actor: "api",
1108
1134
  action: "queries.deleted",
1109
1135
  entityType: "query",
1110
1136
  diff: { deleted: body.queries.filter((q) => existing.some((e) => e.query === q)) }
1111
- });
1137
+ }));
1112
1138
  });
1113
1139
  }
1114
1140
  const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
@@ -1138,13 +1164,13 @@ async function queryRoutes(app, opts) {
1138
1164
  }
1139
1165
  }
1140
1166
  if (added.length > 0) {
1141
- writeAuditLog(app.db, {
1167
+ writeAuditLog(app.db, auditFromRequest(request, {
1142
1168
  projectId: project.id,
1143
1169
  actor: "api",
1144
1170
  action: "queries.appended",
1145
1171
  entityType: "query",
1146
1172
  diff: { added }
1147
- });
1173
+ }));
1148
1174
  }
1149
1175
  const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
1150
1176
  return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
@@ -1202,6 +1228,7 @@ async function queryRoutes(app, opts) {
1202
1228
  }
1203
1229
  const now = (/* @__PURE__ */ new Date()).toISOString();
1204
1230
  app.db.transaction((tx) => {
1231
+ preserveSnapshotQueryText(tx, project.id);
1205
1232
  tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
1206
1233
  for (const keyword of body.keywords) {
1207
1234
  tx.insert(queries).values({
@@ -1212,13 +1239,13 @@ async function queryRoutes(app, opts) {
1212
1239
  createdAt: now
1213
1240
  }).run();
1214
1241
  }
1215
- writeAuditLog(tx, {
1242
+ writeAuditLog(tx, auditFromRequest(request, {
1216
1243
  projectId: project.id,
1217
1244
  actor: "api",
1218
1245
  action: "queries.replaced",
1219
1246
  entityType: "query",
1220
1247
  diff: { queries: body.keywords }
1221
- });
1248
+ }));
1222
1249
  });
1223
1250
  const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
1224
1251
  return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
@@ -1234,16 +1261,17 @@ async function queryRoutes(app, opts) {
1234
1261
  const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
1235
1262
  if (idsToDelete.length > 0) {
1236
1263
  app.db.transaction((tx) => {
1264
+ preserveSnapshotQueryText(tx, project.id, idsToDelete);
1237
1265
  for (const id of idsToDelete) {
1238
1266
  tx.delete(queries).where(eq4(queries.id, id)).run();
1239
1267
  }
1240
- writeAuditLog(tx, {
1268
+ writeAuditLog(tx, auditFromRequest(request, {
1241
1269
  projectId: project.id,
1242
1270
  actor: "api",
1243
1271
  action: "queries.deleted",
1244
1272
  entityType: "query",
1245
1273
  diff: { deleted: body.keywords.filter((keyword) => existing.some((e) => e.query === keyword)) }
1246
- });
1274
+ }));
1247
1275
  });
1248
1276
  }
1249
1277
  const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
@@ -1273,13 +1301,13 @@ async function queryRoutes(app, opts) {
1273
1301
  }
1274
1302
  }
1275
1303
  if (added.length > 0) {
1276
- writeAuditLog(app.db, {
1304
+ writeAuditLog(app.db, auditFromRequest(request, {
1277
1305
  projectId: project.id,
1278
1306
  actor: "api",
1279
1307
  action: "queries.appended",
1280
1308
  entityType: "query",
1281
1309
  diff: { added }
1282
- });
1310
+ }));
1283
1311
  }
1284
1312
  const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
1285
1313
  return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
@@ -1639,8 +1667,10 @@ async function runRoutes(app, opts) {
1639
1667
  const limit = parseListLimit(request.query.limit, 500, 5e3);
1640
1668
  const since = parseListSince(request.query.since);
1641
1669
  const includeProbe = request.query.includeProbe === "1" || request.query.includeProbe === "true";
1670
+ const kind = parseListKind(request.query.kind);
1642
1671
  const filters = [gte(runs.createdAt, since)];
1643
1672
  if (!includeProbe) filters.push(notProbeRun());
1673
+ if (kind) filters.push(eq7(runs.kind, kind));
1644
1674
  const rows = app.db.select().from(runs).where(and2(...filters)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(limit).all();
1645
1675
  return reply.send(rows.map(formatRun));
1646
1676
  });
@@ -1749,6 +1779,14 @@ function parseListLimit(raw, defaultValue, max) {
1749
1779
  }
1750
1780
  return Math.min(parsed, max);
1751
1781
  }
1782
+ function parseListKind(raw) {
1783
+ if (raw === void 0 || raw === "") return null;
1784
+ const validKinds = Object.values(RunKinds);
1785
+ if (!validKinds.includes(raw)) {
1786
+ throw validationError(`"kind" must be one of: ${validKinds.join(", ")}`);
1787
+ }
1788
+ return raw;
1789
+ }
1752
1790
  function parseListSince(raw) {
1753
1791
  if (raw === void 0) {
1754
1792
  const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
@@ -2495,9 +2533,24 @@ async function historyRoutes(app) {
2495
2533
  ...snapshot,
2496
2534
  answerMentioned: resolveSnapshotAnswerMentioned(snapshot, project)
2497
2535
  }));
2536
+ const queryByText = /* @__PURE__ */ new Map();
2537
+ for (const q of projectQueries) queryByText.set(q.query, q);
2538
+ function resolveSnapQueryId(snap) {
2539
+ if (snap.queryId) return snap.queryId;
2540
+ if (snap.queryText) {
2541
+ const match = queryByText.get(snap.queryText);
2542
+ if (match) return match.id;
2543
+ }
2544
+ return null;
2545
+ }
2546
+ const allSnapshotsResolved = allSnapshots.map((s) => ({
2547
+ ...s,
2548
+ resolvedQueryId: resolveSnapQueryId(s)
2549
+ }));
2498
2550
  const deduped = /* @__PURE__ */ new Map();
2499
- for (const snap of allSnapshots) {
2500
- const key = `${snap.runId}:${snap.queryId}`;
2551
+ for (const snap of allSnapshotsResolved) {
2552
+ if (!snap.resolvedQueryId) continue;
2553
+ const key = `${snap.runId}:${snap.resolvedQueryId}`;
2501
2554
  const existing = deduped.get(key);
2502
2555
  if (!existing || !existing.answerMentioned && snap.answerMentioned || existing.answerMentioned === snap.answerMentioned && snap.citationState === CitationStates.cited) {
2503
2556
  deduped.set(key, snap);
@@ -2505,15 +2558,17 @@ async function historyRoutes(app) {
2505
2558
  }
2506
2559
  const dedupedSnapshots = [...deduped.values()];
2507
2560
  const rawByQueryProvider = /* @__PURE__ */ new Map();
2508
- for (const snap of allSnapshots) {
2509
- const key = `${snap.queryId}::${snap.provider}`;
2561
+ for (const snap of allSnapshotsResolved) {
2562
+ if (!snap.resolvedQueryId) continue;
2563
+ const key = `${snap.resolvedQueryId}::${snap.provider}`;
2510
2564
  const arr = rawByQueryProvider.get(key);
2511
2565
  if (arr) arr.push(snap);
2512
2566
  else rawByQueryProvider.set(key, [snap]);
2513
2567
  }
2514
2568
  const rawByQueryModel = /* @__PURE__ */ new Map();
2515
- for (const snap of allSnapshots) {
2516
- const key = `${snap.queryId}::${snap.provider}:${snap.model ?? "unknown"}`;
2569
+ for (const snap of allSnapshotsResolved) {
2570
+ if (!snap.resolvedQueryId) continue;
2571
+ const key = `${snap.resolvedQueryId}::${snap.provider}:${snap.model ?? "unknown"}`;
2517
2572
  const arr = rawByQueryModel.get(key);
2518
2573
  if (arr) arr.push(snap);
2519
2574
  else rawByQueryModel.set(key, [snap]);
@@ -2560,7 +2615,7 @@ async function historyRoutes(app) {
2560
2615
  });
2561
2616
  }
2562
2617
  const timeline = projectQueries.map((q) => {
2563
- const qSnapshots = dedupedSnapshots.filter((s) => s.queryId === q.id).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2618
+ const qSnapshots = dedupedSnapshots.filter((s) => s.resolvedQueryId === q.id).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2564
2619
  const runEntries = computeTransitions(qSnapshots);
2565
2620
  const providerRuns = {};
2566
2621
  const providerKeys = [...rawByQueryProvider.keys()].filter((k) => k.startsWith(`${q.id}::`));
@@ -8387,6 +8442,39 @@ var scheduleKindQueryParameter = {
8387
8442
  description: 'Schedulable run kind. Defaults to "answer-visibility" for backward compatibility.',
8388
8443
  schema: { type: "string", enum: ["answer-visibility", "traffic-sync"] }
8389
8444
  };
8445
+ var runsListKindQueryParameter = {
8446
+ name: "kind",
8447
+ in: "query",
8448
+ description: "Restrict results to a single run kind. Without this filter, integration syncs (bing-inspect, gsc-sync, ga-sync) can fill the default 500-row cap within minutes on busy projects and push answer-visibility runs out of the response.",
8449
+ schema: {
8450
+ type: "string",
8451
+ enum: [
8452
+ "answer-visibility",
8453
+ "site-audit",
8454
+ "gsc-sync",
8455
+ "inspect-sitemap",
8456
+ "ga-sync",
8457
+ "bing-inspect",
8458
+ "bing-inspect-sitemap",
8459
+ "backlink-extract",
8460
+ "traffic-sync",
8461
+ "aeo-discover-seed",
8462
+ "aeo-discover-probe"
8463
+ ]
8464
+ }
8465
+ };
8466
+ var runsListSinceQueryParameter = {
8467
+ name: "since",
8468
+ in: "query",
8469
+ description: "Only return runs with created_at >= this ISO 8601 timestamp. Defaults to 30 days ago.",
8470
+ schema: stringSchema
8471
+ };
8472
+ var runsListIncludeProbeQueryParameter = {
8473
+ name: "includeProbe",
8474
+ in: "query",
8475
+ description: 'Set to "1" or "true" to include probe runs. Probes are excluded by default because they are operator/agent test runs and must not pollute dashboard aggregates.',
8476
+ schema: stringSchema
8477
+ };
8390
8478
  var reportAudienceQueryParameter = {
8391
8479
  name: "audience",
8392
8480
  in: "query",
@@ -8970,6 +9058,12 @@ var routeCatalog = [
8970
9058
  path: "/api/v1/runs",
8971
9059
  summary: "List all runs",
8972
9060
  tags: ["runs"],
9061
+ parameters: [
9062
+ limitQueryParameter,
9063
+ runsListSinceQueryParameter,
9064
+ runsListIncludeProbeQueryParameter,
9065
+ runsListKindQueryParameter
9066
+ ],
8973
9067
  responses: {
8974
9068
  200: jsonArrayResponse("Runs returned.", "RunDto")
8975
9069
  }
@@ -12540,6 +12634,9 @@ var GA4_DEFAULT_SYNC_DAYS = 30;
12540
12634
  var GA4_MAX_SYNC_DAYS = 90;
12541
12635
  var GA4_REQUEST_TIMEOUT_MS = 3e4;
12542
12636
  var GA4_MAX_PAGES = 50;
12637
+ var GA4_MAX_CONCURRENT_REQUESTS = 4;
12638
+ var GA4_MAX_RETRIES = 3;
12639
+ var GA4_INITIAL_RETRY_DELAY_MS = 1e3;
12543
12640
  var GA4_DIMENSIONS = {
12544
12641
  date: "date",
12545
12642
  landingPagePlusQueryString: "landingPagePlusQueryString",
@@ -12560,10 +12657,14 @@ var GA4_METRICS = {
12560
12657
  // ../integration-google-analytics/src/types.ts
12561
12658
  var GA4ApiError = class extends Error {
12562
12659
  status;
12563
- constructor(message, status) {
12660
+ /** Seconds the GA4 API asked us to wait before retrying. Populated from the
12661
+ * `Retry-After` response header on 429 and 5xx responses when present. */
12662
+ retryAfterSeconds;
12663
+ constructor(message, status, retryAfterSeconds) {
12564
12664
  super(message);
12565
12665
  this.name = "GA4ApiError";
12566
12666
  this.status = status;
12667
+ if (retryAfterSeconds !== void 0) this.retryAfterSeconds = retryAfterSeconds;
12567
12668
  }
12568
12669
  };
12569
12670
 
@@ -12659,81 +12760,155 @@ async function getAccessToken(clientEmail, privateKey) {
12659
12760
  function escapeRegExp2(str) {
12660
12761
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12661
12762
  }
12763
+ var gaInFlight = 0;
12764
+ var gaWaitQueue = [];
12765
+ async function acquireGa4Slot() {
12766
+ if (gaInFlight < GA4_MAX_CONCURRENT_REQUESTS) {
12767
+ gaInFlight++;
12768
+ return;
12769
+ }
12770
+ await new Promise((resolve) => gaWaitQueue.push(resolve));
12771
+ }
12772
+ function releaseGa4Slot() {
12773
+ const next = gaWaitQueue.shift();
12774
+ if (next) {
12775
+ next();
12776
+ } else {
12777
+ gaInFlight--;
12778
+ }
12779
+ }
12780
+ async function withGa4Limit(fn) {
12781
+ await acquireGa4Slot();
12782
+ try {
12783
+ return await fn();
12784
+ } finally {
12785
+ releaseGa4Slot();
12786
+ }
12787
+ }
12788
+ function parseRetryAfter(headerValue) {
12789
+ if (!headerValue) return void 0;
12790
+ const trimmed = headerValue.trim();
12791
+ const asNum = Number(trimmed);
12792
+ if (Number.isFinite(asNum) && asNum >= 0) return asNum;
12793
+ const asDate = Date.parse(trimmed);
12794
+ if (!Number.isNaN(asDate)) {
12795
+ const seconds = Math.max(0, (asDate - Date.now()) / 1e3);
12796
+ return seconds;
12797
+ }
12798
+ return void 0;
12799
+ }
12800
+ function isRetryableGa4Error(err) {
12801
+ if (err instanceof GA4ApiError) {
12802
+ return err.status === 429 || err.status >= 500;
12803
+ }
12804
+ return false;
12805
+ }
12806
+ async function withGa4Retry(fn, errLabel) {
12807
+ let lastError;
12808
+ for (let attempt = 0; attempt <= GA4_MAX_RETRIES; attempt++) {
12809
+ try {
12810
+ return await fn();
12811
+ } catch (err) {
12812
+ lastError = err;
12813
+ if (attempt >= GA4_MAX_RETRIES || !isRetryableGa4Error(err)) throw err;
12814
+ const ga4Err = err;
12815
+ const computedDelayMs = GA4_INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
12816
+ const delayMs = ga4Err.retryAfterSeconds !== void 0 ? Math.max(0, ga4Err.retryAfterSeconds * 1e3) : computedDelayMs;
12817
+ ga4Log("info", `${errLabel}.retry`, {
12818
+ attempt: attempt + 1,
12819
+ maxAttempts: GA4_MAX_RETRIES + 1,
12820
+ status: ga4Err.status,
12821
+ delayMs,
12822
+ usedRetryAfter: ga4Err.retryAfterSeconds !== void 0
12823
+ });
12824
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
12825
+ }
12826
+ }
12827
+ throw lastError;
12828
+ }
12662
12829
  async function runReport(accessToken, propertyId, request) {
12663
12830
  validatePropertyId(propertyId);
12664
12831
  const safePropertyId = encodeURIComponent(propertyId);
12665
12832
  const url = `${GA4_DATA_API_BASE}/properties/${safePropertyId}:runReport`;
12666
- const res = await fetch(url, {
12667
- method: "POST",
12668
- headers: {
12669
- "Authorization": `Bearer ${accessToken}`,
12670
- "Content-Type": "application/json"
12671
- },
12672
- body: JSON.stringify(request),
12673
- signal: AbortSignal.timeout(GA4_REQUEST_TIMEOUT_MS)
12674
- });
12675
- if (res.status === 401 || res.status === 403) {
12676
- const body = await res.text().catch(() => "");
12677
- let detail = "";
12678
- try {
12679
- const parsed = JSON.parse(body);
12680
- if (parsed.error?.status === "SERVICE_DISABLED") {
12681
- detail = " The Google Analytics Data API is not enabled for this GCP project. Enable it at: https://console.developers.google.com/apis/api/analyticsdata.googleapis.com/overview";
12682
- } else if (parsed.error?.message) {
12683
- detail = ` ${parsed.error.message}`;
12833
+ return withGa4Limit(() => withGa4Retry(async () => {
12834
+ const res = await fetch(url, {
12835
+ method: "POST",
12836
+ headers: {
12837
+ "Authorization": `Bearer ${accessToken}`,
12838
+ "Content-Type": "application/json"
12839
+ },
12840
+ body: JSON.stringify(request),
12841
+ signal: AbortSignal.timeout(GA4_REQUEST_TIMEOUT_MS)
12842
+ });
12843
+ if (res.status === 401 || res.status === 403) {
12844
+ const body = await res.text().catch(() => "");
12845
+ let detail = "";
12846
+ try {
12847
+ const parsed = JSON.parse(body);
12848
+ if (parsed.error?.status === "SERVICE_DISABLED") {
12849
+ detail = " The Google Analytics Data API is not enabled for this GCP project. Enable it at: https://console.developers.google.com/apis/api/analyticsdata.googleapis.com/overview";
12850
+ } else if (parsed.error?.message) {
12851
+ detail = ` ${parsed.error.message}`;
12852
+ }
12853
+ } catch {
12854
+ if (body.length < 200) detail = ` ${body}`;
12684
12855
  }
12685
- } catch {
12686
- if (body.length < 200) detail = ` ${body}`;
12856
+ ga4Log("error", "report.auth-failed", { propertyId, httpStatus: res.status });
12857
+ throw new GA4ApiError(
12858
+ `GA4 API authentication failed \u2014 check service account permissions.${detail}`,
12859
+ res.status
12860
+ );
12687
12861
  }
12688
- ga4Log("error", "report.auth-failed", { propertyId, httpStatus: res.status });
12689
- throw new GA4ApiError(
12690
- `GA4 API authentication failed \u2014 check service account permissions.${detail}`,
12691
- res.status
12692
- );
12693
- }
12694
- if (res.status === 429) {
12695
- ga4Log("error", "report.rate-limited", { propertyId });
12696
- throw new GA4ApiError("GA4 API rate limit exceeded", 429);
12697
- }
12698
- if (!res.ok) {
12699
- const body = await res.text();
12700
- ga4Log("error", "report.error", { propertyId, httpStatus: res.status });
12701
- const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
12702
- throw new GA4ApiError(`GA4 API error (${res.status}): ${detail}`, res.status);
12703
- }
12704
- return await res.json();
12862
+ if (res.status === 429) {
12863
+ const retryAfterSeconds = parseRetryAfter(res.headers.get("retry-after"));
12864
+ ga4Log("error", "report.rate-limited", { propertyId, retryAfterSeconds });
12865
+ throw new GA4ApiError("GA4 API rate limit exceeded", 429, retryAfterSeconds);
12866
+ }
12867
+ if (!res.ok) {
12868
+ const body = await res.text();
12869
+ const retryAfterSeconds = res.status >= 500 ? parseRetryAfter(res.headers.get("retry-after")) : void 0;
12870
+ ga4Log("error", "report.error", { propertyId, httpStatus: res.status, retryAfterSeconds });
12871
+ const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
12872
+ throw new GA4ApiError(`GA4 API error (${res.status}): ${detail}`, res.status, retryAfterSeconds);
12873
+ }
12874
+ return await res.json();
12875
+ }, "report"));
12705
12876
  }
12706
12877
  async function batchRunReports(accessToken, propertyId, requests) {
12707
12878
  const url = `${GA4_DATA_API_BASE}/properties/${propertyId}:batchRunReports`;
12708
- const res = await fetch(url, {
12709
- method: "POST",
12710
- headers: {
12711
- "Authorization": `Bearer ${accessToken}`,
12712
- "Content-Type": "application/json"
12713
- },
12714
- body: JSON.stringify({ requests }),
12715
- signal: AbortSignal.timeout(GA4_REQUEST_TIMEOUT_MS)
12716
- });
12717
- if (res.status === 401 || res.status === 403) {
12718
- const body = await res.text().catch(() => "");
12719
- ga4Log("error", "batch-report.auth-failed", { propertyId, httpStatus: res.status });
12720
- throw new GA4ApiError(
12721
- `GA4 API authentication failed \u2014 check service account permissions. ${body}`,
12722
- res.status
12723
- );
12724
- }
12725
- if (res.status === 429) {
12726
- ga4Log("error", "batch-report.rate-limited", { propertyId });
12727
- throw new GA4ApiError("GA4 API rate limit exceeded", 429);
12728
- }
12729
- if (!res.ok) {
12730
- const body = await res.text();
12731
- ga4Log("error", "batch-report.error", { propertyId, httpStatus: res.status });
12732
- const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
12733
- throw new GA4ApiError(`GA4 API error (${res.status}): ${detail}`, res.status);
12734
- }
12735
- const data = await res.json();
12736
- return data.reports;
12879
+ return withGa4Limit(() => withGa4Retry(async () => {
12880
+ const res = await fetch(url, {
12881
+ method: "POST",
12882
+ headers: {
12883
+ "Authorization": `Bearer ${accessToken}`,
12884
+ "Content-Type": "application/json"
12885
+ },
12886
+ body: JSON.stringify({ requests }),
12887
+ signal: AbortSignal.timeout(GA4_REQUEST_TIMEOUT_MS)
12888
+ });
12889
+ if (res.status === 401 || res.status === 403) {
12890
+ const body = await res.text().catch(() => "");
12891
+ ga4Log("error", "batch-report.auth-failed", { propertyId, httpStatus: res.status });
12892
+ throw new GA4ApiError(
12893
+ `GA4 API authentication failed \u2014 check service account permissions. ${body}`,
12894
+ res.status
12895
+ );
12896
+ }
12897
+ if (res.status === 429) {
12898
+ const retryAfterSeconds = parseRetryAfter(res.headers.get("retry-after"));
12899
+ ga4Log("error", "batch-report.rate-limited", { propertyId, retryAfterSeconds });
12900
+ throw new GA4ApiError("GA4 API rate limit exceeded", 429, retryAfterSeconds);
12901
+ }
12902
+ if (!res.ok) {
12903
+ const body = await res.text();
12904
+ const retryAfterSeconds = res.status >= 500 ? parseRetryAfter(res.headers.get("retry-after")) : void 0;
12905
+ ga4Log("error", "batch-report.error", { propertyId, httpStatus: res.status, retryAfterSeconds });
12906
+ const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
12907
+ throw new GA4ApiError(`GA4 API error (${res.status}): ${detail}`, res.status, retryAfterSeconds);
12908
+ }
12909
+ const data = await res.json();
12910
+ return data.reports;
12911
+ }, "batch-report"));
12737
12912
  }
12738
12913
  function formatDate2(d) {
12739
12914
  return d.toISOString().slice(0, 10);
@@ -17423,7 +17598,7 @@ async function queryBacklinks(opts) {
17423
17598
  const reversed = opts.targets.map(reverseDomain);
17424
17599
  const targetList = reversed.map(quote).join(", ");
17425
17600
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
17426
- const sql16 = `
17601
+ const sql17 = `
17427
17602
  WITH vertices AS (
17428
17603
  SELECT * FROM read_csv(
17429
17604
  ${quote(opts.vertexPath)},
@@ -17459,7 +17634,7 @@ async function queryBacklinks(opts) {
17459
17634
  const conn = await instance.connect();
17460
17635
  let rows;
17461
17636
  try {
17462
- const reader = await conn.runAndReadAll(sql16);
17637
+ const reader = await conn.runAndReadAll(sql17);
17463
17638
  rows = reader.getRowObjects();
17464
17639
  } finally {
17465
17640
  conn.disconnectSync?.();
@@ -18131,130 +18306,2384 @@ async function listCloudRunTrafficEvents(accessToken, options) {
18131
18306
  };
18132
18307
  }
18133
18308
 
18134
- // ../integration-traffic/src/rules.ts
18135
- var LEGACY_CHATGPT_DOMAIN = "chat.openai.com";
18136
- var DEFAULT_AI_CRAWLER_RULES = [
18137
- {
18138
- id: "openai-gptbot",
18139
- operator: "OpenAI",
18140
- product: "GPTBot",
18141
- purpose: "training",
18142
- userAgentPatterns: [/GPTBot\//i]
18143
- },
18144
- {
18145
- id: "openai-searchbot",
18146
- operator: "OpenAI",
18147
- product: "OAI-SearchBot",
18148
- purpose: "search",
18149
- userAgentPatterns: [/OAI-SearchBot\//i]
18150
- },
18151
- {
18152
- id: "openai-chatgpt-user",
18153
- operator: "OpenAI",
18154
- product: "ChatGPT-User",
18155
- purpose: "user-agent",
18156
- userAgentPatterns: [/ChatGPT-User\//i]
18157
- },
18158
- {
18159
- id: "anthropic-claudebot",
18160
- operator: "Anthropic",
18161
- product: "ClaudeBot",
18162
- purpose: "training",
18163
- userAgentPatterns: [/ClaudeBot\//i, /Claude-Web\//i, /anthropic-ai/i]
18164
- },
18165
- {
18166
- id: "perplexity-bot",
18167
- operator: "Perplexity",
18168
- product: "PerplexityBot",
18169
- purpose: "search",
18170
- userAgentPatterns: [/PerplexityBot\//i]
18171
- },
18172
- {
18173
- id: "google-extended",
18174
- operator: "Google",
18175
- product: "Google-Extended",
18176
- purpose: "training-control",
18177
- userAgentPatterns: [/Google-Extended/i]
18178
- },
18179
- {
18180
- id: "bytespider",
18181
- operator: "ByteDance",
18182
- product: "Bytespider",
18183
- purpose: "training",
18184
- userAgentPatterns: [/Bytespider/i]
18185
- },
18186
- {
18187
- id: "applebot-extended",
18188
- operator: "Apple",
18189
- product: "Applebot-Extended",
18190
- purpose: "training",
18191
- userAgentPatterns: [/Applebot-Extended/i]
18192
- },
18193
- {
18194
- id: "meta-externalagent",
18195
- operator: "Meta",
18196
- product: "meta-externalagent",
18197
- purpose: "training",
18198
- userAgentPatterns: [/meta-externalagent/i]
18199
- },
18200
- {
18201
- id: "ccbot",
18202
- operator: "Common Crawl",
18203
- product: "CCBot",
18204
- purpose: "crawl",
18205
- userAgentPatterns: [/CCBot\//i]
18206
- },
18207
- {
18208
- id: "cohere-ai",
18209
- operator: "Cohere",
18210
- product: "cohere-ai",
18211
- purpose: "training",
18212
- userAgentPatterns: [/cohere-ai/i]
18213
- },
18214
- {
18215
- id: "diffbot",
18216
- operator: "Diffbot",
18217
- product: "Diffbot",
18218
- purpose: "crawl",
18219
- userAgentPatterns: [/Diffbot/i]
18220
- },
18221
- {
18222
- id: "mistral-ai",
18223
- operator: "Mistral AI",
18224
- product: "MistralAI-User",
18225
- purpose: "crawl",
18226
- userAgentPatterns: [/MistralAI/i]
18227
- }
18228
- ];
18229
- var DEFAULT_AI_REFERRER_RULES = [
18230
- { domain: AI_ENGINE_DOMAINS.chatgpt, operator: "OpenAI", product: "ChatGPT" },
18231
- { domain: LEGACY_CHATGPT_DOMAIN, operator: "OpenAI", product: "ChatGPT" },
18232
- { domain: AI_ENGINE_DOMAINS.perplexity, operator: "Perplexity", product: "Perplexity" },
18233
- { domain: AI_ENGINE_DOMAINS.claude, operator: "Anthropic", product: "Claude" },
18234
- { domain: AI_ENGINE_DOMAINS.gemini, operator: "Google", product: "Gemini" },
18235
- { domain: AI_ENGINE_DOMAINS.copilotMicrosoft, operator: "Microsoft", product: "Copilot" },
18236
- { domain: AI_ENGINE_DOMAINS.phind, operator: "Phind", product: "Phind" },
18237
- { domain: AI_ENGINE_DOMAINS.you, operator: "You.com", product: "You.com" },
18238
- { domain: AI_ENGINE_DOMAINS.metaAi, operator: "Meta", product: "Meta AI" }
18239
- ];
18309
+ // ../integration-traffic/src/ip-ranges/anthropic.json
18310
+ var anthropic_default = {
18311
+ _source: "ARIN RDAP \u2014 every network registered to Anthropic, PBC (handle AP-2440)",
18312
+ _query: "https://rdap.arin.net/registry/entity/AP-2440",
18313
+ _notes: "Anthropic does not publish a clean machine-readable IP-range JSON like Google or OpenAI do. These prefixes are taken straight from ARIN's authoritative allocation records (NOT bgp.tools' AS-based view, which can include misleading announcements \u2014 e.g. AS60808 is registered to Anthropic but announces 209.249.57.0/24 which actually belongs to Mitel Networks). 216.73.216.0/22 (AWS-ANTHROPIC) is the load-bearing prefix \u2014 empirical Cloud Run logs show 100% of real ClaudeBot traffic comes from there; the other two are Anthropic's own allocations for non-crawler infra (kept since the verification gate still requires a Claude-* UA match). Refresh by re-running the ARIN RDAP query above and replacing the prefix list.",
18314
+ prefixes: [
18315
+ { ipv4Prefix: "216.73.216.0/22" },
18316
+ { ipv4Prefix: "160.79.104.0/21" },
18317
+ { ipv6Prefix: "2607:6bc0::/32" }
18318
+ ]
18319
+ };
18240
18320
 
18241
- // ../integration-traffic/src/classifier.ts
18242
- function normalizeHost(host) {
18243
- return host.trim().toLowerCase().replace(/^www\./, "");
18244
- }
18245
- function hostMatches(host, domain) {
18246
- const normalizedHost = normalizeHost(host);
18247
- const normalizedDomain = normalizeHost(domain);
18248
- return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
18249
- }
18250
- function utmTokenMatchesDomain(utmSource, domain) {
18251
- if (hostMatches(utmSource, domain)) return true;
18252
- const normalizedUtm = normalizeHost(utmSource);
18253
- const firstLabel = normalizeHost(domain).split(".")[0];
18254
- return Boolean(firstLabel) && normalizedUtm === firstLabel;
18255
- }
18256
- function hostFromUrl(value) {
18257
- if (!value) return null;
18321
+ // ../integration-traffic/src/ip-ranges/bingbot.json
18322
+ var bingbot_default = {
18323
+ _source: "https://www.bing.com/toolbox/bingbot.json",
18324
+ creationTime: "2024-01-03T10:00:00.121331",
18325
+ prefixes: [
18326
+ {
18327
+ ipv4Prefix: "157.55.39.0/24"
18328
+ },
18329
+ {
18330
+ ipv4Prefix: "207.46.13.0/24"
18331
+ },
18332
+ {
18333
+ ipv4Prefix: "40.77.167.0/24"
18334
+ },
18335
+ {
18336
+ ipv4Prefix: "13.66.139.0/24"
18337
+ },
18338
+ {
18339
+ ipv4Prefix: "13.66.144.0/24"
18340
+ },
18341
+ {
18342
+ ipv4Prefix: "52.167.144.0/24"
18343
+ },
18344
+ {
18345
+ ipv4Prefix: "13.67.10.16/28"
18346
+ },
18347
+ {
18348
+ ipv4Prefix: "13.69.66.240/28"
18349
+ },
18350
+ {
18351
+ ipv4Prefix: "13.71.172.224/28"
18352
+ },
18353
+ {
18354
+ ipv4Prefix: "139.217.52.0/28"
18355
+ },
18356
+ {
18357
+ ipv4Prefix: "191.233.204.224/28"
18358
+ },
18359
+ {
18360
+ ipv4Prefix: "20.36.108.32/28"
18361
+ },
18362
+ {
18363
+ ipv4Prefix: "20.43.120.16/28"
18364
+ },
18365
+ {
18366
+ ipv4Prefix: "40.79.131.208/28"
18367
+ },
18368
+ {
18369
+ ipv4Prefix: "40.79.186.176/28"
18370
+ },
18371
+ {
18372
+ ipv4Prefix: "52.231.148.0/28"
18373
+ },
18374
+ {
18375
+ ipv4Prefix: "20.79.107.240/28"
18376
+ },
18377
+ {
18378
+ ipv4Prefix: "51.105.67.0/28"
18379
+ },
18380
+ {
18381
+ ipv4Prefix: "20.125.163.80/28"
18382
+ },
18383
+ {
18384
+ ipv4Prefix: "40.77.188.0/22"
18385
+ },
18386
+ {
18387
+ ipv4Prefix: "65.55.210.0/24"
18388
+ },
18389
+ {
18390
+ ipv4Prefix: "199.30.24.0/23"
18391
+ },
18392
+ {
18393
+ ipv4Prefix: "40.77.202.0/24"
18394
+ },
18395
+ {
18396
+ ipv4Prefix: "40.77.139.0/25"
18397
+ },
18398
+ {
18399
+ ipv4Prefix: "20.74.197.0/28"
18400
+ },
18401
+ {
18402
+ ipv4Prefix: "20.15.133.160/27"
18403
+ },
18404
+ {
18405
+ ipv4Prefix: "40.77.177.0/24"
18406
+ },
18407
+ {
18408
+ ipv4Prefix: "40.77.178.0/23"
18409
+ }
18410
+ ]
18411
+ };
18412
+
18413
+ // ../integration-traffic/src/ip-ranges/chatgpt-user.json
18414
+ var chatgpt_user_default = {
18415
+ _source: "https://openai.com/chatgpt-user.json",
18416
+ creationTime: "2026-05-17T23:03:25.747192",
18417
+ prefixes: [
18418
+ {
18419
+ ipv4Prefix: "104.210.139.192/28"
18420
+ },
18421
+ {
18422
+ ipv4Prefix: "104.210.139.224/28"
18423
+ },
18424
+ {
18425
+ ipv4Prefix: "13.65.138.112/28"
18426
+ },
18427
+ {
18428
+ ipv4Prefix: "13.65.138.96/28"
18429
+ },
18430
+ {
18431
+ ipv4Prefix: "13.67.72.16/28"
18432
+ },
18433
+ {
18434
+ ipv4Prefix: "13.70.107.160/28"
18435
+ },
18436
+ {
18437
+ ipv4Prefix: "13.71.2.208/28"
18438
+ },
18439
+ {
18440
+ ipv4Prefix: "13.76.115.224/28"
18441
+ },
18442
+ {
18443
+ ipv4Prefix: "13.76.115.240/28"
18444
+ },
18445
+ {
18446
+ ipv4Prefix: "13.76.116.80/28"
18447
+ },
18448
+ {
18449
+ ipv4Prefix: "13.76.32.208/28"
18450
+ },
18451
+ {
18452
+ ipv4Prefix: "13.83.167.128/28"
18453
+ },
18454
+ {
18455
+ ipv4Prefix: "13.83.237.176/28"
18456
+ },
18457
+ {
18458
+ ipv4Prefix: "132.196.82.48/28"
18459
+ },
18460
+ {
18461
+ ipv4Prefix: "135.119.134.128/28"
18462
+ },
18463
+ {
18464
+ ipv4Prefix: "135.119.134.192/28"
18465
+ },
18466
+ {
18467
+ ipv4Prefix: "135.220.73.208/28"
18468
+ },
18469
+ {
18470
+ ipv4Prefix: "135.220.73.240/28"
18471
+ },
18472
+ {
18473
+ ipv4Prefix: "135.237.131.208/28"
18474
+ },
18475
+ {
18476
+ ipv4Prefix: "135.237.133.48/28"
18477
+ },
18478
+ {
18479
+ ipv4Prefix: "137.135.191.176/28"
18480
+ },
18481
+ {
18482
+ ipv4Prefix: "138.91.30.48/28"
18483
+ },
18484
+ {
18485
+ ipv4Prefix: "138.91.46.96/28"
18486
+ },
18487
+ {
18488
+ ipv4Prefix: "168.63.252.240/28"
18489
+ },
18490
+ {
18491
+ ipv4Prefix: "172.170.8.208/28"
18492
+ },
18493
+ {
18494
+ ipv4Prefix: "172.171.4.176/28"
18495
+ },
18496
+ {
18497
+ ipv4Prefix: "172.178.140.144/28"
18498
+ },
18499
+ {
18500
+ ipv4Prefix: "172.178.141.112/28"
18501
+ },
18502
+ {
18503
+ ipv4Prefix: "172.178.141.128/28"
18504
+ },
18505
+ {
18506
+ ipv4Prefix: "172.183.143.224/28"
18507
+ },
18508
+ {
18509
+ ipv4Prefix: "172.183.222.128/28"
18510
+ },
18511
+ {
18512
+ ipv4Prefix: "172.196.40.208/28"
18513
+ },
18514
+ {
18515
+ ipv4Prefix: "172.202.102.112/28"
18516
+ },
18517
+ {
18518
+ ipv4Prefix: "172.204.16.64/28"
18519
+ },
18520
+ {
18521
+ ipv4Prefix: "172.204.27.16/28"
18522
+ },
18523
+ {
18524
+ ipv4Prefix: "172.212.159.64/28"
18525
+ },
18526
+ {
18527
+ ipv4Prefix: "172.212.172.160/28"
18528
+ },
18529
+ {
18530
+ ipv4Prefix: "172.213.11.144/28"
18531
+ },
18532
+ {
18533
+ ipv4Prefix: "172.213.12.112/28"
18534
+ },
18535
+ {
18536
+ ipv4Prefix: "172.213.21.112/28"
18537
+ },
18538
+ {
18539
+ ipv4Prefix: "172.213.21.144/28"
18540
+ },
18541
+ {
18542
+ ipv4Prefix: "172.213.21.16/28"
18543
+ },
18544
+ {
18545
+ ipv4Prefix: "172.215.218.96/28"
18546
+ },
18547
+ {
18548
+ ipv4Prefix: "191.233.1.112/28"
18549
+ },
18550
+ {
18551
+ ipv4Prefix: "191.233.1.128/28"
18552
+ },
18553
+ {
18554
+ ipv4Prefix: "191.233.1.224/28"
18555
+ },
18556
+ {
18557
+ ipv4Prefix: "191.233.194.32/28"
18558
+ },
18559
+ {
18560
+ ipv4Prefix: "191.233.196.112/28"
18561
+ },
18562
+ {
18563
+ ipv4Prefix: "191.233.199.160/28"
18564
+ },
18565
+ {
18566
+ ipv4Prefix: "191.233.2.0/28"
18567
+ },
18568
+ {
18569
+ ipv4Prefix: "191.234.167.128/28"
18570
+ },
18571
+ {
18572
+ ipv4Prefix: "191.235.66.16/28"
18573
+ },
18574
+ {
18575
+ ipv4Prefix: "191.235.98.144/28"
18576
+ },
18577
+ {
18578
+ ipv4Prefix: "191.235.99.80/28"
18579
+ },
18580
+ {
18581
+ ipv4Prefix: "191.237.249.64/28"
18582
+ },
18583
+ {
18584
+ ipv4Prefix: "191.239.245.16/28"
18585
+ },
18586
+ {
18587
+ ipv4Prefix: "20.0.53.96/28"
18588
+ },
18589
+ {
18590
+ ipv4Prefix: "20.102.212.144/28"
18591
+ },
18592
+ {
18593
+ ipv4Prefix: "20.113.218.16/28"
18594
+ },
18595
+ {
18596
+ ipv4Prefix: "20.113.225.112/28"
18597
+ },
18598
+ {
18599
+ ipv4Prefix: "20.125.112.224/28"
18600
+ },
18601
+ {
18602
+ ipv4Prefix: "20.125.144.144/28"
18603
+ },
18604
+ {
18605
+ ipv4Prefix: "20.161.75.208/28"
18606
+ },
18607
+ {
18608
+ ipv4Prefix: "20.168.7.192/28"
18609
+ },
18610
+ {
18611
+ ipv4Prefix: "20.168.7.240/28"
18612
+ },
18613
+ {
18614
+ ipv4Prefix: "20.169.72.112/28"
18615
+ },
18616
+ {
18617
+ ipv4Prefix: "20.169.72.96/28"
18618
+ },
18619
+ {
18620
+ ipv4Prefix: "20.169.73.176/28"
18621
+ },
18622
+ {
18623
+ ipv4Prefix: "20.169.73.32/28"
18624
+ },
18625
+ {
18626
+ ipv4Prefix: "20.169.73.64/28"
18627
+ },
18628
+ {
18629
+ ipv4Prefix: "20.169.78.112/28"
18630
+ },
18631
+ {
18632
+ ipv4Prefix: "20.169.78.128/28"
18633
+ },
18634
+ {
18635
+ ipv4Prefix: "20.169.78.144/28"
18636
+ },
18637
+ {
18638
+ ipv4Prefix: "20.169.78.160/28"
18639
+ },
18640
+ {
18641
+ ipv4Prefix: "20.169.78.176/28"
18642
+ },
18643
+ {
18644
+ ipv4Prefix: "20.169.78.192/28"
18645
+ },
18646
+ {
18647
+ ipv4Prefix: "20.169.78.208/28"
18648
+ },
18649
+ {
18650
+ ipv4Prefix: "20.169.78.48/28"
18651
+ },
18652
+ {
18653
+ ipv4Prefix: "20.169.78.64/28"
18654
+ },
18655
+ {
18656
+ ipv4Prefix: "20.169.78.80/28"
18657
+ },
18658
+ {
18659
+ ipv4Prefix: "20.169.78.96/28"
18660
+ },
18661
+ {
18662
+ ipv4Prefix: "20.169.86.224/28"
18663
+ },
18664
+ {
18665
+ ipv4Prefix: "20.169.86.240/28"
18666
+ },
18667
+ {
18668
+ ipv4Prefix: "20.169.87.112/28"
18669
+ },
18670
+ {
18671
+ ipv4Prefix: "20.17.108.96/28"
18672
+ },
18673
+ {
18674
+ ipv4Prefix: "20.172.29.32/28"
18675
+ },
18676
+ {
18677
+ ipv4Prefix: "20.193.233.240/28"
18678
+ },
18679
+ {
18680
+ ipv4Prefix: "20.193.50.32/28"
18681
+ },
18682
+ {
18683
+ ipv4Prefix: "20.194.0.208/28"
18684
+ },
18685
+ {
18686
+ ipv4Prefix: "20.194.1.0/28"
18687
+ },
18688
+ {
18689
+ ipv4Prefix: "20.194.157.176/28"
18690
+ },
18691
+ {
18692
+ ipv4Prefix: "20.198.67.96/28"
18693
+ },
18694
+ {
18695
+ ipv4Prefix: "20.199.211.160/28"
18696
+ },
18697
+ {
18698
+ ipv4Prefix: "20.203.245.32/28"
18699
+ },
18700
+ {
18701
+ ipv4Prefix: "20.204.24.240/28"
18702
+ },
18703
+ {
18704
+ ipv4Prefix: "20.206.107.192/28"
18705
+ },
18706
+ {
18707
+ ipv4Prefix: "20.210.154.128/28"
18708
+ },
18709
+ {
18710
+ ipv4Prefix: "20.210.174.208/28"
18711
+ },
18712
+ {
18713
+ ipv4Prefix: "20.210.211.192/28"
18714
+ },
18715
+ {
18716
+ ipv4Prefix: "20.215.187.208/28"
18717
+ },
18718
+ {
18719
+ ipv4Prefix: "20.215.188.192/28"
18720
+ },
18721
+ {
18722
+ ipv4Prefix: "20.215.214.16/28"
18723
+ },
18724
+ {
18725
+ ipv4Prefix: "20.215.219.128/28"
18726
+ },
18727
+ {
18728
+ ipv4Prefix: "20.215.219.160/28"
18729
+ },
18730
+ {
18731
+ ipv4Prefix: "20.215.219.208/28"
18732
+ },
18733
+ {
18734
+ ipv4Prefix: "20.215.220.112/28"
18735
+ },
18736
+ {
18737
+ ipv4Prefix: "20.215.220.128/28"
18738
+ },
18739
+ {
18740
+ ipv4Prefix: "20.215.220.144/28"
18741
+ },
18742
+ {
18743
+ ipv4Prefix: "20.215.220.160/28"
18744
+ },
18745
+ {
18746
+ ipv4Prefix: "20.215.220.176/28"
18747
+ },
18748
+ {
18749
+ ipv4Prefix: "20.215.220.192/28"
18750
+ },
18751
+ {
18752
+ ipv4Prefix: "20.215.220.208/28"
18753
+ },
18754
+ {
18755
+ ipv4Prefix: "20.215.220.64/28"
18756
+ },
18757
+ {
18758
+ ipv4Prefix: "20.215.220.80/28"
18759
+ },
18760
+ {
18761
+ ipv4Prefix: "20.215.220.96/28"
18762
+ },
18763
+ {
18764
+ ipv4Prefix: "20.226.32.80/28"
18765
+ },
18766
+ {
18767
+ ipv4Prefix: "20.227.140.32/28"
18768
+ },
18769
+ {
18770
+ ipv4Prefix: "20.228.106.176/28"
18771
+ },
18772
+ {
18773
+ ipv4Prefix: "20.235.75.208/28"
18774
+ },
18775
+ {
18776
+ ipv4Prefix: "20.235.87.224/28"
18777
+ },
18778
+ {
18779
+ ipv4Prefix: "20.249.63.208/28"
18780
+ },
18781
+ {
18782
+ ipv4Prefix: "20.27.94.128/28"
18783
+ },
18784
+ {
18785
+ ipv4Prefix: "20.42.250.32/28"
18786
+ },
18787
+ {
18788
+ ipv4Prefix: "20.45.178.144/28"
18789
+ },
18790
+ {
18791
+ ipv4Prefix: "20.55.229.144/28"
18792
+ },
18793
+ {
18794
+ ipv4Prefix: "20.57.199.192/28"
18795
+ },
18796
+ {
18797
+ ipv4Prefix: "20.63.221.64/28"
18798
+ },
18799
+ {
18800
+ ipv4Prefix: "20.97.189.96/28"
18801
+ },
18802
+ {
18803
+ ipv4Prefix: "23.102.140.144/28"
18804
+ },
18805
+ {
18806
+ ipv4Prefix: "23.102.141.32/28"
18807
+ },
18808
+ {
18809
+ ipv4Prefix: "23.97.109.224/28"
18810
+ },
18811
+ {
18812
+ ipv4Prefix: "23.98.142.176/28"
18813
+ },
18814
+ {
18815
+ ipv4Prefix: "23.98.179.16/28"
18816
+ },
18817
+ {
18818
+ ipv4Prefix: "23.98.186.176/28"
18819
+ },
18820
+ {
18821
+ ipv4Prefix: "23.98.186.192/28"
18822
+ },
18823
+ {
18824
+ ipv4Prefix: "23.98.186.64/28"
18825
+ },
18826
+ {
18827
+ ipv4Prefix: "23.98.186.96/28"
18828
+ },
18829
+ {
18830
+ ipv4Prefix: "4.151.119.48/28"
18831
+ },
18832
+ {
18833
+ ipv4Prefix: "4.151.241.240/28"
18834
+ },
18835
+ {
18836
+ ipv4Prefix: "4.151.71.176/28"
18837
+ },
18838
+ {
18839
+ ipv4Prefix: "4.189.118.208/28"
18840
+ },
18841
+ {
18842
+ ipv4Prefix: "4.189.119.48/28"
18843
+ },
18844
+ {
18845
+ ipv4Prefix: "4.196.118.112/28"
18846
+ },
18847
+ {
18848
+ ipv4Prefix: "4.196.198.80/28"
18849
+ },
18850
+ {
18851
+ ipv4Prefix: "4.197.115.112/28"
18852
+ },
18853
+ {
18854
+ ipv4Prefix: "4.197.19.176/28"
18855
+ },
18856
+ {
18857
+ ipv4Prefix: "4.197.22.112/28"
18858
+ },
18859
+ {
18860
+ ipv4Prefix: "4.197.64.0/28"
18861
+ },
18862
+ {
18863
+ ipv4Prefix: "4.197.64.16/28"
18864
+ },
18865
+ {
18866
+ ipv4Prefix: "4.197.64.48/28"
18867
+ },
18868
+ {
18869
+ ipv4Prefix: "4.197.64.64/28"
18870
+ },
18871
+ {
18872
+ ipv4Prefix: "4.198.72.16/28"
18873
+ },
18874
+ {
18875
+ ipv4Prefix: "4.205.128.176/28"
18876
+ },
18877
+ {
18878
+ ipv4Prefix: "4.226.226.32/28"
18879
+ },
18880
+ {
18881
+ ipv4Prefix: "40.116.73.208/28"
18882
+ },
18883
+ {
18884
+ ipv4Prefix: "40.122.235.112/28"
18885
+ },
18886
+ {
18887
+ ipv4Prefix: "40.67.183.160/28"
18888
+ },
18889
+ {
18890
+ ipv4Prefix: "40.67.183.176/28"
18891
+ },
18892
+ {
18893
+ ipv4Prefix: "40.75.14.224/28"
18894
+ },
18895
+ {
18896
+ ipv4Prefix: "40.81.134.128/28"
18897
+ },
18898
+ {
18899
+ ipv4Prefix: "40.81.134.144/28"
18900
+ },
18901
+ {
18902
+ ipv4Prefix: "40.81.234.144/28"
18903
+ },
18904
+ {
18905
+ ipv4Prefix: "40.84.181.32/28"
18906
+ },
18907
+ {
18908
+ ipv4Prefix: "40.84.221.208/28"
18909
+ },
18910
+ {
18911
+ ipv4Prefix: "40.84.221.224/28"
18912
+ },
18913
+ {
18914
+ ipv4Prefix: "48.193.44.32/28"
18915
+ },
18916
+ {
18917
+ ipv4Prefix: "51.107.70.192/28"
18918
+ },
18919
+ {
18920
+ ipv4Prefix: "51.116.2.64/28"
18921
+ },
18922
+ {
18923
+ ipv4Prefix: "51.116.2.80/28"
18924
+ },
18925
+ {
18926
+ ipv4Prefix: "51.8.155.48/28"
18927
+ },
18928
+ {
18929
+ ipv4Prefix: "51.8.155.64/28"
18930
+ },
18931
+ {
18932
+ ipv4Prefix: "51.8.155.80/28"
18933
+ },
18934
+ {
18935
+ ipv4Prefix: "52.148.129.32/28"
18936
+ },
18937
+ {
18938
+ ipv4Prefix: "52.153.130.48/28"
18939
+ },
18940
+ {
18941
+ ipv4Prefix: "52.153.130.64/28"
18942
+ },
18943
+ {
18944
+ ipv4Prefix: "52.154.22.48/28"
18945
+ },
18946
+ {
18947
+ ipv4Prefix: "52.156.77.144/28"
18948
+ },
18949
+ {
18950
+ ipv4Prefix: "52.159.227.32/28"
18951
+ },
18952
+ {
18953
+ ipv4Prefix: "52.159.249.96/28"
18954
+ },
18955
+ {
18956
+ ipv4Prefix: "52.165.212.16/28"
18957
+ },
18958
+ {
18959
+ ipv4Prefix: "52.165.212.32/28"
18960
+ },
18961
+ {
18962
+ ipv4Prefix: "52.165.212.48/28"
18963
+ },
18964
+ {
18965
+ ipv4Prefix: "52.172.129.160/28"
18966
+ },
18967
+ {
18968
+ ipv4Prefix: "52.172.251.112/28"
18969
+ },
18970
+ {
18971
+ ipv4Prefix: "52.173.123.0/28"
18972
+ },
18973
+ {
18974
+ ipv4Prefix: "52.173.219.112/28"
18975
+ },
18976
+ {
18977
+ ipv4Prefix: "52.173.219.96/28"
18978
+ },
18979
+ {
18980
+ ipv4Prefix: "52.173.221.16/28"
18981
+ },
18982
+ {
18983
+ ipv4Prefix: "52.173.221.176/28"
18984
+ },
18985
+ {
18986
+ ipv4Prefix: "52.173.221.208/28"
18987
+ },
18988
+ {
18989
+ ipv4Prefix: "52.173.234.16/28"
18990
+ },
18991
+ {
18992
+ ipv4Prefix: "52.173.234.80/28"
18993
+ },
18994
+ {
18995
+ ipv4Prefix: "52.173.235.80/28"
18996
+ },
18997
+ {
18998
+ ipv4Prefix: "52.176.139.176/28"
18999
+ },
19000
+ {
19001
+ ipv4Prefix: "52.187.246.128/28"
19002
+ },
19003
+ {
19004
+ ipv4Prefix: "52.190.137.144/28"
19005
+ },
19006
+ {
19007
+ ipv4Prefix: "52.190.137.16/28"
19008
+ },
19009
+ {
19010
+ ipv4Prefix: "52.190.139.48/28"
19011
+ },
19012
+ {
19013
+ ipv4Prefix: "52.190.142.64/28"
19014
+ },
19015
+ {
19016
+ ipv4Prefix: "52.190.190.16/28"
19017
+ },
19018
+ {
19019
+ ipv4Prefix: "52.225.75.208/28"
19020
+ },
19021
+ {
19022
+ ipv4Prefix: "52.230.163.32/28"
19023
+ },
19024
+ {
19025
+ ipv4Prefix: "52.230.164.176/28"
19026
+ },
19027
+ {
19028
+ ipv4Prefix: "52.231.30.48/28"
19029
+ },
19030
+ {
19031
+ ipv4Prefix: "52.231.34.176/28"
19032
+ },
19033
+ {
19034
+ ipv4Prefix: "52.231.39.144/28"
19035
+ },
19036
+ {
19037
+ ipv4Prefix: "52.231.39.192/28"
19038
+ },
19039
+ {
19040
+ ipv4Prefix: "52.231.49.48/28"
19041
+ },
19042
+ {
19043
+ ipv4Prefix: "52.231.50.64/28"
19044
+ },
19045
+ {
19046
+ ipv4Prefix: "52.236.94.144/28"
19047
+ },
19048
+ {
19049
+ ipv4Prefix: "52.241.146.208/28"
19050
+ },
19051
+ {
19052
+ ipv4Prefix: "52.242.132.224/28"
19053
+ },
19054
+ {
19055
+ ipv4Prefix: "52.242.132.240/28"
19056
+ },
19057
+ {
19058
+ ipv4Prefix: "52.242.245.208/28"
19059
+ },
19060
+ {
19061
+ ipv4Prefix: "52.252.113.240/28"
19062
+ },
19063
+ {
19064
+ ipv4Prefix: "52.255.109.112/28"
19065
+ },
19066
+ {
19067
+ ipv4Prefix: "52.255.109.128/28"
19068
+ },
19069
+ {
19070
+ ipv4Prefix: "52.255.109.144/28"
19071
+ },
19072
+ {
19073
+ ipv4Prefix: "52.255.109.80/28"
19074
+ },
19075
+ {
19076
+ ipv4Prefix: "52.255.109.96/28"
19077
+ },
19078
+ {
19079
+ ipv4Prefix: "52.255.111.0/28"
19080
+ },
19081
+ {
19082
+ ipv4Prefix: "52.255.111.112/28"
19083
+ },
19084
+ {
19085
+ ipv4Prefix: "52.255.111.16/28"
19086
+ },
19087
+ {
19088
+ ipv4Prefix: "52.255.111.32/28"
19089
+ },
19090
+ {
19091
+ ipv4Prefix: "52.255.111.48/28"
19092
+ },
19093
+ {
19094
+ ipv4Prefix: "52.255.111.80/28"
19095
+ },
19096
+ {
19097
+ ipv4Prefix: "57.151.131.224/28"
19098
+ },
19099
+ {
19100
+ ipv4Prefix: "57.154.174.112/28"
19101
+ },
19102
+ {
19103
+ ipv4Prefix: "57.154.175.0/28"
19104
+ },
19105
+ {
19106
+ ipv4Prefix: "57.154.187.32/28"
19107
+ },
19108
+ {
19109
+ ipv4Prefix: "68.154.28.96/28"
19110
+ },
19111
+ {
19112
+ ipv4Prefix: "68.218.30.112/28"
19113
+ },
19114
+ {
19115
+ ipv4Prefix: "68.220.57.64/28"
19116
+ },
19117
+ {
19118
+ ipv4Prefix: "68.221.67.160/28"
19119
+ },
19120
+ {
19121
+ ipv4Prefix: "68.221.67.192/28"
19122
+ },
19123
+ {
19124
+ ipv4Prefix: "68.221.67.224/28"
19125
+ },
19126
+ {
19127
+ ipv4Prefix: "68.221.67.240/28"
19128
+ },
19129
+ {
19130
+ ipv4Prefix: "68.221.75.16/28"
19131
+ },
19132
+ {
19133
+ ipv4Prefix: "70.153.76.16/28"
19134
+ },
19135
+ {
19136
+ ipv4Prefix: "74.226.253.160/28"
19137
+ },
19138
+ {
19139
+ ipv4Prefix: "74.249.86.176/28"
19140
+ },
19141
+ {
19142
+ ipv4Prefix: "74.7.35.112/28"
19143
+ },
19144
+ {
19145
+ ipv4Prefix: "74.7.35.48/28"
19146
+ },
19147
+ {
19148
+ ipv4Prefix: "74.7.36.64/28"
19149
+ },
19150
+ {
19151
+ ipv4Prefix: "74.7.36.80/28"
19152
+ },
19153
+ {
19154
+ ipv4Prefix: "74.7.36.96/28"
19155
+ },
19156
+ {
19157
+ ipv4Prefix: "85.211.241.128/28"
19158
+ },
19159
+ {
19160
+ ipv4Prefix: "9.160.163.224/28"
19161
+ },
19162
+ {
19163
+ ipv4Prefix: "9.160.164.128/28"
19164
+ },
19165
+ {
19166
+ ipv4Prefix: "9.234.96.192/28"
19167
+ }
19168
+ ]
19169
+ };
19170
+
19171
+ // ../integration-traffic/src/ip-ranges/googlebot.json
19172
+ var googlebot_default = {
19173
+ _source: "https://developers.google.com/static/search/apis/ipranges/googlebot.json",
19174
+ creationTime: "2026-05-18T14:46:14.000000",
19175
+ prefixes: [
19176
+ {
19177
+ ipv6Prefix: "2001:4860:4801:10::/64"
19178
+ },
19179
+ {
19180
+ ipv6Prefix: "2001:4860:4801:11::/64"
19181
+ },
19182
+ {
19183
+ ipv6Prefix: "2001:4860:4801:12::/64"
19184
+ },
19185
+ {
19186
+ ipv6Prefix: "2001:4860:4801:13::/64"
19187
+ },
19188
+ {
19189
+ ipv6Prefix: "2001:4860:4801:14::/64"
19190
+ },
19191
+ {
19192
+ ipv6Prefix: "2001:4860:4801:15::/64"
19193
+ },
19194
+ {
19195
+ ipv6Prefix: "2001:4860:4801:16::/64"
19196
+ },
19197
+ {
19198
+ ipv6Prefix: "2001:4860:4801:17::/64"
19199
+ },
19200
+ {
19201
+ ipv6Prefix: "2001:4860:4801:18::/64"
19202
+ },
19203
+ {
19204
+ ipv6Prefix: "2001:4860:4801:19::/64"
19205
+ },
19206
+ {
19207
+ ipv6Prefix: "2001:4860:4801:1a::/64"
19208
+ },
19209
+ {
19210
+ ipv6Prefix: "2001:4860:4801:1b::/64"
19211
+ },
19212
+ {
19213
+ ipv6Prefix: "2001:4860:4801:1c::/64"
19214
+ },
19215
+ {
19216
+ ipv6Prefix: "2001:4860:4801:1d::/64"
19217
+ },
19218
+ {
19219
+ ipv6Prefix: "2001:4860:4801:1e::/64"
19220
+ },
19221
+ {
19222
+ ipv6Prefix: "2001:4860:4801:1f::/64"
19223
+ },
19224
+ {
19225
+ ipv6Prefix: "2001:4860:4801:20::/64"
19226
+ },
19227
+ {
19228
+ ipv6Prefix: "2001:4860:4801:21::/64"
19229
+ },
19230
+ {
19231
+ ipv6Prefix: "2001:4860:4801:22::/64"
19232
+ },
19233
+ {
19234
+ ipv6Prefix: "2001:4860:4801:23::/64"
19235
+ },
19236
+ {
19237
+ ipv6Prefix: "2001:4860:4801:24::/64"
19238
+ },
19239
+ {
19240
+ ipv6Prefix: "2001:4860:4801:25::/64"
19241
+ },
19242
+ {
19243
+ ipv6Prefix: "2001:4860:4801:26::/64"
19244
+ },
19245
+ {
19246
+ ipv6Prefix: "2001:4860:4801:27::/64"
19247
+ },
19248
+ {
19249
+ ipv6Prefix: "2001:4860:4801:28::/64"
19250
+ },
19251
+ {
19252
+ ipv6Prefix: "2001:4860:4801:29::/64"
19253
+ },
19254
+ {
19255
+ ipv6Prefix: "2001:4860:4801:2::/64"
19256
+ },
19257
+ {
19258
+ ipv6Prefix: "2001:4860:4801:2a::/64"
19259
+ },
19260
+ {
19261
+ ipv6Prefix: "2001:4860:4801:2b::/64"
19262
+ },
19263
+ {
19264
+ ipv6Prefix: "2001:4860:4801:2c::/64"
19265
+ },
19266
+ {
19267
+ ipv6Prefix: "2001:4860:4801:2d::/64"
19268
+ },
19269
+ {
19270
+ ipv6Prefix: "2001:4860:4801:2e::/64"
19271
+ },
19272
+ {
19273
+ ipv6Prefix: "2001:4860:4801:2f::/64"
19274
+ },
19275
+ {
19276
+ ipv6Prefix: "2001:4860:4801:30::/64"
19277
+ },
19278
+ {
19279
+ ipv6Prefix: "2001:4860:4801:31::/64"
19280
+ },
19281
+ {
19282
+ ipv6Prefix: "2001:4860:4801:32::/64"
19283
+ },
19284
+ {
19285
+ ipv6Prefix: "2001:4860:4801:33::/64"
19286
+ },
19287
+ {
19288
+ ipv6Prefix: "2001:4860:4801:34::/64"
19289
+ },
19290
+ {
19291
+ ipv6Prefix: "2001:4860:4801:35::/64"
19292
+ },
19293
+ {
19294
+ ipv6Prefix: "2001:4860:4801:36::/64"
19295
+ },
19296
+ {
19297
+ ipv6Prefix: "2001:4860:4801:37::/64"
19298
+ },
19299
+ {
19300
+ ipv6Prefix: "2001:4860:4801:38::/64"
19301
+ },
19302
+ {
19303
+ ipv6Prefix: "2001:4860:4801:39::/64"
19304
+ },
19305
+ {
19306
+ ipv6Prefix: "2001:4860:4801:3a::/64"
19307
+ },
19308
+ {
19309
+ ipv6Prefix: "2001:4860:4801:3b::/64"
19310
+ },
19311
+ {
19312
+ ipv6Prefix: "2001:4860:4801:3c::/64"
19313
+ },
19314
+ {
19315
+ ipv6Prefix: "2001:4860:4801:3d::/64"
19316
+ },
19317
+ {
19318
+ ipv6Prefix: "2001:4860:4801:3e::/64"
19319
+ },
19320
+ {
19321
+ ipv6Prefix: "2001:4860:4801:3f::/64"
19322
+ },
19323
+ {
19324
+ ipv6Prefix: "2001:4860:4801:40::/64"
19325
+ },
19326
+ {
19327
+ ipv6Prefix: "2001:4860:4801:41::/64"
19328
+ },
19329
+ {
19330
+ ipv6Prefix: "2001:4860:4801:42::/64"
19331
+ },
19332
+ {
19333
+ ipv6Prefix: "2001:4860:4801:44::/64"
19334
+ },
19335
+ {
19336
+ ipv6Prefix: "2001:4860:4801:45::/64"
19337
+ },
19338
+ {
19339
+ ipv6Prefix: "2001:4860:4801:46::/64"
19340
+ },
19341
+ {
19342
+ ipv6Prefix: "2001:4860:4801:47::/64"
19343
+ },
19344
+ {
19345
+ ipv6Prefix: "2001:4860:4801:48::/64"
19346
+ },
19347
+ {
19348
+ ipv6Prefix: "2001:4860:4801:49::/64"
19349
+ },
19350
+ {
19351
+ ipv6Prefix: "2001:4860:4801:4a::/64"
19352
+ },
19353
+ {
19354
+ ipv6Prefix: "2001:4860:4801:4b::/64"
19355
+ },
19356
+ {
19357
+ ipv6Prefix: "2001:4860:4801:4c::/64"
19358
+ },
19359
+ {
19360
+ ipv6Prefix: "2001:4860:4801:4d::/64"
19361
+ },
19362
+ {
19363
+ ipv6Prefix: "2001:4860:4801:4e::/64"
19364
+ },
19365
+ {
19366
+ ipv6Prefix: "2001:4860:4801:50::/64"
19367
+ },
19368
+ {
19369
+ ipv6Prefix: "2001:4860:4801:51::/64"
19370
+ },
19371
+ {
19372
+ ipv6Prefix: "2001:4860:4801:52::/64"
19373
+ },
19374
+ {
19375
+ ipv6Prefix: "2001:4860:4801:53::/64"
19376
+ },
19377
+ {
19378
+ ipv6Prefix: "2001:4860:4801:54::/64"
19379
+ },
19380
+ {
19381
+ ipv6Prefix: "2001:4860:4801:55::/64"
19382
+ },
19383
+ {
19384
+ ipv6Prefix: "2001:4860:4801:56::/64"
19385
+ },
19386
+ {
19387
+ ipv6Prefix: "2001:4860:4801:57::/64"
19388
+ },
19389
+ {
19390
+ ipv6Prefix: "2001:4860:4801:58::/64"
19391
+ },
19392
+ {
19393
+ ipv6Prefix: "2001:4860:4801:59::/64"
19394
+ },
19395
+ {
19396
+ ipv6Prefix: "2001:4860:4801:60::/64"
19397
+ },
19398
+ {
19399
+ ipv6Prefix: "2001:4860:4801:61::/64"
19400
+ },
19401
+ {
19402
+ ipv6Prefix: "2001:4860:4801:62::/64"
19403
+ },
19404
+ {
19405
+ ipv6Prefix: "2001:4860:4801:63::/64"
19406
+ },
19407
+ {
19408
+ ipv6Prefix: "2001:4860:4801:64::/64"
19409
+ },
19410
+ {
19411
+ ipv6Prefix: "2001:4860:4801:65::/64"
19412
+ },
19413
+ {
19414
+ ipv6Prefix: "2001:4860:4801:66::/64"
19415
+ },
19416
+ {
19417
+ ipv6Prefix: "2001:4860:4801:67::/64"
19418
+ },
19419
+ {
19420
+ ipv6Prefix: "2001:4860:4801:68::/64"
19421
+ },
19422
+ {
19423
+ ipv6Prefix: "2001:4860:4801:69::/64"
19424
+ },
19425
+ {
19426
+ ipv6Prefix: "2001:4860:4801:6a::/64"
19427
+ },
19428
+ {
19429
+ ipv6Prefix: "2001:4860:4801:6b::/64"
19430
+ },
19431
+ {
19432
+ ipv6Prefix: "2001:4860:4801:6c::/64"
19433
+ },
19434
+ {
19435
+ ipv6Prefix: "2001:4860:4801:6d::/64"
19436
+ },
19437
+ {
19438
+ ipv6Prefix: "2001:4860:4801:6e::/64"
19439
+ },
19440
+ {
19441
+ ipv6Prefix: "2001:4860:4801:6f::/64"
19442
+ },
19443
+ {
19444
+ ipv6Prefix: "2001:4860:4801:70::/64"
19445
+ },
19446
+ {
19447
+ ipv6Prefix: "2001:4860:4801:71::/64"
19448
+ },
19449
+ {
19450
+ ipv6Prefix: "2001:4860:4801:72::/64"
19451
+ },
19452
+ {
19453
+ ipv6Prefix: "2001:4860:4801:73::/64"
19454
+ },
19455
+ {
19456
+ ipv6Prefix: "2001:4860:4801:74::/64"
19457
+ },
19458
+ {
19459
+ ipv6Prefix: "2001:4860:4801:75::/64"
19460
+ },
19461
+ {
19462
+ ipv6Prefix: "2001:4860:4801:76::/64"
19463
+ },
19464
+ {
19465
+ ipv6Prefix: "2001:4860:4801:77::/64"
19466
+ },
19467
+ {
19468
+ ipv6Prefix: "2001:4860:4801:78::/64"
19469
+ },
19470
+ {
19471
+ ipv6Prefix: "2001:4860:4801:79::/64"
19472
+ },
19473
+ {
19474
+ ipv6Prefix: "2001:4860:4801:7a::/64"
19475
+ },
19476
+ {
19477
+ ipv6Prefix: "2001:4860:4801:7b::/64"
19478
+ },
19479
+ {
19480
+ ipv6Prefix: "2001:4860:4801:7c::/64"
19481
+ },
19482
+ {
19483
+ ipv6Prefix: "2001:4860:4801:7d::/64"
19484
+ },
19485
+ {
19486
+ ipv6Prefix: "2001:4860:4801:80::/64"
19487
+ },
19488
+ {
19489
+ ipv6Prefix: "2001:4860:4801:81::/64"
19490
+ },
19491
+ {
19492
+ ipv6Prefix: "2001:4860:4801:82::/64"
19493
+ },
19494
+ {
19495
+ ipv6Prefix: "2001:4860:4801:83::/64"
19496
+ },
19497
+ {
19498
+ ipv6Prefix: "2001:4860:4801:84::/64"
19499
+ },
19500
+ {
19501
+ ipv6Prefix: "2001:4860:4801:85::/64"
19502
+ },
19503
+ {
19504
+ ipv6Prefix: "2001:4860:4801:86::/64"
19505
+ },
19506
+ {
19507
+ ipv6Prefix: "2001:4860:4801:87::/64"
19508
+ },
19509
+ {
19510
+ ipv6Prefix: "2001:4860:4801:88::/64"
19511
+ },
19512
+ {
19513
+ ipv6Prefix: "2001:4860:4801:90::/64"
19514
+ },
19515
+ {
19516
+ ipv6Prefix: "2001:4860:4801:91::/64"
19517
+ },
19518
+ {
19519
+ ipv6Prefix: "2001:4860:4801:92::/64"
19520
+ },
19521
+ {
19522
+ ipv6Prefix: "2001:4860:4801:93::/64"
19523
+ },
19524
+ {
19525
+ ipv6Prefix: "2001:4860:4801:94::/64"
19526
+ },
19527
+ {
19528
+ ipv6Prefix: "2001:4860:4801:95::/64"
19529
+ },
19530
+ {
19531
+ ipv6Prefix: "2001:4860:4801:96::/64"
19532
+ },
19533
+ {
19534
+ ipv6Prefix: "2001:4860:4801:97::/64"
19535
+ },
19536
+ {
19537
+ ipv6Prefix: "2001:4860:4801:a0::/64"
19538
+ },
19539
+ {
19540
+ ipv6Prefix: "2001:4860:4801:a1::/64"
19541
+ },
19542
+ {
19543
+ ipv6Prefix: "2001:4860:4801:a2::/64"
19544
+ },
19545
+ {
19546
+ ipv6Prefix: "2001:4860:4801:a3::/64"
19547
+ },
19548
+ {
19549
+ ipv6Prefix: "2001:4860:4801:a4::/64"
19550
+ },
19551
+ {
19552
+ ipv6Prefix: "2001:4860:4801:a5::/64"
19553
+ },
19554
+ {
19555
+ ipv6Prefix: "2001:4860:4801:a6::/64"
19556
+ },
19557
+ {
19558
+ ipv6Prefix: "2001:4860:4801:a7::/64"
19559
+ },
19560
+ {
19561
+ ipv6Prefix: "2001:4860:4801:a8::/64"
19562
+ },
19563
+ {
19564
+ ipv6Prefix: "2001:4860:4801:a9::/64"
19565
+ },
19566
+ {
19567
+ ipv6Prefix: "2001:4860:4801:aa::/64"
19568
+ },
19569
+ {
19570
+ ipv6Prefix: "2001:4860:4801:ab::/64"
19571
+ },
19572
+ {
19573
+ ipv6Prefix: "2001:4860:4801:ac::/64"
19574
+ },
19575
+ {
19576
+ ipv6Prefix: "2001:4860:4801:ad::/64"
19577
+ },
19578
+ {
19579
+ ipv6Prefix: "2001:4860:4801:ae::/64"
19580
+ },
19581
+ {
19582
+ ipv6Prefix: "2001:4860:4801:b0::/64"
19583
+ },
19584
+ {
19585
+ ipv6Prefix: "2001:4860:4801:b1::/64"
19586
+ },
19587
+ {
19588
+ ipv6Prefix: "2001:4860:4801:b2::/64"
19589
+ },
19590
+ {
19591
+ ipv6Prefix: "2001:4860:4801:b3::/64"
19592
+ },
19593
+ {
19594
+ ipv6Prefix: "2001:4860:4801:b4::/64"
19595
+ },
19596
+ {
19597
+ ipv6Prefix: "2001:4860:4801:b5::/64"
19598
+ },
19599
+ {
19600
+ ipv6Prefix: "2001:4860:4801:b6::/64"
19601
+ },
19602
+ {
19603
+ ipv6Prefix: "2001:4860:4801:c::/64"
19604
+ },
19605
+ {
19606
+ ipv6Prefix: "2001:4860:4801:f::/64"
19607
+ },
19608
+ {
19609
+ ipv4Prefix: "192.178.4.0/27"
19610
+ },
19611
+ {
19612
+ ipv4Prefix: "192.178.4.128/27"
19613
+ },
19614
+ {
19615
+ ipv4Prefix: "192.178.4.160/27"
19616
+ },
19617
+ {
19618
+ ipv4Prefix: "192.178.4.192/27"
19619
+ },
19620
+ {
19621
+ ipv4Prefix: "192.178.4.224/27"
19622
+ },
19623
+ {
19624
+ ipv4Prefix: "192.178.4.32/27"
19625
+ },
19626
+ {
19627
+ ipv4Prefix: "192.178.4.64/27"
19628
+ },
19629
+ {
19630
+ ipv4Prefix: "192.178.4.96/27"
19631
+ },
19632
+ {
19633
+ ipv4Prefix: "192.178.5.0/27"
19634
+ },
19635
+ {
19636
+ ipv4Prefix: "192.178.6.0/27"
19637
+ },
19638
+ {
19639
+ ipv4Prefix: "192.178.6.128/27"
19640
+ },
19641
+ {
19642
+ ipv4Prefix: "192.178.6.160/27"
19643
+ },
19644
+ {
19645
+ ipv4Prefix: "192.178.6.192/27"
19646
+ },
19647
+ {
19648
+ ipv4Prefix: "192.178.6.224/27"
19649
+ },
19650
+ {
19651
+ ipv4Prefix: "192.178.6.32/27"
19652
+ },
19653
+ {
19654
+ ipv4Prefix: "192.178.6.64/27"
19655
+ },
19656
+ {
19657
+ ipv4Prefix: "192.178.6.96/27"
19658
+ },
19659
+ {
19660
+ ipv4Prefix: "192.178.7.0/27"
19661
+ },
19662
+ {
19663
+ ipv4Prefix: "192.178.7.128/27"
19664
+ },
19665
+ {
19666
+ ipv4Prefix: "192.178.7.160/27"
19667
+ },
19668
+ {
19669
+ ipv4Prefix: "192.178.7.192/27"
19670
+ },
19671
+ {
19672
+ ipv4Prefix: "192.178.7.224/27"
19673
+ },
19674
+ {
19675
+ ipv4Prefix: "192.178.7.32/27"
19676
+ },
19677
+ {
19678
+ ipv4Prefix: "192.178.7.64/27"
19679
+ },
19680
+ {
19681
+ ipv4Prefix: "192.178.7.96/27"
19682
+ },
19683
+ {
19684
+ ipv4Prefix: "34.100.182.96/28"
19685
+ },
19686
+ {
19687
+ ipv4Prefix: "34.101.50.144/28"
19688
+ },
19689
+ {
19690
+ ipv4Prefix: "34.118.254.0/28"
19691
+ },
19692
+ {
19693
+ ipv4Prefix: "34.118.66.0/28"
19694
+ },
19695
+ {
19696
+ ipv4Prefix: "34.126.178.96/28"
19697
+ },
19698
+ {
19699
+ ipv4Prefix: "34.146.150.144/28"
19700
+ },
19701
+ {
19702
+ ipv4Prefix: "34.147.110.144/28"
19703
+ },
19704
+ {
19705
+ ipv4Prefix: "34.151.74.144/28"
19706
+ },
19707
+ {
19708
+ ipv4Prefix: "34.152.50.64/28"
19709
+ },
19710
+ {
19711
+ ipv4Prefix: "34.154.114.144/28"
19712
+ },
19713
+ {
19714
+ ipv4Prefix: "34.155.98.32/28"
19715
+ },
19716
+ {
19717
+ ipv4Prefix: "34.165.18.176/28"
19718
+ },
19719
+ {
19720
+ ipv4Prefix: "34.175.160.64/28"
19721
+ },
19722
+ {
19723
+ ipv4Prefix: "34.176.130.16/28"
19724
+ },
19725
+ {
19726
+ ipv4Prefix: "34.22.85.0/27"
19727
+ },
19728
+ {
19729
+ ipv4Prefix: "34.64.82.64/28"
19730
+ },
19731
+ {
19732
+ ipv4Prefix: "34.65.242.112/28"
19733
+ },
19734
+ {
19735
+ ipv4Prefix: "34.80.50.80/28"
19736
+ },
19737
+ {
19738
+ ipv4Prefix: "34.88.194.0/28"
19739
+ },
19740
+ {
19741
+ ipv4Prefix: "34.89.10.80/28"
19742
+ },
19743
+ {
19744
+ ipv4Prefix: "34.89.198.80/28"
19745
+ },
19746
+ {
19747
+ ipv4Prefix: "34.96.162.48/28"
19748
+ },
19749
+ {
19750
+ ipv4Prefix: "35.247.243.240/28"
19751
+ },
19752
+ {
19753
+ ipv4Prefix: "66.249.64.0/27"
19754
+ },
19755
+ {
19756
+ ipv4Prefix: "66.249.64.128/27"
19757
+ },
19758
+ {
19759
+ ipv4Prefix: "66.249.64.160/27"
19760
+ },
19761
+ {
19762
+ ipv4Prefix: "66.249.64.192/27"
19763
+ },
19764
+ {
19765
+ ipv4Prefix: "66.249.64.224/27"
19766
+ },
19767
+ {
19768
+ ipv4Prefix: "66.249.64.32/27"
19769
+ },
19770
+ {
19771
+ ipv4Prefix: "66.249.64.64/27"
19772
+ },
19773
+ {
19774
+ ipv4Prefix: "66.249.64.96/27"
19775
+ },
19776
+ {
19777
+ ipv4Prefix: "66.249.65.0/27"
19778
+ },
19779
+ {
19780
+ ipv4Prefix: "66.249.65.128/27"
19781
+ },
19782
+ {
19783
+ ipv4Prefix: "66.249.65.160/27"
19784
+ },
19785
+ {
19786
+ ipv4Prefix: "66.249.65.192/27"
19787
+ },
19788
+ {
19789
+ ipv4Prefix: "66.249.65.224/27"
19790
+ },
19791
+ {
19792
+ ipv4Prefix: "66.249.65.32/27"
19793
+ },
19794
+ {
19795
+ ipv4Prefix: "66.249.65.64/27"
19796
+ },
19797
+ {
19798
+ ipv4Prefix: "66.249.65.96/27"
19799
+ },
19800
+ {
19801
+ ipv4Prefix: "66.249.66.0/27"
19802
+ },
19803
+ {
19804
+ ipv4Prefix: "66.249.66.128/27"
19805
+ },
19806
+ {
19807
+ ipv4Prefix: "66.249.66.160/27"
19808
+ },
19809
+ {
19810
+ ipv4Prefix: "66.249.66.192/27"
19811
+ },
19812
+ {
19813
+ ipv4Prefix: "66.249.66.224/27"
19814
+ },
19815
+ {
19816
+ ipv4Prefix: "66.249.66.32/27"
19817
+ },
19818
+ {
19819
+ ipv4Prefix: "66.249.66.64/27"
19820
+ },
19821
+ {
19822
+ ipv4Prefix: "66.249.66.96/27"
19823
+ },
19824
+ {
19825
+ ipv4Prefix: "66.249.67.0/27"
19826
+ },
19827
+ {
19828
+ ipv4Prefix: "66.249.67.32/27"
19829
+ },
19830
+ {
19831
+ ipv4Prefix: "66.249.67.64/27"
19832
+ },
19833
+ {
19834
+ ipv4Prefix: "66.249.68.0/27"
19835
+ },
19836
+ {
19837
+ ipv4Prefix: "66.249.68.128/27"
19838
+ },
19839
+ {
19840
+ ipv4Prefix: "66.249.68.160/27"
19841
+ },
19842
+ {
19843
+ ipv4Prefix: "66.249.68.192/27"
19844
+ },
19845
+ {
19846
+ ipv4Prefix: "66.249.68.32/27"
19847
+ },
19848
+ {
19849
+ ipv4Prefix: "66.249.68.64/27"
19850
+ },
19851
+ {
19852
+ ipv4Prefix: "66.249.68.96/27"
19853
+ },
19854
+ {
19855
+ ipv4Prefix: "66.249.69.0/27"
19856
+ },
19857
+ {
19858
+ ipv4Prefix: "66.249.69.128/27"
19859
+ },
19860
+ {
19861
+ ipv4Prefix: "66.249.69.160/27"
19862
+ },
19863
+ {
19864
+ ipv4Prefix: "66.249.69.192/27"
19865
+ },
19866
+ {
19867
+ ipv4Prefix: "66.249.69.224/27"
19868
+ },
19869
+ {
19870
+ ipv4Prefix: "66.249.69.32/27"
19871
+ },
19872
+ {
19873
+ ipv4Prefix: "66.249.69.64/27"
19874
+ },
19875
+ {
19876
+ ipv4Prefix: "66.249.69.96/27"
19877
+ },
19878
+ {
19879
+ ipv4Prefix: "66.249.70.0/27"
19880
+ },
19881
+ {
19882
+ ipv4Prefix: "66.249.70.128/27"
19883
+ },
19884
+ {
19885
+ ipv4Prefix: "66.249.70.160/27"
19886
+ },
19887
+ {
19888
+ ipv4Prefix: "66.249.70.192/27"
19889
+ },
19890
+ {
19891
+ ipv4Prefix: "66.249.70.224/27"
19892
+ },
19893
+ {
19894
+ ipv4Prefix: "66.249.70.32/27"
19895
+ },
19896
+ {
19897
+ ipv4Prefix: "66.249.70.64/27"
19898
+ },
19899
+ {
19900
+ ipv4Prefix: "66.249.70.96/27"
19901
+ },
19902
+ {
19903
+ ipv4Prefix: "66.249.71.0/27"
19904
+ },
19905
+ {
19906
+ ipv4Prefix: "66.249.71.128/27"
19907
+ },
19908
+ {
19909
+ ipv4Prefix: "66.249.71.160/27"
19910
+ },
19911
+ {
19912
+ ipv4Prefix: "66.249.71.192/27"
19913
+ },
19914
+ {
19915
+ ipv4Prefix: "66.249.71.224/27"
19916
+ },
19917
+ {
19918
+ ipv4Prefix: "66.249.71.32/27"
19919
+ },
19920
+ {
19921
+ ipv4Prefix: "66.249.71.64/27"
19922
+ },
19923
+ {
19924
+ ipv4Prefix: "66.249.71.96/27"
19925
+ },
19926
+ {
19927
+ ipv4Prefix: "66.249.72.0/27"
19928
+ },
19929
+ {
19930
+ ipv4Prefix: "66.249.72.128/27"
19931
+ },
19932
+ {
19933
+ ipv4Prefix: "66.249.72.160/27"
19934
+ },
19935
+ {
19936
+ ipv4Prefix: "66.249.72.192/27"
19937
+ },
19938
+ {
19939
+ ipv4Prefix: "66.249.72.224/27"
19940
+ },
19941
+ {
19942
+ ipv4Prefix: "66.249.72.32/27"
19943
+ },
19944
+ {
19945
+ ipv4Prefix: "66.249.72.64/27"
19946
+ },
19947
+ {
19948
+ ipv4Prefix: "66.249.72.96/27"
19949
+ },
19950
+ {
19951
+ ipv4Prefix: "66.249.73.0/27"
19952
+ },
19953
+ {
19954
+ ipv4Prefix: "66.249.73.128/27"
19955
+ },
19956
+ {
19957
+ ipv4Prefix: "66.249.73.160/27"
19958
+ },
19959
+ {
19960
+ ipv4Prefix: "66.249.73.192/27"
19961
+ },
19962
+ {
19963
+ ipv4Prefix: "66.249.73.224/27"
19964
+ },
19965
+ {
19966
+ ipv4Prefix: "66.249.73.32/27"
19967
+ },
19968
+ {
19969
+ ipv4Prefix: "66.249.73.64/27"
19970
+ },
19971
+ {
19972
+ ipv4Prefix: "66.249.73.96/27"
19973
+ },
19974
+ {
19975
+ ipv4Prefix: "66.249.74.0/27"
19976
+ },
19977
+ {
19978
+ ipv4Prefix: "66.249.74.128/27"
19979
+ },
19980
+ {
19981
+ ipv4Prefix: "66.249.74.160/27"
19982
+ },
19983
+ {
19984
+ ipv4Prefix: "66.249.74.192/27"
19985
+ },
19986
+ {
19987
+ ipv4Prefix: "66.249.74.224/27"
19988
+ },
19989
+ {
19990
+ ipv4Prefix: "66.249.74.32/27"
19991
+ },
19992
+ {
19993
+ ipv4Prefix: "66.249.74.64/27"
19994
+ },
19995
+ {
19996
+ ipv4Prefix: "66.249.74.96/27"
19997
+ },
19998
+ {
19999
+ ipv4Prefix: "66.249.75.0/27"
20000
+ },
20001
+ {
20002
+ ipv4Prefix: "66.249.75.128/27"
20003
+ },
20004
+ {
20005
+ ipv4Prefix: "66.249.75.160/27"
20006
+ },
20007
+ {
20008
+ ipv4Prefix: "66.249.75.192/27"
20009
+ },
20010
+ {
20011
+ ipv4Prefix: "66.249.75.224/27"
20012
+ },
20013
+ {
20014
+ ipv4Prefix: "66.249.75.32/27"
20015
+ },
20016
+ {
20017
+ ipv4Prefix: "66.249.75.64/27"
20018
+ },
20019
+ {
20020
+ ipv4Prefix: "66.249.75.96/27"
20021
+ },
20022
+ {
20023
+ ipv4Prefix: "66.249.76.0/27"
20024
+ },
20025
+ {
20026
+ ipv4Prefix: "66.249.76.128/27"
20027
+ },
20028
+ {
20029
+ ipv4Prefix: "66.249.76.160/27"
20030
+ },
20031
+ {
20032
+ ipv4Prefix: "66.249.76.192/27"
20033
+ },
20034
+ {
20035
+ ipv4Prefix: "66.249.76.224/27"
20036
+ },
20037
+ {
20038
+ ipv4Prefix: "66.249.76.32/27"
20039
+ },
20040
+ {
20041
+ ipv4Prefix: "66.249.76.64/27"
20042
+ },
20043
+ {
20044
+ ipv4Prefix: "66.249.76.96/27"
20045
+ },
20046
+ {
20047
+ ipv4Prefix: "66.249.77.0/27"
20048
+ },
20049
+ {
20050
+ ipv4Prefix: "66.249.77.128/27"
20051
+ },
20052
+ {
20053
+ ipv4Prefix: "66.249.77.160/27"
20054
+ },
20055
+ {
20056
+ ipv4Prefix: "66.249.77.192/27"
20057
+ },
20058
+ {
20059
+ ipv4Prefix: "66.249.77.224/27"
20060
+ },
20061
+ {
20062
+ ipv4Prefix: "66.249.77.32/27"
20063
+ },
20064
+ {
20065
+ ipv4Prefix: "66.249.77.64/27"
20066
+ },
20067
+ {
20068
+ ipv4Prefix: "66.249.77.96/27"
20069
+ },
20070
+ {
20071
+ ipv4Prefix: "66.249.78.0/27"
20072
+ },
20073
+ {
20074
+ ipv4Prefix: "66.249.78.128/27"
20075
+ },
20076
+ {
20077
+ ipv4Prefix: "66.249.78.160/27"
20078
+ },
20079
+ {
20080
+ ipv4Prefix: "66.249.78.32/27"
20081
+ },
20082
+ {
20083
+ ipv4Prefix: "66.249.78.64/27"
20084
+ },
20085
+ {
20086
+ ipv4Prefix: "66.249.78.96/27"
20087
+ },
20088
+ {
20089
+ ipv4Prefix: "66.249.79.0/27"
20090
+ },
20091
+ {
20092
+ ipv4Prefix: "66.249.79.128/27"
20093
+ },
20094
+ {
20095
+ ipv4Prefix: "66.249.79.160/27"
20096
+ },
20097
+ {
20098
+ ipv4Prefix: "66.249.79.192/27"
20099
+ },
20100
+ {
20101
+ ipv4Prefix: "66.249.79.224/27"
20102
+ },
20103
+ {
20104
+ ipv4Prefix: "66.249.79.32/27"
20105
+ },
20106
+ {
20107
+ ipv4Prefix: "66.249.79.64/27"
20108
+ }
20109
+ ]
20110
+ };
20111
+
20112
+ // ../integration-traffic/src/ip-ranges/gptbot.json
20113
+ var gptbot_default = {
20114
+ _source: "https://openai.com/gptbot.json",
20115
+ creationTime: "2025-10-30T11:00:00.000000",
20116
+ prefixes: [
20117
+ {
20118
+ ipv4Prefix: "132.196.86.0/24"
20119
+ },
20120
+ {
20121
+ ipv4Prefix: "172.182.202.0/25"
20122
+ },
20123
+ {
20124
+ ipv4Prefix: "172.182.204.0/24"
20125
+ },
20126
+ {
20127
+ ipv4Prefix: "172.182.207.0/25"
20128
+ },
20129
+ {
20130
+ ipv4Prefix: "172.182.214.0/24"
20131
+ },
20132
+ {
20133
+ ipv4Prefix: "172.182.215.0/24"
20134
+ },
20135
+ {
20136
+ ipv4Prefix: "20.125.66.80/28"
20137
+ },
20138
+ {
20139
+ ipv4Prefix: "20.171.206.0/24"
20140
+ },
20141
+ {
20142
+ ipv4Prefix: "20.171.207.0/24"
20143
+ },
20144
+ {
20145
+ ipv4Prefix: "4.227.36.0/25"
20146
+ },
20147
+ {
20148
+ ipv4Prefix: "52.230.152.0/24"
20149
+ },
20150
+ {
20151
+ ipv4Prefix: "74.7.175.128/25"
20152
+ },
20153
+ {
20154
+ ipv4Prefix: "74.7.227.0/25"
20155
+ },
20156
+ {
20157
+ ipv4Prefix: "74.7.227.128/25"
20158
+ },
20159
+ {
20160
+ ipv4Prefix: "74.7.228.0/25"
20161
+ },
20162
+ {
20163
+ ipv4Prefix: "74.7.230.0/25"
20164
+ },
20165
+ {
20166
+ ipv4Prefix: "74.7.241.0/25"
20167
+ },
20168
+ {
20169
+ ipv4Prefix: "74.7.241.128/25"
20170
+ },
20171
+ {
20172
+ ipv4Prefix: "74.7.242.0/25"
20173
+ },
20174
+ {
20175
+ ipv4Prefix: "74.7.243.128/25"
20176
+ },
20177
+ {
20178
+ ipv4Prefix: "74.7.244.0/25"
20179
+ }
20180
+ ]
20181
+ };
20182
+
20183
+ // ../integration-traffic/src/ip-ranges/oai-searchbot.json
20184
+ var oai_searchbot_default = {
20185
+ _source: "https://openai.com/searchbot.json",
20186
+ creationTime: "2026-01-02T11:00:00.000000",
20187
+ prefixes: [
20188
+ {
20189
+ ipv4Prefix: "104.210.140.128/28"
20190
+ },
20191
+ {
20192
+ ipv4Prefix: "135.234.64.0/24"
20193
+ },
20194
+ {
20195
+ ipv4Prefix: "172.182.193.224/28"
20196
+ },
20197
+ {
20198
+ ipv4Prefix: "172.182.193.80/28"
20199
+ },
20200
+ {
20201
+ ipv4Prefix: "172.182.194.144/28"
20202
+ },
20203
+ {
20204
+ ipv4Prefix: "172.182.194.32/28"
20205
+ },
20206
+ {
20207
+ ipv4Prefix: "172.182.195.48/28"
20208
+ },
20209
+ {
20210
+ ipv4Prefix: "172.182.209.208/28"
20211
+ },
20212
+ {
20213
+ ipv4Prefix: "172.182.211.192/28"
20214
+ },
20215
+ {
20216
+ ipv4Prefix: "172.182.213.192/28"
20217
+ },
20218
+ {
20219
+ ipv4Prefix: "172.182.224.0/28"
20220
+ },
20221
+ {
20222
+ ipv4Prefix: "172.203.190.128/28"
20223
+ },
20224
+ {
20225
+ ipv4Prefix: "20.14.99.96/28"
20226
+ },
20227
+ {
20228
+ ipv4Prefix: "20.168.18.32/28"
20229
+ },
20230
+ {
20231
+ ipv4Prefix: "20.169.6.224/28"
20232
+ },
20233
+ {
20234
+ ipv4Prefix: "20.169.7.48/28"
20235
+ },
20236
+ {
20237
+ ipv4Prefix: "20.169.77.0/25"
20238
+ },
20239
+ {
20240
+ ipv4Prefix: "20.171.123.64/28"
20241
+ },
20242
+ {
20243
+ ipv4Prefix: "20.171.53.224/28"
20244
+ },
20245
+ {
20246
+ ipv4Prefix: "20.25.151.224/28"
20247
+ },
20248
+ {
20249
+ ipv4Prefix: "20.42.10.176/28"
20250
+ },
20251
+ {
20252
+ ipv4Prefix: "4.227.36.0/25"
20253
+ },
20254
+ {
20255
+ ipv4Prefix: "40.67.175.0/25"
20256
+ },
20257
+ {
20258
+ ipv4Prefix: "40.90.214.16/28"
20259
+ },
20260
+ {
20261
+ ipv4Prefix: "51.8.102.0/24"
20262
+ },
20263
+ {
20264
+ ipv4Prefix: "74.7.175.128/25"
20265
+ },
20266
+ {
20267
+ ipv4Prefix: "74.7.228.0/25"
20268
+ },
20269
+ {
20270
+ ipv4Prefix: "74.7.228.128/25"
20271
+ },
20272
+ {
20273
+ ipv4Prefix: "74.7.229.0/25"
20274
+ },
20275
+ {
20276
+ ipv4Prefix: "74.7.229.128/25"
20277
+ },
20278
+ {
20279
+ ipv4Prefix: "74.7.230.0/25"
20280
+ },
20281
+ {
20282
+ ipv4Prefix: "74.7.241.128/25"
20283
+ },
20284
+ {
20285
+ ipv4Prefix: "74.7.242.128/25"
20286
+ },
20287
+ {
20288
+ ipv4Prefix: "74.7.243.0/25"
20289
+ },
20290
+ {
20291
+ ipv4Prefix: "74.7.244.0/25"
20292
+ }
20293
+ ]
20294
+ };
20295
+
20296
+ // ../integration-traffic/src/ip-ranges/perplexity-user.json
20297
+ var perplexity_user_default = {
20298
+ _source: "https://www.perplexity.ai/perplexity-user.json",
20299
+ creationTime: "2025-10-17T10:17:00.000000",
20300
+ prefixes: [
20301
+ {
20302
+ ipv4Prefix: "44.208.221.197/32"
20303
+ },
20304
+ {
20305
+ ipv4Prefix: "34.193.163.52/32"
20306
+ },
20307
+ {
20308
+ ipv4Prefix: "18.97.21.0/30"
20309
+ },
20310
+ {
20311
+ ipv4Prefix: "18.97.43.80/29"
20312
+ }
20313
+ ]
20314
+ };
20315
+
20316
+ // ../integration-traffic/src/ip-ranges/perplexitybot.json
20317
+ var perplexitybot_default = {
20318
+ _source: "https://www.perplexity.ai/perplexitybot.json",
20319
+ creationTime: "2025-02-07T16:56:00.000000",
20320
+ prefixes: [
20321
+ {
20322
+ ipv4Prefix: "107.20.236.150/32"
20323
+ },
20324
+ {
20325
+ ipv4Prefix: "3.224.62.45/32"
20326
+ },
20327
+ {
20328
+ ipv4Prefix: "18.210.92.235/32"
20329
+ },
20330
+ {
20331
+ ipv4Prefix: "3.222.232.239/32"
20332
+ },
20333
+ {
20334
+ ipv4Prefix: "3.211.124.183/32"
20335
+ },
20336
+ {
20337
+ ipv4Prefix: "3.231.139.107/32"
20338
+ },
20339
+ {
20340
+ ipv4Prefix: "18.97.1.228/30"
20341
+ },
20342
+ {
20343
+ ipv4Prefix: "18.97.9.96/29"
20344
+ }
20345
+ ]
20346
+ };
20347
+
20348
+ // ../integration-traffic/src/ip-verify.ts
20349
+ var RULE_ID_TO_RANGES = {
20350
+ // OpenAI — three separate published lists (training crawler vs
20351
+ // user-on-behalf fetcher vs search engine; OpenAI maintains the
20352
+ // split because the IPs really do differ between products).
20353
+ // src: https://openai.com/gptbot.json
20354
+ "openai-gptbot": gptbot_default,
20355
+ // src: https://openai.com/chatgpt-user.json
20356
+ "openai-chatgpt-user": chatgpt_user_default,
20357
+ // src: https://openai.com/searchbot.json
20358
+ "openai-searchbot": oai_searchbot_default,
20359
+ // Search engines.
20360
+ // src: https://developers.google.com/static/search/apis/ipranges/googlebot.json
20361
+ // (also covers Gemini grounding — Google doesn't publish a
20362
+ // separate Gemini list; Google-Extended traffic comes from the
20363
+ // same Googlebot ranges)
20364
+ "googlebot": googlebot_default,
20365
+ // src: https://www.bing.com/toolbox/bingbot.json
20366
+ // (also covers Copilot grounding — Microsoft routes Copilot's
20367
+ // web fetches through bingbot infrastructure)
20368
+ "bingbot": bingbot_default,
20369
+ // Perplexity — split between crawler and user-on-behalf fetcher,
20370
+ // same shape as OpenAI's split.
20371
+ // src: https://www.perplexity.ai/perplexitybot.json
20372
+ "perplexity-bot": perplexitybot_default,
20373
+ // src: https://www.perplexity.ai/perplexity-user.json
20374
+ "perplexity-user": perplexity_user_default,
20375
+ // Anthropic — no machine-readable JSON published. The bundled
20376
+ // anthropic.json is the set of networks registered to Anthropic,
20377
+ // PBC at ARIN (the authoritative allocation record). Maintained by
20378
+ // hand; refresh by re-querying the ARIN entity below. The crawler
20379
+ // block is AWS-ANTHROPIC 216.73.216.0/22 — empirical Cloud Run
20380
+ // logs show all real ClaudeBot traffic comes from there. Same raw
20381
+ // set is shared across every Claude-* UA the classifier emits.
20382
+ // src: https://rdap.arin.net/registry/entity/AP-2440
20383
+ "anthropic-claudebot": anthropic_default
20384
+ };
20385
+ var CACHE = (() => {
20386
+ const cache = /* @__PURE__ */ new Map();
20387
+ for (const [ruleId, raw] of Object.entries(RULE_ID_TO_RANGES)) {
20388
+ const parsed = [];
20389
+ for (const entry of raw.prefixes) {
20390
+ const cidr = entry.ipv4Prefix ?? entry.ipv6Prefix;
20391
+ if (!cidr) continue;
20392
+ const p = parseCidr(cidr);
20393
+ if (p) parsed.push(p);
20394
+ }
20395
+ cache.set(ruleId, parsed);
20396
+ }
20397
+ return cache;
20398
+ })();
20399
+ function parseIp(ip) {
20400
+ if (!ip) return null;
20401
+ const mappedMatch = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(ip);
20402
+ if (mappedMatch) return parseIp(mappedMatch[1]);
20403
+ if (ip.includes(":")) {
20404
+ const sides = ip.split("::");
20405
+ if (sides.length > 2) return null;
20406
+ const left = sides[0].length > 0 ? sides[0].split(":") : [];
20407
+ const right = sides.length === 2 && sides[1].length > 0 ? sides[1].split(":") : [];
20408
+ const groupCount = left.length + right.length;
20409
+ if (groupCount > 8) return null;
20410
+ if (sides.length === 1 && groupCount !== 8) return null;
20411
+ const fill = 8 - groupCount;
20412
+ const groups = [...left, ...new Array(fill).fill("0"), ...right];
20413
+ let addr2 = 0n;
20414
+ for (const g of groups) {
20415
+ if (g.length === 0 || g.length > 4) return null;
20416
+ const n = Number.parseInt(g, 16);
20417
+ if (!Number.isFinite(n) || n < 0 || n > 65535) return null;
20418
+ addr2 = addr2 << 16n | BigInt(n);
20419
+ }
20420
+ return { version: 6, addr: addr2 };
20421
+ }
20422
+ const octets = ip.split(".");
20423
+ if (octets.length !== 4) return null;
20424
+ let addr = 0n;
20425
+ for (const o of octets) {
20426
+ if (o.length === 0 || o.length > 3) return null;
20427
+ const n = Number.parseInt(o, 10);
20428
+ if (!Number.isInteger(n) || n < 0 || n > 255) return null;
20429
+ addr = addr << 8n | BigInt(n);
20430
+ }
20431
+ return { version: 4, addr };
20432
+ }
20433
+ function parseCidr(cidr) {
20434
+ const [ipPart, prefixStr] = cidr.split("/");
20435
+ if (!ipPart || !prefixStr) return null;
20436
+ const prefix = Number.parseInt(prefixStr, 10);
20437
+ if (!Number.isInteger(prefix)) return null;
20438
+ const parsed = parseIp(ipPart);
20439
+ if (!parsed) return null;
20440
+ const totalBits = parsed.version === 4 ? 32 : 128;
20441
+ if (prefix < 0 || prefix > totalBits) return null;
20442
+ const allOnes = (1n << BigInt(totalBits)) - 1n;
20443
+ const mask = allOnes >> BigInt(totalBits - prefix) << BigInt(totalBits - prefix);
20444
+ return {
20445
+ version: parsed.version,
20446
+ network: parsed.addr & mask,
20447
+ mask
20448
+ };
20449
+ }
20450
+ function verifyIpForRule(ip, ruleId) {
20451
+ if (!ip) return false;
20452
+ const ranges = CACHE.get(ruleId);
20453
+ if (!ranges || ranges.length === 0) return false;
20454
+ const parsed = parseIp(ip);
20455
+ if (!parsed) return false;
20456
+ for (const cidr of ranges) {
20457
+ if (parsed.version !== cidr.version) continue;
20458
+ if ((parsed.addr & cidr.mask) === cidr.network) return true;
20459
+ }
20460
+ return false;
20461
+ }
20462
+
20463
+ // ../integration-traffic/src/rules.ts
20464
+ var LEGACY_CHATGPT_DOMAIN = "chat.openai.com";
20465
+ var DEFAULT_AI_CRAWLER_RULES = [
20466
+ {
20467
+ id: "openai-gptbot",
20468
+ operator: "OpenAI",
20469
+ product: "GPTBot",
20470
+ purpose: "training",
20471
+ userAgentPatterns: [/GPTBot\//i]
20472
+ },
20473
+ {
20474
+ id: "openai-searchbot",
20475
+ operator: "OpenAI",
20476
+ product: "OAI-SearchBot",
20477
+ purpose: "search",
20478
+ userAgentPatterns: [/OAI-SearchBot\//i]
20479
+ },
20480
+ {
20481
+ id: "openai-chatgpt-user",
20482
+ operator: "OpenAI",
20483
+ product: "ChatGPT-User",
20484
+ purpose: "user-agent",
20485
+ userAgentPatterns: [/ChatGPT-User\//i]
20486
+ },
20487
+ {
20488
+ id: "anthropic-claudebot",
20489
+ operator: "Anthropic",
20490
+ product: "ClaudeBot",
20491
+ purpose: "training",
20492
+ // Anthropic ships several Claude-* crawlers (ClaudeBot for training,
20493
+ // Claude-Web for chat fetches, Claude-SearchBot for search). The
20494
+ // `Claude-` prefix + `Bot/` suffix is the stable shape — pattern is
20495
+ // permissive enough to catch new Claude-* variants as Anthropic
20496
+ // adds them, without matching unrelated UAs that happen to mention
20497
+ // "claude".
20498
+ userAgentPatterns: [
20499
+ /ClaudeBot\//i,
20500
+ /Claude-Web\//i,
20501
+ /Claude-SearchBot\//i,
20502
+ /Claude-[A-Z]+Bot\//i,
20503
+ /anthropic-ai/i
20504
+ ]
20505
+ },
20506
+ {
20507
+ id: "perplexity-bot",
20508
+ operator: "Perplexity",
20509
+ product: "PerplexityBot",
20510
+ purpose: "search",
20511
+ userAgentPatterns: [/PerplexityBot\//i]
20512
+ },
20513
+ {
20514
+ // User-initiated fetches when a Perplexity user opens a citation
20515
+ // link. Separate from PerplexityBot (crawl) — different ranges and
20516
+ // different operational signal. Perplexity publishes both UA
20517
+ // patterns at perplexity.ai/perplexity-user.json.
20518
+ id: "perplexity-user",
20519
+ operator: "Perplexity",
20520
+ product: "Perplexity-User",
20521
+ purpose: "user-agent",
20522
+ userAgentPatterns: [/Perplexity-User\//i]
20523
+ },
20524
+ {
20525
+ id: "google-extended",
20526
+ operator: "Google",
20527
+ product: "Google-Extended",
20528
+ purpose: "training-control",
20529
+ userAgentPatterns: [/Google-Extended/i]
20530
+ },
20531
+ {
20532
+ id: "bytespider",
20533
+ operator: "ByteDance",
20534
+ product: "Bytespider",
20535
+ purpose: "training",
20536
+ userAgentPatterns: [/Bytespider/i]
20537
+ },
20538
+ {
20539
+ id: "applebot-extended",
20540
+ operator: "Apple",
20541
+ product: "Applebot-Extended",
20542
+ purpose: "training",
20543
+ userAgentPatterns: [/Applebot-Extended/i]
20544
+ },
20545
+ {
20546
+ // Apple's general crawler (separate from Applebot-Extended, which is
20547
+ // the training-opt-out signaling UA). Both indexes pages for Apple
20548
+ // services (Siri/Spotlight); only Applebot-Extended is gated by
20549
+ // training-data opt-out.
20550
+ id: "applebot",
20551
+ operator: "Apple",
20552
+ product: "Applebot",
20553
+ purpose: "crawl",
20554
+ userAgentPatterns: [/Applebot\//i]
20555
+ },
20556
+ {
20557
+ id: "meta-externalagent",
20558
+ operator: "Meta",
20559
+ product: "meta-externalagent",
20560
+ purpose: "training",
20561
+ userAgentPatterns: [/meta-externalagent/i]
20562
+ },
20563
+ {
20564
+ id: "ccbot",
20565
+ operator: "Common Crawl",
20566
+ product: "CCBot",
20567
+ purpose: "crawl",
20568
+ userAgentPatterns: [/CCBot\//i]
20569
+ },
20570
+ {
20571
+ id: "cohere-ai",
20572
+ operator: "Cohere",
20573
+ product: "cohere-ai",
20574
+ purpose: "training",
20575
+ userAgentPatterns: [/cohere-ai/i]
20576
+ },
20577
+ {
20578
+ id: "diffbot",
20579
+ operator: "Diffbot",
20580
+ product: "Diffbot",
20581
+ purpose: "crawl",
20582
+ userAgentPatterns: [/Diffbot/i]
20583
+ },
20584
+ {
20585
+ id: "mistral-ai",
20586
+ operator: "Mistral AI",
20587
+ product: "MistralAI-User",
20588
+ purpose: "crawl",
20589
+ // Mistral ships both `MistralAI-User/*` (chat-on-behalf-of-user
20590
+ // fetches) and `MistralBot/*` (general crawler). Earlier rule only
20591
+ // matched `MistralAI` and missed the bot — caught on 2026-05-18
20592
+ // when canonry.ai/canonry-landing's classification chart went flat
20593
+ // and the bot UA was sitting in the `unknown` bucket.
20594
+ userAgentPatterns: [/MistralAI/i, /MistralBot/i]
20595
+ },
20596
+ {
20597
+ id: "deepseek",
20598
+ operator: "DeepSeek",
20599
+ product: "DeepSeekBot",
20600
+ purpose: "training",
20601
+ userAgentPatterns: [/DeepSeekBot/i]
20602
+ },
20603
+ // Classic search-engine crawlers. Not strictly "AI" by training origin,
20604
+ // but the same audience: machine traffic indexing the site for query
20605
+ // surfaces. Operators tracking AI visibility want this signal too —
20606
+ // SERP indexing is the upstream that feeds AI answer engines (Bing
20607
+ // powers ChatGPT search; Google powers Gemini grounding). Classified
20608
+ // alongside LLM crawlers; the dashboard's "AI crawler hits" label is
20609
+ // imprecise here but functionally correct (these are still bots, not
20610
+ // humans).
20611
+ {
20612
+ id: "googlebot",
20613
+ operator: "Google",
20614
+ product: "Googlebot",
20615
+ purpose: "search",
20616
+ // Googlebot has Smartphone / Desktop / Image / News / Video variants.
20617
+ // All match the `Googlebot/` prefix on first appearance in the UA.
20618
+ // Excludes `Googlebot-Image` etc. that ride a `Googlebot-` prefix —
20619
+ // they also match `Googlebot/` in their UA strings.
20620
+ userAgentPatterns: [/Googlebot[/-]/i]
20621
+ },
20622
+ {
20623
+ id: "bingbot",
20624
+ operator: "Microsoft",
20625
+ product: "bingbot",
20626
+ purpose: "search",
20627
+ userAgentPatterns: [/bingbot\//i]
20628
+ },
20629
+ {
20630
+ id: "duckduckbot",
20631
+ operator: "DuckDuckGo",
20632
+ product: "DuckDuckBot",
20633
+ purpose: "search",
20634
+ userAgentPatterns: [/DuckDuckBot/i]
20635
+ },
20636
+ {
20637
+ id: "yandexbot",
20638
+ operator: "Yandex",
20639
+ product: "YandexBot",
20640
+ purpose: "search",
20641
+ userAgentPatterns: [/YandexBot\//i]
20642
+ },
20643
+ {
20644
+ id: "baiduspider",
20645
+ operator: "Baidu",
20646
+ product: "Baiduspider",
20647
+ purpose: "search",
20648
+ userAgentPatterns: [/Baiduspider/i]
20649
+ },
20650
+ {
20651
+ id: "amazonbot",
20652
+ operator: "Amazon",
20653
+ product: "Amazonbot",
20654
+ purpose: "crawl",
20655
+ userAgentPatterns: [/Amazonbot\//i]
20656
+ }
20657
+ ];
20658
+ var DEFAULT_AI_REFERRER_RULES = [
20659
+ { domain: AI_ENGINE_DOMAINS.chatgpt, operator: "OpenAI", product: "ChatGPT" },
20660
+ { domain: LEGACY_CHATGPT_DOMAIN, operator: "OpenAI", product: "ChatGPT" },
20661
+ { domain: AI_ENGINE_DOMAINS.perplexity, operator: "Perplexity", product: "Perplexity" },
20662
+ { domain: AI_ENGINE_DOMAINS.claude, operator: "Anthropic", product: "Claude" },
20663
+ { domain: AI_ENGINE_DOMAINS.gemini, operator: "Google", product: "Gemini" },
20664
+ { domain: AI_ENGINE_DOMAINS.copilotMicrosoft, operator: "Microsoft", product: "Copilot" },
20665
+ { domain: AI_ENGINE_DOMAINS.phind, operator: "Phind", product: "Phind" },
20666
+ { domain: AI_ENGINE_DOMAINS.you, operator: "You.com", product: "You.com" },
20667
+ { domain: AI_ENGINE_DOMAINS.metaAi, operator: "Meta", product: "Meta AI" }
20668
+ ];
20669
+
20670
+ // ../integration-traffic/src/classifier.ts
20671
+ function normalizeHost(host) {
20672
+ return host.trim().toLowerCase().replace(/^www\./, "");
20673
+ }
20674
+ function hostMatches(host, domain) {
20675
+ const normalizedHost = normalizeHost(host);
20676
+ const normalizedDomain = normalizeHost(domain);
20677
+ return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
20678
+ }
20679
+ function utmTokenMatchesDomain(utmSource, domain) {
20680
+ if (hostMatches(utmSource, domain)) return true;
20681
+ const normalizedUtm = normalizeHost(utmSource);
20682
+ const firstLabel = normalizeHost(domain).split(".")[0];
20683
+ return Boolean(firstLabel) && normalizedUtm === firstLabel;
20684
+ }
20685
+ function hostFromUrl(value) {
20686
+ if (!value) return null;
18258
20687
  try {
18259
20688
  return normalizeHost(new URL(value).hostname);
18260
20689
  } catch {
@@ -18280,12 +20709,13 @@ function classifyCrawler(event) {
18280
20709
  if (!userAgent) return null;
18281
20710
  for (const rule of DEFAULT_AI_CRAWLER_RULES) {
18282
20711
  if (rule.userAgentPatterns.some((pattern) => pattern.test(userAgent))) {
20712
+ const verified = verifyIpForRule(event.remoteIp, rule.id);
18283
20713
  return {
18284
20714
  botId: rule.id,
18285
20715
  operator: rule.operator,
18286
20716
  product: rule.product,
18287
20717
  purpose: rule.purpose,
18288
- verificationStatus: "claimed_unverified",
20718
+ verificationStatus: verified ? "verified" : "claimed_unverified",
18289
20719
  matchedUserAgent: userAgent
18290
20720
  };
18291
20721
  }
@@ -20736,6 +23166,79 @@ var providersConfiguredCheck = {
20736
23166
  };
20737
23167
  var PROVIDERS_CHECKS = [providersConfiguredCheck];
20738
23168
 
23169
+ // ../api-routes/src/doctor/checks/runtime-state.ts
23170
+ import fs7 from "fs";
23171
+ var dbFilePresentCheck = {
23172
+ id: "db.file.present",
23173
+ category: CheckCategories.database,
23174
+ scope: CheckScopes.global,
23175
+ title: "Database file present",
23176
+ run: (ctx) => {
23177
+ const path16 = ctx.runtimeStatePaths?.databasePath;
23178
+ if (!path16) {
23179
+ return {
23180
+ status: CheckStatuses.skipped,
23181
+ code: "db.file.path-not-wired",
23182
+ summary: "No database file path configured for this deployment (cloud DB).",
23183
+ remediation: null
23184
+ };
23185
+ }
23186
+ if (!fs7.existsSync(path16)) {
23187
+ return {
23188
+ status: CheckStatuses.fail,
23189
+ code: "db.file.missing",
23190
+ summary: `Database file at \`${path16}\` has been deleted while the daemon is running.`,
23191
+ remediation: "Restart `canonry serve` so a fresh database is created and migrations re-run. Until you do, the daemon will keep serving stale data from a deleted-but-open file handle and writes will be lost.",
23192
+ details: { path: path16 }
23193
+ };
23194
+ }
23195
+ return {
23196
+ status: CheckStatuses.ok,
23197
+ code: "db.file.present",
23198
+ summary: `Database file present at \`${path16}\`.`,
23199
+ remediation: null,
23200
+ details: { path: path16 }
23201
+ };
23202
+ }
23203
+ };
23204
+ var configFilePresentCheck = {
23205
+ id: "config.file.present",
23206
+ category: CheckCategories.config,
23207
+ scope: CheckScopes.global,
23208
+ title: "Config file present",
23209
+ run: (ctx) => {
23210
+ const path16 = ctx.runtimeStatePaths?.configPath;
23211
+ if (!path16) {
23212
+ return {
23213
+ status: CheckStatuses.skipped,
23214
+ code: "config.file.path-not-wired",
23215
+ summary: "No config file path configured for this deployment.",
23216
+ remediation: null
23217
+ };
23218
+ }
23219
+ if (!fs7.existsSync(path16)) {
23220
+ return {
23221
+ status: CheckStatuses.fail,
23222
+ code: "config.file.missing",
23223
+ summary: `Config file at \`${path16}\` has been deleted while the daemon is running.`,
23224
+ remediation: "Restart `canonry serve` after the file is restored (provider keys, OAuth tokens, and integration credentials live in this file; the in-memory copy is read-only until restart).",
23225
+ details: { path: path16 }
23226
+ };
23227
+ }
23228
+ return {
23229
+ status: CheckStatuses.ok,
23230
+ code: "config.file.present",
23231
+ summary: `Config file present at \`${path16}\`.`,
23232
+ remediation: null,
23233
+ details: { path: path16 }
23234
+ };
23235
+ }
23236
+ };
23237
+ var RUNTIME_STATE_CHECKS = [
23238
+ dbFilePresentCheck,
23239
+ configFilePresentCheck
23240
+ ];
23241
+
20739
23242
  // ../api-routes/src/doctor/checks/traffic-source.ts
20740
23243
  import { and as and20, eq as eq25, gte as gte4, ne as ne4, sql as sql11 } from "drizzle-orm";
20741
23244
  var RECENT_DATA_WARN_DAYS = 7;
@@ -21041,6 +23544,9 @@ var TRAFFIC_SOURCE_CHECKS = [
21041
23544
 
21042
23545
  // ../api-routes/src/doctor/registry.ts
21043
23546
  var ALL_CHECKS = [
23547
+ // Runtime-state checks run first so file-system gone errors surface
23548
+ // before any auth/integration checks try to touch the (orphaned) DB.
23549
+ ...RUNTIME_STATE_CHECKS,
21044
23550
  ...GOOGLE_AUTH_CHECKS,
21045
23551
  ...BING_AUTH_CHECKS,
21046
23552
  ...GA_AUTH_CHECKS,
@@ -21131,7 +23637,8 @@ async function doctorRoutes(app, opts) {
21131
23637
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
21132
23638
  redirectUri,
21133
23639
  providerSummary: opts.providerSummary,
21134
- trafficSourceValidators: opts.trafficSourceValidators
23640
+ trafficSourceValidators: opts.trafficSourceValidators,
23641
+ runtimeStatePaths: opts.runtimeStatePaths
21135
23642
  };
21136
23643
  return runChecks(ctx, ALL_CHECKS, { checkIds });
21137
23644
  });
@@ -21152,7 +23659,8 @@ async function doctorRoutes(app, opts) {
21152
23659
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
21153
23660
  redirectUri,
21154
23661
  providerSummary: opts.providerSummary,
21155
- trafficSourceValidators: opts.trafficSourceValidators
23662
+ trafficSourceValidators: opts.trafficSourceValidators,
23663
+ runtimeStatePaths: opts.runtimeStatePaths
21156
23664
  };
21157
23665
  return runChecks(ctx, ALL_CHECKS, { checkIds });
21158
23666
  });
@@ -21641,6 +24149,21 @@ async function apiRoutes(app, opts) {
21641
24149
  }
21642
24150
  });
21643
24151
  });
24152
+ if (opts.runtimeStatePaths) {
24153
+ const { databasePath, configPath } = opts.runtimeStatePaths;
24154
+ const isDiagnosticUrl = (url) => url === "/health" || /\/doctor(?:\?|$)/.test(url);
24155
+ app.addHook("onRequest", async (request) => {
24156
+ if (isDiagnosticUrl(request.url)) return;
24157
+ const missing = [];
24158
+ if (!fs8.existsSync(databasePath)) missing.push(`database file \`${databasePath}\``);
24159
+ if (configPath && !fs8.existsSync(configPath)) missing.push(`config file \`${configPath}\``);
24160
+ if (missing.length === 0) return;
24161
+ throw runtimeStateMissing(
24162
+ `Runtime state missing: ${missing.join(" and ")}. Restart \`canonry serve\` so a fresh state is created (the daemon's open file handles still point at the deleted inode, so writes are being lost).`,
24163
+ { missing }
24164
+ );
24165
+ });
24166
+ }
21644
24167
  await app.register(async (api) => {
21645
24168
  if (!opts.skipAuth) {
21646
24169
  await authPlugin(api, {
@@ -21764,7 +24287,8 @@ async function apiRoutes(app, opts) {
21764
24287
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
21765
24288
  publicUrl: opts.publicUrl,
21766
24289
  providerSummary: opts.providerSummary,
21767
- trafficSourceValidators: buildTrafficSourceValidators(opts)
24290
+ trafficSourceValidators: buildTrafficSourceValidators(opts),
24291
+ runtimeStatePaths: opts.runtimeStatePaths
21768
24292
  });
21769
24293
  if (opts.registerAuthenticatedRoutes) {
21770
24294
  await opts.registerAuthenticatedRoutes(api);
@@ -21950,7 +24474,7 @@ async function withRetry(fn, options = {}) {
21950
24474
  }
21951
24475
 
21952
24476
  // ../provider-gemini/src/normalize.ts
21953
- var DEFAULT_MODEL = "gemini-3-flash";
24477
+ var DEFAULT_MODEL = "gemini-2.5-flash";
21954
24478
  function isVertexConfig(config) {
21955
24479
  return !!config.vertexProject;
21956
24480
  }
@@ -22287,15 +24811,14 @@ var geminiAdapter = {
22287
24811
  keyUrl: "https://aistudio.google.com/apikey",
22288
24812
  // Upstream model list: https://ai.google.dev/gemini-api/docs/models
22289
24813
  modelRegistry: {
22290
- defaultModel: "gemini-3-flash",
24814
+ defaultModel: "gemini-2.5-flash",
22291
24815
  validationPattern: /./,
22292
- validationHint: "any valid Google model name (e.g. gemini-3-flash, learnlm-1.5-pro-experimental)",
24816
+ validationHint: "any valid Google model name (e.g. gemini-2.5-flash, learnlm-1.5-pro-experimental)",
22293
24817
  knownModels: [
22294
- { id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
22295
- { id: "gemini-3-flash", displayName: "Gemini 3 Flash", tier: "standard" },
22296
- { id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
22297
- { id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
22298
- { id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
24818
+ { id: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", tier: "flagship" },
24819
+ { id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" },
24820
+ { id: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash-Lite", tier: "economy" },
24821
+ { id: "gemini-2.0-flash", displayName: "Gemini 2.0 Flash", tier: "standard" }
22299
24822
  ]
22300
24823
  },
22301
24824
  validateConfig(config) {
@@ -23711,12 +26234,12 @@ function sleep2(ms) {
23711
26234
  }
23712
26235
 
23713
26236
  // ../provider-cdp/src/screenshot.ts
23714
- import fs7 from "fs";
26237
+ import fs9 from "fs";
23715
26238
  import path8 from "path";
23716
26239
  async function captureElementScreenshot(client, selector, outputPath) {
23717
26240
  const dir = path8.dirname(outputPath);
23718
- if (!fs7.existsSync(dir)) {
23719
- fs7.mkdirSync(dir, { recursive: true });
26241
+ if (!fs9.existsSync(dir)) {
26242
+ fs9.mkdirSync(dir, { recursive: true });
23720
26243
  }
23721
26244
  let clip;
23722
26245
  try {
@@ -23750,7 +26273,7 @@ async function captureElementScreenshot(client, selector, outputPath) {
23750
26273
  }
23751
26274
  const { data } = await client.Page.captureScreenshot(screenshotParams);
23752
26275
  const buffer = Buffer.from(data, "base64");
23753
- fs7.writeFileSync(outputPath, buffer);
26276
+ fs9.writeFileSync(outputPath, buffer);
23754
26277
  return outputPath;
23755
26278
  }
23756
26279
 
@@ -24508,7 +27031,7 @@ function removeWordpressConnection(config, projectName) {
24508
27031
 
24509
27032
  // src/job-runner.ts
24510
27033
  import crypto25 from "crypto";
24511
- import fs8 from "fs";
27034
+ import fs10 from "fs";
24512
27035
  import path10 from "path";
24513
27036
  import os5 from "os";
24514
27037
  import { and as and22, eq as eq28, inArray as inArray10, sql as sql12 } from "drizzle-orm";
@@ -25002,12 +27525,12 @@ var JobRunner = class {
25002
27525
  allBrandNames
25003
27526
  );
25004
27527
  let screenshotRelPath = null;
25005
- if (raw.screenshotPath && fs8.existsSync(raw.screenshotPath)) {
27528
+ if (raw.screenshotPath && fs10.existsSync(raw.screenshotPath)) {
25006
27529
  const snapshotId = crypto25.randomUUID();
25007
27530
  const screenshotDir = path10.join(os5.homedir(), ".canonry", "screenshots", runId);
25008
- if (!fs8.existsSync(screenshotDir)) fs8.mkdirSync(screenshotDir, { recursive: true });
27531
+ if (!fs10.existsSync(screenshotDir)) fs10.mkdirSync(screenshotDir, { recursive: true });
25009
27532
  const destPath = path10.join(screenshotDir, `${snapshotId}.png`);
25010
- fs8.renameSync(raw.screenshotPath, destPath);
27533
+ fs10.renameSync(raw.screenshotPath, destPath);
25011
27534
  screenshotRelPath = `${runId}/${snapshotId}.png`;
25012
27535
  this.db.insert(querySnapshots).values({
25013
27536
  id: snapshotId,
@@ -26045,7 +28568,7 @@ function computeSummary(rows) {
26045
28568
 
26046
28569
  // src/backlink-extract.ts
26047
28570
  import crypto30 from "crypto";
26048
- import fs9 from "fs";
28571
+ import fs11 from "fs";
26049
28572
  import { and as and26, desc as desc16, eq as eq33 } from "drizzle-orm";
26050
28573
  var log7 = createLogger("BacklinkExtract");
26051
28574
  function defaultDeps2() {
@@ -26074,7 +28597,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
26074
28597
  if (!sync.vertexPath || !sync.edgesPath) {
26075
28598
  throw new Error(`Release ${sync.release} is missing cached file paths`);
26076
28599
  }
26077
- if (!fs9.existsSync(sync.vertexPath) || !fs9.existsSync(sync.edgesPath)) {
28600
+ if (!fs11.existsSync(sync.vertexPath) || !fs11.existsSync(sync.edgesPath)) {
26078
28601
  throw new Error(
26079
28602
  `Cache for release ${sync.release} is missing from disk (expected at ${sync.vertexPath}). The sync record exists in the database, but the ~16 GB dump was deleted or never present on this machine. Re-sync this release from the Backlinks admin page to restore the cache.`
26080
28603
  );
@@ -26465,7 +28988,7 @@ function buildDiscoveryInsightTitle(input) {
26465
28988
  }
26466
28989
 
26467
28990
  // src/commands/backfill.ts
26468
- import { and as and28, eq as eq35, inArray as inArray11 } from "drizzle-orm";
28991
+ import { and as and28, eq as eq35, inArray as inArray11, isNull, sql as sql15 } from "drizzle-orm";
26469
28992
  var SNAPSHOT_BATCH_SIZE = 500;
26470
28993
  async function backfillAnswerVisibilityCommand(opts) {
26471
28994
  const config = loadConfig();
@@ -26922,7 +29445,7 @@ function readStoredGroundingSources(rawResponse) {
26922
29445
  return result;
26923
29446
  }
26924
29447
  async function backfillInsightsCommand(project, opts) {
26925
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-XMZEWLCW.js");
29448
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-2XL2M7QP.js");
26926
29449
  const config = loadConfig();
26927
29450
  const db = createClient(config.database);
26928
29451
  migrate(db);
@@ -26970,6 +29493,247 @@ Backfill ${isDryRun ? "preview" : "complete"}.`);
26970
29493
  console.log(` No DB writes performed. Re-run without --dry-run to apply.`);
26971
29494
  }
26972
29495
  }
29496
+ function replayQueryAuditLog(events) {
29497
+ const active = [];
29498
+ for (const ev of events) {
29499
+ let diff;
29500
+ try {
29501
+ diff = ev.diff ? JSON.parse(ev.diff) : {};
29502
+ } catch {
29503
+ continue;
29504
+ }
29505
+ if (ev.action === "keywords.appended" || ev.action === "queries.appended") {
29506
+ const added = Array.isArray(diff.added) ? diff.added : [];
29507
+ for (const q of added) {
29508
+ active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
29509
+ }
29510
+ } else if (ev.action === "keywords.deleted" || ev.action === "queries.deleted") {
29511
+ const deleted = Array.isArray(diff.deleted) ? diff.deleted : [];
29512
+ for (const q of deleted) {
29513
+ for (let i = active.length - 1; i >= 0; i--) {
29514
+ if (active[i].text === q && active[i].deletedAt === null) {
29515
+ active[i].deletedAt = ev.createdAt;
29516
+ break;
29517
+ }
29518
+ }
29519
+ }
29520
+ } else if (ev.action === "queries.replaced") {
29521
+ const newSet = Array.isArray(diff.queries) ? diff.queries : [];
29522
+ for (const e of active) {
29523
+ if (e.deletedAt === null) e.deletedAt = ev.createdAt;
29524
+ }
29525
+ for (const q of newSet) {
29526
+ active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
29527
+ }
29528
+ }
29529
+ }
29530
+ return active;
29531
+ }
29532
+ function activeQueriesAt(history, t) {
29533
+ return history.filter((e) => e.addedAt <= t && (e.deletedAt === null || e.deletedAt > t)).map((e) => e.text);
29534
+ }
29535
+ var CONTENT_MATCH_STOPWORDS = /* @__PURE__ */ new Set([
29536
+ "the",
29537
+ "a",
29538
+ "an",
29539
+ "and",
29540
+ "or",
29541
+ "but",
29542
+ "is",
29543
+ "are",
29544
+ "was",
29545
+ "were",
29546
+ "be",
29547
+ "been",
29548
+ "being",
29549
+ "have",
29550
+ "has",
29551
+ "had",
29552
+ "do",
29553
+ "does",
29554
+ "did",
29555
+ "will",
29556
+ "would",
29557
+ "could",
29558
+ "should",
29559
+ "may",
29560
+ "might",
29561
+ "can",
29562
+ "this",
29563
+ "that",
29564
+ "these",
29565
+ "those",
29566
+ "i",
29567
+ "you",
29568
+ "he",
29569
+ "she",
29570
+ "it",
29571
+ "we",
29572
+ "they",
29573
+ "what",
29574
+ "which",
29575
+ "who",
29576
+ "how",
29577
+ "why",
29578
+ "when",
29579
+ "where",
29580
+ "with",
29581
+ "for",
29582
+ "from",
29583
+ "of",
29584
+ "in",
29585
+ "on",
29586
+ "at",
29587
+ "to",
29588
+ "by",
29589
+ "as",
29590
+ "best",
29591
+ "most"
29592
+ ]);
29593
+ function tokenizeForMatch(s) {
29594
+ return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3 && !CONTENT_MATCH_STOPWORDS.has(t));
29595
+ }
29596
+ function contentMatchScore(queryText, answerText) {
29597
+ const queryTokens = [...new Set(tokenizeForMatch(queryText))];
29598
+ if (queryTokens.length === 0) return 0;
29599
+ const answerHead = answerText.slice(0, 300).toLowerCase();
29600
+ const hit = queryTokens.filter((t) => answerHead.includes(t)).length;
29601
+ return hit / queryTokens.length;
29602
+ }
29603
+ async function backfillSnapshotAttributionCommand(opts) {
29604
+ const config = loadConfig();
29605
+ const db = createClient(config.database);
29606
+ migrate(db);
29607
+ const project = db.select().from(projects).where(eq35(projects.name, opts.project)).get();
29608
+ if (!project) {
29609
+ throw new Error(`Project "${opts.project}" not found`);
29610
+ }
29611
+ const isJson = opts.format === "json";
29612
+ const isDryRun = opts.dryRun === true;
29613
+ if (!isJson) {
29614
+ const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
29615
+ process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
29616
+ `);
29617
+ }
29618
+ const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and28(
29619
+ eq35(auditLog.projectId, project.id),
29620
+ inArray11(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
29621
+ )).orderBy(auditLog.createdAt).all();
29622
+ const history = replayQueryAuditLog(events);
29623
+ const orphanRuns = db.select({
29624
+ runId: runs.id,
29625
+ createdAt: runs.createdAt,
29626
+ location: runs.location
29627
+ }).from(runs).innerJoin(querySnapshots, eq35(querySnapshots.runId, runs.id)).where(and28(
29628
+ eq35(runs.projectId, project.id),
29629
+ isNull(querySnapshots.queryId),
29630
+ isNull(querySnapshots.queryText)
29631
+ )).groupBy(runs.id).orderBy(runs.createdAt).all();
29632
+ const result = {
29633
+ project: project.name,
29634
+ examinedRuns: orphanRuns.length,
29635
+ orphanSnapshots: 0,
29636
+ recoveredByPosition: 0,
29637
+ recoveredByContent: 0,
29638
+ unrecovered: 0,
29639
+ dryRun: isDryRun,
29640
+ perRun: []
29641
+ };
29642
+ const updates = [];
29643
+ for (const run of orphanRuns) {
29644
+ const activeAt = activeQueriesAt(history, run.createdAt);
29645
+ const orphanSnaps = db.select({
29646
+ id: querySnapshots.id,
29647
+ provider: querySnapshots.provider,
29648
+ createdAt: querySnapshots.createdAt,
29649
+ answerText: querySnapshots.answerText
29650
+ }).from(querySnapshots).where(and28(
29651
+ eq35(querySnapshots.runId, run.runId),
29652
+ isNull(querySnapshots.queryId),
29653
+ isNull(querySnapshots.queryText)
29654
+ )).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
29655
+ const byProvider = /* @__PURE__ */ new Map();
29656
+ for (const s of orphanSnaps) {
29657
+ const arr = byProvider.get(s.provider);
29658
+ if (arr) arr.push(s);
29659
+ else byProvider.set(s.provider, [s]);
29660
+ }
29661
+ let runPosition = 0;
29662
+ let runContent = 0;
29663
+ let runUnrecovered = 0;
29664
+ for (const [, snaps] of byProvider) {
29665
+ if (snaps.length === activeAt.length) {
29666
+ for (let i = 0; i < snaps.length; i++) {
29667
+ updates.push({ id: snaps[i].id, queryText: activeAt[i] });
29668
+ runPosition++;
29669
+ }
29670
+ } else if (snaps.length < activeAt.length) {
29671
+ const LOOKAHEAD_LIMIT = 5;
29672
+ let cidx = 0;
29673
+ for (const snap of snaps) {
29674
+ const answerText = snap.answerText ?? "";
29675
+ let matchedIdx = -1;
29676
+ for (let i = cidx; i < Math.min(activeAt.length, cidx + LOOKAHEAD_LIMIT); i++) {
29677
+ if (contentMatchScore(activeAt[i], answerText) >= 1) {
29678
+ matchedIdx = i;
29679
+ break;
29680
+ }
29681
+ }
29682
+ if (matchedIdx >= 0) {
29683
+ updates.push({ id: snap.id, queryText: activeAt[matchedIdx] });
29684
+ runContent++;
29685
+ cidx = matchedIdx + 1;
29686
+ } else {
29687
+ runUnrecovered++;
29688
+ }
29689
+ }
29690
+ } else {
29691
+ runUnrecovered += snaps.length;
29692
+ }
29693
+ }
29694
+ result.orphanSnapshots += orphanSnaps.length;
29695
+ result.recoveredByPosition += runPosition;
29696
+ result.recoveredByContent += runContent;
29697
+ result.unrecovered += runUnrecovered;
29698
+ result.perRun.push({
29699
+ runId: run.runId,
29700
+ runCreatedAt: run.createdAt,
29701
+ activeQueryCount: activeAt.length,
29702
+ orphanSnaps: orphanSnaps.length,
29703
+ recoveredPosition: runPosition,
29704
+ recoveredContent: runContent,
29705
+ unrecovered: runUnrecovered
29706
+ });
29707
+ if (!isJson) {
29708
+ process.stderr.write(
29709
+ ` ${run.createdAt} loc=${run.location ?? "-"} \u2192 ${orphanSnaps.length} orphan; ${runPosition} position-matched, ${runContent} content-matched, ${runUnrecovered} unrecovered
29710
+ `
29711
+ );
29712
+ }
29713
+ }
29714
+ if (!isDryRun && updates.length > 0) {
29715
+ db.transaction((tx) => {
29716
+ for (const u of updates) {
29717
+ tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq35(querySnapshots.id, u.id)).run();
29718
+ }
29719
+ });
29720
+ }
29721
+ if (isJson) {
29722
+ console.log(JSON.stringify(result, null, 2));
29723
+ return;
29724
+ }
29725
+ console.log(`
29726
+ Snapshot attribution ${isDryRun ? "preview" : "recovery"} complete.`);
29727
+ console.log(` Examined runs: ${result.examinedRuns}`);
29728
+ console.log(` Orphan snapshots: ${result.orphanSnapshots}`);
29729
+ console.log(` Position-matched: ${result.recoveredByPosition}`);
29730
+ console.log(` Content-matched: ${result.recoveredByContent}`);
29731
+ console.log(` Unrecovered: ${result.unrecovered}`);
29732
+ if (isDryRun) {
29733
+ console.log(`
29734
+ No DB writes performed. Re-run without --dry-run to apply.`);
29735
+ }
29736
+ }
26973
29737
  function reparseProviderSnapshot(provider, rawResponse) {
26974
29738
  const envelope = parseJsonColumn(rawResponse, {});
26975
29739
  const apiResponse = resolveStoredApiResponse(envelope);
@@ -27017,6 +29781,157 @@ function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
27017
29781
  ...apiResponse ? { apiResponse } : {}
27018
29782
  });
27019
29783
  }
29784
+ async function backfillTrafficClassificationCommand(opts) {
29785
+ const config = loadConfig();
29786
+ const db = createClient(config.database);
29787
+ migrate(db);
29788
+ const projectFilter = opts?.project?.trim();
29789
+ const isDryRun = opts?.dryRun === true;
29790
+ const isJson = opts?.format === "json";
29791
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq35(projects.name, projectFilter)).all() : db.select().from(projects).all();
29792
+ if (scopedProjects.length === 0) {
29793
+ if (projectFilter && !isJson) {
29794
+ process.stderr.write(`No project named "${projectFilter}".
29795
+ `);
29796
+ }
29797
+ if (isJson) console.log(JSON.stringify({ project: projectFilter ?? null, examined: 0, reclassified: 0 }));
29798
+ return;
29799
+ }
29800
+ if (!isJson) {
29801
+ const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
29802
+ const scope = projectFilter ? `"${projectFilter}"` : "all projects";
29803
+ process.stderr.write(`Re-classifying unknown traffic samples for ${scope}${mode}...
29804
+ `);
29805
+ }
29806
+ const projectIds = scopedProjects.map((p) => p.id);
29807
+ const result = {
29808
+ project: projectFilter ?? null,
29809
+ examined: 0,
29810
+ reclassified: 0,
29811
+ unknownBefore: 0,
29812
+ unknownAfter: 0,
29813
+ dryRun: isDryRun,
29814
+ byBot: {}
29815
+ };
29816
+ const unknownCountRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and28(
29817
+ eq35(rawEventSamples.eventType, "unknown"),
29818
+ inArray11(rawEventSamples.projectId, projectIds)
29819
+ )).get();
29820
+ result.unknownBefore = Number(unknownCountRow?.n ?? 0);
29821
+ const unknownSamples = db.select({
29822
+ id: rawEventSamples.id,
29823
+ projectId: rawEventSamples.projectId,
29824
+ sourceId: rawEventSamples.sourceId,
29825
+ ts: rawEventSamples.ts,
29826
+ userAgent: rawEventSamples.userAgent,
29827
+ pathNormalized: rawEventSamples.pathNormalized,
29828
+ status: rawEventSamples.status
29829
+ }).from(rawEventSamples).where(and28(
29830
+ eq35(rawEventSamples.eventType, "unknown"),
29831
+ inArray11(rawEventSamples.projectId, projectIds)
29832
+ )).all();
29833
+ result.examined = unknownSamples.length;
29834
+ if (unknownSamples.length === 0) {
29835
+ result.unknownAfter = result.unknownBefore;
29836
+ emitTrafficClassificationReport(result, isJson);
29837
+ return;
29838
+ }
29839
+ const now = (/* @__PURE__ */ new Date()).toISOString();
29840
+ for (const snap of unknownSamples) {
29841
+ if (!snap.userAgent) continue;
29842
+ const probe = {
29843
+ sourceType: TrafficSourceTypes["cloud-run"],
29844
+ evidenceKind: TrafficEvidenceKinds["raw-request"],
29845
+ confidence: TrafficEventConfidences.observed,
29846
+ eventId: snap.id,
29847
+ observedAt: snap.ts,
29848
+ method: "GET",
29849
+ requestUrl: `https://placeholder${snap.pathNormalized}`,
29850
+ host: "placeholder",
29851
+ path: snap.pathNormalized,
29852
+ queryString: null,
29853
+ status: snap.status ?? 200,
29854
+ userAgent: snap.userAgent,
29855
+ remoteIp: null,
29856
+ referer: null,
29857
+ latencyMs: null,
29858
+ requestSizeBytes: null,
29859
+ responseSizeBytes: null,
29860
+ providerResource: { type: "cloud_run_revision", labels: {} },
29861
+ providerLabels: {}
29862
+ };
29863
+ const classified = classifyCrawler(probe);
29864
+ if (!classified) continue;
29865
+ result.reclassified++;
29866
+ result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
29867
+ if (isDryRun) continue;
29868
+ db.update(rawEventSamples).set({ eventType: TrafficEventKinds.crawler }).where(eq35(rawEventSamples.id, snap.id)).run();
29869
+ const tsHour = new Date(snap.ts);
29870
+ tsHour.setUTCMinutes(0, 0, 0);
29871
+ db.insert(crawlerEventsHourly).values({
29872
+ projectId: snap.projectId,
29873
+ sourceId: snap.sourceId,
29874
+ tsHour: tsHour.toISOString(),
29875
+ botId: classified.botId,
29876
+ operator: classified.operator,
29877
+ verificationStatus: classified.verificationStatus,
29878
+ pathNormalized: snap.pathNormalized,
29879
+ status: snap.status ?? 200,
29880
+ hits: 1,
29881
+ sampledUserAgent: snap.userAgent,
29882
+ createdAt: now,
29883
+ updatedAt: now
29884
+ }).onConflictDoUpdate({
29885
+ target: [
29886
+ crawlerEventsHourly.projectId,
29887
+ crawlerEventsHourly.sourceId,
29888
+ crawlerEventsHourly.tsHour,
29889
+ crawlerEventsHourly.botId,
29890
+ crawlerEventsHourly.verificationStatus,
29891
+ crawlerEventsHourly.pathNormalized,
29892
+ crawlerEventsHourly.status
29893
+ ],
29894
+ set: {
29895
+ hits: sql15`${crawlerEventsHourly.hits} + 1`,
29896
+ updatedAt: now
29897
+ }
29898
+ }).run();
29899
+ }
29900
+ if (!isDryRun) {
29901
+ const afterRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and28(
29902
+ eq35(rawEventSamples.eventType, "unknown"),
29903
+ inArray11(rawEventSamples.projectId, projectIds)
29904
+ )).get();
29905
+ result.unknownAfter = Number(afterRow?.n ?? 0);
29906
+ } else {
29907
+ result.unknownAfter = result.unknownBefore - result.reclassified;
29908
+ }
29909
+ emitTrafficClassificationReport(result, isJson);
29910
+ }
29911
+ function emitTrafficClassificationReport(result, isJson) {
29912
+ if (isJson) {
29913
+ console.log(JSON.stringify(result, null, 2));
29914
+ return;
29915
+ }
29916
+ const verb = result.dryRun ? "Would re-classify" : "Re-classified";
29917
+ console.log(`
29918
+ Traffic classification ${result.dryRun ? "preview" : "recovery"} complete.`);
29919
+ console.log(` Examined unknowns: ${result.examined}`);
29920
+ console.log(` ${verb}: ${result.reclassified}`);
29921
+ console.log(` Unknown before: ${result.unknownBefore}`);
29922
+ console.log(` Unknown after: ${result.unknownAfter}`);
29923
+ if (result.reclassified > 0) {
29924
+ console.log(` By bot:`);
29925
+ const sorted = Object.entries(result.byBot).sort(([, a], [, b]) => b - a);
29926
+ for (const [bot, count] of sorted) {
29927
+ console.log(` ${count.toString().padStart(5)} ${bot}`);
29928
+ }
29929
+ }
29930
+ if (result.dryRun) {
29931
+ console.log(`
29932
+ No DB writes performed. Re-run without --dry-run to apply.`);
29933
+ }
29934
+ }
27020
29935
 
27021
29936
  // src/provider-registry.ts
27022
29937
  var ProviderRegistry = class {
@@ -27569,7 +30484,7 @@ import crypto34 from "crypto";
27569
30484
  import { eq as eq40 } from "drizzle-orm";
27570
30485
 
27571
30486
  // src/agent/session.ts
27572
- import fs12 from "fs";
30487
+ import fs14 from "fs";
27573
30488
  import path14 from "path";
27574
30489
  import { Agent } from "@mariozechner/pi-agent-core";
27575
30490
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
@@ -27710,7 +30625,7 @@ function buildAgentProvidersResponse(config) {
27710
30625
  }
27711
30626
 
27712
30627
  // src/agent/skill-paths.ts
27713
- import fs10 from "fs";
30628
+ import fs12 from "fs";
27714
30629
  import path12 from "path";
27715
30630
  import { fileURLToPath } from "url";
27716
30631
  function resolveAeroSkillDir(pkgDir) {
@@ -27721,14 +30636,14 @@ function resolveAeroSkillDir(pkgDir) {
27721
30636
  path12.join(here, "../../../../skills/aero")
27722
30637
  ];
27723
30638
  for (const candidate of candidates) {
27724
- if (fs10.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
30639
+ if (fs12.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
27725
30640
  }
27726
30641
  throw new Error(`Aero skill not found. Searched:
27727
30642
  ${candidates.join("\n ")}`);
27728
30643
  }
27729
30644
 
27730
30645
  // src/agent/skill-tools.ts
27731
- import fs11 from "fs";
30646
+ import fs13 from "fs";
27732
30647
  import path13 from "path";
27733
30648
  import { Type } from "@sinclair/typebox";
27734
30649
  var MAX_DOC_CHARS = 2e4;
@@ -27751,12 +30666,12 @@ function parseDescription(body) {
27751
30666
  }
27752
30667
  function scanSkillDocs(skillDir) {
27753
30668
  const refsDir = path13.join(skillDir ?? resolveAeroSkillDir(), "references");
27754
- if (!fs11.existsSync(refsDir)) return [];
30669
+ if (!fs13.existsSync(refsDir)) return [];
27755
30670
  const entries = [];
27756
- for (const file of fs11.readdirSync(refsDir)) {
30671
+ for (const file of fs13.readdirSync(refsDir)) {
27757
30672
  if (!file.endsWith(".md")) continue;
27758
30673
  const filePath = path13.join(refsDir, file);
27759
- const body = fs11.readFileSync(filePath, "utf-8");
30674
+ const body = fs13.readFileSync(filePath, "utf-8");
27760
30675
  entries.push({
27761
30676
  slug: file.replace(/\.md$/, ""),
27762
30677
  description: parseDescription(body),
@@ -27800,7 +30715,7 @@ function buildReadSkillDocTool() {
27800
30715
  });
27801
30716
  }
27802
30717
  const filePath = path13.join(skillDir, "references", `${match.slug}.md`);
27803
- const content = fs11.readFileSync(filePath, "utf-8");
30718
+ const content = fs13.readFileSync(filePath, "utf-8");
27804
30719
  if (content.length > MAX_DOC_CHARS) {
27805
30720
  return textResult({
27806
30721
  slug: match.slug,
@@ -27894,10 +30809,10 @@ function ensureBuiltinsRegistered() {
27894
30809
  }
27895
30810
  function loadAeroSystemPrompt(pkgDir) {
27896
30811
  const skillDir = resolveAeroSkillDir(pkgDir);
27897
- const skillBody = fs12.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
30812
+ const skillBody = fs14.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
27898
30813
  const soulPath = path14.join(skillDir, "soul.md");
27899
- if (!fs12.existsSync(soulPath)) return skillBody;
27900
- const soulBody = fs12.readFileSync(soulPath, "utf-8");
30814
+ if (!fs14.existsSync(soulPath)) return skillBody;
30815
+ const soulBody = fs14.readFileSync(soulPath, "utf-8");
27901
30816
  return `${soulBody.trimEnd()}
27902
30817
 
27903
30818
  ---
@@ -27955,7 +30870,7 @@ function resolveSessionProviderAndModel(config, opts) {
27955
30870
 
27956
30871
  // src/agent/memory-store.ts
27957
30872
  import crypto33 from "crypto";
27958
- import { and as and31, desc as desc18, eq as eq39, like as like2, sql as sql15 } from "drizzle-orm";
30873
+ import { and as and31, desc as desc18, eq as eq39, like as like2, sql as sql16 } from "drizzle-orm";
27959
30874
  var COMPACTION_KEY_PREFIX = "compaction:";
27960
30875
  var COMPACTION_NOTES_PER_SESSION = 3;
27961
30876
  function rowToDto2(row) {
@@ -28041,7 +30956,7 @@ function writeCompactionNote(db, args) {
28041
30956
  ).orderBy(desc18(agentMemory.updatedAt)).all();
28042
30957
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
28043
30958
  if (stale.length > 0) {
28044
- tx.delete(agentMemory).where(sql15`${agentMemory.id} IN (${sql15.join(stale.map((s) => sql15`${s}`), sql15`, `)})`).run();
30959
+ tx.delete(agentMemory).where(sql16`${agentMemory.id} IN (${sql16.join(stale.map((s) => sql16`${s}`), sql16`, `)})`).run();
28045
30960
  }
28046
30961
  const row = tx.select().from(agentMemory).where(and31(eq39(agentMemory.projectId, args.projectId), eq39(agentMemory.key, key))).get();
28047
30962
  if (row) inserted = rowToDto2(row);
@@ -29834,7 +32749,7 @@ async function createServer(opts) {
29834
32749
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
29835
32750
  const snapshotService = new SnapshotService(registry);
29836
32751
  const orphanedOpenClawDir = path15.join(os6.homedir(), ".openclaw-aero");
29837
- if (fs13.existsSync(orphanedOpenClawDir)) {
32752
+ if (fs15.existsSync(orphanedOpenClawDir)) {
29838
32753
  app.log.warn(
29839
32754
  { path: orphanedOpenClawDir },
29840
32755
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -30181,6 +33096,24 @@ async function createServer(opts) {
30181
33096
  sessionCookieName: SESSION_COOKIE_NAME,
30182
33097
  resolveSessionApiKeyId,
30183
33098
  explainContentRecommendation,
33099
+ // On-disk paths the daemon depends on. The api-routes plugin uses these
33100
+ // to fail loud (HTTP 503) when the operator wipes the DB or config out
33101
+ // from under a running serve — SQLite holds the inode open across
33102
+ // `unlink`, so without this the daemon keeps serving stale data from
33103
+ // an orphaned file and `rm ~/.canonry/data.db` silently does nothing.
33104
+ //
33105
+ // Only attach `configPath` if it actually exists at construction time:
33106
+ // production always boots via `serveCommand`, which calls `loadConfig()`
33107
+ // and would have thrown if the file were missing; tests that construct
33108
+ // `createServer` directly (bypassing `loadConfig`) won't have written
33109
+ // a config and shouldn't get 503s from a stub-missing file.
33110
+ runtimeStatePaths: (() => {
33111
+ const configPath = getConfigPath();
33112
+ return {
33113
+ databasePath: opts.config.database,
33114
+ configPath: fs15.existsSync(configPath) ? configPath : null
33115
+ };
33116
+ })(),
30184
33117
  // Local canonry serve runs on the operator's machine, where pointing a
30185
33118
  // webhook at localhost (Discord test container, Pipedream-mock dev server,
30186
33119
  // etc.) is a legitimate workflow. Default to allowing it for the local
@@ -30553,7 +33486,7 @@ async function createServer(opts) {
30553
33486
  });
30554
33487
  const dirname = path15.dirname(fileURLToPath2(import.meta.url));
30555
33488
  const assetsDir = path15.join(dirname, "..", "assets");
30556
- if (fs13.existsSync(assetsDir)) {
33489
+ if (fs15.existsSync(assetsDir)) {
30557
33490
  const indexPath = path15.join(assetsDir, "index.html");
30558
33491
  const injectConfig = (html) => {
30559
33492
  const clientConfig = {};
@@ -30586,8 +33519,8 @@ async function createServer(opts) {
30586
33519
  }
30587
33520
  });
30588
33521
  const serveIndex = (_request, reply) => {
30589
- if (fs13.existsSync(indexPath)) {
30590
- const html = fs13.readFileSync(indexPath, "utf-8");
33522
+ if (fs15.existsSync(indexPath)) {
33523
+ const html = fs15.readFileSync(indexPath, "utf-8");
30591
33524
  return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
30592
33525
  }
30593
33526
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -30607,8 +33540,8 @@ async function createServer(opts) {
30607
33540
  if (basePath && !url.startsWith(basePath)) {
30608
33541
  return reply.status(404).send({ error: "Not found", path: request.url });
30609
33542
  }
30610
- if (fs13.existsSync(indexPath)) {
30611
- const html = fs13.readFileSync(indexPath, "utf-8");
33543
+ if (fs15.existsSync(indexPath)) {
33544
+ const html = fs15.readFileSync(indexPath, "utf-8");
30612
33545
  return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
30613
33546
  }
30614
33547
  return reply.status(404).send({ error: "Not found" });
@@ -30696,6 +33629,8 @@ export {
30696
33629
  backfillAiReferralPathsCommand,
30697
33630
  backfillAnswerMentionsCommand,
30698
33631
  backfillInsightsCommand,
33632
+ backfillSnapshotAttributionCommand,
33633
+ backfillTrafficClassificationCommand,
30699
33634
  renderReportHtml,
30700
33635
  setGoogleAuthConfig,
30701
33636
  formatAuditFactorScore,