@canonry/canonry 7.17.1 → 7.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +8 -6
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -1
  3. package/assets/assets/{AuditHistoryPanel-CzjskfMX.js → AuditHistoryPanel-CgKpjgpq.js} +1 -1
  4. package/assets/assets/BacklinksPage-BBqbRSeG.js +1 -0
  5. package/assets/assets/Hint-DICI5Jq9.js +1 -0
  6. package/assets/assets/{HistoryPage-BZgVoYGm.js → HistoryPage-D8nQ8E8_.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-EBI5sn-D.js → MeasurementPropertyPage-CQ8lLoeA.js} +1 -1
  8. package/assets/assets/{ProjectPage-lGGjM_qj.js → ProjectPage-CP4fvluh.js} +6 -6
  9. package/assets/assets/{RunsPage-CuHi8241.js → RunsPage-D0qkTwas.js} +1 -1
  10. package/assets/assets/{SettingsPage-D0fgAoVu.js → SettingsPage-DaqR1wKC.js} +1 -1
  11. package/assets/assets/SiteHealthSection-dGXxZ8IR.js +4 -0
  12. package/assets/assets/{TrafficPage-DD65RqeR.js → TrafficPage-DodOmnVv.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-CFVCj13J.js → TrafficSourceDetailPage-C7b-BcKF.js} +1 -1
  14. package/assets/assets/{extract-error-message-D118msoE.js → extract-error-message-BYvzTsML.js} +1 -1
  15. package/assets/assets/index-BVzozur0.js +86 -0
  16. package/assets/assets/{react-sigma_core.esm.min-DdlHLMEz.js → react-sigma_core.esm.min-DXcq1O0r.js} +1 -1
  17. package/assets/assets/{v2-overview-adapter-CoCb2aB0.js → v2-overview-adapter-1MHnrdnr.js} +1 -1
  18. package/assets/assets/{vendor-lucide-N6vOpZmh.js → vendor-lucide-Y6jANGfG.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-ORA5R7QM.js → chunk-5M2RY5WU.js} +132 -104
  21. package/dist/{chunk-ZQ7C6IFA.js → chunk-COQOQYRZ.js} +749 -614
  22. package/dist/{chunk-FK7AWV57.js → chunk-EAAM47H6.js} +24 -9
  23. package/dist/{chunk-PLS4EM2Y.js → chunk-JMC7CEJB.js} +4 -4
  24. package/dist/{chunk-OPJCFDNR.js → chunk-JUOFNJLV.js} +3 -3
  25. package/dist/{chunk-C6GWKYGE.js → chunk-U7D7LNVJ.js} +5 -4
  26. package/dist/{chunk-MHB23E3N.js → chunk-X2S7D3MN.js} +123 -82
  27. package/dist/cli.js +30 -22
  28. package/dist/{demo-server-WQDHQG6P.js → demo-server-XWRIXME5.js} +6 -5
  29. package/dist/index.d.ts +7 -5
  30. package/dist/index.js +6 -6
  31. package/dist/{intelligence-service-JWZRZ6JZ.js → intelligence-service-332S7D7Y.js} +2 -2
  32. package/dist/mcp.js +4 -4
  33. package/package.json +12 -12
  34. package/assets/assets/BacklinksPage-ToENGBdp.js +0 -1
  35. package/assets/assets/SiteHealthSection-DhLxldgs.js +0 -4
  36. package/assets/assets/index-DxFd1t4G.js +0 -86
@@ -86,6 +86,7 @@ import {
86
86
  LOGIN_FAILED_MESSAGE,
87
87
  MEASUREMENT_CHANGES_DEFAULT_SORT,
88
88
  MEASUREMENT_CHANGES_NOISE_ANSWERS,
89
+ MEASUREMENT_DEFAULT_QUERY_CLASS,
89
90
  MEASUREMENT_DRAFT_MAX_ASSIGNMENTS,
90
91
  MEASUREMENT_DRAFT_MAX_ASSIGNMENTS_PER_ACTION,
91
92
  MEASUREMENT_DRAFT_MAX_AUTHORING_BYTES,
@@ -303,6 +304,7 @@ import {
303
304
  coerceSkillManifest,
304
305
  compactDateToIso,
305
306
  compareSemver,
307
+ compareText,
306
308
  competitorAliasBlockRequestSchema,
307
309
  competitorAliasProjectIdentity,
308
310
  competitorAliasesRequestSchema,
@@ -376,6 +378,7 @@ import {
376
378
  emptyCitationVisibility,
377
379
  emptySentimentCounts,
378
380
  escapeLikePattern,
381
+ escapeRegExp,
379
382
  evaluateModelPointerExposure,
380
383
  expandQueryTemplate,
381
384
  expandResearchTemplate,
@@ -493,6 +496,7 @@ import {
493
496
  isoDateDaysBeforeInTimeZone,
494
497
  keywordDtoSchema,
495
498
  keywordGenerateRequestSchema,
499
+ keywordGenerateResponseSchema,
496
500
  latestProjectRunDtoSchema,
497
501
  linearTrend,
498
502
  locationContextSchema,
@@ -577,6 +581,7 @@ import {
577
581
  measurementPropertyEvidenceResponseSchema,
578
582
  measurementPropertyQuestionsQuerySchema,
579
583
  measurementPropertyQuestionsResponseSchema,
584
+ measurementQueryClassFilterSchema,
580
585
  measurementQuerySetDetailSchema,
581
586
  measurementQuerySetListResponseSchema,
582
587
  measurementQuerySetUpsertRequestSchema,
@@ -604,6 +609,7 @@ import {
604
609
  normalizeCompetitorAliases,
605
610
  normalizeCompetitorDomain,
606
611
  normalizeIdTokens,
612
+ normalizeIdentityText,
607
613
  normalizeMeasurementExactUrl,
608
614
  normalizeMeasurementExecutionQueryText,
609
615
  normalizeMeasurementHost,
@@ -643,8 +649,10 @@ import {
643
649
  planCompetitorAliases,
644
650
  planCompetitorAutoAliases,
645
651
  prepareBrandMatchText,
652
+ projectConfigExportSchema,
646
653
  projectConfigSchema,
647
654
  projectCreateRequestSchema,
655
+ projectDefaultLocationResponseSchema,
648
656
  projectDtoSchema,
649
657
  projectOverviewDtoSchema,
650
658
  projectSearchResponseSchema,
@@ -656,6 +664,7 @@ import {
656
664
  providersFailing,
657
665
  queryDtoSchema,
658
666
  queryGenerateRequestSchema,
667
+ queryGenerateResponseSchema,
659
668
  queryTrackingCommitRequestSchema,
660
669
  queryTrackingCommitResponseSchema,
661
670
  queryTrackingPreviewRequestSchema,
@@ -777,6 +786,7 @@ import {
777
786
  snapshotReportSchema,
778
787
  snapshotRequestInputSchema,
779
788
  snapshotRequestSchema,
789
+ sortedUnique,
780
790
  sourceBreakdownDtoSchema,
781
791
  sourceBreakdownQuerySchema,
782
792
  splitList,
@@ -833,12 +843,12 @@ import {
833
843
  wordpressManualAssistDtoSchema,
834
844
  wordpressOnboardResultDtoSchema,
835
845
  wordpressPageDetailDtoSchema,
836
- wordpressPageSummaryDtoSchema,
846
+ wordpressPageListDtoSchema,
837
847
  wordpressSchemaBlockDtoSchema,
838
848
  wordpressSchemaDeployResultDtoSchema,
839
849
  wordpressSchemaStatusResultDtoSchema,
840
850
  wordpressStatusDtoSchema
841
- } from "./chunk-MHB23E3N.js";
851
+ } from "./chunk-X2S7D3MN.js";
842
852
 
843
853
  // src/intelligence-service.ts
844
854
  import { eq as eq89, desc as desc41, asc as asc18, and as and75, ne as ne16, or as or22, inArray as inArray36, gte as gte24, lte as lte20, isNull as isNull17, sql as sql32, exists as exists2 } from "drizzle-orm";
@@ -1040,7 +1050,7 @@ var projects = sqliteTable("projects", {
1040
1050
  locations: text("locations", { mode: "json" }).$type().notNull().default([]),
1041
1051
  defaultLocation: text("default_location"),
1042
1052
  autoExtractBacklinks: integer("auto_extract_backlinks", { mode: "boolean" }).notNull().default(false),
1043
- configSource: text("config_source").notNull().default("cli"),
1053
+ configSource: text("config_source").$type().notNull().default("cli"),
1044
1054
  configRevision: integer("config_revision").notNull().default(1),
1045
1055
  icpDescription: text("icp_description"),
1046
1056
  createdAt: text("created_at").notNull(),
@@ -9682,11 +9692,13 @@ var SentimentRepository = class {
9682
9692
  }
9683
9693
  };
9684
9694
 
9685
- // ../intelligence/src/regressions.ts
9695
+ // ../intelligence/src/snapshot-key.ts
9686
9696
  function snapshotKey(snap) {
9687
9697
  const loc = snap.location ?? "__none__";
9688
9698
  return JSON.stringify([snap.query, snap.provider, loc]);
9689
9699
  }
9700
+
9701
+ // ../intelligence/src/regressions.ts
9690
9702
  function detectRegressions(currentRun, previousRun) {
9691
9703
  if ((currentRun.location ?? null) !== (previousRun.location ?? null)) {
9692
9704
  return [];
@@ -9726,24 +9738,20 @@ function observedKeys(run, keyOf) {
9726
9738
  }
9727
9739
 
9728
9740
  // ../intelligence/src/gains.ts
9729
- function snapshotKey2(snap) {
9730
- const loc = snap.location ?? "__none__";
9731
- return JSON.stringify([snap.query, snap.provider, loc]);
9732
- }
9733
9741
  function detectGains(currentRun, previousRun) {
9734
9742
  if ((currentRun.location ?? null) !== (previousRun.location ?? null)) {
9735
9743
  return [];
9736
9744
  }
9737
9745
  const gains = [];
9738
- const previousObserved = observedKeys(previousRun, snapshotKey2);
9746
+ const previousObserved = observedKeys(previousRun, snapshotKey);
9739
9747
  const previousCited = /* @__PURE__ */ new Set();
9740
9748
  for (const snap of previousRun.snapshots) {
9741
9749
  if (snap.cited) {
9742
- previousCited.add(snapshotKey2(snap));
9750
+ previousCited.add(snapshotKey(snap));
9743
9751
  }
9744
9752
  }
9745
9753
  for (const snap of currentRun.snapshots) {
9746
- const key = snapshotKey2(snap);
9754
+ const key = snapshotKey(snap);
9747
9755
  if (snap.cited && previousObserved.has(key) && !previousCited.has(key)) {
9748
9756
  gains.push({
9749
9757
  query: snap.query,
@@ -12451,6 +12459,11 @@ var FORWARDED_HEADERS = ["x-forwarded-for", "forwarded", "x-real-ip"];
12451
12459
  function hasForwardedHeaders(request) {
12452
12460
  return FORWARDED_HEADERS.some((header2) => request.headers[header2] !== void 0);
12453
12461
  }
12462
+ var PROXY_PATH_HEADERS = ["via", "cf-connecting-ip", "true-client-ip"];
12463
+ var FORWARDED_HEADER_PREFIX = "x-forwarded-";
12464
+ function hasProxyHeaders(request) {
12465
+ return hasForwardedHeaders(request) || PROXY_PATH_HEADERS.some((header2) => request.headers[header2] !== void 0) || Object.keys(request.headers).some((header2) => header2.startsWith(FORWARDED_HEADER_PREFIX));
12466
+ }
12454
12467
  function resolveCallerKey(request, trustProxyConfigured) {
12455
12468
  const socketAddress = request.socket.remoteAddress ?? request.ip;
12456
12469
  if (!trustProxyConfigured) return socketAddress;
@@ -14418,8 +14431,7 @@ function rowKey(row) {
14418
14431
  return { queryText: row.queryText, queryId: row.queryId, executionNodeKey: row.executionNodeKey };
14419
14432
  }
14420
14433
  function compareRows(left, right) {
14421
- const order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
14422
- return left.queryText.localeCompare(right.queryText, "en") || order(left.queryText, right.queryText) || order(left.queryId, right.queryId) || order(left.executionNodeKey ?? "", right.executionNodeKey ?? "");
14434
+ return left.queryText.localeCompare(right.queryText, "en") || compareText(left.queryText, right.queryText) || compareText(left.queryId, right.queryId) || compareText(left.executionNodeKey ?? "", right.executionNodeKey ?? "");
14423
14435
  }
14424
14436
  function withheldCounts(counts2) {
14425
14437
  return Object.fromEntries(Object.entries(counts2).map(([key, value]) => [key, OPERATIONAL_OUTCOMES.has(key) ? value : 0]));
@@ -14987,9 +14999,6 @@ import { and as and7, desc as desc4, eq as eq12, exists, inArray as inArray4, is
14987
14999
  import crypto7 from "crypto";
14988
15000
  import { and as and6, eq as eq11, gt as gt2, lte as lte4 } from "drizzle-orm";
14989
15001
  var MEASUREMENT_RECEIPT_TTL_MS = 24 * 60 * 60 * 1e3;
14990
- function compareText(left, right) {
14991
- return left < right ? -1 : left > right ? 1 : 0;
14992
- }
14993
15002
  function canonicalJsonValue(value) {
14994
15003
  if (Array.isArray(value)) return value.map(canonicalJsonValue);
14995
15004
  if (value && typeof value === "object") {
@@ -15957,10 +15966,6 @@ import { and as and11, desc as desc7, eq as eq17, gt as gt3, inArray as inArray6
15957
15966
  import { and as and9, desc as desc5, eq as eq15, gte as gte2, inArray as inArray5, isNull as isNull4, lt as lt4, lte as lte5, ne as ne4 } from "drizzle-orm";
15958
15967
 
15959
15968
  // ../api-routes/src/measurement-report.ts
15960
- var compareText2 = (left, right) => left < right ? -1 : left > right ? 1 : 0;
15961
- function sortedUnique(values) {
15962
- return [...new Set(values)].sort(compareText2);
15963
- }
15964
15969
  function normalizedHost(value) {
15965
15970
  try {
15966
15971
  return normalizeMeasurementHost(value);
@@ -16042,7 +16047,7 @@ function classifySourceAttribution(value, routes, normalizedOwnedHosts) {
16042
16047
  const claim = routeClaim(source, route);
16043
16048
  if (claim) claims.push(claim);
16044
16049
  }
16045
- claims.sort((left, right) => right.modeRank - left.modeRank || right.pathLength - left.pathLength || compareText2(left.targetId, right.targetId) || compareText2(left.urlId, right.urlId));
16050
+ claims.sort((left, right) => right.modeRank - left.modeRank || right.pathLength - left.pathLength || compareText(left.targetId, right.targetId) || compareText(left.urlId, right.urlId));
16046
16051
  const best = claims.at(0);
16047
16052
  if (!best) {
16048
16053
  return {
@@ -16083,8 +16088,7 @@ function classifySourceForUsageEdge(source, usageEdge) {
16083
16088
  }
16084
16089
  function normalizeMeasurementLocation(value) {
16085
16090
  if (value === null) return null;
16086
- const normalized = value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
16087
- return normalized || null;
16091
+ return normalizeIdentityText(value) || null;
16088
16092
  }
16089
16093
  function words(value) {
16090
16094
  return value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
@@ -16142,7 +16146,7 @@ function mentionedTargetsForAliases(answerText, aliases, preparedWords) {
16142
16146
  }
16143
16147
  function identityAmbiguityPatterns(aliases) {
16144
16148
  const patterns = aliases.flatMap((alias2) => {
16145
- const name = words(alias2).join(" ").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16149
+ const name = escapeRegExp(words(alias2).join(" "));
16146
16150
  if (!name) return [];
16147
16151
  return [{
16148
16152
  clarification: new RegExp(`(?:^| )which (?:specific )?${name} (?:(?:(?:do|did) )?you mean|(?:(?:are|were) you|you(?: re| are| were)?) (?:asking about|referring to|talking about))(?= |$)`, "u"),
@@ -16158,7 +16162,15 @@ function identityAmbiguityPatterns(aliases) {
16158
16162
  multipleEntities: patterns.flatMap((pattern) => pattern.multipleEntities)
16159
16163
  };
16160
16164
  }
16161
- function resolveMentionIdentity(text2, targets, aliases, citedTargetIds, ambiguityPatterns = new Map(targets.map((target) => [target.id, identityAmbiguityPatterns(target.aliases)]))) {
16165
+ var NO_TARGETS = /* @__PURE__ */ new Set();
16166
+ function queryNamedTargetIds(queryText, brandedTargetIds, aliases) {
16167
+ if (!queryText || brandedTargetIds === void 0) return NO_TARGETS;
16168
+ const branded = new Set(brandedTargetIds);
16169
+ if (branded.size === 0) return NO_TARGETS;
16170
+ const named = [...mentionedTargetsForAliases(queryText, aliases).mentioned].filter((id) => branded.has(id));
16171
+ return named.length === 0 ? NO_TARGETS : new Set(named);
16172
+ }
16173
+ function resolveMentionIdentity(text2, targets, aliases, citedTargetIds, ambiguityPatterns = new Map(targets.map((target) => [target.id, identityAmbiguityPatterns(target.aliases)])), queryNamed = NO_TARGETS) {
16162
16174
  const textWords = text2 === null ? [] : words(text2);
16163
16175
  const state = mentionedTargetsForAliases(text2, aliases, textWords);
16164
16176
  if (text2 === null) return state;
@@ -16166,8 +16178,10 @@ function resolveMentionIdentity(text2, targets, aliases, citedTargetIds, ambigui
16166
16178
  for (const target of targets) {
16167
16179
  if (!state.mentioned.has(target.id)) continue;
16168
16180
  const patterns = ambiguityPatterns.get(target.id);
16169
- const qualified = citedTargetIds.has(target.id) || (target.identityAliases?.length ?? 0) > 0 && containsAnyAlias(text2, target.identityAliases);
16170
- const unresolved = patterns?.clarification.some((pattern) => pattern.test(normalized)) || !qualified && (patterns?.multipleEntities.some((pattern) => pattern.test(normalized)) || (target.identityAliases?.length ?? 0) > 0);
16181
+ const cited = citedTargetIds.has(target.id);
16182
+ const qualified = cited || (target.identityAliases?.length ?? 0) > 0 && containsAnyAlias(text2, target.identityAliases);
16183
+ const clarified = cited || queryNamed.has(target.id);
16184
+ const unresolved = !clarified && patterns?.clarification.some((pattern) => pattern.test(normalized)) || !qualified && (patterns?.multipleEntities.some((pattern) => pattern.test(normalized)) || (target.identityAliases?.length ?? 0) > 0);
16171
16185
  if (unresolved) {
16172
16186
  state.mentioned.delete(target.id);
16173
16187
  state.unknown.add(target.id);
@@ -16182,13 +16196,14 @@ function createTargetMentionReader(targets, ownedHosts = []) {
16182
16196
  const ambiguityPatterns = new Map(targets.map((target) => [target.id, identityAmbiguityPatterns(target.aliases)]));
16183
16197
  const routes = compiledTargetRoutes(targets);
16184
16198
  const normalizedOwnedHosts = ownedHosts.map(normalizedHost);
16185
- return (answerText, targetIds, citedUrls = []) => {
16199
+ return (answerText, targetIds, citedUrls = [], query = null) => {
16186
16200
  if (answerText === null || targetIds.length === 0) return null;
16187
16201
  const citedTargetIds = new Set(citedUrls.flatMap((url) => {
16188
16202
  const source = classifySourceAttribution(url, routes, normalizedOwnedHosts);
16189
16203
  return source.classification === "matched" ? source.matchedTargetIds : [];
16190
16204
  }));
16191
- const state = resolveMentionIdentity(answerProseForMentions(answerText), targets, aliases, citedTargetIds, ambiguityPatterns);
16205
+ const queryNamed = query === null ? NO_TARGETS : queryNamedTargetIds(query.text, query.brandedTargetIds.filter((id) => targetIds.includes(id)), aliases);
16206
+ const state = resolveMentionIdentity(answerProseForMentions(answerText), targets, aliases, citedTargetIds, ambiguityPatterns, queryNamed);
16192
16207
  if (targetIds.some((id) => state.mentioned.has(id))) return true;
16193
16208
  return targetIds.some((id) => !mentionableIds.has(id) || state.unknown.has(id)) ? null : false;
16194
16209
  };
@@ -16245,7 +16260,7 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16245
16260
  const observationsBySlot = /* @__PURE__ */ new Map();
16246
16261
  const historical = /* @__PURE__ */ new Set();
16247
16262
  const evidenceIncomplete = /* @__PURE__ */ new Set();
16248
- for (const slot of [...input.expectedSlots].sort((left, right) => compareText2(left.id, right.id))) {
16263
+ for (const slot of [...input.expectedSlots].sort((left, right) => compareText(left.id, right.id))) {
16249
16264
  const rows = candidates.get(slot.id) ?? [];
16250
16265
  if (rows.length > 1) {
16251
16266
  for (const row of rows) ambiguous.add(row.id);
@@ -16291,17 +16306,33 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16291
16306
  const mentionableIds = new Set(
16292
16307
  mentionAliases.map((alias2) => alias2.targetId)
16293
16308
  );
16309
+ const queryNamedByExecution = /* @__PURE__ */ new Map();
16310
+ const queryNamedFor = (slot) => {
16311
+ const cached = queryNamedByExecution.get(slot.executionId);
16312
+ if (cached !== void 0) return cached;
16313
+ const branded = (edgesByExecution.get(slot.executionId) ?? []).flatMap((edge) => edge.type === "target" && edge.queryClass === "branded" ? [edge.targetId] : []);
16314
+ const named = queryNamedTargetIds(slot.queryText, branded, aliasesByFirstWord);
16315
+ queryNamedByExecution.set(slot.executionId, named);
16316
+ return named;
16317
+ };
16294
16318
  for (const observation of observationsBySlot.values()) {
16295
16319
  const citedTargetIds = new Set(observation.sourceUrls.flatMap((url) => {
16296
16320
  const source = sourceAttribution(url);
16297
16321
  return source.classification === "matched" ? source.matchedTargetIds : [];
16298
16322
  }));
16299
- const mentions = resolveMentionIdentity(observation.mentionText, input.targets, aliasesByFirstWord, citedTargetIds, ambiguityPatterns);
16323
+ const mentions = resolveMentionIdentity(
16324
+ observation.mentionText,
16325
+ input.targets,
16326
+ aliasesByFirstWord,
16327
+ citedTargetIds,
16328
+ ambiguityPatterns,
16329
+ queryNamedFor(observation.slot)
16330
+ );
16300
16331
  observation.mentionedTargetIds = mentions.mentioned;
16301
16332
  observation.unknownMentionTargetIds = mentions.unknown;
16302
16333
  observation.citedTargetIds = citedTargetIds;
16303
16334
  if (!detailedEvidence) continue;
16304
- const edges = [...edgesByExecution.get(observation.slot.executionId) ?? []].sort((left, right) => compareText2(left.id, right.id));
16335
+ const edges = [...edgesByExecution.get(observation.slot.executionId) ?? []].sort((left, right) => compareText(left.id, right.id));
16305
16336
  for (const edge of edges) {
16306
16337
  const sources = observation.sourceUrls.map((sourceUrl) => {
16307
16338
  const { classification, normalizedUrl, matchedTargetIds, matchedUrlIds } = classifySourceForUsageEdge(sourceAttribution(sourceUrl), edge);
@@ -16336,7 +16367,7 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16336
16367
  });
16337
16368
  }
16338
16369
  }
16339
- answers.sort((left, right) => compareText2(left.expectedSlotId, right.expectedSlotId) || compareText2(left.usageEdgeId, right.usageEdgeId));
16370
+ answers.sort((left, right) => compareText(left.expectedSlotId, right.expectedSlotId) || compareText(left.usageEdgeId, right.usageEdgeId));
16340
16371
  const evidence = answers.flatMap((answer) => answer.sources.map((source) => ({
16341
16372
  observationId: answer.observationId,
16342
16373
  expectedSlotId: answer.expectedSlotId,
@@ -16355,7 +16386,7 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16355
16386
  matchedTargetIds: source.matchedTargetIds,
16356
16387
  matchedUrlIds: source.matchedUrlIds
16357
16388
  })));
16358
- evidence.sort((left, right) => compareText2(left.expectedSlotId, right.expectedSlotId) || compareText2(left.usageEdgeId, right.usageEdgeId) || compareText2(left.sourceUrl, right.sourceUrl) || compareText2(left.classification, right.classification));
16389
+ evidence.sort((left, right) => compareText(left.expectedSlotId, right.expectedSlotId) || compareText(left.usageEdgeId, right.usageEdgeId) || compareText(left.sourceUrl, right.sourceUrl) || compareText(left.classification, right.classification));
16359
16390
  options.onSourceAttributionComputed?.(sourceAttributions.size);
16360
16391
  return {
16361
16392
  observationsBySlot,
@@ -16387,7 +16418,7 @@ function slotsForEdges(expectedSlots, edges) {
16387
16418
  for (const slot of expectedSlots) {
16388
16419
  if (executionIds.has(slot.executionId)) slots.set(slot.id, slot);
16389
16420
  }
16390
- return [...slots.values()].sort((left, right) => compareText2(left.id, right.id));
16421
+ return [...slots.values()].sort((left, right) => compareText(left.id, right.id));
16391
16422
  }
16392
16423
  function completeness(slots, prepared) {
16393
16424
  const observations = slots.flatMap((slot) => {
@@ -16450,7 +16481,7 @@ function providersFor(slots) {
16450
16481
  return sortedUnique(slots.map((slot) => slot.provider));
16451
16482
  }
16452
16483
  function buildSovForSlots(slots, competitors2, projectBrandNames, projectDomain, prepared) {
16453
- const sortedCompetitors = [...competitors2].sort((left, right) => compareText2(left.domain, right.domain));
16484
+ const sortedCompetitors = [...competitors2].sort((left, right) => compareText(left.domain, right.domain));
16454
16485
  const rows = [
16455
16486
  { domain: projectDomain, own: true, aliases: projectBrandNames },
16456
16487
  ...sortedCompetitors.map((competitor) => ({
@@ -16614,7 +16645,7 @@ function indexedSlotsForEdges(edges, indexes) {
16614
16645
  for (const edge of edges) {
16615
16646
  for (const slot of indexes.slotsByExecutionId.get(edge.executionId) ?? []) slots.set(slot.id, slot);
16616
16647
  }
16617
- return [...slots.values()].sort((left, right) => compareText2(left.id, right.id));
16648
+ return [...slots.values()].sort((left, right) => compareText(left.id, right.id));
16618
16649
  }
16619
16650
  function indexedAnsweredSlots(slots, indexes) {
16620
16651
  return slots.filter((slot) => indexes.answeredSlotIds.has(slot.id));
@@ -16778,22 +16809,19 @@ function buildMeasurementReport(input, options = {}) {
16778
16809
  const prepared = prepareReport(input, options);
16779
16810
  return {
16780
16811
  revision: input.revision,
16781
- groups: [...input.groups].sort((left, right) => compareText2(left.id, right.id)).map((group) => buildGroupReport(group, input, prepared)),
16782
- targets: [...input.targets].sort((left, right) => compareText2(left.id, right.id)).map((target) => buildTargetReport(target, input, prepared)),
16812
+ groups: [...input.groups].sort((left, right) => compareText(left.id, right.id)).map((group) => buildGroupReport(group, input, prepared)),
16813
+ targets: [...input.targets].sort((left, right) => compareText(left.id, right.id)).map((target) => buildTargetReport(target, input, prepared)),
16783
16814
  evidence: prepared.evidence,
16784
16815
  diagnostics: prepared.diagnostics
16785
16816
  };
16786
16817
  }
16787
16818
 
16788
16819
  // ../api-routes/src/measurement-report-adapter.ts
16789
- function compareText3(left, right) {
16790
- return left < right ? -1 : left > right ? 1 : 0;
16791
- }
16792
16820
  function normalizedProviders(values) {
16793
16821
  const providers = values.map((value) => value.trim().toLocaleLowerCase("en"));
16794
16822
  if (providers.some((value) => !value)) throw new Error("measurement manifest provider must be non-empty");
16795
16823
  if (new Set(providers).size !== providers.length) throw new Error("measurement manifest providers must be unique");
16796
- return providers.sort(compareText3);
16824
+ return providers.sort(compareText);
16797
16825
  }
16798
16826
  function canonicalContext(value) {
16799
16827
  if (value === null) return "null";
@@ -16826,7 +16854,7 @@ function frozenExecutionNodes(plan) {
16826
16854
  }
16827
16855
  function manifestFromNodes(nodes, providersFor2) {
16828
16856
  const expectedSlots = [];
16829
- for (const node of [...nodes].sort((left, right) => compareText3(left.stableKey, right.stableKey))) {
16857
+ for (const node of [...nodes].sort((left, right) => compareText(left.stableKey, right.stableKey))) {
16830
16858
  const providers = providersFor2(node);
16831
16859
  if (node.expectedSnapshots !== providers.length) {
16832
16860
  throw new Error(`measurement manifest provider roster does not satisfy execution ${node.stableKey}`);
@@ -17069,9 +17097,10 @@ function buildMeasurementPlanV2ReportInput(revision, plan, manifest, snapshots)
17069
17097
  }
17070
17098
  };
17071
17099
  }
17072
- function responseFromReport(revision, report, run) {
17100
+ function responseFromReport(revision, report, run, queryClass) {
17073
17101
  return {
17074
17102
  revision,
17103
+ queryClass,
17075
17104
  run,
17076
17105
  groups: report.groups,
17077
17106
  targets: report.targets,
@@ -17134,31 +17163,38 @@ function measurementRunExpectedSlots(run, plan) {
17134
17163
  if (run.measurementManifest === null) manifestFailure(`missing for run ${run.id}`);
17135
17164
  return parseManifest(run.measurementManifest, frozenExecutionNodes(plan), run.measurementScope === null);
17136
17165
  }
17137
- function storedMeasurementPlanV2Report(db, projectId, version, plan, runId) {
17166
+ function storedMeasurementPlanV2Report(db, projectId, version, plan, runId, queryClass) {
17138
17167
  const run = runId ? pinnedMeasurementRun(db, projectId, version.id, runId, { exactVersion: true }) : latestMeasurementRun(db, projectId, version.id, [RunStatuses.completed, RunStatuses.partial], {}, { exactVersion: true });
17139
17168
  if (!run) {
17140
17169
  const empty = buildMeasurementPlanV2ReportInput(version.revision, plan, { schemaVersion: 1, expectedSlots: [] }, []);
17141
17170
  return {
17142
17171
  kind: "no-population",
17143
17172
  reason: "no-run",
17144
- report: responseFromReport(version.revision, buildMeasurementReport(empty.input), null)
17173
+ report: responseFromReport(version.revision, buildMeasurementReport(classInput(empty, queryClass)), null, queryClass)
17145
17174
  };
17146
17175
  }
17147
17176
  const manifest = measurementRunExpectedSlots(run, plan);
17148
17177
  const snapshots = db.select().from(querySnapshots).where(eq15(querySnapshots.runId, run.id)).all();
17149
- const { input } = buildMeasurementPlanV2ReportInput(version.revision, plan, manifest, snapshots);
17178
+ const materialized = buildMeasurementPlanV2ReportInput(version.revision, plan, manifest, snapshots);
17150
17179
  return {
17151
17180
  kind: "report",
17152
- report: responseFromReport(version.revision, buildMeasurementReport(input), {
17181
+ report: responseFromReport(version.revision, buildMeasurementReport(classInput(materialized, queryClass)), {
17153
17182
  id: run.id,
17154
17183
  status: run.status === RunStatuses.completed ? RunStatuses.completed : RunStatuses.partial,
17155
17184
  createdAt: run.createdAt,
17156
17185
  startedAt: run.startedAt,
17157
17186
  finishedAt: run.finishedAt
17158
- })
17187
+ }, queryClass)
17159
17188
  };
17160
17189
  }
17161
- function buildStoredMeasurementReport(db, projectId, revision, runId) {
17190
+ function classInput(materialized, queryClass) {
17191
+ if (queryClass === "all") return materialized.input;
17192
+ return {
17193
+ ...materialized.input,
17194
+ usageEdges: materialized.input.usageEdges.filter((edge) => materialized.edgeQueryClass.get(edge.id) === queryClass)
17195
+ };
17196
+ }
17197
+ function buildStoredMeasurementReport(db, projectId, revision, runId, queryClass) {
17162
17198
  const version = db.select().from(measurementPlanVersions).where(and9(
17163
17199
  eq15(measurementPlanVersions.projectId, projectId),
17164
17200
  eq15(measurementPlanVersions.revision, revision)
@@ -17166,7 +17202,12 @@ function buildStoredMeasurementReport(db, projectId, revision, runId) {
17166
17202
  if (!version) return { kind: "no-plan", revision };
17167
17203
  const stored = parseStoredMeasurementPlanAnyVersion(version.canonicalJson);
17168
17204
  if (stored.schemaVersion === MEASUREMENT_PLAN_V2_SCHEMA_VERSION) {
17169
- return storedMeasurementPlanV2Report(db, projectId, version, stored, runId);
17205
+ return storedMeasurementPlanV2Report(db, projectId, version, stored, runId, queryClass ?? MEASUREMENT_DEFAULT_QUERY_CLASS);
17206
+ }
17207
+ if (queryClass !== void 0 && queryClass !== "all") {
17208
+ throw validationError(
17209
+ `Revision ${revision} is a schema v1 plan, which records no query class. Omit queryClass to read every answer.`
17210
+ );
17170
17211
  }
17171
17212
  const plan = stored;
17172
17213
  const run = runId ? pinnedMeasurementRun(db, projectId, version.id, runId, { exactVersion: true }) : db.select().from(runs).where(and9(
@@ -17193,7 +17234,7 @@ function buildStoredMeasurementReport(db, projectId, revision, runId) {
17193
17234
  )).orderBy(desc5(runs.createdAt), desc5(runs.id)).get();
17194
17235
  if (!selectedRun) {
17195
17236
  const report2 = buildMeasurementReport(reportInput(version.revision, plan, { schemaVersion: 1, expectedSlots: [] }, [], false));
17196
- return { kind: "no-population", reason: "no-run", report: responseFromReport(version.revision, report2, null) };
17237
+ return { kind: "no-population", reason: "no-run", report: responseFromReport(version.revision, report2, null, null) };
17197
17238
  }
17198
17239
  const legacySnapshots = db.select().from(querySnapshots).where(eq15(querySnapshots.runId, selectedRun.id)).all();
17199
17240
  const providers = [...new Set(legacySnapshots.map((snapshot) => snapshot.provider.trim().toLocaleLowerCase("en")).filter(Boolean))];
@@ -17201,12 +17242,12 @@ function buildStoredMeasurementReport(db, projectId, revision, runId) {
17201
17242
  manifest = buildMeasurementRunManifest(plan, providers);
17202
17243
  } catch {
17203
17244
  const report2 = buildMeasurementReport(reportInput(version.revision, plan, { schemaVersion: 1, expectedSlots: [] }, [], false));
17204
- return { kind: "no-population", reason: "no-run", report: responseFromReport(version.revision, report2, null) };
17245
+ return { kind: "no-population", reason: "no-run", report: responseFromReport(version.revision, report2, null, null) };
17205
17246
  }
17206
17247
  legacy = true;
17207
17248
  } else {
17208
17249
  const report2 = buildMeasurementReport(reportInput(version.revision, plan, { schemaVersion: 1, expectedSlots: [] }, [], false));
17209
- return { kind: "no-population", reason: "no-run", report: responseFromReport(version.revision, report2, null) };
17250
+ return { kind: "no-population", reason: "no-run", report: responseFromReport(version.revision, report2, null, null) };
17210
17251
  }
17211
17252
  const snapshots = db.select().from(querySnapshots).where(eq15(querySnapshots.runId, selectedRun.id)).all();
17212
17253
  const report = buildMeasurementReport(reportInput(version.revision, plan, manifest, snapshots, legacy));
@@ -17218,7 +17259,7 @@ function buildStoredMeasurementReport(db, projectId, revision, runId) {
17218
17259
  createdAt: selectedRun.createdAt,
17219
17260
  startedAt: selectedRun.startedAt,
17220
17261
  finishedAt: selectedRun.finishedAt
17221
- })
17262
+ }, null)
17222
17263
  };
17223
17264
  }
17224
17265
 
@@ -17324,9 +17365,6 @@ function expectedSlotsFor(nodes, providers, models) {
17324
17365
  ...models[provider] ? { requestedModel: models[provider] } : {}
17325
17366
  })));
17326
17367
  }
17327
- function compareText4(left, right) {
17328
- return left < right ? -1 : left > right ? 1 : 0;
17329
- }
17330
17368
  function normalizedLocationIdentity(value) {
17331
17369
  if (!value) return null;
17332
17370
  return [value.country, value.region, value.city, value.label, value.timezone ?? ""].map((part) => part.trim().toLocaleLowerCase("en")).join("\0");
@@ -17353,7 +17391,7 @@ function materializeV2ExecutionNodes(nodes, instanceModels) {
17353
17391
  const answering = /* @__PURE__ */ new Map();
17354
17392
  const retired = /* @__PURE__ */ new Set();
17355
17393
  const claimed = /* @__PURE__ */ new Set();
17356
- for (const node of [...nodes].sort((left, right) => compareText4(left.stableKey, right.stableKey))) {
17394
+ for (const node of [...nodes].sort((left, right) => compareText(left.stableKey, right.stableKey))) {
17357
17395
  const nodeProviders = normalizeProviders(node.context.providers);
17358
17396
  const declared = nodeProviderModels(node);
17359
17397
  const resolved = /* @__PURE__ */ new Map();
@@ -17385,11 +17423,11 @@ function materializeV2ExecutionNodes(nodes, instanceModels) {
17385
17423
  }
17386
17424
  return {
17387
17425
  expectedSlots,
17388
- providers: [...providers].sort(compareText4),
17426
+ providers: [...providers].sort(compareText),
17389
17427
  models: Object.fromEntries([...models].flatMap(([provider, model]) => {
17390
17428
  if (model) return [[provider, model]];
17391
17429
  const current = answering.get(provider);
17392
- return retired.has(provider) && current ? [[provider, [...current].sort(compareText4).join(" + ")]] : [];
17430
+ return retired.has(provider) && current ? [[provider, [...current].sort(compareText).join(" + ")]] : [];
17393
17431
  }))
17394
17432
  };
17395
17433
  }
@@ -17405,12 +17443,9 @@ function sliceForV2(plan, params) {
17405
17443
  function quotedList(values) {
17406
17444
  return values.map((value) => `"${value}"`).join(", ");
17407
17445
  }
17408
- function sortedUnique2(values) {
17409
- return [...new Set(values)].sort(compareText4);
17410
- }
17411
17446
  function resolveV2RunScope(plan, scope) {
17412
- const requestedGroups = sortedUnique2(scope.groups ?? []);
17413
- const requestedTargets = sortedUnique2(scope.targets ?? []);
17447
+ const requestedGroups = sortedUnique(scope.groups ?? []);
17448
+ const requestedTargets = sortedUnique(scope.targets ?? []);
17414
17449
  const groupsByKey = new Map(plan.groups.map((group) => [group.stableKey, group]));
17415
17450
  const targetKeys = new Set(plan.targets.map((target) => target.stableKey));
17416
17451
  const unknownGroups = requestedGroups.filter((key) => !groupsByKey.has(key));
@@ -17429,7 +17464,7 @@ function resolveV2RunScope(plan, scope) {
17429
17464
  for (const key of requestedGroups) {
17430
17465
  for (const targetKey of groupsByKey.get(key).targetKeys) selected.add(targetKey);
17431
17466
  }
17432
- const resolvedTargets = [...selected].sort(compareText4);
17467
+ const resolvedTargets = [...selected].sort(compareText);
17433
17468
  const usedNodeKeys = new Set(plan.usageEdges.filter((edge) => selected.has(edge.targetKey)).map((edge) => edge.executionNodeKey));
17434
17469
  const executionNodes = plan.executionNodes.filter((node) => usedNodeKeys.has(node.stableKey));
17435
17470
  if (executionNodes.length === 0) {
@@ -17444,7 +17479,7 @@ function resolveV2RunScope(plan, scope) {
17444
17479
  };
17445
17480
  }
17446
17481
  function resolveV2QueryScope(plan, queryTexts) {
17447
- const requested = sortedUnique2(queryTexts.map(normalizeMeasurementExecutionQueryText).filter(Boolean));
17482
+ const requested = sortedUnique(queryTexts.map(normalizeMeasurementExecutionQueryText).filter(Boolean));
17448
17483
  const measured = new Set(plan.executionNodes.map((node) => normalizeMeasurementExecutionQueryText(node.queryText)));
17449
17484
  const unknown = requested.filter((text2) => !measured.has(text2));
17450
17485
  if (unknown.length) {
@@ -19708,6 +19743,9 @@ var CURRENT_RUN_STATUSES = [
19708
19743
  RunStatuses.partial,
19709
19744
  RunStatuses.failed
19710
19745
  ];
19746
+ function servedQueryClass(query) {
19747
+ return query.queryClass ?? MEASUREMENT_DEFAULT_QUERY_CLASS;
19748
+ }
19711
19749
  var MEASUREMENT_OVERVIEW_CACHE_MAX_ENTRIES = 32;
19712
19750
  function cacheKeyOf(key) {
19713
19751
  return JSON.stringify([
@@ -19750,6 +19788,9 @@ function parseOverviewQuery(raw) {
19750
19788
  };
19751
19789
  const parsed = measurementOverviewQuerySchema.safeParse(candidate);
19752
19790
  if (!parsed.success) throw validationError("Invalid measurement overview query", { issues: parsed.error.issues });
19791
+ if (parsed.data.marketKey !== void 0 && parsed.data.scope !== "market") {
19792
+ throw validationError(`"marketKey" is accepted only when scope is "market"; scope "${parsed.data.scope}" does not narrow to a market.`);
19793
+ }
19753
19794
  return parsed.data;
19754
19795
  }
19755
19796
  function activeMeasurementPlan(db, projectId) {
@@ -19804,20 +19845,18 @@ function countMetric(rate2) {
19804
19845
  if (rate2.numerator === null) return unavailable2(metricReason(rate2.reason));
19805
19846
  return { state: "available", value: rate2.numerator, numerator: rate2.numerator, denominator: rate2.denominator };
19806
19847
  }
19807
- function normalizedText(value) {
19808
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
19809
- }
19810
19848
  function overviewFilterFingerprint(query) {
19811
19849
  const filters = {
19812
19850
  scope: query.scope,
19813
19851
  groupKey: query.groupKey ?? null,
19852
+ marketKey: query.marketKey ?? null,
19814
19853
  targetKey: query.targetKey ?? null,
19815
- queryClass: query.queryClass ?? "all",
19816
- provider: query.provider === void 0 ? null : normalizedText(query.provider),
19817
- location: query.location === void 0 ? null : normalizedText(query.location),
19854
+ queryClass: servedQueryClass(query),
19855
+ provider: query.provider === void 0 ? null : normalizeIdentityText(query.provider),
19856
+ location: query.location === void 0 ? null : normalizeIdentityText(query.location),
19818
19857
  from: query.from ?? null,
19819
19858
  to: query.to ?? null,
19820
- search: query.search === void 0 ? null : normalizedText(query.search),
19859
+ search: query.search === void 0 ? null : normalizeIdentityText(query.search),
19821
19860
  compact: query.compact ?? false
19822
19861
  };
19823
19862
  return createHash4("sha256").update(JSON.stringify(filters)).digest("base64url");
@@ -19826,29 +19865,30 @@ function overviewAggregateFingerprint(query) {
19826
19865
  const filters = {
19827
19866
  scope: query.scope,
19828
19867
  groupKey: query.groupKey ?? null,
19868
+ marketKey: query.marketKey ?? null,
19829
19869
  targetKey: query.targetKey ?? null,
19830
- queryClass: query.queryClass ?? "all",
19831
- provider: query.provider === void 0 ? null : normalizedText(query.provider),
19832
- location: query.location === void 0 ? null : normalizedText(query.location),
19870
+ queryClass: servedQueryClass(query),
19871
+ provider: query.provider === void 0 ? null : normalizeIdentityText(query.provider),
19872
+ location: query.location === void 0 ? null : normalizeIdentityText(query.location),
19833
19873
  from: query.from ?? null,
19834
19874
  to: query.to ?? null
19835
19875
  };
19836
19876
  return createHash4("sha256").update(JSON.stringify(filters)).digest("base64url");
19837
19877
  }
19838
19878
  function compareLabels(left, right) {
19839
- const leftLabel = normalizedText(left.label);
19840
- const rightLabel = normalizedText(right.label);
19879
+ const leftLabel = normalizeIdentityText(left.label);
19880
+ const rightLabel = normalizeIdentityText(right.label);
19841
19881
  if (leftLabel !== rightLabel) return leftLabel < rightLabel ? -1 : 1;
19842
- return left.targetKey < right.targetKey ? -1 : left.targetKey > right.targetKey ? 1 : 0;
19882
+ return compareText(left.targetKey, right.targetKey);
19843
19883
  }
19844
19884
  function compareLabelSort(left, right, descending) {
19845
- const leftLabel = normalizedText(left.label);
19846
- const rightLabel = normalizedText(right.label);
19885
+ const leftLabel = normalizeIdentityText(left.label);
19886
+ const rightLabel = normalizeIdentityText(right.label);
19847
19887
  if (leftLabel !== rightLabel) {
19848
19888
  const compared = leftLabel < rightLabel ? -1 : 1;
19849
19889
  return descending ? -compared : compared;
19850
19890
  }
19851
- return left.targetKey < right.targetKey ? -1 : left.targetKey > right.targetKey ? 1 : 0;
19891
+ return compareText(left.targetKey, right.targetKey);
19852
19892
  }
19853
19893
  function metricForSort(row, sort) {
19854
19894
  switch (sort) {
@@ -19877,7 +19917,7 @@ function compareRows2(left, right, sort) {
19877
19917
  return compareLabels(left, right);
19878
19918
  }
19879
19919
  function legacyCursorOf(row) {
19880
- return Buffer.from(`${normalizedText(row.label)}:${row.targetKey}`, "utf8").toString("base64url");
19920
+ return Buffer.from(`${normalizeIdentityText(row.label)}:${row.targetKey}`, "utf8").toString("base64url");
19881
19921
  }
19882
19922
  function cursorOf(row, sort, displayedRunId, filterFingerprint2, planVersionId, evidenceFingerprint) {
19883
19923
  const cursor = {
@@ -20071,6 +20111,7 @@ function resolveScope(plan, query) {
20071
20111
  group: v2GroupOf(plan, groupKey)
20072
20112
  };
20073
20113
  }
20114
+ if (query.scope === "market") return resolveMarketScope(plan, query.marketKey);
20074
20115
  if (query.scope === "property") {
20075
20116
  if (query.targetKey === void 0) throw validationError('"targetKey" is required when scope is "property".');
20076
20117
  const target = plan.targets.find((candidate) => candidate.stableKey === query.targetKey);
@@ -20079,6 +20120,24 @@ function resolveScope(plan, query) {
20079
20120
  }
20080
20121
  return { kind: "all", label: "All Properties", targetKeys: plan.targets.map((target) => target.stableKey), group: null };
20081
20122
  }
20123
+ function resolveMarketScope(plan, marketKey) {
20124
+ if (marketKey === void 0) throw validationError('"marketKey" is required when scope is "market".');
20125
+ const notInRevision = () => validationError(`Measurement market "${marketKey}" is not in the active revision.`);
20126
+ if (plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) throw notInRevision();
20127
+ const market = plan.reportingScopes?.find((candidate) => candidate.stableKey === marketKey);
20128
+ if (!market) throw notInRevision();
20129
+ const members = new Set(market.usageEdges.map(measurementV2UsageEdgeKey));
20130
+ const edges = plan.usageEdges.filter((edge) => members.has(measurementV2UsageEdgeKey(edge)));
20131
+ const targetKeys = new Set(edges.map((edge) => edge.targetKey));
20132
+ return {
20133
+ kind: "market",
20134
+ key: market.stableKey,
20135
+ label: market.label,
20136
+ targetKeys: plan.targets.map((target) => target.stableKey).filter((targetKey) => targetKeys.has(targetKey)),
20137
+ group: null,
20138
+ edgeIds: new Set(edges.map(v2UsageEdgeId))
20139
+ };
20140
+ }
20082
20141
  function namedIdentitiesFor(plan, scope, queryClass) {
20083
20142
  if (scope.kind !== "group" || queryClass !== "non-brand" || scope.group === null) return [];
20084
20143
  if (scope.group.competitors.length === 0) return [];
@@ -20116,12 +20175,9 @@ function planExpectedSlots(plan) {
20116
20175
  }
20117
20176
  function matchesSearch(row, search) {
20118
20177
  if (search === void 0) return true;
20119
- const needle = normalizedText(search);
20178
+ const needle = normalizeIdentityText(search);
20120
20179
  if (needle === "") return true;
20121
- return normalizedText(row.label).includes(needle) || normalizedText(row.targetKey).includes(needle);
20122
- }
20123
- function compareGroupText(left, right) {
20124
- return left < right ? -1 : left > right ? 1 : 0;
20180
+ return normalizeIdentityText(row.label).includes(needle) || normalizeIdentityText(row.targetKey).includes(needle);
20125
20181
  }
20126
20182
  function propertyLocations(plan) {
20127
20183
  const groupsByKey = new Map(plan.groups.map((group) => [group.stableKey, group]));
@@ -20138,7 +20194,7 @@ function propertyLocations(plan) {
20138
20194
  }
20139
20195
  return levels;
20140
20196
  };
20141
- const byLabel = (left, right) => compareGroupText(left.label, right.label) || compareGroupText(left.stableKey, right.stableKey);
20197
+ const byLabel = (left, right) => compareText(left.label, right.label) || compareText(left.stableKey, right.stableKey);
20142
20198
  const metros = /* @__PURE__ */ new Map();
20143
20199
  const submarkets = /* @__PURE__ */ new Map();
20144
20200
  const push = (map, key, value) => {
@@ -20199,7 +20255,7 @@ function planV1Overview(db, projectId, active, query, scope) {
20199
20255
  return {
20200
20256
  mode: "active-v1",
20201
20257
  scope: scopeDto(scope),
20202
- queryClass: query.queryClass ?? "all",
20258
+ queryClass: servedQueryClass(query),
20203
20259
  measurement: {
20204
20260
  state: displayed ? displayedState(displayed.status) : "not_measured",
20205
20261
  ...displayed ? { currentRunId: displayed.id, displayedRunId: displayed.id } : {},
@@ -20220,7 +20276,7 @@ function planV1Overview(db, projectId, active, query, scope) {
20220
20276
  };
20221
20277
  }
20222
20278
  function planV2Overview(db, projectId, active, plan, query, scope, cache) {
20223
- const queryClass = query.queryClass ?? "all";
20279
+ const queryClass = servedQueryClass(query);
20224
20280
  const displayed = selectDisplayedRun(db, projectId, active, query);
20225
20281
  const current = latestMeasurementRun(db, projectId, active.version.id, CURRENT_RUN_STATUSES);
20226
20282
  const currentDto = current ? { currentRunId: current.id } : {};
@@ -20275,8 +20331,8 @@ function planV2Overview(db, projectId, active, plan, query, scope, cache) {
20275
20331
  }, () => {
20276
20332
  const manifest = measurementRunExpectedSlots(displayed, plan);
20277
20333
  const { input, edgeQueryClass } = buildMeasurementPlanV2ReportInput(active.version.revision, plan, manifest, snapshots);
20278
- const expectedSlots = input.expectedSlots.filter((slot) => (query.provider === void 0 || slot.provider === normalizedText(query.provider)) && (query.location === void 0 || normalizedText(slot.location ?? "") === normalizedText(query.location)));
20279
- const usageEdges = queryClass === "all" ? input.usageEdges : input.usageEdges.filter((edge) => edgeQueryClass.get(edge.id) === queryClass);
20334
+ const expectedSlots = input.expectedSlots.filter((slot) => (query.provider === void 0 || slot.provider === normalizeIdentityText(query.provider)) && (query.location === void 0 || normalizeIdentityText(slot.location ?? "") === normalizeIdentityText(query.location)));
20335
+ const usageEdges = input.usageEdges.filter((edge) => (queryClass === "all" || edgeQueryClass.get(edge.id) === queryClass) && (scope.edgeIds === void 0 || scope.edgeIds.has(edge.id)));
20280
20336
  return buildMeasurementOverview({
20281
20337
  ...input,
20282
20338
  expectedSlots,
@@ -20514,7 +20570,7 @@ function readCompetitorIdentityChangedAt(db, projectId) {
20514
20570
  return renamed !== null && renamed > audited ? renamed : audited;
20515
20571
  }
20516
20572
  function pooledRunIds(pooled) {
20517
- return [...pooled].sort((left, right) => left.createdAt === right.createdAt ? compareStoredIds(right.id, left.id) : compareStoredIds(right.createdAt, left.createdAt)).slice(0, POOLED_RUN_ID_LIMIT).map((run) => run.id);
20573
+ return [...pooled].sort((left, right) => left.createdAt === right.createdAt ? compareText(right.id, left.id) : compareText(right.createdAt, left.createdAt)).slice(0, POOLED_RUN_ID_LIMIT).map((run) => run.id);
20518
20574
  }
20519
20575
  function readCompetitorLandscape(app, projectName, query, selection) {
20520
20576
  const project = resolveProject(app.db, projectName);
@@ -20666,7 +20722,7 @@ function readCompetitorLandscape(app, projectName, query, selection) {
20666
20722
  else excluded.nonCompleted++;
20667
20723
  excludedByModel.set(key, excluded);
20668
20724
  }
20669
- const sortedGroups = [...modelGroups.values()].sort((left, right) => compareStoredIds(left.provider, right.provider) || (left.model === null ? right.model === null ? 0 : -1 : right.model === null ? 1 : compareStoredIds(left.model, right.model)));
20725
+ const sortedGroups = [...modelGroups.values()].sort((left, right) => compareText(left.provider, right.provider) || (left.model === null ? right.model === null ? 0 : -1 : right.model === null ? 1 : compareText(left.model, right.model)));
20670
20726
  const groups = sortedGroups.slice(0, COMPETITOR_LANDSCAPE_MODEL_GROUP_LIMIT).map((group) => {
20671
20727
  const groupHistory = buildHistory(group.snapshots);
20672
20728
  const groupObserved = groupHistory.observed.slice(0, COMPETITOR_LANDSCAPE_RANKED_ROW_LIMIT);
@@ -20823,12 +20879,23 @@ function scopeForFrozenPlan(plan, kind, groupKey) {
20823
20879
  ids.add(edge.targetKey);
20824
20880
  targetIdsByExecution.set(edge.executionNodeKey, ids);
20825
20881
  }
20882
+ const queryTextByExecution = new Map(plan.executionNodes.map((node) => [node.stableKey, node.queryText]));
20883
+ const brandedTargetIdsByExecution = /* @__PURE__ */ new Map();
20884
+ for (const assignment of plan.assignments) {
20885
+ if (assignment.queryClass !== "branded") continue;
20886
+ const ids = brandedTargetIdsByExecution.get(assignment.executionNodeKey) ?? [];
20887
+ ids.push(assignment.targetKey);
20888
+ brandedTargetIdsByExecution.set(assignment.executionNodeKey, ids);
20889
+ }
20826
20890
  return {
20827
20891
  executionNodeKeys,
20828
20892
  queryClassesByExecution,
20829
20893
  mentioned: (answerText, executionId, citedUrls) => {
20830
20894
  const ids = executionId === null ? [] : [...targetIdsByExecution.get(executionId) ?? []];
20831
- return readMention(answerText, ids, citedUrls);
20895
+ const queryText = executionId === null ? void 0 : queryTextByExecution.get(executionId);
20896
+ const brandedTargetIds = executionId === null ? void 0 : brandedTargetIdsByExecution.get(executionId);
20897
+ const query = queryText === void 0 || brandedTargetIds === void 0 ? null : { text: queryText, brandedTargetIds };
20898
+ return readMention(answerText, ids, citedUrls, query);
20832
20899
  },
20833
20900
  competitors: mergePins(groups.flatMap((group) => group.competitors.map((competitor) => ({
20834
20901
  domain: competitor.domain,
@@ -20878,9 +20945,6 @@ function snapshotMatchesFilters(input) {
20878
20945
  function requestedModel(model) {
20879
20946
  return model?.trim() || null;
20880
20947
  }
20881
- function compareStoredIds(left, right) {
20882
- return left < right ? -1 : left > right ? 1 : 0;
20883
- }
20884
20948
  function normalizedDomain(value) {
20885
20949
  return registrableDomain(value) || hostOf(value);
20886
20950
  }
@@ -21853,8 +21917,8 @@ function hash(value) {
21853
21917
  function answerIdentityFrom(project, competitorRows2, marketNames = /* @__PURE__ */ new Map()) {
21854
21918
  const projectDomains = effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains });
21855
21919
  const projectBrandNames = effectiveBrandNames({ displayName: project.displayName, aliases: project.aliases });
21856
- const names = competitorRows2.map((row) => [row.domain, competitorIdentityAliases(row)]).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
21857
- const market = [...marketNames].map(([domain, learned]) => [domain, [...learned]]).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
21920
+ const names = competitorRows2.map((row) => [row.domain, competitorIdentityAliases(row)]).sort(([left], [right]) => compareText(left, right));
21921
+ const market = [...marketNames].map(([domain, learned]) => [domain, [...learned]]).sort(([left], [right]) => compareText(left, right));
21858
21922
  return {
21859
21923
  projectDomains,
21860
21924
  projectBrandNames,
@@ -22304,12 +22368,6 @@ var MeasurementDiscoveryConfigurationError = class extends Error {
22304
22368
  this.name = "MeasurementDiscoveryConfigurationError";
22305
22369
  }
22306
22370
  };
22307
- function compareText5(a, b) {
22308
- return a < b ? -1 : a > b ? 1 : 0;
22309
- }
22310
- function sortedUnique3(values) {
22311
- return [...new Set(values)].sort(compareText5);
22312
- }
22313
22371
  function normalizeDeclaredHost(value, label) {
22314
22372
  const normalized = value.trim().toLowerCase().replace(/\.$/, "");
22315
22373
  if (!normalized || normalized.includes("://") || normalized.includes("/") || normalized.includes("@")) {
@@ -22374,7 +22432,7 @@ function normalizePatternValue(value, label) {
22374
22432
  return normalized;
22375
22433
  }
22376
22434
  function compileRules(input) {
22377
- const ownedHosts = sortedUnique3(input.ownedHosts.map((host, index2) => normalizeDeclaredHost(host, `ownedHosts[${index2}]`)));
22435
+ const ownedHosts = sortedUnique(input.ownedHosts.map((host, index2) => normalizeDeclaredHost(host, `ownedHosts[${index2}]`)));
22378
22436
  if (ownedHosts.length === 0) {
22379
22437
  throw new MeasurementDiscoveryConfigurationError("ownedHosts must contain at least one hostname");
22380
22438
  }
@@ -22385,7 +22443,7 @@ function compileRules(input) {
22385
22443
  if (aliases.some((alias2) => alias2.host === primary.host)) {
22386
22444
  throw new MeasurementDiscoveryConfigurationError("alias rules must use another owned host");
22387
22445
  }
22388
- const excludedSlugSuffixes = sortedUnique3(
22446
+ const excludedSlugSuffixes = sortedUnique(
22389
22447
  (input.rules.excludedSlugSuffixes ?? []).map(
22390
22448
  (value, index2) => normalizePatternValue(value, `rules.excludedSlugSuffixes[${index2}]`)
22391
22449
  )
@@ -22393,7 +22451,7 @@ function compileRules(input) {
22393
22451
  const excludedSlugPatterns = (input.rules.excludedSlugPatterns ?? []).map((pattern, index2) => ({
22394
22452
  kind: pattern.kind,
22395
22453
  value: normalizePatternValue(pattern.value, `rules.excludedSlugPatterns[${index2}].value`)
22396
- })).sort((a, b) => compareText5(`${a.kind}:${a.value}`, `${b.kind}:${b.value}`));
22454
+ })).sort((a, b) => compareText(`${a.kind}:${a.value}`, `${b.kind}:${b.value}`));
22397
22455
  return { ownedHosts, rules: { primary, aliases, excludedSlugSuffixes, excludedSlugPatterns } };
22398
22456
  }
22399
22457
  function parseSitemapUrl(value) {
@@ -22473,10 +22531,10 @@ function classifyMeasurementSitemapUrls(input) {
22473
22531
  const shared = [];
22474
22532
  const unmatched = [];
22475
22533
  const excluded = [];
22476
- const canonicalEntries = [...canonicalGroups.entries()].sort(([a], [b]) => compareText5(a, b));
22534
+ const canonicalEntries = [...canonicalGroups.entries()].sort(([a], [b]) => compareText(a, b));
22477
22535
  let classifiedUrlCount = 0;
22478
22536
  for (const [canonicalUrl, group] of canonicalEntries) {
22479
- const orderedGroup = [...group].sort((a, b) => compareText5(a.sourceUrl, b.sourceUrl));
22537
+ const orderedGroup = [...group].sort((a, b) => compareText(a.sourceUrl, b.sourceUrl));
22480
22538
  const representative = orderedGroup[0];
22481
22539
  for (let index2 = 1; index2 < orderedGroup.length; index2 += 1) {
22482
22540
  diagnostics.push({
@@ -22534,7 +22592,7 @@ function classifyMeasurementSitemapUrls(input) {
22534
22592
  else unmatched.push(item(canonicalUrl, "unmatched", "unmatched-path"));
22535
22593
  }
22536
22594
  const proposed = [...candidatesBySlug.values()].map(({ slug, stableKey, url }) => {
22537
- const aliasCoverageUrls = sortedUnique3((aliasesBySlug.get(slug) ?? []).map((match) => match.url));
22595
+ const aliasCoverageUrls = sortedUnique((aliasesBySlug.get(slug) ?? []).map((match) => match.url));
22538
22596
  return {
22539
22597
  classification: "proposed",
22540
22598
  reason: "primary-match",
@@ -22544,7 +22602,7 @@ function classifyMeasurementSitemapUrls(input) {
22544
22602
  primaryUrl: url,
22545
22603
  aliasCoverageUrls
22546
22604
  };
22547
- }).sort((a, b) => compareText5(a.stableKey, b.stableKey));
22605
+ }).sort((a, b) => compareText(a.stableKey, b.stableKey));
22548
22606
  const aliases = [];
22549
22607
  for (const [slug, matches] of aliasesBySlug) {
22550
22608
  const target = candidatesBySlug.get(slug);
@@ -22562,14 +22620,14 @@ function classifyMeasurementSitemapUrls(input) {
22562
22620
  }
22563
22621
  for (const match of matches) unmatched.push(item(match.url, "unmatched", "alias-without-primary"));
22564
22622
  }
22565
- const sortItems = (items) => items.sort((a, b) => compareText5(a.url, b.url));
22623
+ const sortItems = (items) => items.sort((a, b) => compareText(a.url, b.url));
22566
22624
  return {
22567
22625
  proposed,
22568
- aliases: aliases.sort((a, b) => compareText5(a.url, b.url)),
22626
+ aliases: aliases.sort((a, b) => compareText(a.url, b.url)),
22569
22627
  shared: sortItems(shared),
22570
22628
  unmatched: sortItems(unmatched),
22571
22629
  excluded: sortItems(excluded),
22572
- diagnostics: diagnostics.sort((a, b) => compareText5(`${a.kind}:${a.url}`, `${b.kind}:${b.url}`))
22630
+ diagnostics: diagnostics.sort((a, b) => compareText(`${a.kind}:${a.url}`, `${b.kind}:${b.url}`))
22573
22631
  };
22574
22632
  }
22575
22633
 
@@ -22580,7 +22638,117 @@ import { gunzip } from "zlib";
22580
22638
  import { promisify as promisify2 } from "util";
22581
22639
  import http from "http";
22582
22640
  import https from "https";
22641
+ import net2 from "net";
22642
+
22643
+ // ../api-routes/src/egress-policy.ts
22583
22644
  import net from "net";
22645
+ function blockedAddressReason(value, options = {}) {
22646
+ const address = stripZoneId(stripIpv6Brackets(value));
22647
+ if (options.allowLoopback && isLoopbackAddress(address)) return null;
22648
+ const family = net.isIP(address);
22649
+ if (family === 4) return blockedIpv4Reason(ipv4Octets(address));
22650
+ if (family === 6) {
22651
+ const bytes = ipv6Bytes(address);
22652
+ return bytes ? blockedIpv6Reason(bytes) : "which could not be read as an address";
22653
+ }
22654
+ return "which is not an IP address";
22655
+ }
22656
+ function isLoopbackAddress(value) {
22657
+ const address = stripZoneId(stripIpv6Brackets(value));
22658
+ const family = net.isIP(address);
22659
+ if (family === 4) return ipv4Octets(address)[0] === 127;
22660
+ if (family !== 6) return false;
22661
+ const bytes = ipv6Bytes(address);
22662
+ if (!bytes) return false;
22663
+ const mapped = mappedIpv4(bytes);
22664
+ if (mapped) return mapped[0] === 127;
22665
+ return bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;
22666
+ }
22667
+ function stripIpv6Brackets(value) {
22668
+ return value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
22669
+ }
22670
+ function stripZoneId(address) {
22671
+ return address.split("%")[0];
22672
+ }
22673
+ function ipv4Octets(address) {
22674
+ return address.split(".").map((part) => Number.parseInt(part, 10));
22675
+ }
22676
+ function blockedIpv4Reason(octets) {
22677
+ const [first, second] = octets;
22678
+ if (first === 0) return "which is unspecified space";
22679
+ if (first === 127) return "which is loopback";
22680
+ if (first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168) {
22681
+ return "which is private space";
22682
+ }
22683
+ if (first === 100 && second >= 64 && second <= 127) return "which is carrier-grade NAT space";
22684
+ if (first === 169 && second === 254) return "which is link-local";
22685
+ if (first >= 224 && first <= 239) return "which is multicast";
22686
+ if (first === 192 && second === 0 && octets[2] === 0 || first === 198 && (second === 18 || second === 19) || first >= 240) {
22687
+ return "which is reserved";
22688
+ }
22689
+ return null;
22690
+ }
22691
+ function blockedIpv6Reason(bytes) {
22692
+ const first = bytes[0];
22693
+ if (first === 0) {
22694
+ const nat64 = nat64Ipv4(bytes);
22695
+ if (nat64) return blockedIpv4Reason(nat64) === null ? null : `which is the NAT64 form of ${nat64.join(".")}`;
22696
+ const mapped = mappedIpv4(bytes);
22697
+ return mapped ? `which is the IPv6 form of ${mapped.join(".")}` : "which is reserved IPv6 space";
22698
+ }
22699
+ if (first === 1 && bytes.slice(1, 8).every((byte) => byte === 0)) return "which is reserved IPv6 space";
22700
+ if (first === 255) return "which is multicast";
22701
+ if ((first & 254) === 252) return "which is unique-local";
22702
+ if (first === 254 && (bytes[1] & 192) === 128) return "which is link-local";
22703
+ if (first === 254 && (bytes[1] & 192) === 192) return "which is reserved site-local space";
22704
+ if (first === 32 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0) return "which is a Teredo tunnel";
22705
+ if (first === 32 && bytes[1] === 2) return "which is a 6to4 tunnel";
22706
+ if (first === 32 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 2 && bytes[4] === 0 && bytes[5] === 0) {
22707
+ return "which is reserved IPv6 space";
22708
+ }
22709
+ return null;
22710
+ }
22711
+ function mappedIpv4(bytes) {
22712
+ const isMapped = bytes.slice(0, 10).every((byte) => byte === 0) && bytes[10] === 255 && bytes[11] === 255;
22713
+ return isMapped ? [bytes[12], bytes[13], bytes[14], bytes[15]] : null;
22714
+ }
22715
+ var NAT64_WELL_KNOWN_PREFIX = [0, 100, 255, 155, 0, 0, 0, 0, 0, 0, 0, 0];
22716
+ function nat64Ipv4(bytes) {
22717
+ const isNat64 = NAT64_WELL_KNOWN_PREFIX.every((byte, index2) => bytes[index2] === byte);
22718
+ return isNat64 ? [bytes[12], bytes[13], bytes[14], bytes[15]] : null;
22719
+ }
22720
+ function ipv6Bytes(address) {
22721
+ if (!net.isIPv6(address)) return null;
22722
+ const halves = address.split("::");
22723
+ if (halves.length > 2) return null;
22724
+ const expand = (text2) => {
22725
+ if (text2 === "") return [];
22726
+ const groups2 = [];
22727
+ const chunks = text2.split(":");
22728
+ for (const [index2, chunk] of chunks.entries()) {
22729
+ if (index2 === chunks.length - 1 && chunk.includes(".")) {
22730
+ const octets = ipv4Octets(chunk);
22731
+ groups2.push(octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]);
22732
+ continue;
22733
+ }
22734
+ groups2.push(Number.parseInt(chunk, 16));
22735
+ }
22736
+ return groups2;
22737
+ };
22738
+ const head = expand(halves[0]);
22739
+ const tail = halves.length === 2 ? expand(halves[1]) : [];
22740
+ const missing = 8 - head.length - tail.length;
22741
+ if (halves.length === 1 ? missing !== 0 : missing < 0) return null;
22742
+ const groups = [...head, ...new Array(halves.length === 2 ? missing : 0).fill(0), ...tail];
22743
+ const bytes = new Uint8Array(16);
22744
+ for (const [index2, group] of groups.entries()) {
22745
+ bytes[index2 * 2] = group >> 8;
22746
+ bytes[index2 * 2 + 1] = group & 255;
22747
+ }
22748
+ return bytes;
22749
+ }
22750
+
22751
+ // ../api-routes/src/measurement-sitemap-fetch.ts
22584
22752
  var gunzipAsync = promisify2(gunzip);
22585
22753
  var DEFAULT_MEASUREMENT_SITEMAP_LIMITS = {
22586
22754
  timeoutMs: 1e4,
@@ -22630,7 +22798,7 @@ async function fetchMeasurementSitemap(sitemapUrl, options = {}) {
22630
22798
  }
22631
22799
  }
22632
22800
  await visit(sitemapUrl, 0);
22633
- return { urls: [...urls].sort(compareText6), fetchedSitemaps: visited.size, bytesChecksum: documents.digest("hex") };
22801
+ return { urls: [...urls].sort(compareText), fetchedSitemaps: visited.size, bytesChecksum: documents.digest("hex") };
22634
22802
  }
22635
22803
  async function resolveMeasurementSitemapTarget(rawUrl, options = {}) {
22636
22804
  let parsed;
@@ -22647,7 +22815,7 @@ async function resolveMeasurementSitemapTarget(rawUrl, options = {}) {
22647
22815
  }
22648
22816
  const hostname = stripIpv6Brackets(parsed.hostname);
22649
22817
  if (!hostname) return { ok: false, message: "the URL has no hostname" };
22650
- const literalFamily = net.isIPv6(hostname) ? 6 : net.isIPv4(hostname) ? 4 : null;
22818
+ const literalFamily = net2.isIPv6(hostname) ? 6 : net2.isIPv4(hostname) ? 4 : null;
22651
22819
  const addresses = literalFamily ? [{ address: hostname, family: literalFamily }] : await (options.resolveAddresses ?? resolveSitemapAddresses)(hostname);
22652
22820
  if (addresses.length === 0) return { ok: false, message: `${hostname} does not resolve` };
22653
22821
  for (const entry of addresses) {
@@ -22663,85 +22831,6 @@ async function resolveSitemapAddresses(hostname) {
22663
22831
  if (ipv6.status === "fulfilled") for (const address of ipv6.value) found.set(`6:${address}`, { address, family: 6 });
22664
22832
  return [...found.values()];
22665
22833
  }
22666
- function blockedAddressReason(value) {
22667
- const address = stripIpv6Brackets(value).split("%")[0];
22668
- const family = net.isIP(address);
22669
- if (family === 4) return blockedIpv4Reason(ipv4Octets(address));
22670
- if (family === 6) {
22671
- const bytes = ipv6Bytes(address);
22672
- return bytes ? blockedIpv6Reason(bytes) : "which could not be read as an address";
22673
- }
22674
- return "which is not an IP address";
22675
- }
22676
- function ipv4Octets(address) {
22677
- return address.split(".").map((part) => Number.parseInt(part, 10));
22678
- }
22679
- function blockedIpv4Reason(octets) {
22680
- const [first, second] = octets;
22681
- if (first === 0) return "which is unspecified space";
22682
- if (first === 127) return "which is loopback";
22683
- if (first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168) {
22684
- return "which is private space";
22685
- }
22686
- if (first === 100 && second >= 64 && second <= 127) return "which is carrier-grade NAT space";
22687
- if (first === 169 && second === 254) return "which is link-local";
22688
- if (first >= 224 && first <= 239) return "which is multicast";
22689
- if (first === 192 && second === 0 && octets[2] === 0 || first === 198 && (second === 18 || second === 19) || first >= 240) {
22690
- return "which is reserved";
22691
- }
22692
- return null;
22693
- }
22694
- function blockedIpv6Reason(bytes) {
22695
- const first = bytes[0];
22696
- if (first === 0) {
22697
- const mapped = mappedIpv4(bytes);
22698
- return mapped ? `which is the IPv6 form of ${mapped.join(".")}` : "which is reserved IPv6 space";
22699
- }
22700
- if (first === 255) return "which is multicast";
22701
- if ((first & 254) === 252) return "which is unique-local";
22702
- if (first === 254 && (bytes[1] & 192) === 128) return "which is link-local";
22703
- if (first === 254 && (bytes[1] & 192) === 192) return "which is reserved site-local space";
22704
- if (first === 32 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0) return "which is a Teredo tunnel";
22705
- if (first === 32 && bytes[1] === 2) return "which is a 6to4 tunnel";
22706
- return null;
22707
- }
22708
- function mappedIpv4(bytes) {
22709
- const isMapped = bytes.slice(0, 10).every((byte) => byte === 0) && bytes[10] === 255 && bytes[11] === 255;
22710
- return isMapped ? [bytes[12], bytes[13], bytes[14], bytes[15]] : null;
22711
- }
22712
- function ipv6Bytes(address) {
22713
- if (!net.isIPv6(address)) return null;
22714
- const halves = address.split("::");
22715
- if (halves.length > 2) return null;
22716
- const expand = (text2) => {
22717
- if (text2 === "") return [];
22718
- const groups2 = [];
22719
- const chunks = text2.split(":");
22720
- for (const [index2, chunk] of chunks.entries()) {
22721
- if (index2 === chunks.length - 1 && chunk.includes(".")) {
22722
- const octets = ipv4Octets(chunk);
22723
- groups2.push(octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]);
22724
- continue;
22725
- }
22726
- groups2.push(Number.parseInt(chunk, 16));
22727
- }
22728
- return groups2;
22729
- };
22730
- const head = expand(halves[0]);
22731
- const tail = halves.length === 2 ? expand(halves[1]) : [];
22732
- const missing = 8 - head.length - tail.length;
22733
- if (halves.length === 1 ? missing !== 0 : missing < 0) return null;
22734
- const groups = [...head, ...new Array(halves.length === 2 ? missing : 0).fill(0), ...tail];
22735
- const bytes = new Uint8Array(16);
22736
- for (const [index2, group] of groups.entries()) {
22737
- bytes[index2 * 2] = group >> 8;
22738
- bytes[index2 * 2 + 1] = group & 255;
22739
- }
22740
- return bytes;
22741
- }
22742
- function stripIpv6Brackets(value) {
22743
- return value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
22744
- }
22745
22834
  async function requestFollowingValidatedRedirects(startUrl, resolveTarget, transport, limits, deadlineAt) {
22746
22835
  let current = startUrl;
22747
22836
  for (let redirects = 0; ; redirects += 1) {
@@ -22798,7 +22887,7 @@ async function requestPinnedSitemap(target, limits) {
22798
22887
  path: `${target.url.pathname}${target.url.search}`,
22799
22888
  headers: { Host: target.url.host, Accept: "application/xml,text/xml,*/*;q=0.1", "Accept-Encoding": "gzip" }
22800
22889
  };
22801
- if (secure) requestOptions.servername = target.url.hostname.replace(/^\[|\]$/g, "");
22890
+ if (secure) requestOptions.servername = stripIpv6Brackets(target.url.hostname);
22802
22891
  return await new Promise((resolve, reject) => {
22803
22892
  let settled = false;
22804
22893
  const succeed = (response) => {
@@ -22854,11 +22943,11 @@ async function decodeSitemapBody(body, headers, maxBodyBytes) {
22854
22943
  }
22855
22944
  function sitemapIndexLocations(xml) {
22856
22945
  if (!hasXmlElement(xml, "sitemapindex")) return [];
22857
- return sortedUniqueUrls(xmlElementContents(xml, "sitemap").flatMap(extractLocation));
22946
+ return sortedUnique(xmlElementContents(xml, "sitemap").flatMap(extractLocation));
22858
22947
  }
22859
22948
  function urlSetLocations(xml) {
22860
22949
  if (!hasXmlElement(xml, "urlset")) return [];
22861
- return sortedUniqueUrls(xmlElementContents(xml, "url").flatMap(extractLocation));
22950
+ return sortedUnique(xmlElementContents(xml, "url").flatMap(extractLocation));
22862
22951
  }
22863
22952
  function extractLocation(xml) {
22864
22953
  const value = xmlElementContents(xml, "loc")[0]?.trim();
@@ -22949,12 +23038,6 @@ function decodeXmlEntities(value) {
22949
23038
  function firstHeader(value) {
22950
23039
  return Array.isArray(value) ? value[0] : value;
22951
23040
  }
22952
- function compareText6(a, b) {
22953
- return a < b ? -1 : a > b ? 1 : 0;
22954
- }
22955
- function sortedUniqueUrls(values) {
22956
- return [...new Set(values)].sort(compareText6);
22957
- }
22958
23041
  function assertLimits(limits) {
22959
23042
  for (const [name, value] of Object.entries(limits)) {
22960
23043
  if (!Number.isSafeInteger(value) || value < 1) throw new Error(`Sitemap ${name} must be a positive safe integer`);
@@ -23008,7 +23091,13 @@ async function measurementServiceRoutes(app, opts) {
23008
23091
  if (runId !== void 0 && (typeof runId !== "string" || runId.trim() === "")) {
23009
23092
  throw validationError('"runId" must be a non-empty string');
23010
23093
  }
23011
- const result = buildStoredMeasurementReport(app.db, project.id, revision, runId);
23094
+ let queryClass;
23095
+ if (request.query.queryClass !== void 0) {
23096
+ const parsedClass = measurementQueryClassFilterSchema.safeParse(request.query.queryClass);
23097
+ if (!parsedClass.success) throw validationError('"queryClass" must be one of non-brand, branded or all');
23098
+ queryClass = parsedClass.data;
23099
+ }
23100
+ const result = buildStoredMeasurementReport(app.db, project.id, revision, runId, queryClass);
23012
23101
  if (result.kind === "no-plan") throw notFound("Measurement plan revision", rawRevision);
23013
23102
  if (runId !== void 0 && result.report.run?.id !== runId) throw notFound("Measurement run", runId);
23014
23103
  return measurementReportResponseSchema.parse(result.report);
@@ -23025,12 +23114,6 @@ var MEASUREMENT_DRAFT_MAX_QUERIES = 1e3;
23025
23114
  var MEASUREMENT_PLAN_V2_MAX_BYTES = 4 * 1024 * 1024;
23026
23115
  var CHECKSUM_PLACEHOLDER = "0".repeat(64);
23027
23116
  var MEASUREMENT_V2_PROVENANCE_EPOCH = "1970-01-01T00:00:00.000Z";
23028
- function compareText7(left, right) {
23029
- return left < right ? -1 : left > right ? 1 : 0;
23030
- }
23031
- function canonicalStrings(values) {
23032
- return [...new Set(values)].sort(compareText7);
23033
- }
23034
23117
  function proposeQueryClass(queryText, brandNames) {
23035
23118
  const matcher = compileBrandAliases(brandNames.filter((name) => brandKeyFromText(name).length > 0));
23036
23119
  return matcherMatchesText(matcher, queryText) ? "branded" : "non-brand";
@@ -23062,12 +23145,17 @@ function parseDraftMatcher(value) {
23062
23145
  function matcherHost(matcher) {
23063
23146
  return matcher.kind === "exact" ? new URL(matcher.url).hostname : matcher.host;
23064
23147
  }
23148
+ function matcherPathIdentity(path8, pathCase) {
23149
+ return pathCase === "insensitive" ? path8.toLocaleLowerCase("en") : path8;
23150
+ }
23065
23151
  function matcherIdentity(matcher) {
23066
23152
  switch (matcher.kind) {
23067
- case "exact":
23068
- return `exact\0${new URL(matcher.url).hostname}\0${new URL(matcher.url).pathname}`;
23153
+ case "exact": {
23154
+ const url = new URL(matcher.url);
23155
+ return `exact\0${url.hostname}\0${matcherPathIdentity(url.pathname, matcher.pathCase)}`;
23156
+ }
23069
23157
  case "prefix":
23070
- return `prefix\0${matcher.host}\0${matcher.pathPrefix}`;
23158
+ return `prefix\0${matcher.host}\0${matcherPathIdentity(matcher.pathPrefix, matcher.pathCase)}`;
23071
23159
  case "host":
23072
23160
  return `host\0${matcher.host}\0`;
23073
23161
  }
@@ -23142,7 +23230,7 @@ function createChecks() {
23142
23230
  };
23143
23231
  }
23144
23232
  function normalizeProviders2(values) {
23145
- return canonicalStrings(values.map((value) => value.trim().toLowerCase()).filter(Boolean));
23233
+ return sortedUnique(values.map((value) => value.trim().toLowerCase()).filter(Boolean));
23146
23234
  }
23147
23235
  function compileMeasurementDraft(authoring, context) {
23148
23236
  const { checks, sink, failed } = createChecks();
@@ -23152,7 +23240,7 @@ function compileMeasurementDraft(authoring, context) {
23152
23240
  } catch {
23153
23241
  sink.fail("invalid-project-identity", "The project canonical domain is not a valid hostname.", ["identities", "projectBrand", "canonicalHost"]);
23154
23242
  }
23155
- const ownedHosts = canonicalStrings([context.canonicalDomain, ...context.ownedDomains].flatMap((value) => {
23243
+ const ownedHosts = sortedUnique([context.canonicalDomain, ...context.ownedDomains].flatMap((value) => {
23156
23244
  try {
23157
23245
  return [normalizeMeasurementHost(value)];
23158
23246
  } catch {
@@ -23216,7 +23304,7 @@ function compileMeasurementDraft(authoring, context) {
23216
23304
  }
23217
23305
  aliasClaims.set(identity, target.stableKey);
23218
23306
  });
23219
- const aliases = canonicalStrings(target.aliases);
23307
+ const aliases = sortedUnique(target.aliases);
23220
23308
  const aliasKeys = aliases.map(measurementMentionAliasKey).filter(Boolean);
23221
23309
  target.identityAliases?.forEach((identityAlias, index2) => {
23222
23310
  const identityKey = measurementMentionAliasKey(identityAlias);
@@ -23233,7 +23321,7 @@ function compileMeasurementDraft(authoring, context) {
23233
23321
  stableKey: target.stableKey,
23234
23322
  label: target.label,
23235
23323
  aliases,
23236
- ...target.identityAliases === void 0 ? {} : { identityAliases: canonicalStrings(target.identityAliases) },
23324
+ ...target.identityAliases === void 0 ? {} : { identityAliases: sortedUnique(target.identityAliases) },
23237
23325
  urlMatchers: matchers,
23238
23326
  mentionNotApplicable: aliases.length === 0,
23239
23327
  discoveryIdentity: target.discoveryIdentity ?? null
@@ -23281,8 +23369,8 @@ function compileMeasurementDraft(authoring, context) {
23281
23369
  stableKey: group.stableKey,
23282
23370
  label: group.label,
23283
23371
  ...group.parentGroupKey === void 0 ? {} : { parentGroupKey: group.parentGroupKey },
23284
- targetKeys: canonicalStrings(group.targetKeys),
23285
- competitors: [...group.competitors].map((competitor) => ({ ...competitor, aliases: canonicalStrings(competitor.aliases) })).sort((left, right) => compareText7(left.stableKey, right.stableKey))
23372
+ targetKeys: sortedUnique(group.targetKeys),
23373
+ competitors: [...group.competitors].map((competitor) => ({ ...competitor, aliases: sortedUnique(competitor.aliases) })).sort((left, right) => compareText(left.stableKey, right.stableKey))
23286
23374
  });
23287
23375
  });
23288
23376
  const resolveContext = (override, path8) => {
@@ -23300,7 +23388,7 @@ function compileMeasurementDraft(authoring, context) {
23300
23388
  }
23301
23389
  models[normalized] = model;
23302
23390
  }
23303
- const labels = canonicalStrings(override?.locations ?? authoring.defaultContext.locations);
23391
+ const labels = sortedUnique(override?.locations ?? authoring.defaultContext.locations);
23304
23392
  const locations = [];
23305
23393
  labels.forEach((label, labelIndex) => {
23306
23394
  const location = locationsByLabel.get(label);
@@ -23461,7 +23549,7 @@ function compileMeasurementDraft(authoring, context) {
23461
23549
  }
23462
23550
  }
23463
23551
  if (failed()) return { ok: false, checks };
23464
- const querySnapshots3 = [...usedQueryIds].sort(compareText7).map((queryId) => ({
23552
+ const querySnapshots3 = [...usedQueryIds].sort(compareText).map((queryId) => ({
23465
23553
  queryId,
23466
23554
  queryText: queriesById.get(queryId),
23467
23555
  // Provenance is frozen at publish time so a later reader can explain the
@@ -23478,7 +23566,7 @@ function compileMeasurementDraft(authoring, context) {
23478
23566
  projectBrand: {
23479
23567
  canonicalHost,
23480
23568
  ownedHosts,
23481
- names: canonicalStrings(context.brandNames)
23569
+ names: sortedUnique(context.brandNames)
23482
23570
  }
23483
23571
  },
23484
23572
  targets: compiledTargets.map(({ target }) => target),
@@ -23527,13 +23615,13 @@ function keyedDiff2(before, after) {
23527
23615
  const added = [];
23528
23616
  const changed = [];
23529
23617
  const unchanged = [];
23530
- for (const stableKey of [...afterByKey.keys()].sort(compareText7)) {
23618
+ for (const stableKey of [...afterByKey.keys()].sort(compareText)) {
23531
23619
  const beforeValue = beforeByKey.get(stableKey);
23532
23620
  if (!beforeValue) added.push(stableKey);
23533
23621
  else if (JSON.stringify(canonicalJsonValue(beforeValue)) === JSON.stringify(canonicalJsonValue(afterByKey.get(stableKey)))) unchanged.push(stableKey);
23534
23622
  else changed.push(stableKey);
23535
23623
  }
23536
- const removed = [...beforeByKey.keys()].filter((key) => !afterByKey.has(key)).sort(compareText7);
23624
+ const removed = [...beforeByKey.keys()].filter((key) => !afterByKey.has(key)).sort(compareText);
23537
23625
  return { added, removed, changed, unchanged };
23538
23626
  }
23539
23627
  function assignmentClasses(plan) {
@@ -23562,8 +23650,8 @@ function diffCompiledPlans(active, candidate, activeRevision) {
23562
23650
  groups: keyedDiff2(active?.groups ?? [], candidate.groups),
23563
23651
  assignments: { added: addedAssignments, removed: removedAssignments, reclassified },
23564
23652
  execution: {
23565
- addedNodeKeys: [...afterNodes].filter((key) => !beforeNodes.has(key)).sort(compareText7),
23566
- removedNodeKeys: [...beforeNodes].filter((key) => !afterNodes.has(key)).sort(compareText7)
23653
+ addedNodeKeys: [...afterNodes].filter((key) => !beforeNodes.has(key)).sort(compareText),
23654
+ removedNodeKeys: [...beforeNodes].filter((key) => !afterNodes.has(key)).sort(compareText)
23567
23655
  }
23568
23656
  };
23569
23657
  }
@@ -23636,9 +23724,6 @@ function requireGroup(authoring, groupKey) {
23636
23724
  function unique(values) {
23637
23725
  return [...new Set(values)];
23638
23726
  }
23639
- function compareText8(left, right) {
23640
- return left < right ? -1 : left > right ? 1 : 0;
23641
- }
23642
23727
  function knownQueryIds(queryIds, context) {
23643
23728
  const uniqueQueryIds = unique(queryIds);
23644
23729
  const unknown = uniqueQueryIds.filter((queryId) => !context.queriesById.has(queryId));
@@ -23692,7 +23777,7 @@ function resolveDraftAudience(authoring, audience) {
23692
23777
  groups.push({ groupKey: group.stableKey, label: group.label, memberCount: memberKeys.length });
23693
23778
  selected.push(...memberKeys);
23694
23779
  }
23695
- const targetKeys = unique(selected).sort(compareText8);
23780
+ const targetKeys = unique(selected).sort(compareText);
23696
23781
  return {
23697
23782
  targetKeys,
23698
23783
  groups,
@@ -24122,9 +24207,6 @@ var MeasurementGroupMembershipImportError = class extends Error {
24122
24207
  statusCode;
24123
24208
  details;
24124
24209
  };
24125
- function compareText9(left, right) {
24126
- return left < right ? -1 : left > right ? 1 : 0;
24127
- }
24128
24210
  function sha256Hex2(value) {
24129
24211
  return crypto20.createHash("sha256").update(value).digest("hex");
24130
24212
  }
@@ -24132,7 +24214,7 @@ function canonicalJsonValue2(value) {
24132
24214
  if (Array.isArray(value)) return value.map(canonicalJsonValue2);
24133
24215
  if (value && typeof value === "object") {
24134
24216
  return Object.fromEntries(
24135
- Object.entries(value).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText9(left, right)).map(([key, child]) => [key, canonicalJsonValue2(child)])
24217
+ Object.entries(value).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText(left, right)).map(([key, child]) => [key, canonicalJsonValue2(child)])
24136
24218
  );
24137
24219
  }
24138
24220
  return value;
@@ -24429,8 +24511,8 @@ function groupIdentities(rows, authoring, segments) {
24429
24511
  groupsByLabel.set(label, matches);
24430
24512
  }
24431
24513
  const identities = [];
24432
- for (const normalizedGroupLabel of [...labels.keys()].sort(compareText9)) {
24433
- const existing = [...groupsByLabel.get(normalizedGroupLabel) ?? []].sort((left, right) => compareText9(left.stableKey, right.stableKey));
24514
+ for (const normalizedGroupLabel of [...labels.keys()].sort(compareText)) {
24515
+ const existing = [...groupsByLabel.get(normalizedGroupLabel) ?? []].sort((left, right) => compareText(left.stableKey, right.stableKey));
24434
24516
  if (existing.length > 1) {
24435
24517
  identities.push({
24436
24518
  kind: "ambiguous",
@@ -24508,8 +24590,8 @@ function groupChangesFromRows(rows, identities) {
24508
24590
  bucket.rows.push(row);
24509
24591
  byGroupKey.set(row.groupKey, bucket);
24510
24592
  }
24511
- return [...byGroupKey.values()].sort((left, right) => compareText9(left.identity.normalizedGroupLabel, right.identity.normalizedGroupLabel)).map(({ identity, rows: matchedRows }) => {
24512
- const targetKeys = [...new Set(matchedRows.map((row) => row.targetKey))].sort(compareText9);
24593
+ return [...byGroupKey.values()].sort((left, right) => compareText(left.identity.normalizedGroupLabel, right.identity.normalizedGroupLabel)).map(({ identity, rows: matchedRows }) => {
24594
+ const targetKeys = sortedUnique(matchedRows.map((row) => row.targetKey));
24513
24595
  const addedTargetKeys = targetKeys.filter((targetKey) => !identity.existingTargetKeys.has(targetKey));
24514
24596
  const unchangedTargetKeys = targetKeys.filter((targetKey) => identity.existingTargetKeys.has(targetKey));
24515
24597
  return {
@@ -24573,7 +24655,7 @@ function resolveRows(parsedRows, authoring, identities) {
24573
24655
  resolved.push(rowFailure(row, "invalid", "missing-group"));
24574
24656
  continue;
24575
24657
  }
24576
- const labelCandidates = [...targetsByLabel.get(row.normalizedProperty) ?? []].sort((left, right) => compareText9(left.stableKey, right.stableKey));
24658
+ const labelCandidates = [...targetsByLabel.get(row.normalizedProperty) ?? []].sort((left, right) => compareText(left.stableKey, right.stableKey));
24577
24659
  if (!labelCandidates.length) {
24578
24660
  resolved.push(rowFailure(row, "unmatched", "property-not-found"));
24579
24661
  continue;
@@ -24654,7 +24736,7 @@ function resolveArtifacts(input) {
24654
24736
  return { rows, groupChanges, counts: countsFromRows(rows, groupChanges), identities };
24655
24737
  }
24656
24738
  function checksumIdentities(identities) {
24657
- return [...identities].sort((left, right) => compareText9(left.normalizedGroupLabel, right.normalizedGroupLabel)).map((identity) => {
24739
+ return [...identities].sort((left, right) => compareText(left.normalizedGroupLabel, right.normalizedGroupLabel)).map((identity) => {
24658
24740
  if (identity.kind === "ready") {
24659
24741
  return {
24660
24742
  normalizedGroupLabel: identity.normalizedGroupLabel,
@@ -24668,7 +24750,7 @@ function checksumIdentities(identities) {
24668
24750
  return {
24669
24751
  normalizedGroupLabel: identity.normalizedGroupLabel,
24670
24752
  outcome: identity.kind,
24671
- groupKeys: [...identity.groupKeys].sort(compareText9)
24753
+ groupKeys: [...identity.groupKeys].sort(compareText)
24672
24754
  };
24673
24755
  }
24674
24756
  return {
@@ -24676,8 +24758,8 @@ function checksumIdentities(identities) {
24676
24758
  outcome: identity.kind,
24677
24759
  groupKey: identity.groupKey,
24678
24760
  evidence: [...identity.evidence].sort((left, right) => {
24679
- const bySource = compareText9(left.source, right.source);
24680
- return bySource || compareText9(left.normalizedGroupLabel ?? "", right.normalizedGroupLabel ?? "");
24761
+ const bySource = compareText(left.source, right.source);
24762
+ return bySource || compareText(left.normalizedGroupLabel ?? "", right.normalizedGroupLabel ?? "");
24681
24763
  })
24682
24764
  };
24683
24765
  });
@@ -24697,7 +24779,7 @@ function resolveGroupMembershipPreview(input) {
24697
24779
  })),
24698
24780
  rows: artifacts.rows,
24699
24781
  proposedGroups: checksumIdentities(artifacts.identities),
24700
- persistedSegments: [...input.segments].sort((left, right) => compareText9(left.stableKey, right.stableKey)).map((segment) => ({ stableKey: segment.stableKey, kind: segment.kind, retiredAt: segment.retiredAt }))
24782
+ persistedSegments: [...input.segments].sort((left, right) => compareText(left.stableKey, right.stableKey)).map((segment) => ({ stableKey: segment.stableKey, kind: segment.kind, retiredAt: segment.retiredAt }))
24701
24783
  });
24702
24784
  return {
24703
24785
  draftEtag: input.draftEtag,
@@ -24825,9 +24907,6 @@ function applyReviewedGroupMembership(authoring, preview, review) {
24825
24907
 
24826
24908
  // ../api-routes/src/measurement-draft.ts
24827
24909
  var MEASUREMENT_GROUP_MEMBERSHIP_BODY_LIMIT = MEASUREMENT_GROUP_MEMBERSHIP_CSV_MAX_BYTES * 7 + 4096;
24828
- function compareText10(left, right) {
24829
- return left < right ? -1 : left > right ? 1 : 0;
24830
- }
24831
24910
  function authoringIdentity(authoring) {
24832
24911
  return canonicalJson(authoring);
24833
24912
  }
@@ -24896,7 +24975,7 @@ function emptyAuthoring(project, opts) {
24896
24975
  defaultContext: {
24897
24976
  providers,
24898
24977
  ...Object.keys(models).length ? { models } : {},
24899
- locations: project.locations.map((location) => location.label).sort(compareText10)
24978
+ locations: project.locations.map((location) => location.label).sort(compareText)
24900
24979
  },
24901
24980
  targets: [],
24902
24981
  assignments: [],
@@ -25106,7 +25185,7 @@ function pageLimit(value) {
25106
25185
  }
25107
25186
  function paginate(rows, query, sortKey, searchable) {
25108
25187
  const needle = query.search?.trim().toLowerCase();
25109
- const filtered = (needle ? rows.filter((row) => searchable(row).toLowerCase().includes(needle)) : [...rows]).sort((left, right) => compareText10(sortKey(left), sortKey(right)));
25188
+ const filtered = (needle ? rows.filter((row) => searchable(row).toLowerCase().includes(needle)) : [...rows]).sort((left, right) => compareText(sortKey(left), sortKey(right)));
25110
25189
  const after = query.cursor ? Buffer.from(query.cursor, "base64url").toString("utf8") : null;
25111
25190
  const remaining = after === null ? filtered : filtered.filter((row) => sortKey(row) > after);
25112
25191
  const limit = pageLimit(query.limit);
@@ -26112,22 +26191,22 @@ function proposeMeasurementDiscoveryBindings(input) {
26112
26191
  if (excluded.has(candidate.slug) || excluded.has(identity.toLowerCase())) continue;
26113
26192
  const parts = parseMeasurementDiscoveryIdentity(identity);
26114
26193
  if (!parts) continue;
26115
- const candidates = missing.map((entry) => ({ targetKey: entry.target.stableKey, score: rebindScore(parts, entry.parts) })).filter((entry) => entry.score > MEASUREMENT_REBIND_THRESHOLD).sort((left, right) => right.score - left.score || compareText11(left.targetKey, right.targetKey));
26194
+ const candidates = missing.map((entry) => ({ targetKey: entry.target.stableKey, score: rebindScore(parts, entry.parts) })).filter((entry) => entry.score > MEASUREMENT_REBIND_THRESHOLD).sort((left, right) => right.score - left.score || compareText(left.targetKey, right.targetKey));
26116
26195
  proposals.push({
26117
26196
  discoveryIdentity: identity,
26118
26197
  stableKey: candidate.stableKey,
26119
26198
  slug: candidate.slug,
26120
26199
  label: candidate.label,
26121
26200
  discoveredUrl: candidate.primaryUrl,
26122
- urlMatchers: [.../* @__PURE__ */ new Set([candidate.primaryUrl, ...candidate.aliasCoverageUrls])].sort(compareText11),
26201
+ urlMatchers: sortedUnique([candidate.primaryUrl, ...candidate.aliasCoverageUrls]),
26123
26202
  kind: candidates.length === 1 ? "rebind" : candidates.length === 0 ? "new-target" : "ambiguous",
26124
26203
  ...candidates.length === 1 ? { rebindTargetKey: candidates[0].targetKey } : {},
26125
26204
  candidates
26126
26205
  });
26127
26206
  }
26128
26207
  return {
26129
- proposals: proposals.sort((left, right) => compareText11(left.discoveryIdentity, right.discoveryIdentity)),
26130
- unchanged: input.targets.filter((target) => target.discoveryIdentity !== void 0 && present.has(target.discoveryIdentity)).map((target) => target.stableKey).sort(compareText11)
26208
+ proposals: proposals.sort((left, right) => compareText(left.discoveryIdentity, right.discoveryIdentity)),
26209
+ unchanged: input.targets.filter((target) => target.discoveryIdentity !== void 0 && present.has(target.discoveryIdentity)).map((target) => target.stableKey).sort(compareText)
26131
26210
  };
26132
26211
  }
26133
26212
  function rebindScore(left, right) {
@@ -26150,15 +26229,12 @@ function normalizePathTemplate(value) {
26150
26229
  function hostOfUrl(value) {
26151
26230
  return normalizeMeasurementHost(new URL(value).hostname);
26152
26231
  }
26153
- function compareText11(left, right) {
26154
- return left < right ? -1 : left > right ? 1 : 0;
26155
- }
26156
26232
  function canonicalJson2(value) {
26157
26233
  const canonical = (input) => {
26158
26234
  if (Array.isArray(input)) return input.map(canonical);
26159
26235
  if (input && typeof input === "object") {
26160
26236
  return Object.fromEntries(
26161
- Object.entries(input).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText11(left, right)).map(([key, child]) => [key, canonical(child)])
26237
+ Object.entries(input).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText(left, right)).map(([key, child]) => [key, canonical(child)])
26162
26238
  );
26163
26239
  }
26164
26240
  return input;
@@ -26173,20 +26249,10 @@ function measurementDiscoveryInputChecksum(input) {
26173
26249
  compilerVersion: MEASUREMENT_DISCOVERY_COMPILER_VERSION,
26174
26250
  sitemapUrl: input.sitemapUrl,
26175
26251
  rule: input.rule,
26176
- exclusions: [...new Set(input.exclusions.map((value) => value.trim().toLowerCase()))].sort(compareText11),
26252
+ exclusions: sortedUnique(input.exclusions.map((value) => value.trim().toLowerCase())),
26177
26253
  bytesChecksum: input.bytesChecksum
26178
26254
  }));
26179
26255
  }
26180
- function draftCounts2(authoring) {
26181
- return {
26182
- targets: authoring.targets.length,
26183
- includedTargets: authoring.targets.filter((target) => target.status === "included").length,
26184
- assignments: authoring.assignments.length,
26185
- unclassifiedAssignments: authoring.assignments.filter((assignment) => assignment.queryClass === "unclassified").length,
26186
- groups: authoring.groups.length,
26187
- competitors: authoring.groups.reduce((total, group) => total + group.competitors.length, 0)
26188
- };
26189
- }
26190
26256
  function loadDraft(app, projectId, projectName) {
26191
26257
  const row = app.db.select().from(measurementPlanDrafts).where(eq32(measurementPlanDrafts.projectId, projectId)).get();
26192
26258
  if (!row) throw notFound("Measurement plan draft", projectName);
@@ -26243,7 +26309,7 @@ function commitDraft(app, input) {
26243
26309
  etag: measurementDraftEtag(etagVersion),
26244
26310
  changed: input.changed,
26245
26311
  warnings: input.warnings,
26246
- counts: draftCounts2(authoring.data)
26312
+ counts: draftCounts(authoring.data)
26247
26313
  });
26248
26314
  const actor = actorReference(input.request);
26249
26315
  app.db.transaction((tx) => {
@@ -26346,7 +26412,7 @@ async function measurementDiscoveryV2Routes(app) {
26346
26412
  if (!parsed.success) {
26347
26413
  throw validationError("Invalid sitemap import request", { issues: parsed.error.issues });
26348
26414
  }
26349
- const exclusions = [...new Set((parsed.data.exclusions ?? []).map((value) => value.trim().toLowerCase()))].sort(compareText11);
26415
+ const exclusions = sortedUnique((parsed.data.exclusions ?? []).map((value) => value.trim().toLowerCase()));
26350
26416
  const idempotencyKey = requireIdempotencyKey2(request, "import-sitemap");
26351
26417
  const requestChecksum2 = sha2562(canonicalJson2({ ...parsed.data, exclusions }));
26352
26418
  const replay = replayedResponse(app, project.id, "import-sitemap", idempotencyKey, requestChecksum2);
@@ -26491,7 +26557,7 @@ async function measurementDiscoveryV2Routes(app) {
26491
26557
  const rebound = {
26492
26558
  ...existing,
26493
26559
  ...relabelled,
26494
- urlMatchers: [.../* @__PURE__ */ new Set([...existing.urlMatchers, ...proposal.urlMatchers])].sort(compareText11),
26560
+ urlMatchers: sortedUnique([...existing.urlMatchers, ...proposal.urlMatchers]),
26495
26561
  discoveredUrl: proposal.discoveredUrl,
26496
26562
  discoveryIdentity: proposal.discoveryIdentity
26497
26563
  };
@@ -26549,15 +26615,12 @@ function parseEvidenceQuery(raw) {
26549
26615
  }
26550
26616
  return parsed.data;
26551
26617
  }
26552
- function normalizedText2(value) {
26553
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
26554
- }
26555
26618
  function filterFingerprint(query) {
26556
26619
  const filters = {
26557
26620
  targetKey: query.targetKey,
26558
26621
  queryClass: query.queryClass ?? "all",
26559
- provider: query.provider === void 0 ? null : normalizedText2(query.provider),
26560
- location: query.location === void 0 ? null : normalizedText2(query.location)
26622
+ provider: query.provider === void 0 ? null : normalizeIdentityText(query.provider),
26623
+ location: query.location === void 0 ? null : normalizeIdentityText(query.location)
26561
26624
  };
26562
26625
  return createHash5("sha256").update(JSON.stringify(filters)).digest("base64url");
26563
26626
  }
@@ -26619,7 +26682,7 @@ function propertyEvidenceRows(db, plan, active, run, query, queryClass) {
26619
26682
  const manifest = measurementRunExpectedSlots(run, plan);
26620
26683
  const { input, edgeQueryClass } = buildMeasurementPlanV2ReportInput(active.version.revision, plan, manifest, snapshots);
26621
26684
  const ownEdgeIds = new Set(input.usageEdges.filter((edge) => edge.type === "target" && edge.targetId === query.targetKey).filter((edge) => queryClass === "all" || edgeQueryClass.get(edge.id) === queryClass).map((edge) => edge.id));
26622
- const provider = query.provider === void 0 ? void 0 : normalizedText2(query.provider);
26685
+ const provider = query.provider === void 0 ? void 0 : normalizeIdentityText(query.provider);
26623
26686
  const location = query.location === void 0 ? void 0 : normalizeMeasurementLocation(query.location);
26624
26687
  const owned = (row) => ownEdgeIds.has(row.usageEdgeId) && (provider === void 0 || row.provider === provider) && (location === void 0 || normalizeMeasurementLocation(row.location) === location);
26625
26688
  const built = buildMeasurementEvidence(input);
@@ -26734,12 +26797,6 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
26734
26797
  RunStatuses.partial
26735
26798
  ]);
26736
26799
  var { rawResponse: omittedRawResponse, ...measurementQuestionSnapshotColumns } = getTableColumns(querySnapshots);
26737
- function compareText12(left, right) {
26738
- return left < right ? -1 : left > right ? 1 : 0;
26739
- }
26740
- function normalizeText(value) {
26741
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
26742
- }
26743
26800
  function excerptOf(answer) {
26744
26801
  const normalized = answer.normalize("NFKC").trim().replace(/\s+/g, " ");
26745
26802
  return normalized.length <= 240 ? normalized : normalized.slice(0, 240);
@@ -26826,16 +26883,16 @@ function selectMeasurementQuestionRun(db, projectId, active, runId) {
26826
26883
  return run;
26827
26884
  }
26828
26885
  function slotKey(executionId, provider) {
26829
- return `${executionId}\0${normalizeText(provider)}`;
26886
+ return `${executionId}\0${normalizeIdentityText(provider)}`;
26830
26887
  }
26831
26888
  function scopedManifest(manifest, scope) {
26832
26889
  if (scope === void 0) return manifest;
26833
26890
  const executionIds = new Set(scope.executionIds);
26834
- const provider = scope.provider === void 0 ? void 0 : normalizeText(scope.provider);
26891
+ const provider = scope.provider === void 0 ? void 0 : normalizeIdentityText(scope.provider);
26835
26892
  const location = scope.location === void 0 ? void 0 : normalizeMeasurementLocation(scope.location);
26836
26893
  return {
26837
26894
  ...manifest,
26838
- expectedSlots: manifest.expectedSlots.filter((slot) => executionIds.has(slot.executionId) && (provider === void 0 || normalizeText(slot.provider) === provider) && (location === void 0 || normalizeMeasurementLocation(slot.context?.label ?? null) === location))
26895
+ expectedSlots: manifest.expectedSlots.filter((slot) => executionIds.has(slot.executionId) && (provider === void 0 || normalizeIdentityText(slot.provider) === provider) && (location === void 0 || normalizeMeasurementLocation(slot.context?.label ?? null) === location))
26839
26896
  };
26840
26897
  }
26841
26898
  function selectedQuestionSnapshots(db, runId, manifest, scope) {
@@ -26846,7 +26903,7 @@ function selectedQuestionSnapshots(db, runId, manifest, scope) {
26846
26903
  conditions.push(inArray15(querySnapshots.measurementExecutionId, executionIds));
26847
26904
  }
26848
26905
  if (scope?.provider !== void 0) {
26849
- conditions.push(sql12`lower(trim(${querySnapshots.provider})) = ${normalizeText(scope.provider)}`);
26906
+ conditions.push(sql12`lower(trim(${querySnapshots.provider})) = ${normalizeIdentityText(scope.provider)}`);
26850
26907
  }
26851
26908
  return db.select(measurementQuestionSnapshotColumns).from(querySnapshots).where(and26(...conditions)).all();
26852
26909
  }
@@ -26921,7 +26978,7 @@ function questionRows(plan, target, materialized, filters = {}) {
26921
26978
  for (const slot of materialized.input.expectedSlots) {
26922
26979
  const assignment = assignmentsByExecution.get(slot.executionId);
26923
26980
  if (!assignment) continue;
26924
- if (filters.provider !== void 0 && normalizeText(slot.provider) !== normalizeText(filters.provider)) continue;
26981
+ if (filters.provider !== void 0 && normalizeIdentityText(slot.provider) !== normalizeIdentityText(filters.provider)) continue;
26925
26982
  if (filters.location !== void 0 && normalizeMeasurementLocation(slot.location) !== normalizeMeasurementLocation(filters.location)) continue;
26926
26983
  const question = queryById.get(assignment.queryId);
26927
26984
  const manifestSlot = materialized.manifestSlotsByIdentity.get(slotKey(slot.executionId, slot.provider));
@@ -26958,7 +27015,7 @@ function questionRows(plan, target, materialized, filters = {}) {
26958
27015
  const key = `${row.queryId}\0${row.provider}\0${row.location ?? ""}`;
26959
27016
  if (!deduped.has(key)) deduped.set(key, row);
26960
27017
  }
26961
- return [...deduped.values()].sort((left, right) => compareText12(left.queryId, right.queryId) || compareText12(left.provider, right.provider) || compareText12(left.location ?? "", right.location ?? "") || compareText12(left.text, right.text));
27018
+ return [...deduped.values()].sort((left, right) => compareText(left.queryId, right.queryId) || compareText(left.provider, right.provider) || compareText(left.location ?? "", right.location ?? "") || compareText(left.text, right.text));
26962
27019
  }
26963
27020
  function recommendedInstead(candidates, target) {
26964
27021
  const ownNames = new Set([target.label, ...target.aliases].map(brandKeyFromText).filter((key) => key.length > 0));
@@ -27095,12 +27152,6 @@ async function measurementQuestionReadRoutes(app) {
27095
27152
 
27096
27153
  // ../api-routes/src/measurement-portfolio-reads.ts
27097
27154
  var DEFAULT_LIMIT3 = 10;
27098
- function compareText13(left, right) {
27099
- return left < right ? -1 : left > right ? 1 : 0;
27100
- }
27101
- function normalizeText2(value) {
27102
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
27103
- }
27104
27155
  function parseLimitQuery(raw, schema, message) {
27105
27156
  const candidate = { ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) } };
27106
27157
  const parsed = schema.safeParse(candidate);
@@ -27173,9 +27224,9 @@ function materializeWithDb(db, active, plan, run) {
27173
27224
  return materializeMeasurementQuestionRun(db, active, plan, run);
27174
27225
  }
27175
27226
  function filteredEvaluator(materialized, filters, targetKeys) {
27176
- const provider = filters.provider === void 0 ? void 0 : normalizeText2(filters.provider);
27227
+ const provider = filters.provider === void 0 ? void 0 : normalizeIdentityText(filters.provider);
27177
27228
  const location = filters.location === void 0 ? void 0 : normalizeMeasurementLocation(filters.location);
27178
- const expectedSlots = materialized.input.expectedSlots.filter((slot) => (provider === void 0 || normalizeText2(slot.provider) === provider) && (location === void 0 || normalizeMeasurementLocation(slot.location) === location));
27229
+ const expectedSlots = materialized.input.expectedSlots.filter((slot) => (provider === void 0 || normalizeIdentityText(slot.provider) === provider) && (location === void 0 || normalizeMeasurementLocation(slot.location) === location));
27179
27230
  const usageEdges = materialized.input.usageEdges.filter((edge) => filters.queryClass === "all" || materialized.edgeQueryClass.get(edge.id) === filters.queryClass);
27180
27231
  return createMeasurementOverviewEvaluator({
27181
27232
  ...materialized.input,
@@ -27230,12 +27281,12 @@ function createTargetAnswerReader(plan, materialized, filters) {
27230
27281
  }
27231
27282
  const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
27232
27283
  const questionsById = new Map(plan.querySnapshots.map((question) => [question.queryId, question.queryText]));
27233
- const provider = filters.provider === void 0 ? void 0 : normalizeText2(filters.provider);
27284
+ const provider = filters.provider === void 0 ? void 0 : normalizeIdentityText(filters.provider);
27234
27285
  const location = filters.location === void 0 ? void 0 : normalizeMeasurementLocation(filters.location);
27235
27286
  const slotsByExecution = /* @__PURE__ */ new Map();
27236
27287
  const slotOrder = /* @__PURE__ */ new Map();
27237
27288
  materialized.input.expectedSlots.forEach((slot, index2) => {
27238
- if (provider !== void 0 && normalizeText2(slot.provider) !== provider) return;
27289
+ if (provider !== void 0 && normalizeIdentityText(slot.provider) !== provider) return;
27239
27290
  if (location !== void 0 && normalizeMeasurementLocation(slot.location) !== location) return;
27240
27291
  pushTo(slotsByExecution, slot.executionId, slot);
27241
27292
  slotOrder.set(slot.id, index2);
@@ -27316,7 +27367,7 @@ function domainRows(answers, limit, readHost = createCitedHostReader()) {
27316
27367
  const domains = new Set(answerDomains(answer, readHost).values());
27317
27368
  for (const domain of domains) counts2.set(domain, (counts2.get(domain) ?? 0) + 1);
27318
27369
  }
27319
- const rows = [...counts2].map(([domain, count6]) => ({ domain, answers: count6 })).sort((left, right) => right.answers - left.answers || compareText13(left.domain, right.domain));
27370
+ const rows = [...counts2].map(([domain, count6]) => ({ domain, answers: count6 })).sort((left, right) => right.answers - left.answers || compareText(left.domain, right.domain));
27320
27371
  return { rows: rows.slice(0, limit), total: rows.length };
27321
27372
  }
27322
27373
  function targetAliasKeys(target) {
@@ -27341,7 +27392,7 @@ function recommendationRows(target, population) {
27341
27392
  forEachNameWrittenInstead(target, population.answers, (key, name, answer, firstInAnswer) => {
27342
27393
  const existing = grouped.get(key);
27343
27394
  if (existing) {
27344
- if (compareText13(name, existing.name) < 0) existing.name = name;
27395
+ if (compareText(name, existing.name) < 0) existing.name = name;
27345
27396
  if (!firstInAnswer) return;
27346
27397
  existing.occurrences++;
27347
27398
  existing.providers.add(answer.provider);
@@ -27356,8 +27407,8 @@ function recommendationRows(target, population) {
27356
27407
  }
27357
27408
  });
27358
27409
  return [...grouped.values()].map((row) => {
27359
- const providers = [...row.providers].sort(compareText13);
27360
- const questions = [...row.questions].sort(compareText13);
27410
+ const providers = [...row.providers].sort(compareText);
27411
+ const questions = [...row.questions].sort(compareText);
27361
27412
  return {
27362
27413
  name: row.name,
27363
27414
  occurrences: row.occurrences,
@@ -27368,7 +27419,7 @@ function recommendationRows(target, population) {
27368
27419
  questionTotal: questions.length,
27369
27420
  questionsTruncated: questions.length > 5
27370
27421
  };
27371
- }).sort((left, right) => right.occurrences - left.occurrences || compareText13(left.name, right.name));
27422
+ }).sort((left, right) => right.occurrences - left.occurrences || compareText(left.name, right.name));
27372
27423
  }
27373
27424
  function namedInsteadFields(names, limit = MEASUREMENT_PORTFOLIO_ROW_EVIDENCE_LIMIT) {
27374
27425
  const returned = names.slice(0, limit);
@@ -27388,7 +27439,7 @@ function compareWeakest(left, right) {
27388
27439
  const rightUnavailable = right.mentionCoverage.state === "unavailable" || right.citationCoverage.state === "unavailable";
27389
27440
  if (leftUnavailable !== rightUnavailable) return leftUnavailable ? 1 : -1;
27390
27441
  if (leftUnavailable || rightUnavailable) {
27391
- return compareText13(left.label, right.label) || compareText13(left.targetKey, right.targetKey);
27442
+ return compareText(left.label, right.label) || compareText(left.targetKey, right.targetKey);
27392
27443
  }
27393
27444
  if (left.mentionCoverage.state === "available" && right.mentionCoverage.state === "available" && left.mentionCoverage.value !== right.mentionCoverage.value) {
27394
27445
  return left.mentionCoverage.value - right.mentionCoverage.value;
@@ -27396,7 +27447,7 @@ function compareWeakest(left, right) {
27396
27447
  if (left.citationCoverage.state === "available" && right.citationCoverage.state === "available" && left.citationCoverage.value !== right.citationCoverage.value) {
27397
27448
  return left.citationCoverage.value - right.citationCoverage.value;
27398
27449
  }
27399
- return compareText13(left.label, right.label) || compareText13(left.targetKey, right.targetKey);
27450
+ return compareText(left.label, right.label) || compareText(left.targetKey, right.targetKey);
27400
27451
  }
27401
27452
  function requireGroup2(plan, groupKey) {
27402
27453
  const group = plan.groups.find((candidate) => candidate.stableKey === groupKey);
@@ -27498,14 +27549,14 @@ function compareWeakestMarket(a, b) {
27498
27549
  const aUnavailable = a.mentionCoverage.state === "unavailable" || a.citationCoverage.state === "unavailable";
27499
27550
  const bUnavailable = b.mentionCoverage.state === "unavailable" || b.citationCoverage.state === "unavailable";
27500
27551
  if (aUnavailable !== bUnavailable) return aUnavailable ? 1 : -1;
27501
- if (aUnavailable || bUnavailable) return compareText13(a.label, b.label) || compareText13(a.groupKey, b.groupKey);
27552
+ if (aUnavailable || bUnavailable) return compareText(a.label, b.label) || compareText(a.groupKey, b.groupKey);
27502
27553
  if (a.mentionCoverage.state === "available" && b.mentionCoverage.state === "available" && a.mentionCoverage.value !== b.mentionCoverage.value) {
27503
27554
  return a.mentionCoverage.value - b.mentionCoverage.value;
27504
27555
  }
27505
27556
  if (a.citationCoverage.state === "available" && b.citationCoverage.state === "available" && a.citationCoverage.value !== b.citationCoverage.value) {
27506
27557
  return a.citationCoverage.value - b.citationCoverage.value;
27507
27558
  }
27508
- return compareText13(a.label, b.label) || compareText13(a.groupKey, b.groupKey);
27559
+ return compareText(a.label, b.label) || compareText(a.groupKey, b.groupKey);
27509
27560
  }
27510
27561
  function mentionRanking(rows, limit) {
27511
27562
  const eligible = [];
@@ -27526,7 +27577,7 @@ function mentionRanking(rows, limit) {
27526
27577
  });
27527
27578
  }
27528
27579
  }
27529
- const byLabel = (a, b) => compareText13(a.label, b.label) || compareText13(a.targetKey, b.targetKey);
27580
+ const byLabel = (a, b) => compareText(a.label, b.label) || compareText(a.targetKey, b.targetKey);
27530
27581
  const byRate = (direction) => (a, b) => direction * (a.mentionCoverage.value - b.mentionCoverage.value) || byLabel(a, b);
27531
27582
  return {
27532
27583
  eligiblePropertyCount: eligible.length,
@@ -27676,7 +27727,7 @@ function compactPortfolioPage(response, query, versionId, evidence, cursor) {
27676
27727
  function portfolioAnswerEvidence(plan, materialized, filters, targetKeys) {
27677
27728
  const targets = new Set(targetKeys);
27678
27729
  const executions = new Set(plan.assignments.filter((assignment) => targets.has(assignment.targetKey) && (filters.queryClass === "all" || assignment.queryClass === filters.queryClass)).map((assignment) => assignment.executionNodeKey));
27679
- const slots = materialized.input.expectedSlots.filter((slot) => executions.has(slot.executionId) && (filters.provider === void 0 || normalizeText2(slot.provider) === normalizeText2(filters.provider)) && (filters.location === void 0 || normalizeMeasurementLocation(slot.location) === normalizeMeasurementLocation(filters.location)));
27730
+ const slots = materialized.input.expectedSlots.filter((slot) => executions.has(slot.executionId) && (filters.provider === void 0 || normalizeIdentityText(slot.provider) === normalizeIdentityText(filters.provider)) && (filters.location === void 0 || normalizeMeasurementLocation(slot.location) === normalizeMeasurementLocation(filters.location)));
27680
27731
  const mentionsBySlot = /* @__PURE__ */ new Map();
27681
27732
  for (const answer of materialized.evidence.answers) pushTo(mentionsBySlot, answer.expectedSlotId, answer.mentioned);
27682
27733
  const selected = [];
@@ -27818,7 +27869,7 @@ function portfolioResponse(db, active, plan, query, ownDomains) {
27818
27869
  },
27819
27870
  measurement,
27820
27871
  queryClass: query.queryClass,
27821
- engines: [...engines].sort(compareText13),
27872
+ engines: [...engines].sort(compareText),
27822
27873
  metrics: {
27823
27874
  propertiesMentioned: countMetric2(overview.propertiesMentioned),
27824
27875
  propertiesNeverMentioned: neverMentionedMetric(overview.propertiesMentioned, targetKeys.length),
@@ -27885,7 +27936,7 @@ function tieByMetro(rows) {
27885
27936
  add(row.metro);
27886
27937
  for (const other of row.otherMetros ?? []) add(other);
27887
27938
  }
27888
- return [...counts2.values()].sort((left, right) => right.count - left.count || (left.metro === null ? 1 : 0) - (right.metro === null ? 1 : 0) || compareText13(left.metro?.label ?? "", right.metro?.label ?? "") || compareText13(left.metro?.groupKey ?? "", right.metro?.groupKey ?? "")).map(({ metro, count: count6 }) => ({ metro: metro?.label ?? null, count: count6 }));
27939
+ return [...counts2.values()].sort((left, right) => right.count - left.count || (left.metro === null ? 1 : 0) - (right.metro === null ? 1 : 0) || compareText(left.metro?.label ?? "", right.metro?.label ?? "") || compareText(left.metro?.groupKey ?? "", right.metro?.groupKey ?? "")).map(({ metro, count: count6 }) => ({ metro: metro?.label ?? null, count: count6 }));
27889
27940
  }
27890
27941
  function tieNamedInstead(rows) {
27891
27942
  const grouped = /* @__PURE__ */ new Map();
@@ -27893,14 +27944,14 @@ function tieNamedInstead(rows) {
27893
27944
  forEachNameWrittenInstead(target, population.answers, (key, name, answer) => {
27894
27945
  const existing = grouped.get(key);
27895
27946
  if (existing) {
27896
- if (compareText13(name, existing.name) < 0) existing.name = name;
27947
+ if (compareText(name, existing.name) < 0) existing.name = name;
27897
27948
  existing.answers.add(answer.snapshot.id);
27898
27949
  } else {
27899
27950
  grouped.set(key, { name, answers: /* @__PURE__ */ new Set([answer.snapshot.id]) });
27900
27951
  }
27901
27952
  });
27902
27953
  }
27903
- const counted = [...grouped.values()].map((row) => ({ name: row.name, answers: row.answers.size })).sort((left, right) => right.answers - left.answers || compareText13(left.name, right.name));
27954
+ const counted = [...grouped.values()].map((row) => ({ name: row.name, answers: row.answers.size })).sort((left, right) => right.answers - left.answers || compareText(left.name, right.name));
27904
27955
  return { rows: counted.slice(0, MEASUREMENT_PORTFOLIO_TIE_NAMED_INSTEAD_LIMIT), total: counted.length };
27905
27956
  }
27906
27957
  function weakestAnswerSources(rows, ownDomains) {
@@ -28007,10 +28058,10 @@ function scopeKey(run) {
28007
28058
  if (run.measurementScope === null) return "full";
28008
28059
  const scope = run.measurementScope;
28009
28060
  return JSON.stringify({
28010
- groups: [...scope.groups].sort(compareText13),
28011
- targets: [...scope.targets].sort(compareText13),
28012
- queries: [...scope.queries].sort(compareText13),
28013
- resolvedTargets: [...scope.resolvedTargets].sort(compareText13)
28061
+ groups: [...scope.groups].sort(compareText),
28062
+ targets: [...scope.targets].sort(compareText),
28063
+ queries: [...scope.queries].sort(compareText),
28064
+ resolvedTargets: [...scope.resolvedTargets].sort(compareText)
28014
28065
  });
28015
28066
  }
28016
28067
  function previousComparableRun(db, active, current) {
@@ -28142,7 +28193,7 @@ function classifyPropertyMove(mention, citation) {
28142
28193
  };
28143
28194
  }
28144
28195
  function compareChangeLabel(left, right) {
28145
- return compareText13(left.row.label, right.row.label) || compareText13(left.row.targetKey, right.row.targetKey);
28196
+ return compareText(left.row.label, right.row.label) || compareText(left.row.targetKey, right.row.targetKey);
28146
28197
  }
28147
28198
  function compareChangeMagnitude(left, right) {
28148
28199
  const larger = (move) => Math.max(move.mentionSize, move.citationSize);
@@ -28320,11 +28371,11 @@ function qualityResponse(db, active, plan, query) {
28320
28371
  const materialized = materializeWithDb(db, active, plan, run);
28321
28372
  const expected = materialized.manifest.expectedSlots.length;
28322
28373
  const snapshots = materialized.snapshots;
28323
- const expectedSlotKeys = new Set(materialized.manifest.expectedSlots.map((slot) => `${slot.executionId}\0${normalizeText2(slot.provider)}`));
28374
+ const expectedSlotKeys = new Set(materialized.manifest.expectedSlots.map((slot) => `${slot.executionId}\0${normalizeIdentityText(slot.provider)}`));
28324
28375
  const observedSlotKeys = /* @__PURE__ */ new Set();
28325
28376
  for (const snapshot of snapshots) {
28326
28377
  if (snapshot.measurementExecutionId === null) throw new Error("stored result has no frozen execution identity");
28327
- const key = `${snapshot.measurementExecutionId}\0${normalizeText2(snapshot.provider)}`;
28378
+ const key = `${snapshot.measurementExecutionId}\0${normalizeIdentityText(snapshot.provider)}`;
28328
28379
  if (!expectedSlotKeys.has(key) || observedSlotKeys.has(key)) {
28329
28380
  throw new Error("stored results do not map one-to-one to the frozen manifest");
28330
28381
  }
@@ -28515,16 +28566,10 @@ var VisibilityReportScopeError = class extends Error {
28515
28566
  }
28516
28567
  };
28517
28568
  var ALL_CLASSES = ["branded", "non-brand", "unknown"];
28518
- function compareText14(left, right) {
28519
- return left < right ? -1 : left > right ? 1 : 0;
28520
- }
28521
- function normalizeText3(value) {
28522
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
28523
- }
28524
28569
  function locationMatches(location, selected) {
28525
28570
  if (selected.kind === "all") return true;
28526
28571
  if (selected.kind === "none") return location === null;
28527
- return location !== null && normalizeText3(location) === normalizeText3(selected.value);
28572
+ return location !== null && normalizeIdentityText(location) === normalizeIdentityText(selected.value);
28528
28573
  }
28529
28574
  function selectedClasses(value) {
28530
28575
  return value === "all" ? ALL_CLASSES : [value];
@@ -28580,15 +28625,15 @@ function scopeResolution(definition, selection) {
28580
28625
  }
28581
28626
  function scopeTargetKeys(definition, selection) {
28582
28627
  const resolution = scopeResolution(definition, selection);
28583
- if (resolution.market !== void 0) return [...new Set(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey))].sort(compareText14);
28584
- if (selection.scope === "project") return definition.targets.map((target) => target.id).sort(compareText14);
28628
+ if (resolution.market !== void 0) return sortedUnique(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey));
28629
+ if (selection.scope === "project") return definition.targets.map((target) => target.id).sort(compareText);
28585
28630
  if (selection.scope === "property") return [resolution.option.id];
28586
28631
  if (selection.scope === "group") {
28587
28632
  const group = definition.groups.find((candidate) => candidate.id === resolution.option.id);
28588
28633
  if (!group) throw new VisibilityReportScopeError(`Group "${resolution.option.id}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-scope"], kind: "group", key: resolution.option.id });
28589
- return [...group.targetKeys].sort(compareText14);
28634
+ return [...group.targetKeys].sort(compareText);
28590
28635
  }
28591
- return [...new Set(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey))].sort(compareText14);
28636
+ return sortedUnique(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey));
28592
28637
  }
28593
28638
  function observationIndex(run) {
28594
28639
  const index2 = /* @__PURE__ */ new Map();
@@ -28624,16 +28669,16 @@ function candidatesFor(run, selection, queryClass) {
28624
28669
  bySlot.set(slot.id, list);
28625
28670
  }
28626
28671
  }
28627
- const provider = selection.provider === void 0 ? void 0 : normalizeText3(selection.provider);
28628
- const model = selection.model === void 0 ? void 0 : normalizeText3(selection.model);
28672
+ const provider = selection.provider === void 0 ? void 0 : normalizeIdentityText(selection.provider);
28673
+ const model = selection.model === void 0 ? void 0 : normalizeIdentityText(selection.model);
28629
28674
  const candidates = [];
28630
28675
  for (const [slotId, edges] of bySlot) {
28631
28676
  const slot = slotsById.get(slotId);
28632
28677
  if (!slot) throw new Error(`Frozen slot ${slotId} disappeared during reconstruction`);
28633
28678
  const observation = observations.get(slot.id) ?? null;
28634
- if (provider !== void 0 && normalizeText3(slot.provider) !== provider) continue;
28679
+ if (provider !== void 0 && normalizeIdentityText(slot.provider) !== provider) continue;
28635
28680
  if (!locationMatches(slot.location, selection.location)) continue;
28636
- if (model !== void 0 && (observation === null || observation.model === null || normalizeText3(observation.model) !== model)) continue;
28681
+ if (model !== void 0 && (observation === null || observation.model === null || normalizeIdentityText(observation.model) !== model)) continue;
28637
28682
  candidates.push({ run, slot, edges, observation });
28638
28683
  }
28639
28684
  return { resolution, candidates };
@@ -28702,7 +28747,7 @@ function candidateKey(candidate) {
28702
28747
  return `${candidate.run.id}\0${candidate.slot.id}`;
28703
28748
  }
28704
28749
  function selectedTargetKeys(candidates) {
28705
- return [...new Set(candidates.flatMap((candidate) => candidate.edges.map((edge) => edge.targetKey)))].sort(compareText14);
28750
+ return sortedUnique(candidates.flatMap((candidate) => candidate.edges.map((edge) => edge.targetKey)));
28706
28751
  }
28707
28752
  function targetMetrics(candidates, targets, targetKey) {
28708
28753
  const own = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey));
@@ -28773,7 +28818,7 @@ function summary2(candidates, definition, populationTargetKeys = selectedTargetK
28773
28818
  }
28774
28819
  function queryRows(candidates, definition, selection) {
28775
28820
  const targets = targetMap(definition);
28776
- const narrowed = candidates.filter((candidate) => (selection.queryKey === void 0 || candidate.slot.queryKey === selection.queryKey) && (selection.queryId === void 0 || candidate.slot.queryId === selection.queryId) && (selection.search === void 0 || normalizeText3(candidate.slot.query).includes(normalizeText3(selection.search))));
28821
+ const narrowed = candidates.filter((candidate) => (selection.queryKey === void 0 || candidate.slot.queryKey === selection.queryKey) && (selection.queryId === void 0 || candidate.slot.queryId === selection.queryId) && (selection.search === void 0 || normalizeIdentityText(candidate.slot.query).includes(normalizeIdentityText(selection.search))));
28777
28822
  const groups = /* @__PURE__ */ new Map();
28778
28823
  for (const candidate of narrowed) {
28779
28824
  const model = candidate.observation?.model ?? null;
@@ -28786,7 +28831,7 @@ function queryRows(candidates, definition, selection) {
28786
28831
  const first = rows[0];
28787
28832
  const value = coverageSummary(rows, definition, targets);
28788
28833
  return {
28789
- sourceSnapshotIds: [...new Set(rows.flatMap((row) => row.observation ? [row.observation.answerId] : []))].sort(compareText14),
28834
+ sourceSnapshotIds: sortedUnique(rows.flatMap((row) => row.observation ? [row.observation.answerId] : [])),
28790
28835
  queryKey: first.slot.queryKey,
28791
28836
  queryId: first.slot.queryId,
28792
28837
  query: first.slot.query,
@@ -28795,14 +28840,14 @@ function queryRows(candidates, definition, selection) {
28795
28840
  location: first.slot.location,
28796
28841
  targetKeys: selectedTargetKeys(rows),
28797
28842
  ...(() => {
28798
- const marketKeys = [...new Set(rows.flatMap((row) => row.edges.flatMap((edge) => edge.marketKeys)))].sort(compareText14);
28843
+ const marketKeys = sortedUnique(rows.flatMap((row) => row.edges.flatMap((edge) => edge.marketKeys)));
28799
28844
  return marketKeys.length === 0 ? {} : { marketKeys };
28800
28845
  })(),
28801
28846
  answerCount: value.answerCount,
28802
28847
  mentionCoverage: value.mentionCoverage,
28803
28848
  citationCoverage: value.citationCoverage
28804
28849
  };
28805
- }).sort((left, right) => (selection.scope === "property" && selection.marketKey === void 0 ? compareText14((left.marketKeys ?? []).join("\0"), (right.marketKeys ?? []).join("\0")) : 0) || compareText14(normalizeText3(left.query), normalizeText3(right.query)) || compareText14(left.provider, right.provider) || compareText14(left.model ?? "", right.model ?? "") || compareText14(left.location ?? "", right.location ?? "") || compareText14(left.queryKey, right.queryKey));
28850
+ }).sort((left, right) => (selection.scope === "property" && selection.marketKey === void 0 ? compareText((left.marketKeys ?? []).join("\0"), (right.marketKeys ?? []).join("\0")) : 0) || compareText(normalizeIdentityText(left.query), normalizeIdentityText(right.query)) || compareText(left.provider, right.provider) || compareText(left.model ?? "", right.model ?? "") || compareText(left.location ?? "", right.location ?? "") || compareText(left.queryKey, right.queryKey));
28806
28851
  }
28807
28852
  function evidenceRows(candidates, definition, selection) {
28808
28853
  if (selection.queryKey === void 0 && selection.queryId === void 0) return [];
@@ -28830,10 +28875,10 @@ function evidenceRows(candidates, definition, selection) {
28830
28875
  sources: [...observation.sources],
28831
28876
  observedCompetitors: [...observation.observedCompetitorNames]
28832
28877
  };
28833
- }).sort((left, right) => compareText14(left.createdAt, right.createdAt) || compareText14(left.answerId, right.answerId));
28878
+ }).sort((left, right) => compareText(left.createdAt, right.createdAt) || compareText(left.answerId, right.answerId));
28834
28879
  }
28835
28880
  function competitorRows(candidates) {
28836
- const domains = [...new Set(candidates.flatMap((candidate) => candidate.edges.flatMap((edge) => edge.competitorDomains)))].sort(compareText14);
28881
+ const domains = sortedUnique(candidates.flatMap((candidate) => candidate.edges.flatMap((edge) => edge.competitorDomains)));
28837
28882
  return domains.map((domain) => {
28838
28883
  const eligible = candidates.map((candidate) => {
28839
28884
  const edges = candidate.edges.filter((edge) => edge.competitorDomains.includes(domain));
@@ -28864,7 +28909,7 @@ function observedCompetitorRows(candidates) {
28864
28909
  answersByName.set(name, answers);
28865
28910
  }
28866
28911
  }
28867
- return [...answersByName].map(([name, answers]) => ({ name, answerCount: answers.size })).sort((left, right) => compareText14(left.name, right.name));
28912
+ return [...answersByName].map(([name, answers]) => ({ name, answerCount: answers.size })).sort((left, right) => compareText(left.name, right.name));
28868
28913
  }
28869
28914
  function narrowedToTargetKeys(candidate, targetKeys) {
28870
28915
  const edges = candidate.edges.filter((edge) => targetKeys.has(edge.targetKey));
@@ -28883,7 +28928,7 @@ function breakdown(candidates, definition, populationTargetKeys = selectedTarget
28883
28928
  queryCount: new Set(own.map((candidate) => candidate.slot.queryKey)).size,
28884
28929
  ...metrics
28885
28930
  };
28886
- }).sort((left, right) => compareText14(left.label, right.label) || compareText14(left.id, right.id));
28931
+ }).sort((left, right) => compareText(left.label, right.label) || compareText(left.id, right.id));
28887
28932
  const populationTargetSet = new Set(populationTargetKeys);
28888
28933
  const groups = definition.groups.map((group) => ({ group, targetKeys: group.targetKeys.filter((targetKey) => populationTargetSet.has(targetKey)) })).filter(({ targetKeys }) => targetKeys.length > 0).map(({ group, targetKeys }) => {
28889
28934
  const groupTargetKeys = new Set(targetKeys);
@@ -28897,7 +28942,7 @@ function breakdown(candidates, definition, populationTargetKeys = selectedTarget
28897
28942
  mentionCoverage: metrics,
28898
28943
  citationCoverage: citations
28899
28944
  };
28900
- }).sort((left, right) => compareText14(left.label, right.label) || compareText14(left.id, right.id));
28945
+ }).sort((left, right) => compareText(left.label, right.label) || compareText(left.id, right.id));
28901
28946
  return { properties, groups };
28902
28947
  }
28903
28948
  function fingerprint(value) {
@@ -28974,16 +29019,16 @@ function filterOptions(definition, selection, run) {
28974
29019
  const resolution = scopeResolution(definition, selection);
28975
29020
  const slots = definition.slots.filter((slot) => definition.edges.some((edge) => edge.executionId === slot.executionId && resolution.edgeIds.has(edge.id)));
28976
29021
  const observations = run ? observationIndex(run) : /* @__PURE__ */ new Map();
28977
- const providers = [...new Set(slots.map((slot) => slot.provider))].sort(compareText14);
29022
+ const providers = sortedUnique(slots.map((slot) => slot.provider));
28978
29023
  const models = [...new Map(slots.flatMap((slot) => {
28979
29024
  const observation = observations.get(slot.id);
28980
29025
  return observation?.model === null || observation === void 0 ? [] : [[`${slot.provider}\0${observation.model}`, { provider: slot.provider, model: observation.model }]];
28981
- })).values()].sort((left, right) => compareText14(left.provider, right.provider) || compareText14(left.model, right.model));
29026
+ })).values()].sort((left, right) => compareText(left.provider, right.provider) || compareText(left.model, right.model));
28982
29027
  const locations = [{ kind: "all" }];
28983
29028
  const seen2 = /* @__PURE__ */ new Set(["all"]);
28984
29029
  for (const slot of slots) {
28985
29030
  const value = slot.location === null ? { kind: "none" } : { kind: "exact", value: slot.location };
28986
- const key = value.kind === "exact" ? `exact:${normalizeText3(value.value)}` : value.kind;
29031
+ const key = value.kind === "exact" ? `exact:${normalizeIdentityText(value.value)}` : value.kind;
28987
29032
  if (!seen2.has(key)) {
28988
29033
  seen2.add(key);
28989
29034
  locations.push(value);
@@ -29074,7 +29119,7 @@ function emptyPopulation(queryClass, definition, input) {
29074
29119
  };
29075
29120
  }
29076
29121
  function buildVisibilityReport(input) {
29077
- const runs2 = input.runs.filter((run) => !run.probe && inTime(run, input.selection)).sort((left, right) => compareText14(left.createdAt, right.createdAt) || compareText14(left.id, right.id));
29122
+ const runs2 = input.runs.filter((run) => !run.probe && inTime(run, input.selection)).sort((left, right) => compareText(left.createdAt, right.createdAt) || compareText(left.id, right.id));
29078
29123
  const selectedRun = input.selection.runId === void 0 ? input.preferredRunId === void 0 ? runs2.at(-1) : runs2.find((run) => run.id === input.preferredRunId) : runs2.find((run) => run.id === input.selection.runId);
29079
29124
  const definition = selectedRun?.definition ?? input.activeDefinition;
29080
29125
  const selectedResolution = scopeResolution(definition, input.selection);
@@ -29208,9 +29253,6 @@ function visibilityComparisonPopulation(run, selection) {
29208
29253
  }
29209
29254
 
29210
29255
  // ../api-routes/src/visibility-report.ts
29211
- function normalizedText3(value) {
29212
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
29213
- }
29214
29256
  function observedCompetitorNames2(values) {
29215
29257
  const names = [];
29216
29258
  const seen2 = /* @__PURE__ */ new Set();
@@ -29294,7 +29336,7 @@ function competitorSignals(snapshot, matchers, preparedAnswerText = preparedAnsw
29294
29336
  try {
29295
29337
  return normalizeMeasurementHost(value);
29296
29338
  } catch {
29297
- return normalizedText3(value);
29339
+ return normalizeIdentityText(value);
29298
29340
  }
29299
29341
  }));
29300
29342
  const cited = [...matchers.keys()].filter((domain) => citedDomains.has(normalizeMeasurementHost(domain)));
@@ -29462,7 +29504,7 @@ function stableSimpleDefinitionId(definition) {
29462
29504
  return `simple:${createHash8("sha256").update(JSON.stringify(source)).digest("hex")}`;
29463
29505
  }
29464
29506
  function simpleModelFingerprint(definition) {
29465
- const engines = [...definition.engines].map((engine) => ({ provider: normalizedText3(engine.provider), requestedModel: engine.requestedModel })).sort((left, right) => left.provider.localeCompare(right.provider, "en"));
29507
+ const engines = [...definition.engines].map((engine) => ({ provider: normalizeIdentityText(engine.provider), requestedModel: engine.requestedModel })).sort((left, right) => left.provider.localeCompare(right.provider, "en"));
29466
29508
  return `simple-model:${createHash8("sha256").update(JSON.stringify(engines)).digest("hex")}`;
29467
29509
  }
29468
29510
  function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
@@ -29479,21 +29521,21 @@ function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
29479
29521
  matches.push(query);
29480
29522
  queriesByText.set(query.queryText, matches);
29481
29523
  }
29482
- const enginesByProvider = new Map(definition.engines.map((engine) => [normalizedText3(engine.provider), engine]));
29524
+ const enginesByProvider = new Map(definition.engines.map((engine) => [normalizeIdentityText(engine.provider), engine]));
29483
29525
  const competitors2 = definition.competitors === void 0 ? null : frozenCompetitorMatchers(definition.competitors);
29484
29526
  const slots = definition.queries.flatMap((query) => definition.engines.map((engine) => ({
29485
- id: `slot:simple:${query.queryId}:${normalizedText3(engine.provider)}`,
29527
+ id: `slot:simple:${query.queryId}:${normalizeIdentityText(engine.provider)}`,
29486
29528
  executionId: `simple:${query.queryId}`,
29487
29529
  queryKey: query.queryId,
29488
29530
  queryId: query.queryId,
29489
29531
  query: query.queryText,
29490
- provider: normalizedText3(engine.provider),
29532
+ provider: normalizeIdentityText(engine.provider),
29491
29533
  location: definition.location?.label ?? null
29492
29534
  })));
29493
29535
  const slotKeys = new Set(slots.map((slot) => slot.id));
29494
29536
  const observations = [];
29495
29537
  for (const snapshot of snapshots) {
29496
- const provider = normalizedText3(snapshot.provider);
29538
+ const provider = normalizeIdentityText(snapshot.provider);
29497
29539
  const engine = enginesByProvider.get(provider);
29498
29540
  if (!engine) throw new Error(`Frozen simple definition has no provider ${snapshot.provider} for stored snapshot ${snapshot.id}`);
29499
29541
  const query = snapshot.queryId === null ? (() => {
@@ -29568,7 +29610,7 @@ function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
29568
29610
  queryKey: snapshot.queryId ?? `legacy:${snapshot.queryText ?? snapshot.id}`,
29569
29611
  queryId: snapshot.queryId,
29570
29612
  query: snapshot.queryText ?? "",
29571
- provider: normalizedText3(snapshot.provider),
29613
+ provider: normalizeIdentityText(snapshot.provider),
29572
29614
  location: snapshot.location
29573
29615
  }));
29574
29616
  return {
@@ -29937,17 +29979,11 @@ import { and as and29, desc as desc16, eq as eq37, inArray as inArray18, isNotNu
29937
29979
  var SOURCE_LIMIT = 100;
29938
29980
  var MAX_REVIEW_AGE_MS = 15 * 60 * 1e3;
29939
29981
  var MAX_REVIEW_FUTURE_SKEW_MS = 60 * 1e3;
29940
- function compareText15(left, right) {
29941
- return left < right ? -1 : left > right ? 1 : 0;
29942
- }
29943
- function normalizeText4(value) {
29944
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
29945
- }
29946
29982
  function unique2(values) {
29947
29983
  return [...new Set(values)];
29948
29984
  }
29949
- function stableNewQueryId(projectId, normalizedText4) {
29950
- return `qtc_${sha256Hex(`${projectId}\0${normalizedText4}`).slice(0, 32)}`;
29985
+ function stableNewQueryId(projectId, normalizedText) {
29986
+ return `qtc_${sha256Hex(`${projectId}\0${normalizedText}`).slice(0, 32)}`;
29951
29987
  }
29952
29988
  function providerNames(project, opts) {
29953
29989
  return resolveRunProviderSelection({
@@ -29981,7 +30017,7 @@ function simpleDispatchEngines(project, opts) {
29981
30017
  // override/configuration, just as it is in JobRunner, which also resolves
29982
30018
  // a retired override to the id that answers now.
29983
30019
  requestedModel: resolvedOverride(provider, overrides[provider]) ?? summaries.get(provider)?.model ?? null
29984
- })).sort((left, right) => compareText15(left.provider, right.provider));
30020
+ })).sort((left, right) => compareText(left.provider, right.provider));
29985
30021
  }
29986
30022
  function resolvedOverride(provider, model) {
29987
30023
  return model === void 0 ? void 0 : resolveProviderModel(provider, model);
@@ -29992,12 +30028,12 @@ function simpleDefaultLocation(project) {
29992
30028
  }
29993
30029
  function contextKey(context) {
29994
30030
  const location = context.location ? [context.location.label, context.location.city, context.location.region, context.location.country, context.location.timezone ?? ""].join("\0") : "";
29995
- return canonicalJson({ providers: [...context.providers].sort(compareText15), models: context.models, location });
30031
+ return canonicalJson({ providers: [...context.providers].sort(compareText), models: context.models, location });
29996
30032
  }
29997
30033
  function uniqueContexts(contexts) {
29998
30034
  const byKey = /* @__PURE__ */ new Map();
29999
30035
  for (const context of contexts) byKey.set(contextKey(context), context);
30000
- return [...byKey.values()].sort((left, right) => compareText15(contextKey(left), contextKey(right)));
30036
+ return [...byKey.values()].sort((left, right) => compareText(contextKey(left), contextKey(right)));
30001
30037
  }
30002
30038
  function defaultContexts(project, opts, mode) {
30003
30039
  const providers = providerNames(project, opts);
@@ -30028,7 +30064,7 @@ function workspaceFingerprint(project, active, queryRows2) {
30028
30064
  defaultLocation: project.defaultLocation
30029
30065
  },
30030
30066
  active: active ? { id: active.version.id, revision: active.version.revision, compiledChecksum: active.version.compiledChecksum } : null,
30031
- queries: queryRows2.map((row) => ({ id: row.id, query: row.query, provenance: row.provenance })).sort((left, right) => compareText15(left.id, right.id))
30067
+ queries: queryRows2.map((row) => ({ id: row.id, query: row.query, provenance: row.provenance })).sort((left, right) => compareText(left.id, right.id))
30032
30068
  }))}`;
30033
30069
  }
30034
30070
  function readWorkspace(db, project) {
@@ -30065,9 +30101,9 @@ function simpleExecutionSignature(input) {
30065
30101
  return canonicalJson({
30066
30102
  identity: {
30067
30103
  displayName: input.identity.displayName,
30068
- aliases: [...input.identity.aliases].sort(compareText15),
30104
+ aliases: [...input.identity.aliases].sort(compareText),
30069
30105
  canonicalDomain: input.identity.canonicalDomain,
30070
- ownedDomains: [...input.identity.ownedDomains].sort(compareText15)
30106
+ ownedDomains: [...input.identity.ownedDomains].sort(compareText)
30071
30107
  },
30072
30108
  country: input.country,
30073
30109
  language: input.language,
@@ -30075,7 +30111,7 @@ function simpleExecutionSignature(input) {
30075
30111
  engines: input.engines.map((engine) => ({
30076
30112
  provider: engine.provider.trim().toLocaleLowerCase("en"),
30077
30113
  requestedModel: engine.requestedModel
30078
- })).sort((left, right) => compareText15(left.provider, right.provider))
30114
+ })).sort((left, right) => compareText(left.provider, right.provider))
30079
30115
  });
30080
30116
  }
30081
30117
  function currentSimpleExecutionSignature(project, opts) {
@@ -30338,7 +30374,7 @@ function measuredAtByQuery(db, state, opts) {
30338
30374
  for (const [queryId, signatures] of activeSignatures) {
30339
30375
  const measured = measurements.get(queryId);
30340
30376
  if (!measured || [...signatures].some((signature) => !measured.has(signature))) continue;
30341
- const latest = [...measured.values()].sort(compareText15).at(-1);
30377
+ const latest = [...measured.values()].sort(compareText).at(-1);
30342
30378
  if (latest) result.set(queryId, latest);
30343
30379
  }
30344
30380
  return result;
@@ -30353,7 +30389,7 @@ function marketKeysByEdge(plan) {
30353
30389
  else result.set(key, [scope.stableKey]);
30354
30390
  }
30355
30391
  }
30356
- for (const keys of result.values()) keys.sort(compareText15);
30392
+ for (const keys of result.values()) keys.sort(compareText);
30357
30393
  return result;
30358
30394
  }
30359
30395
  function trackedRows(db, state, queryRows2, plan, opts) {
@@ -30362,12 +30398,12 @@ function trackedRows(db, state, queryRows2, plan, opts) {
30362
30398
  return [...queryRows2.values()].map((row) => ({
30363
30399
  queryId: row.id,
30364
30400
  queryText: row.query,
30365
- normalizedText: normalizeText4(row.query),
30401
+ normalizedText: normalizeIdentityText(row.query),
30366
30402
  provenance: parseQueryProvenance(row.provenance),
30367
30403
  state: measured.has(row.id) ? "tracked" : "awaiting-sweep",
30368
30404
  lastMeasuredAt: measured.get(row.id) ?? null,
30369
30405
  assignments: []
30370
- })).sort((left, right) => compareText15(left.normalizedText, right.normalizedText) || compareText15(left.queryId, right.queryId));
30406
+ })).sort((left, right) => compareText(left.normalizedText, right.normalizedText) || compareText(left.queryId, right.queryId));
30371
30407
  }
30372
30408
  const snapshots = new Map(plan.querySnapshots.map((snapshot) => [snapshot.queryId, snapshot]));
30373
30409
  const allIds = unique2([...queryRows2.keys(), ...snapshots.keys()]);
@@ -30405,7 +30441,7 @@ function trackedRows(db, state, queryRows2, plan, opts) {
30405
30441
  } else {
30406
30442
  assignments.set(assignment.targetKey, {
30407
30443
  targetKey: assignment.targetKey,
30408
- groupKeys: [...groupsByTarget.get(assignment.targetKey) ?? []].sort(compareText15),
30444
+ groupKeys: [...groupsByTarget.get(assignment.targetKey) ?? []].sort(compareText),
30409
30445
  marketKeys: [...marketsByEdge.get(edgeKey2) ?? []],
30410
30446
  queryClass: assignment.queryClass,
30411
30447
  classificationSource: source,
@@ -30416,17 +30452,17 @@ function trackedRows(db, state, queryRows2, plan, opts) {
30416
30452
  return {
30417
30453
  queryId,
30418
30454
  queryText,
30419
- normalizedText: normalizeText4(queryText),
30455
+ normalizedText: normalizeIdentityText(queryText),
30420
30456
  provenance: snapshot ? provenanceFromPlan(snapshot.provenance) : parseQueryProvenance(row?.provenance ?? null),
30421
30457
  state: measured.has(queryId) ? "tracked" : "awaiting-sweep",
30422
30458
  lastMeasuredAt: measured.get(queryId) ?? null,
30423
30459
  assignments: [...assignments.values()].map((assignment) => ({
30424
30460
  ...assignment,
30425
- marketKeys: unique2(assignment.marketKeys).sort(compareText15),
30461
+ marketKeys: unique2(assignment.marketKeys).sort(compareText),
30426
30462
  contexts: uniqueContexts(assignment.contexts)
30427
- })).sort((left, right) => compareText15(left.targetKey, right.targetKey))
30463
+ })).sort((left, right) => compareText(left.targetKey, right.targetKey))
30428
30464
  };
30429
- }).sort((left, right) => compareText15(left.normalizedText, right.normalizedText) || compareText15(left.queryId, right.queryId));
30465
+ }).sort((left, right) => compareText(left.normalizedText, right.normalizedText) || compareText(left.queryId, right.queryId));
30430
30466
  }
30431
30467
  function savedSources(db, projectId) {
30432
30468
  const research = db.select({
@@ -30509,9 +30545,9 @@ function resolveAudience(plan, input) {
30509
30545
  throw validationError("Select at least one Property, group, or market for a portfolio query.");
30510
30546
  }
30511
30547
  return {
30512
- targetKeys: [...targetKeys].sort(compareText15),
30513
- groupKeys: groupKeys.sort(compareText15),
30514
- marketKeys: marketKeys.sort(compareText15)
30548
+ targetKeys: [...targetKeys].sort(compareText),
30549
+ groupKeys: groupKeys.sort(compareText),
30550
+ marketKeys: marketKeys.sort(compareText)
30515
30551
  };
30516
30552
  }
30517
30553
  function contextsFromInput(project, contexts) {
@@ -30522,7 +30558,7 @@ function contextsFromInput(project, contexts) {
30522
30558
  if (context.location !== null && !location) {
30523
30559
  throw validationError(`Execution context names a location the project does not configure: ${context.location}`);
30524
30560
  }
30525
- const providers = unique2(context.providers.map((provider) => provider.trim().toLocaleLowerCase("en")).filter(Boolean)).sort(compareText15);
30561
+ const providers = unique2(context.providers.map((provider) => provider.trim().toLocaleLowerCase("en")).filter(Boolean)).sort(compareText);
30526
30562
  for (const provider of Object.keys(context.models)) {
30527
30563
  if (!providers.includes(provider.trim().toLocaleLowerCase("en"))) {
30528
30564
  throw validationError(`Model "${provider}" is not selected by this execution context.`);
@@ -30747,23 +30783,23 @@ function edgeKey(edge) {
30747
30783
  function queryIdsByNormalized(state) {
30748
30784
  const result = /* @__PURE__ */ new Map();
30749
30785
  for (const row of state.queryRows) {
30750
- const key = normalizeText4(row.query);
30786
+ const key = normalizeIdentityText(row.query);
30751
30787
  const ids = result.get(key);
30752
30788
  if (ids) ids.push(row.id);
30753
30789
  else result.set(key, [row.id]);
30754
30790
  }
30755
30791
  for (const snapshot of state.active?.plan.querySnapshots ?? []) {
30756
- const key = normalizeText4(snapshot.queryText);
30792
+ const key = normalizeIdentityText(snapshot.queryText);
30757
30793
  const ids = result.get(key);
30758
30794
  if (ids) ids.push(snapshot.queryId);
30759
30795
  else result.set(key, [snapshot.queryId]);
30760
30796
  }
30761
- for (const ids of result.values()) ids.sort(compareText15);
30797
+ for (const ids of result.values()) ids.sort(compareText);
30762
30798
  return result;
30763
30799
  }
30764
- function preferredQueryId(state, normalizedText4) {
30765
- const candidates = queryIdsByNormalized(state).get(normalizedText4) ?? [];
30766
- const bound = new Set((state.active?.plan.querySnapshots ?? []).filter((snapshot) => normalizeText4(snapshot.queryText) === normalizedText4).map((snapshot) => snapshot.queryId));
30800
+ function preferredQueryId(state, normalizedText) {
30801
+ const candidates = queryIdsByNormalized(state).get(normalizedText) ?? [];
30802
+ const bound = new Set((state.active?.plan.querySnapshots ?? []).filter((snapshot) => normalizeIdentityText(snapshot.queryText) === normalizedText).map((snapshot) => snapshot.queryId));
30767
30803
  return candidates.find((candidate) => bound.has(candidate)) ?? candidates.at(0) ?? null;
30768
30804
  }
30769
30805
  function rowsEqual(before, after) {
@@ -30785,7 +30821,7 @@ function changeRows(ids, rows, plan, fallbackPlan = null) {
30785
30821
  queryText,
30786
30822
  assignmentCount: (plan ?? fallbackPlan)?.assignments.filter((assignment) => assignment.queryId === queryId).length ?? 0
30787
30823
  }];
30788
- }).sort((left, right) => compareText15(left.queryText, right.queryText) || compareText15(left.queryId, right.queryId));
30824
+ }).sort((left, right) => compareText(left.queryText, right.queryText) || compareText(left.queryId, right.queryId));
30789
30825
  }
30790
30826
  function planSnapshot(plan, queryId) {
30791
30827
  return plan?.querySnapshots.find((snapshot) => snapshot.queryId === queryId) ?? null;
@@ -30793,26 +30829,26 @@ function planSnapshot(plan, queryId) {
30793
30829
  function displayIds(rows, plan) {
30794
30830
  return /* @__PURE__ */ new Set([...rows.keys(), ...plan?.querySnapshots.map((snapshot) => snapshot.queryId) ?? []]);
30795
30831
  }
30796
- function candidateIdsForText(state, plan, rows, normalizedText4) {
30832
+ function candidateIdsForText(state, plan, rows, normalizedText) {
30797
30833
  const ids = /* @__PURE__ */ new Set();
30798
30834
  for (const row of rows.values()) {
30799
- if (normalizeText4(row.query) === normalizedText4) ids.add(row.id);
30835
+ if (normalizeIdentityText(row.query) === normalizedText) ids.add(row.id);
30800
30836
  }
30801
30837
  for (const snapshot of plan?.querySnapshots ?? []) {
30802
- if (normalizeText4(snapshot.queryText) === normalizedText4) ids.add(snapshot.queryId);
30838
+ if (normalizeIdentityText(snapshot.queryText) === normalizedText) ids.add(snapshot.queryId);
30803
30839
  }
30804
- const activeId = preferredQueryId(state, normalizedText4);
30840
+ const activeId = preferredQueryId(state, normalizedText);
30805
30841
  return [...ids].sort((left, right) => {
30806
30842
  if (left === activeId) return -1;
30807
30843
  if (right === activeId) return 1;
30808
- return compareText15(left, right);
30844
+ return compareText(left, right);
30809
30845
  });
30810
30846
  }
30811
30847
  function resolveMutationQueryId(state, plan, rows, removal) {
30812
30848
  if (removal.queryId !== void 0) {
30813
30849
  return rows.has(removal.queryId) || planSnapshot(plan, removal.queryId) !== null ? removal.queryId : null;
30814
30850
  }
30815
- const ids = candidateIdsForText(state, plan, rows, normalizeText4(removal.queryText));
30851
+ const ids = candidateIdsForText(state, plan, rows, normalizeIdentityText(removal.queryText));
30816
30852
  return ids[0] ?? null;
30817
30853
  }
30818
30854
  function queryTextForId(state, plan, rows, queryId, fallback) {
@@ -30909,7 +30945,7 @@ function simpleWorkload(project, rows, opts) {
30909
30945
  const result = /* @__PURE__ */ new Map();
30910
30946
  for (const context of defaultContexts(project, opts, "simple")) {
30911
30947
  for (const row of rows.values()) {
30912
- result.set(`${normalizeText4(row.query)}\0${contextKey(context)}`, context.providers.length);
30948
+ result.set(`${normalizeIdentityText(row.query)}\0${contextKey(context)}`, context.providers.length);
30913
30949
  }
30914
30950
  }
30915
30951
  return result;
@@ -30979,7 +31015,7 @@ function mutationPreviewToken(candidate, mutation, reviewedAt) {
30979
31015
  mutation: { additions: mutation.additions, removals: mutation.removals, ...mutation.edits ? { edits: mutation.edits } : {} },
30980
31016
  reviewedAt,
30981
31017
  plan: document,
30982
- queries: [...candidate.queryRows.values()].map((row) => ({ id: row.id, query: row.query, provenance: row.provenance })).sort((left, right) => compareText15(left.id, right.id))
31018
+ queries: [...candidate.queryRows.values()].map((row) => ({ id: row.id, query: row.query, provenance: row.provenance })).sort((left, right) => compareText(left.id, right.id))
30983
31019
  }))}`;
30984
31020
  }
30985
31021
  function assertReviewedAt(reviewedAt) {
@@ -30996,7 +31032,7 @@ function assertSimpleAddition(addition) {
30996
31032
  }
30997
31033
  }
30998
31034
  function addSimpleSource(state, rows, source, reusedQueryIds) {
30999
- const normalized = normalizeText4(source.queryText);
31035
+ const normalized = normalizeIdentityText(source.queryText);
31000
31036
  const existingId = candidateIdsForText(state, null, rows, normalized).at(0) ?? preferredQueryId(state, normalized) ?? stableNewQueryId(state.project.id, normalized);
31001
31037
  const existing = rows.get(existingId);
31002
31038
  if (existing) {
@@ -31026,7 +31062,7 @@ function buildSimpleCandidate(db, state, mutation, opts, manualCapturedAt) {
31026
31062
  }
31027
31063
  const existing = rows.get(edit.queryId);
31028
31064
  if (!existing) throw notFound("Tracked query", edit.queryId);
31029
- if (edit.text === void 0 || normalizeText4(edit.text) === normalizeText4(existing.query)) continue;
31065
+ if (edit.text === void 0 || normalizeIdentityText(edit.text) === normalizeIdentityText(existing.query)) continue;
31030
31066
  rows.delete(edit.queryId);
31031
31067
  mutatedQueryIds.add(edit.queryId);
31032
31068
  addSimpleSource(state, rows, {
@@ -31070,7 +31106,7 @@ function buildSimpleCandidate(db, state, mutation, opts, manualCapturedAt) {
31070
31106
  };
31071
31107
  }
31072
31108
  function addAdvancedSource(state, plan, rows, addition, source, reusedQueryIds, mutatedQueryIds) {
31073
- const normalized = normalizeText4(source.queryText);
31109
+ const normalized = normalizeIdentityText(source.queryText);
31074
31110
  const queryId = candidateIdsForText(state, plan, rows, normalized).at(0) ?? preferredQueryId(state, normalized) ?? stableNewQueryId(state.project.id, normalized);
31075
31111
  const knownBefore = state.queryRows.some((row) => row.id === queryId) || planSnapshot(state.active?.plan ?? null, queryId) !== null;
31076
31112
  const queryText = queryTextForId(state, plan, rows, queryId, source.queryText.trim());
@@ -31146,7 +31182,7 @@ function applyAdvancedEdit(state, plan, rows, edit, capturedAt, reusedQueryIds,
31146
31182
  if (!snapshot && !row) throw notFound("Tracked query", edit.queryId);
31147
31183
  const oldText = snapshot?.queryText ?? row.query;
31148
31184
  const text2 = edit.text ?? oldText;
31149
- const textChanged = normalizeText4(text2) !== normalizeText4(oldText);
31185
+ const textChanged = normalizeIdentityText(text2) !== normalizeIdentityText(oldText);
31150
31186
  const audience = edit.audience === void 0 ? null : resolveAudience(requireAdvanced(state).plan, edit.audience);
31151
31187
  const targetKeys = audience ? new Set(audience.targetKeys) : null;
31152
31188
  const marketKeys = new Set(audience?.marketKeys ?? []);
@@ -31180,7 +31216,7 @@ function applyAdvancedEdit(state, plan, rows, edit, capturedAt, reusedQueryIds,
31180
31216
  }
31181
31217
  return plan;
31182
31218
  }
31183
- const newQueryId = candidateIdsForText(state, plan, rows, normalizeText4(text2)).at(0) ?? preferredQueryId(state, normalizeText4(text2)) ?? stableNewQueryId(state.project.id, normalizeText4(text2));
31219
+ const newQueryId = candidateIdsForText(state, plan, rows, normalizeIdentityText(text2)).at(0) ?? preferredQueryId(state, normalizeIdentityText(text2)) ?? stableNewQueryId(state.project.id, normalizeIdentityText(text2));
31184
31220
  const queryText = queryTextForId(state, plan, rows, newQueryId, text2.trim());
31185
31221
  const provenance = provenanceForId(state, plan, rows, newQueryId, { source: "manual", sourceId: null, capturedAt });
31186
31222
  if (!rows.has(newQueryId)) rows.set(newQueryId, { id: newQueryId, query: queryText, provenance: serializeQueryProvenance(provenance) });
@@ -31281,7 +31317,7 @@ function writeSimpleCandidate(tx, candidate, now) {
31281
31317
  for (const queryId of candidate.createdQueryIds) {
31282
31318
  const planned = candidate.queryRows.get(queryId);
31283
31319
  if (!planned) continue;
31284
- const row = persisted.find((candidateRow) => normalizeText4(candidateRow.query) === normalizeText4(planned.query));
31320
+ const row = persisted.find((candidateRow) => normalizeIdentityText(candidateRow.query) === normalizeIdentityText(planned.query));
31285
31321
  if (!row) continue;
31286
31322
  if (row.id !== planned.id) tx.update(queries).set({ id: planned.id }).where(eq37(queries.id, row.id)).run();
31287
31323
  if (planned.provenance) tx.update(queries).set({ provenance: planned.provenance }).where(eq37(queries.id, planned.id)).run();
@@ -31616,7 +31652,7 @@ import crypto24 from "crypto";
31616
31652
  import dns2 from "dns/promises";
31617
31653
  import http2 from "http";
31618
31654
  import https2 from "https";
31619
- import net2 from "net";
31655
+ import net3 from "net";
31620
31656
  var REQUEST_TIMEOUT_MS = 1e4;
31621
31657
  async function resolveWebhookTarget(raw, options = {}) {
31622
31658
  let parsed;
@@ -31631,13 +31667,13 @@ async function resolveWebhookTarget(raw, options = {}) {
31631
31667
  if (parsed.username || parsed.password) {
31632
31668
  return { ok: false, message: '"url" must not include credentials' };
31633
31669
  }
31634
- const lookupHost = stripIpv6Brackets2(parsed.hostname);
31670
+ const lookupHost = stripIpv6Brackets(parsed.hostname);
31635
31671
  if (!lookupHost) {
31636
31672
  return { ok: false, message: '"url" must include a hostname' };
31637
31673
  }
31638
- const addresses = await resolveHostAddresses(lookupHost);
31674
+ const addresses = await resolveHostAddresses(lookupHost, options.resolveAddresses);
31639
31675
  if (addresses.length === 0) {
31640
- return { ok: false, message: '"url" hostname could not be resolved' };
31676
+ return { ok: false, message: '"url" hostname could not be resolved', unresolved: true };
31641
31677
  }
31642
31678
  const blocked = addresses.find((entry) => isBlockedAddress(entry.address, options));
31643
31679
  if (blocked) {
@@ -31648,7 +31684,8 @@ async function resolveWebhookTarget(raw, options = {}) {
31648
31684
  target: {
31649
31685
  url: parsed,
31650
31686
  address: addresses[0].address,
31651
- family: addresses[0].family
31687
+ family: addresses[0].family,
31688
+ addresses
31652
31689
  }
31653
31690
  };
31654
31691
  }
@@ -31668,16 +31705,23 @@ async function deliverWebhook(target, payload, webhookSecret) {
31668
31705
  }
31669
31706
  return await new Promise((resolve) => {
31670
31707
  const requestOptions = {
31671
- family: target.family,
31708
+ // A connection of its own: a shared agent pools by hostname, so it could
31709
+ // hand this request a socket some other caller dialed unchecked.
31710
+ agent: false,
31711
+ // Dial the checked addresses, falling back from one to the next, so a
31712
+ // receiver that listens on only one of them (`localhost` checks as
31713
+ // 127.0.0.1 and ::1) still gets the delivery.
31714
+ autoSelectFamily: true,
31715
+ lookup: pinnedLookup(checkedAddresses(target)),
31672
31716
  headers,
31673
- hostname: target.address,
31717
+ hostname: stripIpv6Brackets(target.url.hostname),
31674
31718
  method: "POST",
31675
31719
  path: path8,
31676
31720
  port,
31677
31721
  timeout: REQUEST_TIMEOUT_MS
31678
31722
  };
31679
31723
  if (isHttps) {
31680
- requestOptions.servername = stripIpv6Brackets2(target.url.hostname);
31724
+ requestOptions.servername = stripIpv6Brackets(target.url.hostname);
31681
31725
  }
31682
31726
  const request = (isHttps ? https2.request : http2.request)(requestOptions, (response) => {
31683
31727
  response.resume();
@@ -31694,11 +31738,27 @@ async function deliverWebhook(target, payload, webhookSecret) {
31694
31738
  request.end(body);
31695
31739
  });
31696
31740
  }
31697
- async function resolveHostAddresses(hostname) {
31698
- const family = net2.isIP(hostname);
31741
+ function checkedAddresses(target) {
31742
+ return target.addresses ?? [{ address: target.address, family: target.family }];
31743
+ }
31744
+ function pinnedLookup(addresses) {
31745
+ return (_hostname, options, callback) => {
31746
+ if (options.all) callback(null, addresses.map(({ address, family }) => ({ address, family })));
31747
+ else callback(null, addresses[0].address, addresses[0].family);
31748
+ };
31749
+ }
31750
+ var LOCALHOST_ADDRESSES = [{ address: "127.0.0.1", family: 4 }, { address: "::1", family: 6 }];
31751
+ function isLocalhostName(hostname) {
31752
+ const name = hostname.toLowerCase().replace(/\.$/, "");
31753
+ return name === "localhost" || name.endsWith(".localhost");
31754
+ }
31755
+ async function resolveHostAddresses(hostname, resolveAddresses) {
31756
+ const family = net3.isIP(hostname);
31699
31757
  if (family === 4 || family === 6) {
31700
31758
  return [{ address: hostname, family }];
31701
31759
  }
31760
+ if (isLocalhostName(hostname)) return LOCALHOST_ADDRESSES;
31761
+ if (resolveAddresses) return await resolveAddresses(hostname);
31702
31762
  try {
31703
31763
  const [ipv4, ipv6] = await Promise.allSettled([dns2.resolve4(hostname), dns2.resolve6(hostname)]);
31704
31764
  const unique3 = /* @__PURE__ */ new Map();
@@ -31718,65 +31778,7 @@ async function resolveHostAddresses(hostname) {
31718
31778
  }
31719
31779
  }
31720
31780
  function isBlockedAddress(address, options) {
31721
- const normalized = stripIpv6Brackets2(address).toLowerCase();
31722
- const family = net2.isIP(normalized);
31723
- if (family === 4) {
31724
- return isBlockedIpv4(normalized, options);
31725
- }
31726
- if (family === 6) {
31727
- const mappedIpv42 = extractMappedIpv4(normalized);
31728
- if (mappedIpv42) {
31729
- return isBlockedIpv4(mappedIpv42, options);
31730
- }
31731
- return isBlockedIpv6(normalized, options);
31732
- }
31733
- return true;
31734
- }
31735
- function isBlockedIpv4(address, options) {
31736
- const octets = address.split(".").map((part) => Number.parseInt(part, 10));
31737
- if (octets.length !== 4 || octets.some(Number.isNaN)) {
31738
- return true;
31739
- }
31740
- const [first, second] = octets;
31741
- if (first === 127 && !options.allowLoopback) {
31742
- return true;
31743
- }
31744
- return first === 0 || first === 10 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19);
31745
- }
31746
- function isBlockedIpv6(address, options) {
31747
- const normalized = address.split("%")[0].toLowerCase();
31748
- if (normalized === "::") {
31749
- return true;
31750
- }
31751
- if (normalized === "::1") {
31752
- return !options.allowLoopback;
31753
- }
31754
- const firstHextetText = normalized.split(":")[0] ?? "";
31755
- const firstHextet = firstHextetText === "" ? 0 : Number.parseInt(firstHextetText, 16);
31756
- if (Number.isNaN(firstHextet)) {
31757
- return true;
31758
- }
31759
- return firstHextet >= 64512 && firstHextet <= 65023 || firstHextet >= 65152 && firstHextet <= 65215;
31760
- }
31761
- function extractMappedIpv4(address) {
31762
- const normalized = address.toLowerCase();
31763
- if (!normalized.startsWith("::ffff:")) {
31764
- return null;
31765
- }
31766
- const remainder = normalized.slice("::ffff:".length);
31767
- if (net2.isIP(remainder) === 4) {
31768
- return remainder;
31769
- }
31770
- const parts = remainder.split(":");
31771
- if (parts.length !== 2 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) {
31772
- return null;
31773
- }
31774
- const high = Number.parseInt(parts[0], 16);
31775
- const low = Number.parseInt(parts[1], 16);
31776
- return [high >> 8, high & 255, low >> 8, low & 255].join(".");
31777
- }
31778
- function stripIpv6Brackets2(value) {
31779
- return value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
31781
+ return blockedAddressReason(address, { allowLoopback: options.allowLoopback }) !== null;
31780
31782
  }
31781
31783
 
31782
31784
  // ../api-routes/src/apply.ts
@@ -31895,7 +31897,7 @@ async function applyRoutes(app, opts) {
31895
31897
  const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
31896
31898
  if (existing) {
31897
31899
  const prevAliases = existing.aliases;
31898
- aliasesChanged = !aliasArraysEqual2(prevAliases, nextAliases);
31900
+ aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
31899
31901
  }
31900
31902
  if (existing) {
31901
31903
  projectId = existing.id;
@@ -32126,13 +32128,6 @@ async function applyRoutes(app, opts) {
32126
32128
  });
32127
32129
  });
32128
32130
  }
32129
- function aliasArraysEqual2(a, b) {
32130
- if (a.length !== b.length) return false;
32131
- for (let i = 0; i < a.length; i++) {
32132
- if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
32133
- }
32134
- return true;
32135
- }
32136
32131
 
32137
32132
  // ../api-routes/src/history.ts
32138
32133
  import { and as and31, asc as asc6, count, desc as desc17, eq as eq39, gte as gte6, inArray as inArray19, like as like2, ne as ne8, or as or10 } from "drizzle-orm";
@@ -36134,8 +36129,8 @@ function readVisibilityCompare(db, projectName, query) {
36134
36129
  if (scope === "project" && filters.scopeKey !== void 0) throw validationError("scopeKey is not valid for project scope.");
36135
36130
  if (scope === "market" && filters.marketKey !== void 0) throw validationError("marketKey is not valid for market scope.");
36136
36131
  const { fromRaw, toRaw, fromBounds, toBounds } = compareMonths(query.from, query.to);
36137
- const providerKey2 = filters.provider === void 0 ? void 0 : normalizeText3(filters.provider);
36138
- const locationKey2 = filters.location === void 0 ? void 0 : normalizeText3(filters.location);
36132
+ const providerKey2 = filters.provider === void 0 ? void 0 : normalizeIdentityText(filters.provider);
36133
+ const locationKey2 = filters.location === void 0 ? void 0 : normalizeIdentityText(filters.location);
36139
36134
  const scoped = scope !== "project" || filters.marketKey !== void 0;
36140
36135
  let advancedComparison;
36141
36136
  if (activeMeasurementPlan(db, project.id) !== null) {
@@ -36186,7 +36181,7 @@ function readVisibilityCompare(db, projectName, query) {
36186
36181
  }).from(querySnapshots).where(inArray23(querySnapshots.runId, runIds)).all() : [];
36187
36182
  return {
36188
36183
  runCount: runIds.length,
36189
- snapshots: snapshots.filter((snapshot) => (providerKey2 === void 0 || normalizeText3(snapshot.provider) === providerKey2) && (locationKey2 === void 0 || (locationKey2 === "none" ? snapshot.location === null : snapshot.location !== null && normalizeText3(snapshot.location) === locationKey2)))
36184
+ snapshots: snapshots.filter((snapshot) => (providerKey2 === void 0 || normalizeIdentityText(snapshot.provider) === providerKey2) && (locationKey2 === void 0 || (locationKey2 === "none" ? snapshot.location === null : snapshot.location !== null && normalizeIdentityText(snapshot.location) === locationKey2)))
36190
36185
  };
36191
36186
  };
36192
36187
  const fromMonth = loadMonth(fromBounds);
@@ -38513,6 +38508,7 @@ var SCHEMA_TABLE = {
38513
38508
  IndexingRequestResponseDto: indexingRequestResponseDtoSchema,
38514
38509
  IntegrationSettingsSummaryDto: integrationSettingsSummaryDtoSchema,
38515
38510
  KeywordDto: keywordDtoSchema,
38511
+ KeywordGenerateResponse: keywordGenerateResponseSchema,
38516
38512
  LatestProjectRunDto: latestProjectRunDtoSchema,
38517
38513
  RunAdmissionDto: runAdmissionDtoSchema,
38518
38514
  LocationContext: locationContextSchema,
@@ -38589,13 +38585,16 @@ var SCHEMA_TABLE = {
38589
38585
  OrganicEvidenceDto: organicEvidenceDtoSchema,
38590
38586
  ProjectCreateRequest: projectCreateRequestSchema,
38591
38587
  ProjectDto: projectDtoSchema,
38588
+ ProjectDefaultLocationResponse: projectDefaultLocationResponseSchema,
38592
38589
  ProjectUpsertRequest: projectUpsertRequestSchema,
38593
38590
  ProjectConfig: projectConfigSchema,
38591
+ ProjectConfigExport: projectConfigExportSchema,
38594
38592
  ProjectOverviewDto: projectOverviewDtoSchema,
38595
38593
  TrafficAnalyticsResponse: trafficAnalyticsResponseSchema,
38596
38594
  ProjectSearchResponseDto: projectSearchResponseSchema,
38597
38595
  ProviderSummaryEntryDto: providerSummaryEntryDtoSchema,
38598
38596
  QueryDto: queryDtoSchema,
38597
+ QueryGenerateResponse: queryGenerateResponseSchema,
38599
38598
  ReferralAssessment: referralAssessmentSchema,
38600
38599
  ResultsExportDto: resultsExportDtoSchema,
38601
38600
  RunCompletenessDto: runCompletenessDtoSchema,
@@ -38658,7 +38657,7 @@ var SCHEMA_TABLE = {
38658
38657
  WordpressManualAssistDto: wordpressManualAssistDtoSchema,
38659
38658
  WordpressOnboardResultDto: wordpressOnboardResultDtoSchema,
38660
38659
  WordpressPageDetailDto: wordpressPageDetailDtoSchema,
38661
- WordpressPageSummaryDto: wordpressPageSummaryDtoSchema,
38660
+ WordpressPageListDto: wordpressPageListDtoSchema,
38662
38661
  WordpressSchemaBlockDto: wordpressSchemaBlockDtoSchema,
38663
38662
  WordpressSchemaDeployResultDto: wordpressSchemaDeployResultDtoSchema,
38664
38663
  WordpressSchemaStatusResultDto: wordpressSchemaStatusResultDtoSchema,
@@ -38678,10 +38677,27 @@ var SCHEMA_TABLE = {
38678
38677
  var SHARED_ENUM_REFS = [
38679
38678
  { schema: "ScheduleDto", property: "kind", component: "SchedulableRunKind" }
38680
38679
  ];
38680
+ function addNullToNullableEnums(node) {
38681
+ if (Array.isArray(node)) {
38682
+ for (const item2 of node) addNullToNullableEnums(item2);
38683
+ return;
38684
+ }
38685
+ if (node === null || typeof node !== "object") return;
38686
+ const record = node;
38687
+ if (record.nullable === true && Array.isArray(record.enum)) {
38688
+ const values = record.enum;
38689
+ if (!values.includes(null)) record.enum = [...values, null];
38690
+ }
38691
+ for (const value of Object.values(record)) addNullToNullableEnums(value);
38692
+ }
38693
+ var INPUT_FORM_SCHEMAS = /* @__PURE__ */ new Set(["ProjectConfig"]);
38681
38694
  function buildComponentSchemas() {
38682
38695
  const out = {};
38683
38696
  for (const [name, schema] of Object.entries(SCHEMA_TABLE)) {
38684
- out[name] = z.toJSONSchema(schema, { target: "openapi-3.0" });
38697
+ const io = INPUT_FORM_SCHEMAS.has(name) ? "input" : "output";
38698
+ const json = z.toJSONSchema(schema, { target: "openapi-3.0", io });
38699
+ addNullToNullableEnums(json);
38700
+ out[name] = json;
38685
38701
  }
38686
38702
  for (const { schema, property, component } of SHARED_ENUM_REFS) {
38687
38703
  const properties = out[schema]?.properties;
@@ -38844,6 +38860,13 @@ var measurementReportRunParameter = {
38844
38860
  description: "Eligible full measurement run to reconstruct. Omit to use the latest run for the revision.",
38845
38861
  schema: stringSchema
38846
38862
  };
38863
+ var measurementReportQueryClassParameter = {
38864
+ name: "queryClass",
38865
+ in: "query",
38866
+ required: false,
38867
+ description: "Query class every group, Target and evidence row is taken over. Defaults to non-brand; branded is a separate read. all pools branded and non-brand into one rate and is served only when asked for. A schema v1 revision records no class: omit this (or pass all) to read every answer, and the response echoes queryClass null.",
38868
+ schema: { type: "string", enum: ["non-brand", "branded", "all"], default: "non-brand" }
38869
+ };
38847
38870
  var runIdParameter = {
38848
38871
  name: "id",
38849
38872
  in: "path",
@@ -39557,12 +39580,12 @@ var routeCatalog = [
39557
39580
  method: "get",
39558
39581
  path: "/api/v1/projects/{name}/measurement-report",
39559
39582
  summary: "Get a revision-pinned measurement report",
39560
- description: "Builds the Target, group, and evidence report from the immutable plan revision and either its latest eligible stored run or the exact eligible runId supplied by the caller. Missing run population remains explicit and never triggers live provider execution.",
39583
+ description: "Builds the Target, group, and evidence report from the immutable plan revision and either its latest eligible stored run or the exact eligible runId supplied by the caller. On a schema v2 revision every rate is taken over one query class, non-brand unless queryClass names another; the response echoes the class it served. Missing run population remains explicit and never triggers live provider execution.",
39561
39584
  tags: ["measurement-plans"],
39562
- parameters: [nameParameter, measurementReportRevisionParameter, measurementReportRunParameter],
39585
+ parameters: [nameParameter, measurementReportRevisionParameter, measurementReportRunParameter, measurementReportQueryClassParameter],
39563
39586
  responses: {
39564
39587
  200: jsonResponse("Revision-pinned measurement report returned.", "MeasurementReportResponse"),
39565
- 400: errorResponse("The revision query parameter is invalid."),
39588
+ 400: errorResponse("The revision or queryClass parameter is invalid, or a class was requested for a schema v1 revision."),
39566
39589
  404: errorResponse("Project, measurement-plan revision, or requested run not found.")
39567
39590
  }
39568
39591
  },
@@ -39829,15 +39852,16 @@ var routeCatalog = [
39829
39852
  method: "get",
39830
39853
  path: "/api/v1/projects/{name}/measurement-overview",
39831
39854
  summary: "Get the scoped measurement overview",
39832
- description: "Aggregates one revision-pinned run snapshot for All Properties, a group, or a single Property. This is snapshot ranking only: it never infers a trend or compares evidence across revisions. Without runId the most recent completed run pinned to the active revision is used; once paging begins, the cursor pins that revision, displayed run, evidence snapshot, and result filters. A run pinned to another revision is refused rather than joined, and appended evidence on a mutable named run invalidates its cursor. On a schema v2 plan every Property row carries its metro (the top-level group holding it, or null) and, when it sits in several top-level groups, otherMetros. Metrics are computed before search is applied, and a metric with no evidence is unavailable rather than zero. For coverage sorts, unavailable rows form the first bucket in either direction before available numeric rates follow the requested direction.",
39855
+ description: "Aggregates one revision-pinned run snapshot for All Properties, a group, a market, or a single Property. A group reads every query its Properties are assigned; a market reads only its own frozen queries, the population the dashboard's market view reads, and the response scope names which kind was read. Rates are taken over one query class, non-brand unless queryClass names another; all pools branded with non-brand and is served only when asked for. This is snapshot ranking only: it never infers a trend or compares evidence across revisions. Without runId the most recent completed run pinned to the active revision is used; once paging begins, the cursor pins that revision, displayed run, evidence snapshot, and result filters. A run pinned to another revision is refused rather than joined, and appended evidence on a mutable named run invalidates its cursor. On a schema v2 plan every Property row carries its metro (the top-level group holding it, or null) and, when it sits in several top-level groups, otherMetros. Metrics are computed before search is applied, and a metric with no evidence is unavailable rather than zero. For coverage sorts, unavailable rows form the first bucket in either direction before available numeric rates follow the requested direction.",
39833
39856
  tags: ["measurement-plans"],
39834
39857
  parameters: [
39835
39858
  nameParameter,
39836
39859
  { name: "compact", in: "query", description: "Byte-bounded whole-row page; omits optional heavy detail with explicit metadata. Default false for compatibility.", schema: { type: "boolean" } },
39837
- { name: "scope", in: "query", required: true, description: "Reporting scope.", schema: { type: "string", enum: ["all", "group", "property"] } },
39860
+ { name: "scope", in: "query", required: true, description: "Reporting scope. group is every query of the group's Properties; market is only the market's own queries.", schema: { type: "string", enum: ["all", "group", "market", "property"] } },
39838
39861
  { name: "groupKey", in: "query", description: 'Group stable key, required when scope is "group".', schema: stringSchema },
39862
+ { name: "marketKey", in: "query", description: 'Market stable key (a frozen reporting scope), required when scope is "market" and refused (400) with any other scope.', schema: stringSchema },
39839
39863
  { name: "targetKey", in: "query", description: 'Target stable key, required when scope is "property".', schema: stringSchema },
39840
- { name: "queryClass", in: "query", description: "Restrict to one question class. Never pooled across classes.", schema: { type: "string", enum: ["all", "branded", "non-brand"] } },
39864
+ { name: "queryClass", in: "query", description: "Query class the rates are taken over. Defaults to non-brand; read branded as a separate request. all pools branded and non-brand into one rate.", schema: { type: "string", enum: ["non-brand", "branded", "all"], default: "non-brand" } },
39841
39865
  { name: "provider", in: "query", description: "Restrict to one answer provider.", schema: stringSchema },
39842
39866
  { name: "location", in: "query", description: "Restrict to one execution location label.", schema: stringSchema },
39843
39867
  { name: "from", in: "query", description: "Inclusive start of the window (YYYY-MM-DD).", schema: stringSchema },
@@ -40085,14 +40109,14 @@ var routeCatalog = [
40085
40109
  method: "get",
40086
40110
  path: "/api/v1/projects/{name}/measurement-changes",
40087
40111
  summary: "Compare the latest two comparable measurements",
40088
- description: "Compares stored runs only when plan revision, execution identity, and full-or-spot-check scope agree. Deltas are current minus previous; it never crosses a revision or silently joins an engine/model change. changedProperties is ordered by the size of each move unless sort=label, and each row carries signed answer-count deltas, denominatorChanged (a metric was taken over a different number of answers, so its delta is not like for like) and withinNoise (every move is at most 2 answers). A move is the rate change times the larger of the two answer counts, the answer-count delta whenever the denominator held, so a falling rate never reads as a gain and a collapse never reads as noise. distribution buckets every Property in scope, not just the returned rows. With queryClass all, metricsByClass reports branded and non-brand beside the pooled metrics.",
40112
+ description: "Compares stored runs only when plan revision, execution identity, and full-or-spot-check scope agree. Deltas are current minus previous; it never crosses a revision or silently joins an engine/model change. changedProperties is ordered by the size of each move unless sort=label, and each row carries signed answer-count deltas, denominatorChanged (a metric was taken over a different number of answers, so its delta is not like for like) and withinNoise (every move is at most 2 answers). A move is the rate change times the larger of the two answer counts, the answer-count delta whenever the denominator held, so a falling rate never reads as a gain and a collapse never reads as noise. distribution buckets every Property in scope, not just the returned rows. Rates are taken over non-brand queries unless queryClass names another class. With queryClass all, the metrics, rows and distribution pool branded with non-brand, and metricsByClass reports each class beside them.",
40089
40113
  tags: ["measurement-plans"],
40090
40114
  parameters: [
40091
40115
  nameParameter,
40092
40116
  { name: "scope", in: "query", description: "Reporting scope. Defaults to all.", schema: { type: "string", enum: ["all", "group", "property"], default: "all" } },
40093
40117
  { name: "groupKey", in: "query", description: "Required for group scope.", schema: stringSchema },
40094
40118
  { name: "targetKey", in: "query", description: "Required for Property scope.", schema: stringSchema },
40095
- { name: "queryClass", in: "query", description: "Question class. Defaults to all.", schema: { type: "string", enum: ["all", "branded", "non-brand"], default: "all" } },
40119
+ { name: "queryClass", in: "query", description: "Query class. Defaults to non-brand; read branded as a separate request. all pools both classes.", schema: { type: "string", enum: ["non-brand", "branded", "all"], default: "non-brand" } },
40096
40120
  { name: "provider", in: "query", description: "Restrict both runs to one answer provider.", schema: stringSchema },
40097
40121
  { name: "location", in: "query", description: "Restrict both runs to one execution location label.", schema: stringSchema },
40098
40122
  { name: "runId", in: "query", description: "Use this completed or partial run as the current side.", schema: stringSchema },
@@ -40428,7 +40452,7 @@ var routeCatalog = [
40428
40452
  }
40429
40453
  },
40430
40454
  responses: {
40431
- 200: jsonResponse("Default location updated.", "ProjectDto"),
40455
+ 200: jsonResponse("Default location updated.", "ProjectDefaultLocationResponse"),
40432
40456
  400: errorResponse("Invalid location."),
40433
40457
  404: errorResponse("Project not found.")
40434
40458
  }
@@ -40440,7 +40464,7 @@ var routeCatalog = [
40440
40464
  tags: ["projects"],
40441
40465
  parameters: [nameParameter],
40442
40466
  responses: {
40443
- 200: jsonResponse("Project configuration returned.", "ProjectConfig"),
40467
+ 200: jsonResponse("Project configuration returned.", "ProjectConfigExport"),
40444
40468
  404: errorResponse("Project not found.")
40445
40469
  }
40446
40470
  },
@@ -40643,7 +40667,7 @@ var routeCatalog = [
40643
40667
  }
40644
40668
  },
40645
40669
  responses: {
40646
- 200: rawJsonResponse("Query suggestions returned.", { type: "object", properties: { suggestions: { type: "array", items: { type: "string" } } } }),
40670
+ 200: jsonResponse("Query suggestions returned.", "QueryGenerateResponse"),
40647
40671
  501: errorResponse("Query generation is not available.")
40648
40672
  }
40649
40673
  },
@@ -40752,8 +40776,7 @@ var routeCatalog = [
40752
40776
  }
40753
40777
  },
40754
40778
  responses: {
40755
- // TODO: Add `KeywordGenerateResponse` Zod schema (`{ suggestions: string[] }`) in contracts.
40756
- 200: rawJsonResponse("Legacy keyword suggestions returned.", looseObjectSchema),
40779
+ 200: jsonResponse("Legacy keyword suggestions returned.", "KeywordGenerateResponse"),
40757
40780
  501: errorResponse("Legacy keyword generation is not available.")
40758
40781
  }
40759
40782
  },
@@ -44089,7 +44112,7 @@ var routeCatalog = [
44089
44112
  tags: ["wordpress"],
44090
44113
  parameters: [nameParameter, wordpressEnvQueryParameter],
44091
44114
  responses: {
44092
- 200: jsonArrayResponse("WordPress pages returned.", "WordpressPageSummaryDto"),
44115
+ 200: jsonResponse("WordPress pages returned.", "WordpressPageListDto"),
44093
44116
  400: errorResponse("Invalid environment or missing connection."),
44094
44117
  404: errorResponse("Project not found.")
44095
44118
  }
@@ -47812,18 +47835,18 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
47812
47835
  const parsed = JSON.parse(body);
47813
47836
  if (parsed.error) detail = parsed.error;
47814
47837
  if (parsed.error_description) {
47815
- const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp(clientId), "g"), "***").replace(new RegExp(escapeRegExp(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp(code), "g"), "***");
47838
+ const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(code), "g"), "***");
47816
47839
  detail += detail ? `: ${sanitized}` : sanitized;
47817
47840
  }
47818
47841
  } catch {
47819
47842
  const truncated = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
47820
- detail = truncated.replace(new RegExp(escapeRegExp(clientId), "g"), "***").replace(new RegExp(escapeRegExp(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp(code), "g"), "***");
47843
+ detail = truncated.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(code), "g"), "***");
47821
47844
  }
47822
47845
  throw new GoogleAuthError(`Token exchange failed (${res.status}): ${detail}`);
47823
47846
  }
47824
47847
  return await res.json();
47825
47848
  }
47826
- function escapeRegExp(str) {
47849
+ function escapeRegExp2(str) {
47827
47850
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47828
47851
  }
47829
47852
  async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
@@ -47851,12 +47874,12 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
47851
47874
  const parsed = JSON.parse(body);
47852
47875
  if (parsed.error) detail = parsed.error;
47853
47876
  if (parsed.error_description) {
47854
- const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp(clientId), "g"), "***").replace(new RegExp(escapeRegExp(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp(currentRefreshToken), "g"), "***");
47877
+ const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(currentRefreshToken), "g"), "***");
47855
47878
  detail += detail ? `: ${sanitized}` : sanitized;
47856
47879
  }
47857
47880
  } catch {
47858
47881
  const truncated = body.length <= 120 ? body : `${body.slice(0, 120)}...`;
47859
- detail = truncated.replace(new RegExp(escapeRegExp(clientId), "g"), "***").replace(new RegExp(escapeRegExp(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp(currentRefreshToken), "g"), "***");
47882
+ detail = truncated.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(currentRefreshToken), "g"), "***");
47860
47883
  }
47861
47884
  throw new GoogleAuthError(`Token refresh failed (${res.status}): ${detail}`);
47862
47885
  }
@@ -47946,7 +47969,7 @@ function gscClientLog(level, action, ctx) {
47946
47969
  const stream = level === "error" ? process.stderr : process.stdout;
47947
47970
  stream.write(JSON.stringify(entry) + "\n");
47948
47971
  }
47949
- function escapeRegExp2(str) {
47972
+ function escapeRegExp3(str) {
47950
47973
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47951
47974
  }
47952
47975
  async function gscFetchResponse(accessToken, url, opts) {
@@ -47975,7 +47998,7 @@ async function gscFetchResponse(accessToken, url, opts) {
47975
47998
  const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
47976
47999
  let sanitizedDetail = detail;
47977
48000
  if (accessToken) {
47978
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp2(accessToken), "g"), "***");
48001
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
47979
48002
  }
47980
48003
  throw new GoogleApiError(`GSC API error (${res.status}): ${sanitizedDetail}`, res.status);
47981
48004
  }
@@ -48263,15 +48286,12 @@ async function getAccessToken(clientEmail, privateKey) {
48263
48286
  const body = await res.text().catch(() => "");
48264
48287
  ga4Log("error", "token.failed", { httpStatus: res.status });
48265
48288
  const detail = body.length <= 200 ? body : `${body.slice(0, 200)}... [truncated]`;
48266
- const sanitizedDetail = detail.replace(new RegExp(escapeRegExp3(clientEmail), "g"), "***").replace(new RegExp(escapeRegExp3(privateKey.slice(0, 32)), "g"), "***");
48289
+ const sanitizedDetail = detail.replace(new RegExp(escapeRegExp(clientEmail), "g"), "***").replace(new RegExp(escapeRegExp(privateKey.slice(0, 32)), "g"), "***");
48267
48290
  throw new GA4ApiError(`Failed to get access token: ${sanitizedDetail}`, res.status);
48268
48291
  }
48269
48292
  const data = await res.json();
48270
48293
  return data.access_token;
48271
48294
  }
48272
- function escapeRegExp3(str) {
48273
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
48274
- }
48275
48295
  var gaInFlight = 0;
48276
48296
  var gaWaitQueue = [];
48277
48297
  async function acquireGa4Slot() {
@@ -48377,7 +48397,7 @@ async function runReport(accessToken, propertyId, request) {
48377
48397
  }
48378
48398
  let sanitizedDetail = detail;
48379
48399
  if (accessToken) {
48380
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48400
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48381
48401
  }
48382
48402
  ga4Log("error", "report.auth-failed", { propertyId, httpStatus: res.status });
48383
48403
  throw new GA4ApiError(
@@ -48397,7 +48417,7 @@ async function runReport(accessToken, propertyId, request) {
48397
48417
  const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
48398
48418
  let sanitizedDetail = detail;
48399
48419
  if (accessToken) {
48400
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48420
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48401
48421
  }
48402
48422
  throw new GA4ApiError(`GA4 API error (${res.status}): ${sanitizedDetail}`, res.status, retryAfterSeconds);
48403
48423
  }
@@ -48421,7 +48441,7 @@ async function batchRunReports(accessToken, propertyId, requests) {
48421
48441
  ga4Log("error", "batch-report.auth-failed", { propertyId, httpStatus: res.status });
48422
48442
  let sanitizedDetail = body;
48423
48443
  if (accessToken) {
48424
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48444
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48425
48445
  }
48426
48446
  throw new GA4ApiError(
48427
48447
  `GA4 API authentication failed \u2014 check service account permissions. ${sanitizedDetail}`,
@@ -48440,7 +48460,7 @@ async function batchRunReports(accessToken, propertyId, requests) {
48440
48460
  const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
48441
48461
  let sanitizedDetail = detail;
48442
48462
  if (accessToken) {
48443
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48463
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48444
48464
  }
48445
48465
  throw new GA4ApiError(`GA4 API error (${res.status}): ${sanitizedDetail}`, res.status, retryAfterSeconds);
48446
48466
  }
@@ -48742,7 +48762,7 @@ async function listProperties(accessToken) {
48742
48762
  } catch {
48743
48763
  if (raw.length < 200) detail = ` ${raw}`;
48744
48764
  }
48745
- const sanitized = detail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48765
+ const sanitized = detail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48746
48766
  const retryAfterSeconds = res.status === 429 || res.status >= 500 || res.status === 403 && quotaReason ? parseRetryAfter(res.headers.get("retry-after")) : void 0;
48747
48767
  ga4Log("error", "admin.account-summaries-failed", {
48748
48768
  httpStatus: res.status,
@@ -58074,9 +58094,6 @@ function bingClientLog(level, action, ctx) {
58074
58094
  const stream = level === "error" ? process.stderr : process.stdout;
58075
58095
  stream.write(JSON.stringify(entry) + "\n");
58076
58096
  }
58077
- function escapeRegExp4(str) {
58078
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58079
- }
58080
58097
  function parseBingErrorCode(body) {
58081
58098
  try {
58082
58099
  const parsed = JSON.parse(body);
@@ -58113,7 +58130,7 @@ async function bingFetchOnce(apiKey, endpoint, opts) {
58113
58130
  if (!res.ok) {
58114
58131
  const body = await res.text();
58115
58132
  let detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
58116
- detail = detail.replace(new RegExp(escapeRegExp4(apiKey), "g"), "***");
58133
+ detail = detail.replace(new RegExp(escapeRegExp(apiKey), "g"), "***");
58117
58134
  const bingErrorCode = parseBingErrorCode(body);
58118
58135
  const err = new BingApiError(`Bing API error (${res.status}): ${detail}`, res.status, bingErrorCode);
58119
58136
  bingClientLog("error", err.isThrottle ? "http.throttled" : "http.error", {
@@ -58956,7 +58973,7 @@ function findBiggestMover(current, prior) {
58956
58973
  for (const row of current) entry(row.source).current += row.sessions ?? 0;
58957
58974
  for (const row of prior) entry(row.source).prior += row.sessions ?? 0;
58958
58975
  let mover = null;
58959
- const sources = [...bySource.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
58976
+ const sources = [...bySource.keys()].sort(compareText);
58960
58977
  for (const source of sources) {
58961
58978
  const sessions = bySource.get(source);
58962
58979
  const size = Math.abs(sessions.current - sessions.prior);
@@ -60572,7 +60589,7 @@ function buildAuthErrorMessage(res, responseText) {
60572
60589
  async function fetchJson(connection, siteUrl, path8, init) {
60573
60590
  if (siteUrl.startsWith("http:")) {
60574
60591
  }
60575
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path8}`, {
60592
+ const res = await (connection.fetchImpl ?? fetch)(`${normalizeSiteUrl(siteUrl)}${path8}`, {
60576
60593
  ...init,
60577
60594
  headers: {
60578
60595
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -60596,10 +60613,10 @@ async function fetchJson(connection, siteUrl, path8, init) {
60596
60613
  const text2 = await res.text().catch(() => "");
60597
60614
  let detail = text2 || res.statusText;
60598
60615
  if (connection.appPassword) {
60599
- detail = detail.replace(new RegExp(escapeRegExp5(connection.appPassword), "g"), "***");
60616
+ detail = detail.replace(new RegExp(escapeRegExp(connection.appPassword), "g"), "***");
60600
60617
  }
60601
60618
  if (connection.username) {
60602
- detail = detail.replace(new RegExp(escapeRegExp5(connection.username), "g"), "***");
60619
+ detail = detail.replace(new RegExp(escapeRegExp(connection.username), "g"), "***");
60603
60620
  }
60604
60621
  throw new WordpressApiError("UPSTREAM_ERROR", `WordPress API error (${res.status}): ${detail}`, res.status);
60605
60622
  }
@@ -60631,9 +60648,9 @@ async function fetchPageCollectionSummary(connection, siteUrl, options) {
60631
60648
  );
60632
60649
  return response;
60633
60650
  }
60634
- async function fetchText(url) {
60651
+ async function fetchText(connection, url) {
60635
60652
  try {
60636
- const res = await fetch(url, { signal: AbortSignal.timeout(WP_FETCH_TEXT_TIMEOUT_MS) });
60653
+ const res = await (connection.fetchImpl ?? fetch)(url, { signal: AbortSignal.timeout(WP_FETCH_TEXT_TIMEOUT_MS) });
60637
60654
  if (!res.ok) return null;
60638
60655
  return await res.text();
60639
60656
  } catch {
@@ -60775,7 +60792,7 @@ async function verifyWordpressConnection(connection) {
60775
60792
  validateConnection(connection, site.siteUrl);
60776
60793
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
60777
60794
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
60778
- const homeHtml = await fetchText(site.siteUrl);
60795
+ const homeHtml = await fetchText(connection, site.siteUrl);
60779
60796
  return {
60780
60797
  url: site.siteUrl,
60781
60798
  reachable: true,
@@ -60791,7 +60808,7 @@ async function getSiteStatus(connection, env) {
60791
60808
  try {
60792
60809
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
60793
60810
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
60794
- const homeHtml = await fetchText(site.siteUrl);
60811
+ const homeHtml = await fetchText(connection, site.siteUrl);
60795
60812
  const plugins = await listActivePlugins(connection, env);
60796
60813
  return {
60797
60814
  url: site.siteUrl,
@@ -60881,15 +60898,15 @@ async function getPageBySlug(connection, slug, env) {
60881
60898
  }
60882
60899
  return body[0];
60883
60900
  }
60884
- async function fetchRenderedPage(link) {
60901
+ async function fetchRenderedPage(connection, link) {
60885
60902
  if (!link) return null;
60886
- return fetchText(link);
60903
+ return fetchText(connection, link);
60887
60904
  }
60888
60905
  async function getPageDetail(connection, slug, env, plugins) {
60889
60906
  const site = resolveEnvironment(connection, env);
60890
60907
  const resolvedPlugins = plugins === void 0 ? await listActivePlugins(connection, site.env) : plugins;
60891
60908
  const page3 = await getPageBySlug(connection, slug, site.env);
60892
- const html = await fetchRenderedPage(page3.link);
60909
+ const html = await fetchRenderedPage(connection, page3.link);
60893
60910
  const schemaBlocks = html ? extractSchemaBlocks(html) : [];
60894
60911
  const seo = buildSeoState(page3, html, resolvedPlugins);
60895
60912
  return {
@@ -61094,14 +61111,11 @@ var CANONRY_SCHEMA_START = "<!-- canonry:schema:start -->";
61094
61111
  var CANONRY_SCHEMA_END = "<!-- canonry:schema:end -->";
61095
61112
  function stripCanonrySchema(content) {
61096
61113
  const regex = new RegExp(
61097
- `${escapeRegExp5(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp5(CANONRY_SCHEMA_END)}`,
61114
+ `${escapeRegExp(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp(CANONRY_SCHEMA_END)}`,
61098
61115
  "g"
61099
61116
  );
61100
61117
  return content.replace(regex, "").replace(/\n{3,}/g, "\n\n").trim();
61101
61118
  }
61102
- function escapeRegExp5(str) {
61103
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61104
- }
61105
61119
  function injectCanonrySchema(content, schemas) {
61106
61120
  if (schemas.length === 0) return content;
61107
61121
  const blocks = schemas.map((schema) => `<script type="application/ld+json">${JSON.stringify(schema).replace(/<\//g, "<\\/")}</script>`).join("\n");
@@ -61206,7 +61220,7 @@ async function getSchemaStatus(connection, env) {
61206
61220
  const thirdPartySchemas = [];
61207
61221
  if (hasCanonryMarker) {
61208
61222
  const markerRegex = new RegExp(
61209
- `${escapeRegExp5(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp5(CANONRY_SCHEMA_END)}`
61223
+ `${escapeRegExp(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp(CANONRY_SCHEMA_END)}`
61210
61224
  );
61211
61225
  const match = markerRegex.exec(rawContent);
61212
61226
  if (match?.[1]) {
@@ -61249,7 +61263,7 @@ async function getLlmsTxt(connection, env) {
61249
61263
  return {
61250
61264
  env: site.env,
61251
61265
  url,
61252
- content: await fetchText(url)
61266
+ content: await fetchText(connection, url)
61253
61267
  };
61254
61268
  }
61255
61269
  async function buildManualLlmsTxtUpdate(connection, content, env) {
@@ -61431,6 +61445,132 @@ function parseEnv(value) {
61431
61445
  return parsed.success ? parsed.data : void 0;
61432
61446
  }
61433
61447
 
61448
+ // ../api-routes/src/guarded-fetch.ts
61449
+ import { Agent, fetch as undiciFetch } from "undici";
61450
+ var GUARDED_FETCH_MAX_REDIRECTS = 5;
61451
+ var MAX_POOLED_AGENTS = 16;
61452
+ var EgressRefusedError = class extends Error {
61453
+ /**
61454
+ * True when the refused target is a redirect the site chose rather than the
61455
+ * URL the caller asked for. Refusing the caller's URL is a verdict on the
61456
+ * caller's input; refusing a redirect is the site's failure.
61457
+ */
61458
+ redirected;
61459
+ constructor(message, redirected) {
61460
+ super(message);
61461
+ this.name = "EgressRefusedError";
61462
+ this.redirected = redirected;
61463
+ }
61464
+ };
61465
+ var EgressFailedError = class extends Error {
61466
+ constructor(message) {
61467
+ super(message);
61468
+ this.name = "EgressFailedError";
61469
+ }
61470
+ };
61471
+ var CROSS_ORIGIN_DROPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
61472
+ var BODY_HEADERS = ["content-encoding", "content-language", "content-length", "content-location", "content-type"];
61473
+ function createGuardedFetch(options = {}) {
61474
+ const resolveTarget = options.resolveTarget ?? ((url) => resolveWebhookTarget(url, { allowLoopback: options.allowLoopback }));
61475
+ const agents = /* @__PURE__ */ new Map();
61476
+ function agentFor(target) {
61477
+ const addresses = checkedAddresses(target);
61478
+ const key = `${target.url.origin} ${addresses.map(({ address }) => address).sort().join(" ")}`;
61479
+ const agent = agents.get(key) ?? pinnedAgent(addresses);
61480
+ agents.delete(key);
61481
+ agents.set(key, agent);
61482
+ if (agents.size > MAX_POOLED_AGENTS) {
61483
+ const [oldestKey, oldest] = agents.entries().next().value;
61484
+ agents.delete(oldestKey);
61485
+ oldest.close().catch(() => {
61486
+ });
61487
+ }
61488
+ return agent;
61489
+ }
61490
+ return async (input, init = {}) => {
61491
+ let href = String(input);
61492
+ let method = (init.method ?? "GET").toUpperCase();
61493
+ let body = init.body ?? null;
61494
+ const headers = new Headers(init.headers);
61495
+ const signal = init.signal ?? void 0;
61496
+ let mayRedirectToLoopback = true;
61497
+ for (let redirects = 0; ; redirects += 1) {
61498
+ signal?.throwIfAborted();
61499
+ const check2 = await untilAborted(resolveTarget(href), signal);
61500
+ if (!check2.ok) {
61501
+ const parsed = URL.canParse(href) ? new URL(href) : null;
61502
+ if (check2.unresolved) throw new EgressFailedError(`Could not resolve ${parsed?.hostname || href}`);
61503
+ throw new EgressRefusedError(`Refused to connect to ${parsed?.host || href}: ${check2.message.replace(/^"url" /, "")}`, redirects > 0);
61504
+ }
61505
+ const url = check2.target.url;
61506
+ const onLoopback = checkedAddresses(check2.target).map(({ address }) => isLoopbackAddress(address));
61507
+ if (!mayRedirectToLoopback && onLoopback.includes(true)) {
61508
+ throw new EgressRefusedError(`Refused to connect to ${url.host}: a redirect from a site that is not on loopback must not lead to loopback`, true);
61509
+ }
61510
+ mayRedirectToLoopback = !onLoopback.includes(false);
61511
+ const response = await requestPinned(check2.target, agentFor(check2.target), { method, headers, body, signal });
61512
+ const location = isLocationRedirectStatus(response.status) ? response.headers.get("location") : null;
61513
+ if (location === null) return response;
61514
+ await response.body?.cancel();
61515
+ if (redirects >= GUARDED_FETCH_MAX_REDIRECTS) {
61516
+ throw new EgressFailedError(`${url.host} redirected more than ${GUARDED_FETCH_MAX_REDIRECTS} times`);
61517
+ }
61518
+ let next;
61519
+ try {
61520
+ next = new URL(location, url);
61521
+ } catch {
61522
+ throw new EgressFailedError(`${url.host} redirected to an invalid Location`);
61523
+ }
61524
+ const status = response.status;
61525
+ if (status === 303 && method !== "GET" && method !== "HEAD" || (status === 301 || status === 302) && method === "POST") {
61526
+ method = "GET";
61527
+ body = null;
61528
+ for (const name of BODY_HEADERS) headers.delete(name);
61529
+ }
61530
+ if (next.origin !== url.origin) {
61531
+ for (const name of CROSS_ORIGIN_DROPPED_HEADERS) headers.delete(name);
61532
+ }
61533
+ href = next.href;
61534
+ }
61535
+ };
61536
+ }
61537
+ async function untilAborted(operation, signal) {
61538
+ if (!signal) return await operation;
61539
+ let onAbort;
61540
+ try {
61541
+ return await Promise.race([
61542
+ operation,
61543
+ new Promise((_resolve, reject) => {
61544
+ onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new DOMException("This operation was aborted", "AbortError"));
61545
+ signal.addEventListener("abort", onAbort, { once: true });
61546
+ })
61547
+ ]);
61548
+ } finally {
61549
+ if (onAbort) signal.removeEventListener("abort", onAbort);
61550
+ }
61551
+ }
61552
+ async function requestPinned(target, dispatcher, init) {
61553
+ const response = await undiciFetch(target.url, {
61554
+ method: init.method,
61555
+ headers: [...init.headers],
61556
+ body: init.body,
61557
+ signal: init.signal,
61558
+ redirect: "manual",
61559
+ dispatcher
61560
+ });
61561
+ return response;
61562
+ }
61563
+ function pinnedAgent(addresses) {
61564
+ return new Agent({
61565
+ // Try the next checked address when one does not answer, so a dual-stack
61566
+ // site whose first address is unreachable (an IPv6-only network, a dead A
61567
+ // record) still loads, as it does through global `fetch`. Every address
61568
+ // passed the policy, so falling back reaches nothing it refused.
61569
+ autoSelectFamily: true,
61570
+ connect: { lookup: pinnedLookup(addresses) }
61571
+ });
61572
+ }
61573
+
61434
61574
  // ../api-routes/src/wordpress.ts
61435
61575
  function parseEnvInput(value, fieldName = "env") {
61436
61576
  const env = parseEnv(value);
@@ -61458,6 +61598,8 @@ async function withWordpressErrorHandling(handler) {
61458
61598
  return await handler();
61459
61599
  } catch (error) {
61460
61600
  if (error instanceof WordpressApiError) throw toAppError(error);
61601
+ if (error instanceof EgressRefusedError && !error.redirected) throw validationError(error.message);
61602
+ if (error instanceof EgressRefusedError || error instanceof EgressFailedError) throw providerError(error.message);
61461
61603
  throw error;
61462
61604
  }
61463
61605
  }
@@ -61467,6 +61609,10 @@ async function wordpressRoutes(app, opts) {
61467
61609
  if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
61468
61610
  throw validationError("WordPress connection storage is not configured for this deployment");
61469
61611
  }
61612
+ const egressFetch = createGuardedFetch({ allowLoopback });
61613
+ function guarded(connection) {
61614
+ return { ...connection, fetchImpl: egressFetch };
61615
+ }
61470
61616
  async function assertWordpressUrlAllowed(rawUrl, field) {
61471
61617
  const check2 = await resolveWebhookTarget(rawUrl, { allowLoopback });
61472
61618
  if (!check2.ok) {
@@ -61478,7 +61624,7 @@ async function wordpressRoutes(app, opts) {
61478
61624
  if (!connection) {
61479
61625
  throw validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
61480
61626
  }
61481
- return connection;
61627
+ return guarded(connection);
61482
61628
  }
61483
61629
  app.post("/projects/:name/wordpress/connect", async (request) => {
61484
61630
  return withWordpressErrorHandling(async () => {
@@ -61506,8 +61652,8 @@ async function wordpressRoutes(app, opts) {
61506
61652
  createdAt: existing?.createdAt ?? now,
61507
61653
  updatedAt: now
61508
61654
  };
61509
- await verifyWordpressConnection(nextConnection);
61510
- const connection = store.upsertConnection(nextConnection);
61655
+ await verifyWordpressConnection(guarded(nextConnection));
61656
+ const connection = guarded(store.upsertConnection(nextConnection));
61511
61657
  const live = await getSiteStatus(connection, "live");
61512
61658
  const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
61513
61659
  writeAuditLog(app.db, {
@@ -61545,8 +61691,8 @@ async function wordpressRoutes(app, opts) {
61545
61691
  });
61546
61692
  app.get("/projects/:name/wordpress/status", async (request) => {
61547
61693
  const project = resolveProject(app.db, request.params.name);
61548
- const connection = opts.wordpressConnectionStore?.getConnection(project.name);
61549
- if (!connection) {
61694
+ const stored = opts.wordpressConnectionStore?.getConnection(project.name);
61695
+ if (!stored) {
61550
61696
  return {
61551
61697
  connected: false,
61552
61698
  projectName: project.name,
@@ -61556,6 +61702,7 @@ async function wordpressRoutes(app, opts) {
61556
61702
  adminUrl: null
61557
61703
  };
61558
61704
  }
61705
+ const connection = guarded(stored);
61559
61706
  const live = await getSiteStatus(connection, "live");
61560
61707
  const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
61561
61708
  return {
@@ -61840,8 +61987,8 @@ async function wordpressRoutes(app, opts) {
61840
61987
  createdAt: existing?.createdAt ?? now,
61841
61988
  updatedAt: now
61842
61989
  };
61843
- await verifyWordpressConnection(nextConnection);
61844
- connection = store.upsertConnection(nextConnection);
61990
+ await verifyWordpressConnection(guarded(nextConnection));
61991
+ connection = guarded(store.upsertConnection(nextConnection));
61845
61992
  writeAuditLog(app.db, {
61846
61993
  projectId: project.id,
61847
61994
  actor: "api",
@@ -62793,7 +62940,6 @@ async function backlinksRoutes(app, opts) {
62793
62940
  import crypto48 from "crypto";
62794
62941
  import { isIP } from "net";
62795
62942
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
62796
- import { Agent as UndiciAgent } from "undici";
62797
62943
  import { and as and60, desc as desc35, eq as eq69, gte as gte16, inArray as inArray28, lte as lte17, sql as sql28 } from "drizzle-orm";
62798
62944
 
62799
62945
  // ../api-routes/src/cloudflare-worker-version.ts
@@ -78093,7 +78239,7 @@ async function listWordpressTrafficEvents(options) {
78093
78239
  let skippedEntryCount = 0;
78094
78240
  let hasMore = false;
78095
78241
  const events = [];
78096
- const dispatcher = options.dispatcher;
78242
+ const fetchImpl = options.fetchImpl ?? fetch;
78097
78243
  for (let page3 = 0; page3 < maxPages; page3 += 1) {
78098
78244
  const url = new URL(endpoint);
78099
78245
  url.searchParams.set("limit", String(pageSize));
@@ -78107,7 +78253,7 @@ async function listWordpressTrafficEvents(options) {
78107
78253
  url.searchParams.set("until", options.until);
78108
78254
  }
78109
78255
  url.searchParams.set("_cb", randomUUID4());
78110
- const fetchInit = {
78256
+ const response = await fetchImpl(url, {
78111
78257
  method: "GET",
78112
78258
  headers: {
78113
78259
  Authorization: authHeader,
@@ -78115,11 +78261,7 @@ async function listWordpressTrafficEvents(options) {
78115
78261
  "Cache-Control": "no-cache"
78116
78262
  },
78117
78263
  signal: AbortSignal.timeout(timeoutMs)
78118
- };
78119
- if (dispatcher !== void 0) {
78120
- fetchInit.dispatcher = dispatcher;
78121
- }
78122
- const response = await fetch(url, fetchInit);
78264
+ });
78123
78265
  if (!response.ok) {
78124
78266
  const body2 = await readErrorBody2(response);
78125
78267
  throw new WordpressTrafficApiError(
@@ -80570,28 +80712,8 @@ async function trafficRoutes(app, opts) {
80570
80712
  if (!check2.ok) {
80571
80713
  throw validationError(`WordPress baseUrl rejected: ${check2.message}`);
80572
80714
  }
80573
- const { address, family } = check2.target;
80574
- return new UndiciAgent({
80575
- connect: {
80576
- lookup: (_hostname, options, cb) => {
80577
- if (options?.all) {
80578
- cb(null, [{ address, family: family === 6 ? 6 : 4 }]);
80579
- } else {
80580
- cb(null, address, family === 6 ? 6 : 4);
80581
- }
80582
- }
80583
- }
80584
- });
80585
- }
80586
- async function withPinnedWordpressDispatcher(baseUrl, fn) {
80587
- const dispatcher = await assertWordpressTargetAllowed(baseUrl);
80588
- try {
80589
- return await fn(dispatcher);
80590
- } finally {
80591
- await dispatcher.close().catch(() => {
80592
- });
80593
- }
80594
80715
  }
80716
+ const wordpressTrafficFetch = createGuardedFetch({ allowLoopback });
80595
80717
  const vercelMaxPages = opts.defaultVercelMaxPages ?? DEFAULT_VERCEL_MAX_PAGES;
80596
80718
  const vercelSyncDeadlineMs = opts.vercelSyncDeadlineMs ?? DEFAULT_VERCEL_SYNC_DEADLINE_MS;
80597
80719
  const syncWindowMinutes = opts.defaultSyncWindowMinutes ?? DEFAULT_SYNC_WINDOW_MINUTES;
@@ -80743,26 +80865,26 @@ async function trafficRoutes(app, opts) {
80743
80865
  throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
80744
80866
  }
80745
80867
  const { baseUrl, username, applicationPassword, displayName } = parsed.data;
80746
- await withPinnedWordpressDispatcher(baseUrl, async (dispatcher) => {
80747
- try {
80748
- await pullWordpressEvents({
80749
- baseUrl,
80750
- username,
80751
- applicationPassword,
80752
- pageSize: 1,
80753
- maxPages: 1,
80754
- dispatcher
80755
- });
80756
- } catch (e) {
80757
- if (e instanceof WordpressTrafficApiError) {
80758
- throw providerError(
80759
- `WordPress traffic probe failed (HTTP ${e.status}): ${e.message}${e.body ? ` \u2014 ${e.body}` : ""}`
80760
- );
80761
- }
80762
- const msg = describeError(e);
80763
- throw providerError(`WordPress traffic probe failed: ${msg}`);
80868
+ await assertWordpressTargetAllowed(baseUrl);
80869
+ try {
80870
+ await pullWordpressEvents({
80871
+ baseUrl,
80872
+ username,
80873
+ applicationPassword,
80874
+ pageSize: 1,
80875
+ maxPages: 1,
80876
+ fetchImpl: wordpressTrafficFetch
80877
+ });
80878
+ } catch (e) {
80879
+ if (e instanceof WordpressTrafficApiError) {
80880
+ throw providerError(
80881
+ `WordPress traffic probe failed (HTTP ${e.status}): ${e.message}${e.body ? ` \u2014 ${e.body}` : ""}`
80882
+ );
80764
80883
  }
80765
- });
80884
+ if (e instanceof EgressRefusedError && !e.redirected) throw validationError(`WordPress baseUrl rejected: ${e.message}`);
80885
+ const msg = describeError(e);
80886
+ throw providerError(`WordPress traffic probe failed: ${msg}`);
80887
+ }
80766
80888
  const now = (/* @__PURE__ */ new Date()).toISOString();
80767
80889
  const existing = credentialStore.getConnection(project.name);
80768
80890
  credentialStore.upsertConnection({
@@ -81835,10 +81957,9 @@ async function trafficRoutes(app, opts) {
81835
81957
  const windowEndIso = windowEnd.toISOString();
81836
81958
  const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
81837
81959
  const wpMaxPages = opts.defaultWordpressMaxPages ?? DEFAULT_WP_MAX_PAGES;
81838
- let pinnedDispatcher;
81839
81960
  try {
81840
81961
  renewWordpressLease();
81841
- pinnedDispatcher = await assertWordpressTargetAllowed(credential.baseUrl);
81962
+ await assertWordpressTargetAllowed(credential.baseUrl);
81842
81963
  } catch (e) {
81843
81964
  const msg = describeError(e);
81844
81965
  markFailed(msg, "PROVIDER_PULL");
@@ -81858,7 +81979,7 @@ async function trafficRoutes(app, opts) {
81858
81979
  maxPages: 1,
81859
81980
  since: windowStartIso,
81860
81981
  until: windowEndIso,
81861
- dispatcher: pinnedDispatcher
81982
+ fetchImpl: wordpressTrafficFetch
81862
81983
  });
81863
81984
  collected.push(...pageResult.events);
81864
81985
  const previousCursor = cursor;
@@ -81876,10 +81997,8 @@ async function trafficRoutes(app, opts) {
81876
81997
  } catch (e) {
81877
81998
  const msg = describeError(e);
81878
81999
  markFailed(msg, "PROVIDER_PULL");
82000
+ if (e instanceof EgressRefusedError && !e.redirected) throw validationError(`WordPress baseUrl rejected: ${msg}`);
81879
82001
  throw providerError(`WordPress pull failed: ${msg}`);
81880
- } finally {
81881
- await pinnedDispatcher.close().catch(() => {
81882
- });
81883
82002
  }
81884
82003
  } else {
81885
82004
  auditAction = "traffic.vercel.synced";
@@ -84383,7 +84502,7 @@ var DATA_FRESHNESS_CHECKS = [gaRecentDataCheck, gscRecentDataCheck];
84383
84502
  import dns3 from "dns/promises";
84384
84503
  import http3 from "http";
84385
84504
  import https3 from "https";
84386
- import net3 from "net";
84505
+ import net4 from "net";
84387
84506
  var SITE_REACHABILITY_USER_AGENT = "Canonry site-liveness (+https://canonry.ai)";
84388
84507
  var DEFAULT_SITE_MAX_REDIRECTS = 10;
84389
84508
  var DEFAULT_TIMEOUT_MS5 = 1e4;
@@ -84408,7 +84527,7 @@ function errorCode(err) {
84408
84527
  return typeof code === "string" ? code : "";
84409
84528
  }
84410
84529
  async function lookupSiteAddresses(hostname, timeoutMs = DNS_TIMEOUT_MS) {
84411
- const literalFamily = net3.isIPv6(hostname) ? 6 : net3.isIPv4(hostname) ? 4 : null;
84530
+ const literalFamily = net4.isIPv6(hostname) ? 6 : net4.isIPv4(hostname) ? 4 : null;
84412
84531
  if (literalFamily) return { kind: "addresses", addresses: [{ address: hostname, family: literalFamily }] };
84413
84532
  let timer;
84414
84533
  const deadline = new Promise((resolve) => {
@@ -86374,7 +86493,10 @@ var WORDPRESS_PUBLISH_CHECKS = [
86374
86493
  };
86375
86494
  }
86376
86495
  try {
86377
- const status = await verifyWordpressConnection(connection);
86496
+ const status = await verifyWordpressConnection({
86497
+ ...connection,
86498
+ fetchImpl: createGuardedFetch({ allowLoopback: ctx.allowLoopbackWebhooks === true })
86499
+ });
86378
86500
  return {
86379
86501
  status: CheckStatuses.ok,
86380
86502
  code: "wordpress.publish.connected",
@@ -87023,6 +87145,7 @@ async function doctorRoutes(app, opts) {
87023
87145
  googleConnectionStore: opts.googleConnectionStore,
87024
87146
  bingConnectionStore: opts.bingConnectionStore,
87025
87147
  wordpressConnectionStore: opts.wordpressConnectionStore,
87148
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
87026
87149
  ga4CredentialStore: opts.ga4CredentialStore,
87027
87150
  adsCredentialStore: opts.adsCredentialStore,
87028
87151
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
@@ -87056,6 +87179,7 @@ async function doctorRoutes(app, opts) {
87056
87179
  googleConnectionStore: opts.googleConnectionStore,
87057
87180
  bingConnectionStore: opts.bingConnectionStore,
87058
87181
  wordpressConnectionStore: opts.wordpressConnectionStore,
87182
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
87059
87183
  ga4CredentialStore: opts.ga4CredentialStore,
87060
87184
  adsCredentialStore: opts.adsCredentialStore,
87061
87185
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
@@ -90964,6 +91088,7 @@ async function apiRoutes(app, opts) {
90964
91088
  googleConnectionStore: opts.googleConnectionStore,
90965
91089
  bingConnectionStore: opts.bingConnectionStore,
90966
91090
  wordpressConnectionStore: opts.wordpressConnectionStore,
91091
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
90967
91092
  ga4CredentialStore: opts.ga4CredentialStore,
90968
91093
  adsCredentialStore: opts.adsCredentialStore,
90969
91094
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
@@ -91028,6 +91153,7 @@ function buildTrafficSourceValidators(opts) {
91028
91153
  if (opts.wordpressTrafficCredentialStore) {
91029
91154
  const store = opts.wordpressTrafficCredentialStore;
91030
91155
  const pullEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
91156
+ const wordpressTrafficFetch = createGuardedFetch({ allowLoopback: opts.allowLoopbackWebhooks === true });
91031
91157
  validators[TrafficSourceTypes.wordpress] = {
91032
91158
  validateCredentials: async (source) => {
91033
91159
  const record = store.getConnection(source.projectName);
@@ -91045,7 +91171,10 @@ function buildTrafficSourceValidators(opts) {
91045
91171
  username: record.username,
91046
91172
  applicationPassword: record.applicationPassword,
91047
91173
  pageSize: 1,
91048
- maxPages: 1
91174
+ maxPages: 1,
91175
+ // The probe carries Basic-auth credentials: check the stored URL
91176
+ // and every redirect hop as the sync route does.
91177
+ fetchImpl: wordpressTrafficFetch
91049
91178
  });
91050
91179
  return {
91051
91180
  status: CheckStatuses.ok,
@@ -92319,6 +92448,7 @@ export {
92319
92448
  determineCitationState,
92320
92449
  computeCitedCompetitorDomains,
92321
92450
  resolveTrustProxy,
92451
+ hasProxyHeaders,
92322
92452
  USER_SESSION_COOKIE_NAME,
92323
92453
  cookieIsSecure,
92324
92454
  anyUsersExist,
@@ -92331,6 +92461,8 @@ export {
92331
92461
  requireInstanceAdministrator,
92332
92462
  hashApiKey,
92333
92463
  notProbeRun,
92464
+ writeAuditLog,
92465
+ auditFromRequest,
92334
92466
  registerAgentConversationRoutes,
92335
92467
  measurementSlotKey,
92336
92468
  measurementRunSlotState,
@@ -92392,6 +92524,9 @@ export {
92392
92524
  classifyReviewsError,
92393
92525
  getUrlInfo,
92394
92526
  getCrawlIssues,
92527
+ EgressRefusedError,
92528
+ EgressFailedError,
92529
+ createGuardedFetch,
92395
92530
  PLUGIN_DIR,
92396
92531
  DUCKDB_SPEC,
92397
92532
  CC_CACHE_DIR,