@canonry/canonry 7.17.0 → 7.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/assets/assets/{AuditHistoryPanel-CUde9U_6.js → AuditHistoryPanel-Dga03Niw.js} +1 -1
  2. package/assets/assets/BacklinksPage-CfodWuIt.js +1 -0
  3. package/assets/assets/Hint-DICI5Jq9.js +1 -0
  4. package/assets/assets/{HistoryPage-DZSa1gYf.js → HistoryPage-BN_vTU5L.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-DgW17Jpd.js → MeasurementPropertyPage-CjiyzOmE.js} +1 -1
  6. package/assets/assets/ProjectPage-AxclA3YC.js +8 -0
  7. package/assets/assets/{RunsPage-DRfNd-Jp.js → RunsPage-Ca_ziCGC.js} +1 -1
  8. package/assets/assets/{SettingsPage-D38Gl1Nv.js → SettingsPage-SO1xl0d3.js} +1 -1
  9. package/assets/assets/SiteHealthSection-C9JgRzzs.js +4 -0
  10. package/assets/assets/{TrafficPage-DX3qRPgQ.js → TrafficPage-DrcbXNQB.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-CigB82vX.js → TrafficSourceDetailPage-HDZGKgyd.js} +1 -1
  12. package/assets/assets/{extract-error-message-DsMnnkxT.js → extract-error-message-D48vwe7I.js} +1 -1
  13. package/assets/assets/index-BaBHBReI.js +86 -0
  14. package/assets/assets/{react-sigma_core.esm.min-CkYxzGpi.js → react-sigma_core.esm.min-DVHs4npB.js} +1 -1
  15. package/assets/assets/{v2-overview-adapter-5x8UiA07.js → v2-overview-adapter-CTEXuiRt.js} +1 -1
  16. package/assets/assets/{vendor-lucide-N6vOpZmh.js → vendor-lucide-Y6jANGfG.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-WEKVQK3O.js → chunk-3LPQ4JV5.js} +130 -103
  19. package/dist/{chunk-2MBI5SYB.js → chunk-4EDH2ORA.js} +3 -3
  20. package/dist/{chunk-PVWGKDY6.js → chunk-MIRAKDRW.js} +111 -79
  21. package/dist/{chunk-QPOXDS43.js → chunk-NPNU6UHD.js} +5 -4
  22. package/dist/{chunk-WVBLFG7H.js → chunk-P2XY5YX7.js} +2 -2
  23. package/dist/{chunk-RBQUGZM4.js → chunk-QAIJH74T.js} +1 -1
  24. package/dist/{chunk-3NAZXITW.js → chunk-YJS4OE6I.js} +615 -579
  25. package/dist/cli.js +15 -16
  26. package/dist/{demo-server-BVJFYR4V.js → demo-server-54SLPSJF.js} +6 -5
  27. package/dist/index.d.ts +7 -5
  28. package/dist/index.js +6 -6
  29. package/dist/{intelligence-service-FK2W72X7.js → intelligence-service-62CEP6M2.js} +2 -2
  30. package/dist/mcp.js +4 -4
  31. package/package.json +10 -10
  32. package/assets/assets/BacklinksPage-DMQPw8ks.js +0 -1
  33. package/assets/assets/ProjectPage-C8vdtqtA.js +0 -8
  34. package/assets/assets/SiteHealthSection-CR3zB84D.js +0 -4
  35. package/assets/assets/index-DBr6ISbx.js +0 -86
@@ -303,6 +303,7 @@ import {
303
303
  coerceSkillManifest,
304
304
  compactDateToIso,
305
305
  compareSemver,
306
+ compareText,
306
307
  competitorAliasBlockRequestSchema,
307
308
  competitorAliasProjectIdentity,
308
309
  competitorAliasesRequestSchema,
@@ -376,6 +377,7 @@ import {
376
377
  emptyCitationVisibility,
377
378
  emptySentimentCounts,
378
379
  escapeLikePattern,
380
+ escapeRegExp,
379
381
  evaluateModelPointerExposure,
380
382
  expandQueryTemplate,
381
383
  expandResearchTemplate,
@@ -493,6 +495,7 @@ import {
493
495
  isoDateDaysBeforeInTimeZone,
494
496
  keywordDtoSchema,
495
497
  keywordGenerateRequestSchema,
498
+ keywordGenerateResponseSchema,
496
499
  latestProjectRunDtoSchema,
497
500
  linearTrend,
498
501
  locationContextSchema,
@@ -604,6 +607,7 @@ import {
604
607
  normalizeCompetitorAliases,
605
608
  normalizeCompetitorDomain,
606
609
  normalizeIdTokens,
610
+ normalizeIdentityText,
607
611
  normalizeMeasurementExactUrl,
608
612
  normalizeMeasurementExecutionQueryText,
609
613
  normalizeMeasurementHost,
@@ -643,8 +647,10 @@ import {
643
647
  planCompetitorAliases,
644
648
  planCompetitorAutoAliases,
645
649
  prepareBrandMatchText,
650
+ projectConfigExportSchema,
646
651
  projectConfigSchema,
647
652
  projectCreateRequestSchema,
653
+ projectDefaultLocationResponseSchema,
648
654
  projectDtoSchema,
649
655
  projectOverviewDtoSchema,
650
656
  projectSearchResponseSchema,
@@ -656,6 +662,7 @@ import {
656
662
  providersFailing,
657
663
  queryDtoSchema,
658
664
  queryGenerateRequestSchema,
665
+ queryGenerateResponseSchema,
659
666
  queryTrackingCommitRequestSchema,
660
667
  queryTrackingCommitResponseSchema,
661
668
  queryTrackingPreviewRequestSchema,
@@ -777,6 +784,7 @@ import {
777
784
  snapshotReportSchema,
778
785
  snapshotRequestInputSchema,
779
786
  snapshotRequestSchema,
787
+ sortedUnique,
780
788
  sourceBreakdownDtoSchema,
781
789
  sourceBreakdownQuerySchema,
782
790
  splitList,
@@ -833,12 +841,12 @@ import {
833
841
  wordpressManualAssistDtoSchema,
834
842
  wordpressOnboardResultDtoSchema,
835
843
  wordpressPageDetailDtoSchema,
836
- wordpressPageSummaryDtoSchema,
844
+ wordpressPageListDtoSchema,
837
845
  wordpressSchemaBlockDtoSchema,
838
846
  wordpressSchemaDeployResultDtoSchema,
839
847
  wordpressSchemaStatusResultDtoSchema,
840
848
  wordpressStatusDtoSchema
841
- } from "./chunk-PVWGKDY6.js";
849
+ } from "./chunk-MIRAKDRW.js";
842
850
 
843
851
  // src/intelligence-service.ts
844
852
  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 +1048,7 @@ var projects = sqliteTable("projects", {
1040
1048
  locations: text("locations", { mode: "json" }).$type().notNull().default([]),
1041
1049
  defaultLocation: text("default_location"),
1042
1050
  autoExtractBacklinks: integer("auto_extract_backlinks", { mode: "boolean" }).notNull().default(false),
1043
- configSource: text("config_source").notNull().default("cli"),
1051
+ configSource: text("config_source").$type().notNull().default("cli"),
1044
1052
  configRevision: integer("config_revision").notNull().default(1),
1045
1053
  icpDescription: text("icp_description"),
1046
1054
  createdAt: text("created_at").notNull(),
@@ -9682,11 +9690,13 @@ var SentimentRepository = class {
9682
9690
  }
9683
9691
  };
9684
9692
 
9685
- // ../intelligence/src/regressions.ts
9693
+ // ../intelligence/src/snapshot-key.ts
9686
9694
  function snapshotKey(snap) {
9687
9695
  const loc = snap.location ?? "__none__";
9688
9696
  return JSON.stringify([snap.query, snap.provider, loc]);
9689
9697
  }
9698
+
9699
+ // ../intelligence/src/regressions.ts
9690
9700
  function detectRegressions(currentRun, previousRun) {
9691
9701
  if ((currentRun.location ?? null) !== (previousRun.location ?? null)) {
9692
9702
  return [];
@@ -9726,24 +9736,20 @@ function observedKeys(run, keyOf) {
9726
9736
  }
9727
9737
 
9728
9738
  // ../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
9739
  function detectGains(currentRun, previousRun) {
9734
9740
  if ((currentRun.location ?? null) !== (previousRun.location ?? null)) {
9735
9741
  return [];
9736
9742
  }
9737
9743
  const gains = [];
9738
- const previousObserved = observedKeys(previousRun, snapshotKey2);
9744
+ const previousObserved = observedKeys(previousRun, snapshotKey);
9739
9745
  const previousCited = /* @__PURE__ */ new Set();
9740
9746
  for (const snap of previousRun.snapshots) {
9741
9747
  if (snap.cited) {
9742
- previousCited.add(snapshotKey2(snap));
9748
+ previousCited.add(snapshotKey(snap));
9743
9749
  }
9744
9750
  }
9745
9751
  for (const snap of currentRun.snapshots) {
9746
- const key = snapshotKey2(snap);
9752
+ const key = snapshotKey(snap);
9747
9753
  if (snap.cited && previousObserved.has(key) && !previousCited.has(key)) {
9748
9754
  gains.push({
9749
9755
  query: snap.query,
@@ -12451,6 +12457,11 @@ var FORWARDED_HEADERS = ["x-forwarded-for", "forwarded", "x-real-ip"];
12451
12457
  function hasForwardedHeaders(request) {
12452
12458
  return FORWARDED_HEADERS.some((header2) => request.headers[header2] !== void 0);
12453
12459
  }
12460
+ var PROXY_PATH_HEADERS = ["via", "cf-connecting-ip", "true-client-ip"];
12461
+ var FORWARDED_HEADER_PREFIX = "x-forwarded-";
12462
+ function hasProxyHeaders(request) {
12463
+ return hasForwardedHeaders(request) || PROXY_PATH_HEADERS.some((header2) => request.headers[header2] !== void 0) || Object.keys(request.headers).some((header2) => header2.startsWith(FORWARDED_HEADER_PREFIX));
12464
+ }
12454
12465
  function resolveCallerKey(request, trustProxyConfigured) {
12455
12466
  const socketAddress = request.socket.remoteAddress ?? request.ip;
12456
12467
  if (!trustProxyConfigured) return socketAddress;
@@ -14418,8 +14429,7 @@ function rowKey(row) {
14418
14429
  return { queryText: row.queryText, queryId: row.queryId, executionNodeKey: row.executionNodeKey };
14419
14430
  }
14420
14431
  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 ?? "");
14432
+ return left.queryText.localeCompare(right.queryText, "en") || compareText(left.queryText, right.queryText) || compareText(left.queryId, right.queryId) || compareText(left.executionNodeKey ?? "", right.executionNodeKey ?? "");
14423
14433
  }
14424
14434
  function withheldCounts(counts2) {
14425
14435
  return Object.fromEntries(Object.entries(counts2).map(([key, value]) => [key, OPERATIONAL_OUTCOMES.has(key) ? value : 0]));
@@ -14987,9 +14997,6 @@ import { and as and7, desc as desc4, eq as eq12, exists, inArray as inArray4, is
14987
14997
  import crypto7 from "crypto";
14988
14998
  import { and as and6, eq as eq11, gt as gt2, lte as lte4 } from "drizzle-orm";
14989
14999
  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
15000
  function canonicalJsonValue(value) {
14994
15001
  if (Array.isArray(value)) return value.map(canonicalJsonValue);
14995
15002
  if (value && typeof value === "object") {
@@ -15957,10 +15964,6 @@ import { and as and11, desc as desc7, eq as eq17, gt as gt3, inArray as inArray6
15957
15964
  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
15965
 
15959
15966
  // ../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
15967
  function normalizedHost(value) {
15965
15968
  try {
15966
15969
  return normalizeMeasurementHost(value);
@@ -16042,7 +16045,7 @@ function classifySourceAttribution(value, routes, normalizedOwnedHosts) {
16042
16045
  const claim = routeClaim(source, route);
16043
16046
  if (claim) claims.push(claim);
16044
16047
  }
16045
- claims.sort((left, right) => right.modeRank - left.modeRank || right.pathLength - left.pathLength || compareText2(left.targetId, right.targetId) || compareText2(left.urlId, right.urlId));
16048
+ claims.sort((left, right) => right.modeRank - left.modeRank || right.pathLength - left.pathLength || compareText(left.targetId, right.targetId) || compareText(left.urlId, right.urlId));
16046
16049
  const best = claims.at(0);
16047
16050
  if (!best) {
16048
16051
  return {
@@ -16083,8 +16086,7 @@ function classifySourceForUsageEdge(source, usageEdge) {
16083
16086
  }
16084
16087
  function normalizeMeasurementLocation(value) {
16085
16088
  if (value === null) return null;
16086
- const normalized = value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
16087
- return normalized || null;
16089
+ return normalizeIdentityText(value) || null;
16088
16090
  }
16089
16091
  function words(value) {
16090
16092
  return value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
@@ -16142,7 +16144,7 @@ function mentionedTargetsForAliases(answerText, aliases, preparedWords) {
16142
16144
  }
16143
16145
  function identityAmbiguityPatterns(aliases) {
16144
16146
  const patterns = aliases.flatMap((alias2) => {
16145
- const name = words(alias2).join(" ").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16147
+ const name = escapeRegExp(words(alias2).join(" "));
16146
16148
  if (!name) return [];
16147
16149
  return [{
16148
16150
  clarification: new RegExp(`(?:^| )which (?:specific )?${name} (?:(?:(?:do|did) )?you mean|(?:(?:are|were) you|you(?: re| are| were)?) (?:asking about|referring to|talking about))(?= |$)`, "u"),
@@ -16245,7 +16247,7 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16245
16247
  const observationsBySlot = /* @__PURE__ */ new Map();
16246
16248
  const historical = /* @__PURE__ */ new Set();
16247
16249
  const evidenceIncomplete = /* @__PURE__ */ new Set();
16248
- for (const slot of [...input.expectedSlots].sort((left, right) => compareText2(left.id, right.id))) {
16250
+ for (const slot of [...input.expectedSlots].sort((left, right) => compareText(left.id, right.id))) {
16249
16251
  const rows = candidates.get(slot.id) ?? [];
16250
16252
  if (rows.length > 1) {
16251
16253
  for (const row of rows) ambiguous.add(row.id);
@@ -16301,7 +16303,7 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16301
16303
  observation.unknownMentionTargetIds = mentions.unknown;
16302
16304
  observation.citedTargetIds = citedTargetIds;
16303
16305
  if (!detailedEvidence) continue;
16304
- const edges = [...edgesByExecution.get(observation.slot.executionId) ?? []].sort((left, right) => compareText2(left.id, right.id));
16306
+ const edges = [...edgesByExecution.get(observation.slot.executionId) ?? []].sort((left, right) => compareText(left.id, right.id));
16305
16307
  for (const edge of edges) {
16306
16308
  const sources = observation.sourceUrls.map((sourceUrl) => {
16307
16309
  const { classification, normalizedUrl, matchedTargetIds, matchedUrlIds } = classifySourceForUsageEdge(sourceAttribution(sourceUrl), edge);
@@ -16336,7 +16338,7 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16336
16338
  });
16337
16339
  }
16338
16340
  }
16339
- answers.sort((left, right) => compareText2(left.expectedSlotId, right.expectedSlotId) || compareText2(left.usageEdgeId, right.usageEdgeId));
16341
+ answers.sort((left, right) => compareText(left.expectedSlotId, right.expectedSlotId) || compareText(left.usageEdgeId, right.usageEdgeId));
16340
16342
  const evidence = answers.flatMap((answer) => answer.sources.map((source) => ({
16341
16343
  observationId: answer.observationId,
16342
16344
  expectedSlotId: answer.expectedSlotId,
@@ -16355,7 +16357,7 @@ function prepareReport(input, options = {}, detailedEvidence = true) {
16355
16357
  matchedTargetIds: source.matchedTargetIds,
16356
16358
  matchedUrlIds: source.matchedUrlIds
16357
16359
  })));
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));
16360
+ 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
16361
  options.onSourceAttributionComputed?.(sourceAttributions.size);
16360
16362
  return {
16361
16363
  observationsBySlot,
@@ -16387,7 +16389,7 @@ function slotsForEdges(expectedSlots, edges) {
16387
16389
  for (const slot of expectedSlots) {
16388
16390
  if (executionIds.has(slot.executionId)) slots.set(slot.id, slot);
16389
16391
  }
16390
- return [...slots.values()].sort((left, right) => compareText2(left.id, right.id));
16392
+ return [...slots.values()].sort((left, right) => compareText(left.id, right.id));
16391
16393
  }
16392
16394
  function completeness(slots, prepared) {
16393
16395
  const observations = slots.flatMap((slot) => {
@@ -16450,7 +16452,7 @@ function providersFor(slots) {
16450
16452
  return sortedUnique(slots.map((slot) => slot.provider));
16451
16453
  }
16452
16454
  function buildSovForSlots(slots, competitors2, projectBrandNames, projectDomain, prepared) {
16453
- const sortedCompetitors = [...competitors2].sort((left, right) => compareText2(left.domain, right.domain));
16455
+ const sortedCompetitors = [...competitors2].sort((left, right) => compareText(left.domain, right.domain));
16454
16456
  const rows = [
16455
16457
  { domain: projectDomain, own: true, aliases: projectBrandNames },
16456
16458
  ...sortedCompetitors.map((competitor) => ({
@@ -16614,7 +16616,7 @@ function indexedSlotsForEdges(edges, indexes) {
16614
16616
  for (const edge of edges) {
16615
16617
  for (const slot of indexes.slotsByExecutionId.get(edge.executionId) ?? []) slots.set(slot.id, slot);
16616
16618
  }
16617
- return [...slots.values()].sort((left, right) => compareText2(left.id, right.id));
16619
+ return [...slots.values()].sort((left, right) => compareText(left.id, right.id));
16618
16620
  }
16619
16621
  function indexedAnsweredSlots(slots, indexes) {
16620
16622
  return slots.filter((slot) => indexes.answeredSlotIds.has(slot.id));
@@ -16778,22 +16780,19 @@ function buildMeasurementReport(input, options = {}) {
16778
16780
  const prepared = prepareReport(input, options);
16779
16781
  return {
16780
16782
  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)),
16783
+ groups: [...input.groups].sort((left, right) => compareText(left.id, right.id)).map((group) => buildGroupReport(group, input, prepared)),
16784
+ targets: [...input.targets].sort((left, right) => compareText(left.id, right.id)).map((target) => buildTargetReport(target, input, prepared)),
16783
16785
  evidence: prepared.evidence,
16784
16786
  diagnostics: prepared.diagnostics
16785
16787
  };
16786
16788
  }
16787
16789
 
16788
16790
  // ../api-routes/src/measurement-report-adapter.ts
16789
- function compareText3(left, right) {
16790
- return left < right ? -1 : left > right ? 1 : 0;
16791
- }
16792
16791
  function normalizedProviders(values) {
16793
16792
  const providers = values.map((value) => value.trim().toLocaleLowerCase("en"));
16794
16793
  if (providers.some((value) => !value)) throw new Error("measurement manifest provider must be non-empty");
16795
16794
  if (new Set(providers).size !== providers.length) throw new Error("measurement manifest providers must be unique");
16796
- return providers.sort(compareText3);
16795
+ return providers.sort(compareText);
16797
16796
  }
16798
16797
  function canonicalContext(value) {
16799
16798
  if (value === null) return "null";
@@ -16826,7 +16825,7 @@ function frozenExecutionNodes(plan) {
16826
16825
  }
16827
16826
  function manifestFromNodes(nodes, providersFor2) {
16828
16827
  const expectedSlots = [];
16829
- for (const node of [...nodes].sort((left, right) => compareText3(left.stableKey, right.stableKey))) {
16828
+ for (const node of [...nodes].sort((left, right) => compareText(left.stableKey, right.stableKey))) {
16830
16829
  const providers = providersFor2(node);
16831
16830
  if (node.expectedSnapshots !== providers.length) {
16832
16831
  throw new Error(`measurement manifest provider roster does not satisfy execution ${node.stableKey}`);
@@ -17324,9 +17323,6 @@ function expectedSlotsFor(nodes, providers, models) {
17324
17323
  ...models[provider] ? { requestedModel: models[provider] } : {}
17325
17324
  })));
17326
17325
  }
17327
- function compareText4(left, right) {
17328
- return left < right ? -1 : left > right ? 1 : 0;
17329
- }
17330
17326
  function normalizedLocationIdentity(value) {
17331
17327
  if (!value) return null;
17332
17328
  return [value.country, value.region, value.city, value.label, value.timezone ?? ""].map((part) => part.trim().toLocaleLowerCase("en")).join("\0");
@@ -17353,7 +17349,7 @@ function materializeV2ExecutionNodes(nodes, instanceModels) {
17353
17349
  const answering = /* @__PURE__ */ new Map();
17354
17350
  const retired = /* @__PURE__ */ new Set();
17355
17351
  const claimed = /* @__PURE__ */ new Set();
17356
- for (const node of [...nodes].sort((left, right) => compareText4(left.stableKey, right.stableKey))) {
17352
+ for (const node of [...nodes].sort((left, right) => compareText(left.stableKey, right.stableKey))) {
17357
17353
  const nodeProviders = normalizeProviders(node.context.providers);
17358
17354
  const declared = nodeProviderModels(node);
17359
17355
  const resolved = /* @__PURE__ */ new Map();
@@ -17385,11 +17381,11 @@ function materializeV2ExecutionNodes(nodes, instanceModels) {
17385
17381
  }
17386
17382
  return {
17387
17383
  expectedSlots,
17388
- providers: [...providers].sort(compareText4),
17384
+ providers: [...providers].sort(compareText),
17389
17385
  models: Object.fromEntries([...models].flatMap(([provider, model]) => {
17390
17386
  if (model) return [[provider, model]];
17391
17387
  const current = answering.get(provider);
17392
- return retired.has(provider) && current ? [[provider, [...current].sort(compareText4).join(" + ")]] : [];
17388
+ return retired.has(provider) && current ? [[provider, [...current].sort(compareText).join(" + ")]] : [];
17393
17389
  }))
17394
17390
  };
17395
17391
  }
@@ -17405,12 +17401,9 @@ function sliceForV2(plan, params) {
17405
17401
  function quotedList(values) {
17406
17402
  return values.map((value) => `"${value}"`).join(", ");
17407
17403
  }
17408
- function sortedUnique2(values) {
17409
- return [...new Set(values)].sort(compareText4);
17410
- }
17411
17404
  function resolveV2RunScope(plan, scope) {
17412
- const requestedGroups = sortedUnique2(scope.groups ?? []);
17413
- const requestedTargets = sortedUnique2(scope.targets ?? []);
17405
+ const requestedGroups = sortedUnique(scope.groups ?? []);
17406
+ const requestedTargets = sortedUnique(scope.targets ?? []);
17414
17407
  const groupsByKey = new Map(plan.groups.map((group) => [group.stableKey, group]));
17415
17408
  const targetKeys = new Set(plan.targets.map((target) => target.stableKey));
17416
17409
  const unknownGroups = requestedGroups.filter((key) => !groupsByKey.has(key));
@@ -17429,7 +17422,7 @@ function resolveV2RunScope(plan, scope) {
17429
17422
  for (const key of requestedGroups) {
17430
17423
  for (const targetKey of groupsByKey.get(key).targetKeys) selected.add(targetKey);
17431
17424
  }
17432
- const resolvedTargets = [...selected].sort(compareText4);
17425
+ const resolvedTargets = [...selected].sort(compareText);
17433
17426
  const usedNodeKeys = new Set(plan.usageEdges.filter((edge) => selected.has(edge.targetKey)).map((edge) => edge.executionNodeKey));
17434
17427
  const executionNodes = plan.executionNodes.filter((node) => usedNodeKeys.has(node.stableKey));
17435
17428
  if (executionNodes.length === 0) {
@@ -17444,7 +17437,7 @@ function resolveV2RunScope(plan, scope) {
17444
17437
  };
17445
17438
  }
17446
17439
  function resolveV2QueryScope(plan, queryTexts) {
17447
- const requested = sortedUnique2(queryTexts.map(normalizeMeasurementExecutionQueryText).filter(Boolean));
17440
+ const requested = sortedUnique(queryTexts.map(normalizeMeasurementExecutionQueryText).filter(Boolean));
17448
17441
  const measured = new Set(plan.executionNodes.map((node) => normalizeMeasurementExecutionQueryText(node.queryText)));
17449
17442
  const unknown = requested.filter((text2) => !measured.has(text2));
17450
17443
  if (unknown.length) {
@@ -19804,20 +19797,17 @@ function countMetric(rate2) {
19804
19797
  if (rate2.numerator === null) return unavailable2(metricReason(rate2.reason));
19805
19798
  return { state: "available", value: rate2.numerator, numerator: rate2.numerator, denominator: rate2.denominator };
19806
19799
  }
19807
- function normalizedText(value) {
19808
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
19809
- }
19810
19800
  function overviewFilterFingerprint(query) {
19811
19801
  const filters = {
19812
19802
  scope: query.scope,
19813
19803
  groupKey: query.groupKey ?? null,
19814
19804
  targetKey: query.targetKey ?? null,
19815
19805
  queryClass: query.queryClass ?? "all",
19816
- provider: query.provider === void 0 ? null : normalizedText(query.provider),
19817
- location: query.location === void 0 ? null : normalizedText(query.location),
19806
+ provider: query.provider === void 0 ? null : normalizeIdentityText(query.provider),
19807
+ location: query.location === void 0 ? null : normalizeIdentityText(query.location),
19818
19808
  from: query.from ?? null,
19819
19809
  to: query.to ?? null,
19820
- search: query.search === void 0 ? null : normalizedText(query.search),
19810
+ search: query.search === void 0 ? null : normalizeIdentityText(query.search),
19821
19811
  compact: query.compact ?? false
19822
19812
  };
19823
19813
  return createHash4("sha256").update(JSON.stringify(filters)).digest("base64url");
@@ -19828,27 +19818,27 @@ function overviewAggregateFingerprint(query) {
19828
19818
  groupKey: query.groupKey ?? null,
19829
19819
  targetKey: query.targetKey ?? null,
19830
19820
  queryClass: query.queryClass ?? "all",
19831
- provider: query.provider === void 0 ? null : normalizedText(query.provider),
19832
- location: query.location === void 0 ? null : normalizedText(query.location),
19821
+ provider: query.provider === void 0 ? null : normalizeIdentityText(query.provider),
19822
+ location: query.location === void 0 ? null : normalizeIdentityText(query.location),
19833
19823
  from: query.from ?? null,
19834
19824
  to: query.to ?? null
19835
19825
  };
19836
19826
  return createHash4("sha256").update(JSON.stringify(filters)).digest("base64url");
19837
19827
  }
19838
19828
  function compareLabels(left, right) {
19839
- const leftLabel = normalizedText(left.label);
19840
- const rightLabel = normalizedText(right.label);
19829
+ const leftLabel = normalizeIdentityText(left.label);
19830
+ const rightLabel = normalizeIdentityText(right.label);
19841
19831
  if (leftLabel !== rightLabel) return leftLabel < rightLabel ? -1 : 1;
19842
- return left.targetKey < right.targetKey ? -1 : left.targetKey > right.targetKey ? 1 : 0;
19832
+ return compareText(left.targetKey, right.targetKey);
19843
19833
  }
19844
19834
  function compareLabelSort(left, right, descending) {
19845
- const leftLabel = normalizedText(left.label);
19846
- const rightLabel = normalizedText(right.label);
19835
+ const leftLabel = normalizeIdentityText(left.label);
19836
+ const rightLabel = normalizeIdentityText(right.label);
19847
19837
  if (leftLabel !== rightLabel) {
19848
19838
  const compared = leftLabel < rightLabel ? -1 : 1;
19849
19839
  return descending ? -compared : compared;
19850
19840
  }
19851
- return left.targetKey < right.targetKey ? -1 : left.targetKey > right.targetKey ? 1 : 0;
19841
+ return compareText(left.targetKey, right.targetKey);
19852
19842
  }
19853
19843
  function metricForSort(row, sort) {
19854
19844
  switch (sort) {
@@ -19877,7 +19867,7 @@ function compareRows2(left, right, sort) {
19877
19867
  return compareLabels(left, right);
19878
19868
  }
19879
19869
  function legacyCursorOf(row) {
19880
- return Buffer.from(`${normalizedText(row.label)}:${row.targetKey}`, "utf8").toString("base64url");
19870
+ return Buffer.from(`${normalizeIdentityText(row.label)}:${row.targetKey}`, "utf8").toString("base64url");
19881
19871
  }
19882
19872
  function cursorOf(row, sort, displayedRunId, filterFingerprint2, planVersionId, evidenceFingerprint) {
19883
19873
  const cursor = {
@@ -20116,12 +20106,9 @@ function planExpectedSlots(plan) {
20116
20106
  }
20117
20107
  function matchesSearch(row, search) {
20118
20108
  if (search === void 0) return true;
20119
- const needle = normalizedText(search);
20109
+ const needle = normalizeIdentityText(search);
20120
20110
  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;
20111
+ return normalizeIdentityText(row.label).includes(needle) || normalizeIdentityText(row.targetKey).includes(needle);
20125
20112
  }
20126
20113
  function propertyLocations(plan) {
20127
20114
  const groupsByKey = new Map(plan.groups.map((group) => [group.stableKey, group]));
@@ -20138,7 +20125,7 @@ function propertyLocations(plan) {
20138
20125
  }
20139
20126
  return levels;
20140
20127
  };
20141
- const byLabel = (left, right) => compareGroupText(left.label, right.label) || compareGroupText(left.stableKey, right.stableKey);
20128
+ const byLabel = (left, right) => compareText(left.label, right.label) || compareText(left.stableKey, right.stableKey);
20142
20129
  const metros = /* @__PURE__ */ new Map();
20143
20130
  const submarkets = /* @__PURE__ */ new Map();
20144
20131
  const push = (map, key, value) => {
@@ -20275,7 +20262,7 @@ function planV2Overview(db, projectId, active, plan, query, scope, cache) {
20275
20262
  }, () => {
20276
20263
  const manifest = measurementRunExpectedSlots(displayed, plan);
20277
20264
  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)));
20265
+ 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)));
20279
20266
  const usageEdges = queryClass === "all" ? input.usageEdges : input.usageEdges.filter((edge) => edgeQueryClass.get(edge.id) === queryClass);
20280
20267
  return buildMeasurementOverview({
20281
20268
  ...input,
@@ -20514,7 +20501,7 @@ function readCompetitorIdentityChangedAt(db, projectId) {
20514
20501
  return renamed !== null && renamed > audited ? renamed : audited;
20515
20502
  }
20516
20503
  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);
20504
+ 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
20505
  }
20519
20506
  function readCompetitorLandscape(app, projectName, query, selection) {
20520
20507
  const project = resolveProject(app.db, projectName);
@@ -20666,7 +20653,7 @@ function readCompetitorLandscape(app, projectName, query, selection) {
20666
20653
  else excluded.nonCompleted++;
20667
20654
  excludedByModel.set(key, excluded);
20668
20655
  }
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)));
20656
+ 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
20657
  const groups = sortedGroups.slice(0, COMPETITOR_LANDSCAPE_MODEL_GROUP_LIMIT).map((group) => {
20671
20658
  const groupHistory = buildHistory(group.snapshots);
20672
20659
  const groupObserved = groupHistory.observed.slice(0, COMPETITOR_LANDSCAPE_RANKED_ROW_LIMIT);
@@ -20878,9 +20865,6 @@ function snapshotMatchesFilters(input) {
20878
20865
  function requestedModel(model) {
20879
20866
  return model?.trim() || null;
20880
20867
  }
20881
- function compareStoredIds(left, right) {
20882
- return left < right ? -1 : left > right ? 1 : 0;
20883
- }
20884
20868
  function normalizedDomain(value) {
20885
20869
  return registrableDomain(value) || hostOf(value);
20886
20870
  }
@@ -21853,8 +21837,8 @@ function hash(value) {
21853
21837
  function answerIdentityFrom(project, competitorRows2, marketNames = /* @__PURE__ */ new Map()) {
21854
21838
  const projectDomains = effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains });
21855
21839
  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);
21840
+ const names = competitorRows2.map((row) => [row.domain, competitorIdentityAliases(row)]).sort(([left], [right]) => compareText(left, right));
21841
+ const market = [...marketNames].map(([domain, learned]) => [domain, [...learned]]).sort(([left], [right]) => compareText(left, right));
21858
21842
  return {
21859
21843
  projectDomains,
21860
21844
  projectBrandNames,
@@ -22304,12 +22288,6 @@ var MeasurementDiscoveryConfigurationError = class extends Error {
22304
22288
  this.name = "MeasurementDiscoveryConfigurationError";
22305
22289
  }
22306
22290
  };
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
22291
  function normalizeDeclaredHost(value, label) {
22314
22292
  const normalized = value.trim().toLowerCase().replace(/\.$/, "");
22315
22293
  if (!normalized || normalized.includes("://") || normalized.includes("/") || normalized.includes("@")) {
@@ -22374,7 +22352,7 @@ function normalizePatternValue(value, label) {
22374
22352
  return normalized;
22375
22353
  }
22376
22354
  function compileRules(input) {
22377
- const ownedHosts = sortedUnique3(input.ownedHosts.map((host, index2) => normalizeDeclaredHost(host, `ownedHosts[${index2}]`)));
22355
+ const ownedHosts = sortedUnique(input.ownedHosts.map((host, index2) => normalizeDeclaredHost(host, `ownedHosts[${index2}]`)));
22378
22356
  if (ownedHosts.length === 0) {
22379
22357
  throw new MeasurementDiscoveryConfigurationError("ownedHosts must contain at least one hostname");
22380
22358
  }
@@ -22385,7 +22363,7 @@ function compileRules(input) {
22385
22363
  if (aliases.some((alias2) => alias2.host === primary.host)) {
22386
22364
  throw new MeasurementDiscoveryConfigurationError("alias rules must use another owned host");
22387
22365
  }
22388
- const excludedSlugSuffixes = sortedUnique3(
22366
+ const excludedSlugSuffixes = sortedUnique(
22389
22367
  (input.rules.excludedSlugSuffixes ?? []).map(
22390
22368
  (value, index2) => normalizePatternValue(value, `rules.excludedSlugSuffixes[${index2}]`)
22391
22369
  )
@@ -22393,7 +22371,7 @@ function compileRules(input) {
22393
22371
  const excludedSlugPatterns = (input.rules.excludedSlugPatterns ?? []).map((pattern, index2) => ({
22394
22372
  kind: pattern.kind,
22395
22373
  value: normalizePatternValue(pattern.value, `rules.excludedSlugPatterns[${index2}].value`)
22396
- })).sort((a, b) => compareText5(`${a.kind}:${a.value}`, `${b.kind}:${b.value}`));
22374
+ })).sort((a, b) => compareText(`${a.kind}:${a.value}`, `${b.kind}:${b.value}`));
22397
22375
  return { ownedHosts, rules: { primary, aliases, excludedSlugSuffixes, excludedSlugPatterns } };
22398
22376
  }
22399
22377
  function parseSitemapUrl(value) {
@@ -22473,10 +22451,10 @@ function classifyMeasurementSitemapUrls(input) {
22473
22451
  const shared = [];
22474
22452
  const unmatched = [];
22475
22453
  const excluded = [];
22476
- const canonicalEntries = [...canonicalGroups.entries()].sort(([a], [b]) => compareText5(a, b));
22454
+ const canonicalEntries = [...canonicalGroups.entries()].sort(([a], [b]) => compareText(a, b));
22477
22455
  let classifiedUrlCount = 0;
22478
22456
  for (const [canonicalUrl, group] of canonicalEntries) {
22479
- const orderedGroup = [...group].sort((a, b) => compareText5(a.sourceUrl, b.sourceUrl));
22457
+ const orderedGroup = [...group].sort((a, b) => compareText(a.sourceUrl, b.sourceUrl));
22480
22458
  const representative = orderedGroup[0];
22481
22459
  for (let index2 = 1; index2 < orderedGroup.length; index2 += 1) {
22482
22460
  diagnostics.push({
@@ -22534,7 +22512,7 @@ function classifyMeasurementSitemapUrls(input) {
22534
22512
  else unmatched.push(item(canonicalUrl, "unmatched", "unmatched-path"));
22535
22513
  }
22536
22514
  const proposed = [...candidatesBySlug.values()].map(({ slug, stableKey, url }) => {
22537
- const aliasCoverageUrls = sortedUnique3((aliasesBySlug.get(slug) ?? []).map((match) => match.url));
22515
+ const aliasCoverageUrls = sortedUnique((aliasesBySlug.get(slug) ?? []).map((match) => match.url));
22538
22516
  return {
22539
22517
  classification: "proposed",
22540
22518
  reason: "primary-match",
@@ -22544,7 +22522,7 @@ function classifyMeasurementSitemapUrls(input) {
22544
22522
  primaryUrl: url,
22545
22523
  aliasCoverageUrls
22546
22524
  };
22547
- }).sort((a, b) => compareText5(a.stableKey, b.stableKey));
22525
+ }).sort((a, b) => compareText(a.stableKey, b.stableKey));
22548
22526
  const aliases = [];
22549
22527
  for (const [slug, matches] of aliasesBySlug) {
22550
22528
  const target = candidatesBySlug.get(slug);
@@ -22562,14 +22540,14 @@ function classifyMeasurementSitemapUrls(input) {
22562
22540
  }
22563
22541
  for (const match of matches) unmatched.push(item(match.url, "unmatched", "alias-without-primary"));
22564
22542
  }
22565
- const sortItems = (items) => items.sort((a, b) => compareText5(a.url, b.url));
22543
+ const sortItems = (items) => items.sort((a, b) => compareText(a.url, b.url));
22566
22544
  return {
22567
22545
  proposed,
22568
- aliases: aliases.sort((a, b) => compareText5(a.url, b.url)),
22546
+ aliases: aliases.sort((a, b) => compareText(a.url, b.url)),
22569
22547
  shared: sortItems(shared),
22570
22548
  unmatched: sortItems(unmatched),
22571
22549
  excluded: sortItems(excluded),
22572
- diagnostics: diagnostics.sort((a, b) => compareText5(`${a.kind}:${a.url}`, `${b.kind}:${b.url}`))
22550
+ diagnostics: diagnostics.sort((a, b) => compareText(`${a.kind}:${a.url}`, `${b.kind}:${b.url}`))
22573
22551
  };
22574
22552
  }
22575
22553
 
@@ -22580,7 +22558,117 @@ import { gunzip } from "zlib";
22580
22558
  import { promisify as promisify2 } from "util";
22581
22559
  import http from "http";
22582
22560
  import https from "https";
22561
+ import net2 from "net";
22562
+
22563
+ // ../api-routes/src/egress-policy.ts
22583
22564
  import net from "net";
22565
+ function blockedAddressReason(value, options = {}) {
22566
+ const address = stripZoneId(stripIpv6Brackets(value));
22567
+ if (options.allowLoopback && isLoopbackAddress(address)) return null;
22568
+ const family = net.isIP(address);
22569
+ if (family === 4) return blockedIpv4Reason(ipv4Octets(address));
22570
+ if (family === 6) {
22571
+ const bytes = ipv6Bytes(address);
22572
+ return bytes ? blockedIpv6Reason(bytes) : "which could not be read as an address";
22573
+ }
22574
+ return "which is not an IP address";
22575
+ }
22576
+ function isLoopbackAddress(value) {
22577
+ const address = stripZoneId(stripIpv6Brackets(value));
22578
+ const family = net.isIP(address);
22579
+ if (family === 4) return ipv4Octets(address)[0] === 127;
22580
+ if (family !== 6) return false;
22581
+ const bytes = ipv6Bytes(address);
22582
+ if (!bytes) return false;
22583
+ const mapped = mappedIpv4(bytes);
22584
+ if (mapped) return mapped[0] === 127;
22585
+ return bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;
22586
+ }
22587
+ function stripIpv6Brackets(value) {
22588
+ return value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
22589
+ }
22590
+ function stripZoneId(address) {
22591
+ return address.split("%")[0];
22592
+ }
22593
+ function ipv4Octets(address) {
22594
+ return address.split(".").map((part) => Number.parseInt(part, 10));
22595
+ }
22596
+ function blockedIpv4Reason(octets) {
22597
+ const [first, second] = octets;
22598
+ if (first === 0) return "which is unspecified space";
22599
+ if (first === 127) return "which is loopback";
22600
+ if (first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168) {
22601
+ return "which is private space";
22602
+ }
22603
+ if (first === 100 && second >= 64 && second <= 127) return "which is carrier-grade NAT space";
22604
+ if (first === 169 && second === 254) return "which is link-local";
22605
+ if (first >= 224 && first <= 239) return "which is multicast";
22606
+ if (first === 192 && second === 0 && octets[2] === 0 || first === 198 && (second === 18 || second === 19) || first >= 240) {
22607
+ return "which is reserved";
22608
+ }
22609
+ return null;
22610
+ }
22611
+ function blockedIpv6Reason(bytes) {
22612
+ const first = bytes[0];
22613
+ if (first === 0) {
22614
+ const nat64 = nat64Ipv4(bytes);
22615
+ if (nat64) return blockedIpv4Reason(nat64) === null ? null : `which is the NAT64 form of ${nat64.join(".")}`;
22616
+ const mapped = mappedIpv4(bytes);
22617
+ return mapped ? `which is the IPv6 form of ${mapped.join(".")}` : "which is reserved IPv6 space";
22618
+ }
22619
+ if (first === 1 && bytes.slice(1, 8).every((byte) => byte === 0)) return "which is reserved IPv6 space";
22620
+ if (first === 255) return "which is multicast";
22621
+ if ((first & 254) === 252) return "which is unique-local";
22622
+ if (first === 254 && (bytes[1] & 192) === 128) return "which is link-local";
22623
+ if (first === 254 && (bytes[1] & 192) === 192) return "which is reserved site-local space";
22624
+ if (first === 32 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0) return "which is a Teredo tunnel";
22625
+ if (first === 32 && bytes[1] === 2) return "which is a 6to4 tunnel";
22626
+ if (first === 32 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 2 && bytes[4] === 0 && bytes[5] === 0) {
22627
+ return "which is reserved IPv6 space";
22628
+ }
22629
+ return null;
22630
+ }
22631
+ function mappedIpv4(bytes) {
22632
+ const isMapped = bytes.slice(0, 10).every((byte) => byte === 0) && bytes[10] === 255 && bytes[11] === 255;
22633
+ return isMapped ? [bytes[12], bytes[13], bytes[14], bytes[15]] : null;
22634
+ }
22635
+ var NAT64_WELL_KNOWN_PREFIX = [0, 100, 255, 155, 0, 0, 0, 0, 0, 0, 0, 0];
22636
+ function nat64Ipv4(bytes) {
22637
+ const isNat64 = NAT64_WELL_KNOWN_PREFIX.every((byte, index2) => bytes[index2] === byte);
22638
+ return isNat64 ? [bytes[12], bytes[13], bytes[14], bytes[15]] : null;
22639
+ }
22640
+ function ipv6Bytes(address) {
22641
+ if (!net.isIPv6(address)) return null;
22642
+ const halves = address.split("::");
22643
+ if (halves.length > 2) return null;
22644
+ const expand = (text2) => {
22645
+ if (text2 === "") return [];
22646
+ const groups2 = [];
22647
+ const chunks = text2.split(":");
22648
+ for (const [index2, chunk] of chunks.entries()) {
22649
+ if (index2 === chunks.length - 1 && chunk.includes(".")) {
22650
+ const octets = ipv4Octets(chunk);
22651
+ groups2.push(octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]);
22652
+ continue;
22653
+ }
22654
+ groups2.push(Number.parseInt(chunk, 16));
22655
+ }
22656
+ return groups2;
22657
+ };
22658
+ const head = expand(halves[0]);
22659
+ const tail = halves.length === 2 ? expand(halves[1]) : [];
22660
+ const missing = 8 - head.length - tail.length;
22661
+ if (halves.length === 1 ? missing !== 0 : missing < 0) return null;
22662
+ const groups = [...head, ...new Array(halves.length === 2 ? missing : 0).fill(0), ...tail];
22663
+ const bytes = new Uint8Array(16);
22664
+ for (const [index2, group] of groups.entries()) {
22665
+ bytes[index2 * 2] = group >> 8;
22666
+ bytes[index2 * 2 + 1] = group & 255;
22667
+ }
22668
+ return bytes;
22669
+ }
22670
+
22671
+ // ../api-routes/src/measurement-sitemap-fetch.ts
22584
22672
  var gunzipAsync = promisify2(gunzip);
22585
22673
  var DEFAULT_MEASUREMENT_SITEMAP_LIMITS = {
22586
22674
  timeoutMs: 1e4,
@@ -22630,7 +22718,7 @@ async function fetchMeasurementSitemap(sitemapUrl, options = {}) {
22630
22718
  }
22631
22719
  }
22632
22720
  await visit(sitemapUrl, 0);
22633
- return { urls: [...urls].sort(compareText6), fetchedSitemaps: visited.size, bytesChecksum: documents.digest("hex") };
22721
+ return { urls: [...urls].sort(compareText), fetchedSitemaps: visited.size, bytesChecksum: documents.digest("hex") };
22634
22722
  }
22635
22723
  async function resolveMeasurementSitemapTarget(rawUrl, options = {}) {
22636
22724
  let parsed;
@@ -22647,7 +22735,7 @@ async function resolveMeasurementSitemapTarget(rawUrl, options = {}) {
22647
22735
  }
22648
22736
  const hostname = stripIpv6Brackets(parsed.hostname);
22649
22737
  if (!hostname) return { ok: false, message: "the URL has no hostname" };
22650
- const literalFamily = net.isIPv6(hostname) ? 6 : net.isIPv4(hostname) ? 4 : null;
22738
+ const literalFamily = net2.isIPv6(hostname) ? 6 : net2.isIPv4(hostname) ? 4 : null;
22651
22739
  const addresses = literalFamily ? [{ address: hostname, family: literalFamily }] : await (options.resolveAddresses ?? resolveSitemapAddresses)(hostname);
22652
22740
  if (addresses.length === 0) return { ok: false, message: `${hostname} does not resolve` };
22653
22741
  for (const entry of addresses) {
@@ -22663,85 +22751,6 @@ async function resolveSitemapAddresses(hostname) {
22663
22751
  if (ipv6.status === "fulfilled") for (const address of ipv6.value) found.set(`6:${address}`, { address, family: 6 });
22664
22752
  return [...found.values()];
22665
22753
  }
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
22754
  async function requestFollowingValidatedRedirects(startUrl, resolveTarget, transport, limits, deadlineAt) {
22746
22755
  let current = startUrl;
22747
22756
  for (let redirects = 0; ; redirects += 1) {
@@ -22798,7 +22807,7 @@ async function requestPinnedSitemap(target, limits) {
22798
22807
  path: `${target.url.pathname}${target.url.search}`,
22799
22808
  headers: { Host: target.url.host, Accept: "application/xml,text/xml,*/*;q=0.1", "Accept-Encoding": "gzip" }
22800
22809
  };
22801
- if (secure) requestOptions.servername = target.url.hostname.replace(/^\[|\]$/g, "");
22810
+ if (secure) requestOptions.servername = stripIpv6Brackets(target.url.hostname);
22802
22811
  return await new Promise((resolve, reject) => {
22803
22812
  let settled = false;
22804
22813
  const succeed = (response) => {
@@ -22854,11 +22863,11 @@ async function decodeSitemapBody(body, headers, maxBodyBytes) {
22854
22863
  }
22855
22864
  function sitemapIndexLocations(xml) {
22856
22865
  if (!hasXmlElement(xml, "sitemapindex")) return [];
22857
- return sortedUniqueUrls(xmlElementContents(xml, "sitemap").flatMap(extractLocation));
22866
+ return sortedUnique(xmlElementContents(xml, "sitemap").flatMap(extractLocation));
22858
22867
  }
22859
22868
  function urlSetLocations(xml) {
22860
22869
  if (!hasXmlElement(xml, "urlset")) return [];
22861
- return sortedUniqueUrls(xmlElementContents(xml, "url").flatMap(extractLocation));
22870
+ return sortedUnique(xmlElementContents(xml, "url").flatMap(extractLocation));
22862
22871
  }
22863
22872
  function extractLocation(xml) {
22864
22873
  const value = xmlElementContents(xml, "loc")[0]?.trim();
@@ -22949,12 +22958,6 @@ function decodeXmlEntities(value) {
22949
22958
  function firstHeader(value) {
22950
22959
  return Array.isArray(value) ? value[0] : value;
22951
22960
  }
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
22961
  function assertLimits(limits) {
22959
22962
  for (const [name, value] of Object.entries(limits)) {
22960
22963
  if (!Number.isSafeInteger(value) || value < 1) throw new Error(`Sitemap ${name} must be a positive safe integer`);
@@ -23025,12 +23028,6 @@ var MEASUREMENT_DRAFT_MAX_QUERIES = 1e3;
23025
23028
  var MEASUREMENT_PLAN_V2_MAX_BYTES = 4 * 1024 * 1024;
23026
23029
  var CHECKSUM_PLACEHOLDER = "0".repeat(64);
23027
23030
  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
23031
  function proposeQueryClass(queryText, brandNames) {
23035
23032
  const matcher = compileBrandAliases(brandNames.filter((name) => brandKeyFromText(name).length > 0));
23036
23033
  return matcherMatchesText(matcher, queryText) ? "branded" : "non-brand";
@@ -23142,7 +23139,7 @@ function createChecks() {
23142
23139
  };
23143
23140
  }
23144
23141
  function normalizeProviders2(values) {
23145
- return canonicalStrings(values.map((value) => value.trim().toLowerCase()).filter(Boolean));
23142
+ return sortedUnique(values.map((value) => value.trim().toLowerCase()).filter(Boolean));
23146
23143
  }
23147
23144
  function compileMeasurementDraft(authoring, context) {
23148
23145
  const { checks, sink, failed } = createChecks();
@@ -23152,7 +23149,7 @@ function compileMeasurementDraft(authoring, context) {
23152
23149
  } catch {
23153
23150
  sink.fail("invalid-project-identity", "The project canonical domain is not a valid hostname.", ["identities", "projectBrand", "canonicalHost"]);
23154
23151
  }
23155
- const ownedHosts = canonicalStrings([context.canonicalDomain, ...context.ownedDomains].flatMap((value) => {
23152
+ const ownedHosts = sortedUnique([context.canonicalDomain, ...context.ownedDomains].flatMap((value) => {
23156
23153
  try {
23157
23154
  return [normalizeMeasurementHost(value)];
23158
23155
  } catch {
@@ -23216,7 +23213,7 @@ function compileMeasurementDraft(authoring, context) {
23216
23213
  }
23217
23214
  aliasClaims.set(identity, target.stableKey);
23218
23215
  });
23219
- const aliases = canonicalStrings(target.aliases);
23216
+ const aliases = sortedUnique(target.aliases);
23220
23217
  const aliasKeys = aliases.map(measurementMentionAliasKey).filter(Boolean);
23221
23218
  target.identityAliases?.forEach((identityAlias, index2) => {
23222
23219
  const identityKey = measurementMentionAliasKey(identityAlias);
@@ -23233,7 +23230,7 @@ function compileMeasurementDraft(authoring, context) {
23233
23230
  stableKey: target.stableKey,
23234
23231
  label: target.label,
23235
23232
  aliases,
23236
- ...target.identityAliases === void 0 ? {} : { identityAliases: canonicalStrings(target.identityAliases) },
23233
+ ...target.identityAliases === void 0 ? {} : { identityAliases: sortedUnique(target.identityAliases) },
23237
23234
  urlMatchers: matchers,
23238
23235
  mentionNotApplicable: aliases.length === 0,
23239
23236
  discoveryIdentity: target.discoveryIdentity ?? null
@@ -23281,8 +23278,8 @@ function compileMeasurementDraft(authoring, context) {
23281
23278
  stableKey: group.stableKey,
23282
23279
  label: group.label,
23283
23280
  ...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))
23281
+ targetKeys: sortedUnique(group.targetKeys),
23282
+ competitors: [...group.competitors].map((competitor) => ({ ...competitor, aliases: sortedUnique(competitor.aliases) })).sort((left, right) => compareText(left.stableKey, right.stableKey))
23286
23283
  });
23287
23284
  });
23288
23285
  const resolveContext = (override, path8) => {
@@ -23300,7 +23297,7 @@ function compileMeasurementDraft(authoring, context) {
23300
23297
  }
23301
23298
  models[normalized] = model;
23302
23299
  }
23303
- const labels = canonicalStrings(override?.locations ?? authoring.defaultContext.locations);
23300
+ const labels = sortedUnique(override?.locations ?? authoring.defaultContext.locations);
23304
23301
  const locations = [];
23305
23302
  labels.forEach((label, labelIndex) => {
23306
23303
  const location = locationsByLabel.get(label);
@@ -23461,7 +23458,7 @@ function compileMeasurementDraft(authoring, context) {
23461
23458
  }
23462
23459
  }
23463
23460
  if (failed()) return { ok: false, checks };
23464
- const querySnapshots3 = [...usedQueryIds].sort(compareText7).map((queryId) => ({
23461
+ const querySnapshots3 = [...usedQueryIds].sort(compareText).map((queryId) => ({
23465
23462
  queryId,
23466
23463
  queryText: queriesById.get(queryId),
23467
23464
  // Provenance is frozen at publish time so a later reader can explain the
@@ -23478,7 +23475,7 @@ function compileMeasurementDraft(authoring, context) {
23478
23475
  projectBrand: {
23479
23476
  canonicalHost,
23480
23477
  ownedHosts,
23481
- names: canonicalStrings(context.brandNames)
23478
+ names: sortedUnique(context.brandNames)
23482
23479
  }
23483
23480
  },
23484
23481
  targets: compiledTargets.map(({ target }) => target),
@@ -23527,13 +23524,13 @@ function keyedDiff2(before, after) {
23527
23524
  const added = [];
23528
23525
  const changed = [];
23529
23526
  const unchanged = [];
23530
- for (const stableKey of [...afterByKey.keys()].sort(compareText7)) {
23527
+ for (const stableKey of [...afterByKey.keys()].sort(compareText)) {
23531
23528
  const beforeValue = beforeByKey.get(stableKey);
23532
23529
  if (!beforeValue) added.push(stableKey);
23533
23530
  else if (JSON.stringify(canonicalJsonValue(beforeValue)) === JSON.stringify(canonicalJsonValue(afterByKey.get(stableKey)))) unchanged.push(stableKey);
23534
23531
  else changed.push(stableKey);
23535
23532
  }
23536
- const removed = [...beforeByKey.keys()].filter((key) => !afterByKey.has(key)).sort(compareText7);
23533
+ const removed = [...beforeByKey.keys()].filter((key) => !afterByKey.has(key)).sort(compareText);
23537
23534
  return { added, removed, changed, unchanged };
23538
23535
  }
23539
23536
  function assignmentClasses(plan) {
@@ -23562,8 +23559,8 @@ function diffCompiledPlans(active, candidate, activeRevision) {
23562
23559
  groups: keyedDiff2(active?.groups ?? [], candidate.groups),
23563
23560
  assignments: { added: addedAssignments, removed: removedAssignments, reclassified },
23564
23561
  execution: {
23565
- addedNodeKeys: [...afterNodes].filter((key) => !beforeNodes.has(key)).sort(compareText7),
23566
- removedNodeKeys: [...beforeNodes].filter((key) => !afterNodes.has(key)).sort(compareText7)
23562
+ addedNodeKeys: [...afterNodes].filter((key) => !beforeNodes.has(key)).sort(compareText),
23563
+ removedNodeKeys: [...beforeNodes].filter((key) => !afterNodes.has(key)).sort(compareText)
23567
23564
  }
23568
23565
  };
23569
23566
  }
@@ -23636,9 +23633,6 @@ function requireGroup(authoring, groupKey) {
23636
23633
  function unique(values) {
23637
23634
  return [...new Set(values)];
23638
23635
  }
23639
- function compareText8(left, right) {
23640
- return left < right ? -1 : left > right ? 1 : 0;
23641
- }
23642
23636
  function knownQueryIds(queryIds, context) {
23643
23637
  const uniqueQueryIds = unique(queryIds);
23644
23638
  const unknown = uniqueQueryIds.filter((queryId) => !context.queriesById.has(queryId));
@@ -23692,7 +23686,7 @@ function resolveDraftAudience(authoring, audience) {
23692
23686
  groups.push({ groupKey: group.stableKey, label: group.label, memberCount: memberKeys.length });
23693
23687
  selected.push(...memberKeys);
23694
23688
  }
23695
- const targetKeys = unique(selected).sort(compareText8);
23689
+ const targetKeys = unique(selected).sort(compareText);
23696
23690
  return {
23697
23691
  targetKeys,
23698
23692
  groups,
@@ -24122,9 +24116,6 @@ var MeasurementGroupMembershipImportError = class extends Error {
24122
24116
  statusCode;
24123
24117
  details;
24124
24118
  };
24125
- function compareText9(left, right) {
24126
- return left < right ? -1 : left > right ? 1 : 0;
24127
- }
24128
24119
  function sha256Hex2(value) {
24129
24120
  return crypto20.createHash("sha256").update(value).digest("hex");
24130
24121
  }
@@ -24132,7 +24123,7 @@ function canonicalJsonValue2(value) {
24132
24123
  if (Array.isArray(value)) return value.map(canonicalJsonValue2);
24133
24124
  if (value && typeof value === "object") {
24134
24125
  return Object.fromEntries(
24135
- Object.entries(value).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText9(left, right)).map(([key, child]) => [key, canonicalJsonValue2(child)])
24126
+ Object.entries(value).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText(left, right)).map(([key, child]) => [key, canonicalJsonValue2(child)])
24136
24127
  );
24137
24128
  }
24138
24129
  return value;
@@ -24429,8 +24420,8 @@ function groupIdentities(rows, authoring, segments) {
24429
24420
  groupsByLabel.set(label, matches);
24430
24421
  }
24431
24422
  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));
24423
+ for (const normalizedGroupLabel of [...labels.keys()].sort(compareText)) {
24424
+ const existing = [...groupsByLabel.get(normalizedGroupLabel) ?? []].sort((left, right) => compareText(left.stableKey, right.stableKey));
24434
24425
  if (existing.length > 1) {
24435
24426
  identities.push({
24436
24427
  kind: "ambiguous",
@@ -24508,8 +24499,8 @@ function groupChangesFromRows(rows, identities) {
24508
24499
  bucket.rows.push(row);
24509
24500
  byGroupKey.set(row.groupKey, bucket);
24510
24501
  }
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);
24502
+ return [...byGroupKey.values()].sort((left, right) => compareText(left.identity.normalizedGroupLabel, right.identity.normalizedGroupLabel)).map(({ identity, rows: matchedRows }) => {
24503
+ const targetKeys = sortedUnique(matchedRows.map((row) => row.targetKey));
24513
24504
  const addedTargetKeys = targetKeys.filter((targetKey) => !identity.existingTargetKeys.has(targetKey));
24514
24505
  const unchangedTargetKeys = targetKeys.filter((targetKey) => identity.existingTargetKeys.has(targetKey));
24515
24506
  return {
@@ -24573,7 +24564,7 @@ function resolveRows(parsedRows, authoring, identities) {
24573
24564
  resolved.push(rowFailure(row, "invalid", "missing-group"));
24574
24565
  continue;
24575
24566
  }
24576
- const labelCandidates = [...targetsByLabel.get(row.normalizedProperty) ?? []].sort((left, right) => compareText9(left.stableKey, right.stableKey));
24567
+ const labelCandidates = [...targetsByLabel.get(row.normalizedProperty) ?? []].sort((left, right) => compareText(left.stableKey, right.stableKey));
24577
24568
  if (!labelCandidates.length) {
24578
24569
  resolved.push(rowFailure(row, "unmatched", "property-not-found"));
24579
24570
  continue;
@@ -24654,7 +24645,7 @@ function resolveArtifacts(input) {
24654
24645
  return { rows, groupChanges, counts: countsFromRows(rows, groupChanges), identities };
24655
24646
  }
24656
24647
  function checksumIdentities(identities) {
24657
- return [...identities].sort((left, right) => compareText9(left.normalizedGroupLabel, right.normalizedGroupLabel)).map((identity) => {
24648
+ return [...identities].sort((left, right) => compareText(left.normalizedGroupLabel, right.normalizedGroupLabel)).map((identity) => {
24658
24649
  if (identity.kind === "ready") {
24659
24650
  return {
24660
24651
  normalizedGroupLabel: identity.normalizedGroupLabel,
@@ -24668,7 +24659,7 @@ function checksumIdentities(identities) {
24668
24659
  return {
24669
24660
  normalizedGroupLabel: identity.normalizedGroupLabel,
24670
24661
  outcome: identity.kind,
24671
- groupKeys: [...identity.groupKeys].sort(compareText9)
24662
+ groupKeys: [...identity.groupKeys].sort(compareText)
24672
24663
  };
24673
24664
  }
24674
24665
  return {
@@ -24676,8 +24667,8 @@ function checksumIdentities(identities) {
24676
24667
  outcome: identity.kind,
24677
24668
  groupKey: identity.groupKey,
24678
24669
  evidence: [...identity.evidence].sort((left, right) => {
24679
- const bySource = compareText9(left.source, right.source);
24680
- return bySource || compareText9(left.normalizedGroupLabel ?? "", right.normalizedGroupLabel ?? "");
24670
+ const bySource = compareText(left.source, right.source);
24671
+ return bySource || compareText(left.normalizedGroupLabel ?? "", right.normalizedGroupLabel ?? "");
24681
24672
  })
24682
24673
  };
24683
24674
  });
@@ -24697,7 +24688,7 @@ function resolveGroupMembershipPreview(input) {
24697
24688
  })),
24698
24689
  rows: artifacts.rows,
24699
24690
  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 }))
24691
+ persistedSegments: [...input.segments].sort((left, right) => compareText(left.stableKey, right.stableKey)).map((segment) => ({ stableKey: segment.stableKey, kind: segment.kind, retiredAt: segment.retiredAt }))
24701
24692
  });
24702
24693
  return {
24703
24694
  draftEtag: input.draftEtag,
@@ -24825,9 +24816,6 @@ function applyReviewedGroupMembership(authoring, preview, review) {
24825
24816
 
24826
24817
  // ../api-routes/src/measurement-draft.ts
24827
24818
  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
24819
  function authoringIdentity(authoring) {
24832
24820
  return canonicalJson(authoring);
24833
24821
  }
@@ -24896,7 +24884,7 @@ function emptyAuthoring(project, opts) {
24896
24884
  defaultContext: {
24897
24885
  providers,
24898
24886
  ...Object.keys(models).length ? { models } : {},
24899
- locations: project.locations.map((location) => location.label).sort(compareText10)
24887
+ locations: project.locations.map((location) => location.label).sort(compareText)
24900
24888
  },
24901
24889
  targets: [],
24902
24890
  assignments: [],
@@ -25106,7 +25094,7 @@ function pageLimit(value) {
25106
25094
  }
25107
25095
  function paginate(rows, query, sortKey, searchable) {
25108
25096
  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)));
25097
+ const filtered = (needle ? rows.filter((row) => searchable(row).toLowerCase().includes(needle)) : [...rows]).sort((left, right) => compareText(sortKey(left), sortKey(right)));
25110
25098
  const after = query.cursor ? Buffer.from(query.cursor, "base64url").toString("utf8") : null;
25111
25099
  const remaining = after === null ? filtered : filtered.filter((row) => sortKey(row) > after);
25112
25100
  const limit = pageLimit(query.limit);
@@ -26112,22 +26100,22 @@ function proposeMeasurementDiscoveryBindings(input) {
26112
26100
  if (excluded.has(candidate.slug) || excluded.has(identity.toLowerCase())) continue;
26113
26101
  const parts = parseMeasurementDiscoveryIdentity(identity);
26114
26102
  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));
26103
+ 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
26104
  proposals.push({
26117
26105
  discoveryIdentity: identity,
26118
26106
  stableKey: candidate.stableKey,
26119
26107
  slug: candidate.slug,
26120
26108
  label: candidate.label,
26121
26109
  discoveredUrl: candidate.primaryUrl,
26122
- urlMatchers: [.../* @__PURE__ */ new Set([candidate.primaryUrl, ...candidate.aliasCoverageUrls])].sort(compareText11),
26110
+ urlMatchers: sortedUnique([candidate.primaryUrl, ...candidate.aliasCoverageUrls]),
26123
26111
  kind: candidates.length === 1 ? "rebind" : candidates.length === 0 ? "new-target" : "ambiguous",
26124
26112
  ...candidates.length === 1 ? { rebindTargetKey: candidates[0].targetKey } : {},
26125
26113
  candidates
26126
26114
  });
26127
26115
  }
26128
26116
  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)
26117
+ proposals: proposals.sort((left, right) => compareText(left.discoveryIdentity, right.discoveryIdentity)),
26118
+ unchanged: input.targets.filter((target) => target.discoveryIdentity !== void 0 && present.has(target.discoveryIdentity)).map((target) => target.stableKey).sort(compareText)
26131
26119
  };
26132
26120
  }
26133
26121
  function rebindScore(left, right) {
@@ -26150,15 +26138,12 @@ function normalizePathTemplate(value) {
26150
26138
  function hostOfUrl(value) {
26151
26139
  return normalizeMeasurementHost(new URL(value).hostname);
26152
26140
  }
26153
- function compareText11(left, right) {
26154
- return left < right ? -1 : left > right ? 1 : 0;
26155
- }
26156
26141
  function canonicalJson2(value) {
26157
26142
  const canonical = (input) => {
26158
26143
  if (Array.isArray(input)) return input.map(canonical);
26159
26144
  if (input && typeof input === "object") {
26160
26145
  return Object.fromEntries(
26161
- Object.entries(input).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText11(left, right)).map(([key, child]) => [key, canonical(child)])
26146
+ Object.entries(input).filter(([, child]) => child !== void 0).sort(([left], [right]) => compareText(left, right)).map(([key, child]) => [key, canonical(child)])
26162
26147
  );
26163
26148
  }
26164
26149
  return input;
@@ -26173,20 +26158,10 @@ function measurementDiscoveryInputChecksum(input) {
26173
26158
  compilerVersion: MEASUREMENT_DISCOVERY_COMPILER_VERSION,
26174
26159
  sitemapUrl: input.sitemapUrl,
26175
26160
  rule: input.rule,
26176
- exclusions: [...new Set(input.exclusions.map((value) => value.trim().toLowerCase()))].sort(compareText11),
26161
+ exclusions: sortedUnique(input.exclusions.map((value) => value.trim().toLowerCase())),
26177
26162
  bytesChecksum: input.bytesChecksum
26178
26163
  }));
26179
26164
  }
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
26165
  function loadDraft(app, projectId, projectName) {
26191
26166
  const row = app.db.select().from(measurementPlanDrafts).where(eq32(measurementPlanDrafts.projectId, projectId)).get();
26192
26167
  if (!row) throw notFound("Measurement plan draft", projectName);
@@ -26243,7 +26218,7 @@ function commitDraft(app, input) {
26243
26218
  etag: measurementDraftEtag(etagVersion),
26244
26219
  changed: input.changed,
26245
26220
  warnings: input.warnings,
26246
- counts: draftCounts2(authoring.data)
26221
+ counts: draftCounts(authoring.data)
26247
26222
  });
26248
26223
  const actor = actorReference(input.request);
26249
26224
  app.db.transaction((tx) => {
@@ -26346,7 +26321,7 @@ async function measurementDiscoveryV2Routes(app) {
26346
26321
  if (!parsed.success) {
26347
26322
  throw validationError("Invalid sitemap import request", { issues: parsed.error.issues });
26348
26323
  }
26349
- const exclusions = [...new Set((parsed.data.exclusions ?? []).map((value) => value.trim().toLowerCase()))].sort(compareText11);
26324
+ const exclusions = sortedUnique((parsed.data.exclusions ?? []).map((value) => value.trim().toLowerCase()));
26350
26325
  const idempotencyKey = requireIdempotencyKey2(request, "import-sitemap");
26351
26326
  const requestChecksum2 = sha2562(canonicalJson2({ ...parsed.data, exclusions }));
26352
26327
  const replay = replayedResponse(app, project.id, "import-sitemap", idempotencyKey, requestChecksum2);
@@ -26491,7 +26466,7 @@ async function measurementDiscoveryV2Routes(app) {
26491
26466
  const rebound = {
26492
26467
  ...existing,
26493
26468
  ...relabelled,
26494
- urlMatchers: [.../* @__PURE__ */ new Set([...existing.urlMatchers, ...proposal.urlMatchers])].sort(compareText11),
26469
+ urlMatchers: sortedUnique([...existing.urlMatchers, ...proposal.urlMatchers]),
26495
26470
  discoveredUrl: proposal.discoveredUrl,
26496
26471
  discoveryIdentity: proposal.discoveryIdentity
26497
26472
  };
@@ -26549,15 +26524,12 @@ function parseEvidenceQuery(raw) {
26549
26524
  }
26550
26525
  return parsed.data;
26551
26526
  }
26552
- function normalizedText2(value) {
26553
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
26554
- }
26555
26527
  function filterFingerprint(query) {
26556
26528
  const filters = {
26557
26529
  targetKey: query.targetKey,
26558
26530
  queryClass: query.queryClass ?? "all",
26559
- provider: query.provider === void 0 ? null : normalizedText2(query.provider),
26560
- location: query.location === void 0 ? null : normalizedText2(query.location)
26531
+ provider: query.provider === void 0 ? null : normalizeIdentityText(query.provider),
26532
+ location: query.location === void 0 ? null : normalizeIdentityText(query.location)
26561
26533
  };
26562
26534
  return createHash5("sha256").update(JSON.stringify(filters)).digest("base64url");
26563
26535
  }
@@ -26619,7 +26591,7 @@ function propertyEvidenceRows(db, plan, active, run, query, queryClass) {
26619
26591
  const manifest = measurementRunExpectedSlots(run, plan);
26620
26592
  const { input, edgeQueryClass } = buildMeasurementPlanV2ReportInput(active.version.revision, plan, manifest, snapshots);
26621
26593
  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);
26594
+ const provider = query.provider === void 0 ? void 0 : normalizeIdentityText(query.provider);
26623
26595
  const location = query.location === void 0 ? void 0 : normalizeMeasurementLocation(query.location);
26624
26596
  const owned = (row) => ownEdgeIds.has(row.usageEdgeId) && (provider === void 0 || row.provider === provider) && (location === void 0 || normalizeMeasurementLocation(row.location) === location);
26625
26597
  const built = buildMeasurementEvidence(input);
@@ -26734,12 +26706,6 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
26734
26706
  RunStatuses.partial
26735
26707
  ]);
26736
26708
  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
26709
  function excerptOf(answer) {
26744
26710
  const normalized = answer.normalize("NFKC").trim().replace(/\s+/g, " ");
26745
26711
  return normalized.length <= 240 ? normalized : normalized.slice(0, 240);
@@ -26826,16 +26792,16 @@ function selectMeasurementQuestionRun(db, projectId, active, runId) {
26826
26792
  return run;
26827
26793
  }
26828
26794
  function slotKey(executionId, provider) {
26829
- return `${executionId}\0${normalizeText(provider)}`;
26795
+ return `${executionId}\0${normalizeIdentityText(provider)}`;
26830
26796
  }
26831
26797
  function scopedManifest(manifest, scope) {
26832
26798
  if (scope === void 0) return manifest;
26833
26799
  const executionIds = new Set(scope.executionIds);
26834
- const provider = scope.provider === void 0 ? void 0 : normalizeText(scope.provider);
26800
+ const provider = scope.provider === void 0 ? void 0 : normalizeIdentityText(scope.provider);
26835
26801
  const location = scope.location === void 0 ? void 0 : normalizeMeasurementLocation(scope.location);
26836
26802
  return {
26837
26803
  ...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))
26804
+ 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
26805
  };
26840
26806
  }
26841
26807
  function selectedQuestionSnapshots(db, runId, manifest, scope) {
@@ -26846,7 +26812,7 @@ function selectedQuestionSnapshots(db, runId, manifest, scope) {
26846
26812
  conditions.push(inArray15(querySnapshots.measurementExecutionId, executionIds));
26847
26813
  }
26848
26814
  if (scope?.provider !== void 0) {
26849
- conditions.push(sql12`lower(trim(${querySnapshots.provider})) = ${normalizeText(scope.provider)}`);
26815
+ conditions.push(sql12`lower(trim(${querySnapshots.provider})) = ${normalizeIdentityText(scope.provider)}`);
26850
26816
  }
26851
26817
  return db.select(measurementQuestionSnapshotColumns).from(querySnapshots).where(and26(...conditions)).all();
26852
26818
  }
@@ -26921,7 +26887,7 @@ function questionRows(plan, target, materialized, filters = {}) {
26921
26887
  for (const slot of materialized.input.expectedSlots) {
26922
26888
  const assignment = assignmentsByExecution.get(slot.executionId);
26923
26889
  if (!assignment) continue;
26924
- if (filters.provider !== void 0 && normalizeText(slot.provider) !== normalizeText(filters.provider)) continue;
26890
+ if (filters.provider !== void 0 && normalizeIdentityText(slot.provider) !== normalizeIdentityText(filters.provider)) continue;
26925
26891
  if (filters.location !== void 0 && normalizeMeasurementLocation(slot.location) !== normalizeMeasurementLocation(filters.location)) continue;
26926
26892
  const question = queryById.get(assignment.queryId);
26927
26893
  const manifestSlot = materialized.manifestSlotsByIdentity.get(slotKey(slot.executionId, slot.provider));
@@ -26958,7 +26924,7 @@ function questionRows(plan, target, materialized, filters = {}) {
26958
26924
  const key = `${row.queryId}\0${row.provider}\0${row.location ?? ""}`;
26959
26925
  if (!deduped.has(key)) deduped.set(key, row);
26960
26926
  }
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));
26927
+ 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
26928
  }
26963
26929
  function recommendedInstead(candidates, target) {
26964
26930
  const ownNames = new Set([target.label, ...target.aliases].map(brandKeyFromText).filter((key) => key.length > 0));
@@ -27095,12 +27061,6 @@ async function measurementQuestionReadRoutes(app) {
27095
27061
 
27096
27062
  // ../api-routes/src/measurement-portfolio-reads.ts
27097
27063
  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
27064
  function parseLimitQuery(raw, schema, message) {
27105
27065
  const candidate = { ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) } };
27106
27066
  const parsed = schema.safeParse(candidate);
@@ -27173,9 +27133,9 @@ function materializeWithDb(db, active, plan, run) {
27173
27133
  return materializeMeasurementQuestionRun(db, active, plan, run);
27174
27134
  }
27175
27135
  function filteredEvaluator(materialized, filters, targetKeys) {
27176
- const provider = filters.provider === void 0 ? void 0 : normalizeText2(filters.provider);
27136
+ const provider = filters.provider === void 0 ? void 0 : normalizeIdentityText(filters.provider);
27177
27137
  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));
27138
+ const expectedSlots = materialized.input.expectedSlots.filter((slot) => (provider === void 0 || normalizeIdentityText(slot.provider) === provider) && (location === void 0 || normalizeMeasurementLocation(slot.location) === location));
27179
27139
  const usageEdges = materialized.input.usageEdges.filter((edge) => filters.queryClass === "all" || materialized.edgeQueryClass.get(edge.id) === filters.queryClass);
27180
27140
  return createMeasurementOverviewEvaluator({
27181
27141
  ...materialized.input,
@@ -27230,12 +27190,12 @@ function createTargetAnswerReader(plan, materialized, filters) {
27230
27190
  }
27231
27191
  const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
27232
27192
  const questionsById = new Map(plan.querySnapshots.map((question) => [question.queryId, question.queryText]));
27233
- const provider = filters.provider === void 0 ? void 0 : normalizeText2(filters.provider);
27193
+ const provider = filters.provider === void 0 ? void 0 : normalizeIdentityText(filters.provider);
27234
27194
  const location = filters.location === void 0 ? void 0 : normalizeMeasurementLocation(filters.location);
27235
27195
  const slotsByExecution = /* @__PURE__ */ new Map();
27236
27196
  const slotOrder = /* @__PURE__ */ new Map();
27237
27197
  materialized.input.expectedSlots.forEach((slot, index2) => {
27238
- if (provider !== void 0 && normalizeText2(slot.provider) !== provider) return;
27198
+ if (provider !== void 0 && normalizeIdentityText(slot.provider) !== provider) return;
27239
27199
  if (location !== void 0 && normalizeMeasurementLocation(slot.location) !== location) return;
27240
27200
  pushTo(slotsByExecution, slot.executionId, slot);
27241
27201
  slotOrder.set(slot.id, index2);
@@ -27316,7 +27276,7 @@ function domainRows(answers, limit, readHost = createCitedHostReader()) {
27316
27276
  const domains = new Set(answerDomains(answer, readHost).values());
27317
27277
  for (const domain of domains) counts2.set(domain, (counts2.get(domain) ?? 0) + 1);
27318
27278
  }
27319
- const rows = [...counts2].map(([domain, count6]) => ({ domain, answers: count6 })).sort((left, right) => right.answers - left.answers || compareText13(left.domain, right.domain));
27279
+ const rows = [...counts2].map(([domain, count6]) => ({ domain, answers: count6 })).sort((left, right) => right.answers - left.answers || compareText(left.domain, right.domain));
27320
27280
  return { rows: rows.slice(0, limit), total: rows.length };
27321
27281
  }
27322
27282
  function targetAliasKeys(target) {
@@ -27341,7 +27301,7 @@ function recommendationRows(target, population) {
27341
27301
  forEachNameWrittenInstead(target, population.answers, (key, name, answer, firstInAnswer) => {
27342
27302
  const existing = grouped.get(key);
27343
27303
  if (existing) {
27344
- if (compareText13(name, existing.name) < 0) existing.name = name;
27304
+ if (compareText(name, existing.name) < 0) existing.name = name;
27345
27305
  if (!firstInAnswer) return;
27346
27306
  existing.occurrences++;
27347
27307
  existing.providers.add(answer.provider);
@@ -27356,8 +27316,8 @@ function recommendationRows(target, population) {
27356
27316
  }
27357
27317
  });
27358
27318
  return [...grouped.values()].map((row) => {
27359
- const providers = [...row.providers].sort(compareText13);
27360
- const questions = [...row.questions].sort(compareText13);
27319
+ const providers = [...row.providers].sort(compareText);
27320
+ const questions = [...row.questions].sort(compareText);
27361
27321
  return {
27362
27322
  name: row.name,
27363
27323
  occurrences: row.occurrences,
@@ -27368,7 +27328,7 @@ function recommendationRows(target, population) {
27368
27328
  questionTotal: questions.length,
27369
27329
  questionsTruncated: questions.length > 5
27370
27330
  };
27371
- }).sort((left, right) => right.occurrences - left.occurrences || compareText13(left.name, right.name));
27331
+ }).sort((left, right) => right.occurrences - left.occurrences || compareText(left.name, right.name));
27372
27332
  }
27373
27333
  function namedInsteadFields(names, limit = MEASUREMENT_PORTFOLIO_ROW_EVIDENCE_LIMIT) {
27374
27334
  const returned = names.slice(0, limit);
@@ -27388,7 +27348,7 @@ function compareWeakest(left, right) {
27388
27348
  const rightUnavailable = right.mentionCoverage.state === "unavailable" || right.citationCoverage.state === "unavailable";
27389
27349
  if (leftUnavailable !== rightUnavailable) return leftUnavailable ? 1 : -1;
27390
27350
  if (leftUnavailable || rightUnavailable) {
27391
- return compareText13(left.label, right.label) || compareText13(left.targetKey, right.targetKey);
27351
+ return compareText(left.label, right.label) || compareText(left.targetKey, right.targetKey);
27392
27352
  }
27393
27353
  if (left.mentionCoverage.state === "available" && right.mentionCoverage.state === "available" && left.mentionCoverage.value !== right.mentionCoverage.value) {
27394
27354
  return left.mentionCoverage.value - right.mentionCoverage.value;
@@ -27396,7 +27356,7 @@ function compareWeakest(left, right) {
27396
27356
  if (left.citationCoverage.state === "available" && right.citationCoverage.state === "available" && left.citationCoverage.value !== right.citationCoverage.value) {
27397
27357
  return left.citationCoverage.value - right.citationCoverage.value;
27398
27358
  }
27399
- return compareText13(left.label, right.label) || compareText13(left.targetKey, right.targetKey);
27359
+ return compareText(left.label, right.label) || compareText(left.targetKey, right.targetKey);
27400
27360
  }
27401
27361
  function requireGroup2(plan, groupKey) {
27402
27362
  const group = plan.groups.find((candidate) => candidate.stableKey === groupKey);
@@ -27498,14 +27458,14 @@ function compareWeakestMarket(a, b) {
27498
27458
  const aUnavailable = a.mentionCoverage.state === "unavailable" || a.citationCoverage.state === "unavailable";
27499
27459
  const bUnavailable = b.mentionCoverage.state === "unavailable" || b.citationCoverage.state === "unavailable";
27500
27460
  if (aUnavailable !== bUnavailable) return aUnavailable ? 1 : -1;
27501
- if (aUnavailable || bUnavailable) return compareText13(a.label, b.label) || compareText13(a.groupKey, b.groupKey);
27461
+ if (aUnavailable || bUnavailable) return compareText(a.label, b.label) || compareText(a.groupKey, b.groupKey);
27502
27462
  if (a.mentionCoverage.state === "available" && b.mentionCoverage.state === "available" && a.mentionCoverage.value !== b.mentionCoverage.value) {
27503
27463
  return a.mentionCoverage.value - b.mentionCoverage.value;
27504
27464
  }
27505
27465
  if (a.citationCoverage.state === "available" && b.citationCoverage.state === "available" && a.citationCoverage.value !== b.citationCoverage.value) {
27506
27466
  return a.citationCoverage.value - b.citationCoverage.value;
27507
27467
  }
27508
- return compareText13(a.label, b.label) || compareText13(a.groupKey, b.groupKey);
27468
+ return compareText(a.label, b.label) || compareText(a.groupKey, b.groupKey);
27509
27469
  }
27510
27470
  function mentionRanking(rows, limit) {
27511
27471
  const eligible = [];
@@ -27526,7 +27486,7 @@ function mentionRanking(rows, limit) {
27526
27486
  });
27527
27487
  }
27528
27488
  }
27529
- const byLabel = (a, b) => compareText13(a.label, b.label) || compareText13(a.targetKey, b.targetKey);
27489
+ const byLabel = (a, b) => compareText(a.label, b.label) || compareText(a.targetKey, b.targetKey);
27530
27490
  const byRate = (direction) => (a, b) => direction * (a.mentionCoverage.value - b.mentionCoverage.value) || byLabel(a, b);
27531
27491
  return {
27532
27492
  eligiblePropertyCount: eligible.length,
@@ -27676,7 +27636,7 @@ function compactPortfolioPage(response, query, versionId, evidence, cursor) {
27676
27636
  function portfolioAnswerEvidence(plan, materialized, filters, targetKeys) {
27677
27637
  const targets = new Set(targetKeys);
27678
27638
  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)));
27639
+ 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
27640
  const mentionsBySlot = /* @__PURE__ */ new Map();
27681
27641
  for (const answer of materialized.evidence.answers) pushTo(mentionsBySlot, answer.expectedSlotId, answer.mentioned);
27682
27642
  const selected = [];
@@ -27818,7 +27778,7 @@ function portfolioResponse(db, active, plan, query, ownDomains) {
27818
27778
  },
27819
27779
  measurement,
27820
27780
  queryClass: query.queryClass,
27821
- engines: [...engines].sort(compareText13),
27781
+ engines: [...engines].sort(compareText),
27822
27782
  metrics: {
27823
27783
  propertiesMentioned: countMetric2(overview.propertiesMentioned),
27824
27784
  propertiesNeverMentioned: neverMentionedMetric(overview.propertiesMentioned, targetKeys.length),
@@ -27885,7 +27845,7 @@ function tieByMetro(rows) {
27885
27845
  add(row.metro);
27886
27846
  for (const other of row.otherMetros ?? []) add(other);
27887
27847
  }
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 }));
27848
+ 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
27849
  }
27890
27850
  function tieNamedInstead(rows) {
27891
27851
  const grouped = /* @__PURE__ */ new Map();
@@ -27893,14 +27853,14 @@ function tieNamedInstead(rows) {
27893
27853
  forEachNameWrittenInstead(target, population.answers, (key, name, answer) => {
27894
27854
  const existing = grouped.get(key);
27895
27855
  if (existing) {
27896
- if (compareText13(name, existing.name) < 0) existing.name = name;
27856
+ if (compareText(name, existing.name) < 0) existing.name = name;
27897
27857
  existing.answers.add(answer.snapshot.id);
27898
27858
  } else {
27899
27859
  grouped.set(key, { name, answers: /* @__PURE__ */ new Set([answer.snapshot.id]) });
27900
27860
  }
27901
27861
  });
27902
27862
  }
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));
27863
+ 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
27864
  return { rows: counted.slice(0, MEASUREMENT_PORTFOLIO_TIE_NAMED_INSTEAD_LIMIT), total: counted.length };
27905
27865
  }
27906
27866
  function weakestAnswerSources(rows, ownDomains) {
@@ -28007,10 +27967,10 @@ function scopeKey(run) {
28007
27967
  if (run.measurementScope === null) return "full";
28008
27968
  const scope = run.measurementScope;
28009
27969
  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)
27970
+ groups: [...scope.groups].sort(compareText),
27971
+ targets: [...scope.targets].sort(compareText),
27972
+ queries: [...scope.queries].sort(compareText),
27973
+ resolvedTargets: [...scope.resolvedTargets].sort(compareText)
28014
27974
  });
28015
27975
  }
28016
27976
  function previousComparableRun(db, active, current) {
@@ -28142,7 +28102,7 @@ function classifyPropertyMove(mention, citation) {
28142
28102
  };
28143
28103
  }
28144
28104
  function compareChangeLabel(left, right) {
28145
- return compareText13(left.row.label, right.row.label) || compareText13(left.row.targetKey, right.row.targetKey);
28105
+ return compareText(left.row.label, right.row.label) || compareText(left.row.targetKey, right.row.targetKey);
28146
28106
  }
28147
28107
  function compareChangeMagnitude(left, right) {
28148
28108
  const larger = (move) => Math.max(move.mentionSize, move.citationSize);
@@ -28320,11 +28280,11 @@ function qualityResponse(db, active, plan, query) {
28320
28280
  const materialized = materializeWithDb(db, active, plan, run);
28321
28281
  const expected = materialized.manifest.expectedSlots.length;
28322
28282
  const snapshots = materialized.snapshots;
28323
- const expectedSlotKeys = new Set(materialized.manifest.expectedSlots.map((slot) => `${slot.executionId}\0${normalizeText2(slot.provider)}`));
28283
+ const expectedSlotKeys = new Set(materialized.manifest.expectedSlots.map((slot) => `${slot.executionId}\0${normalizeIdentityText(slot.provider)}`));
28324
28284
  const observedSlotKeys = /* @__PURE__ */ new Set();
28325
28285
  for (const snapshot of snapshots) {
28326
28286
  if (snapshot.measurementExecutionId === null) throw new Error("stored result has no frozen execution identity");
28327
- const key = `${snapshot.measurementExecutionId}\0${normalizeText2(snapshot.provider)}`;
28287
+ const key = `${snapshot.measurementExecutionId}\0${normalizeIdentityText(snapshot.provider)}`;
28328
28288
  if (!expectedSlotKeys.has(key) || observedSlotKeys.has(key)) {
28329
28289
  throw new Error("stored results do not map one-to-one to the frozen manifest");
28330
28290
  }
@@ -28515,16 +28475,10 @@ var VisibilityReportScopeError = class extends Error {
28515
28475
  }
28516
28476
  };
28517
28477
  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
28478
  function locationMatches(location, selected) {
28525
28479
  if (selected.kind === "all") return true;
28526
28480
  if (selected.kind === "none") return location === null;
28527
- return location !== null && normalizeText3(location) === normalizeText3(selected.value);
28481
+ return location !== null && normalizeIdentityText(location) === normalizeIdentityText(selected.value);
28528
28482
  }
28529
28483
  function selectedClasses(value) {
28530
28484
  return value === "all" ? ALL_CLASSES : [value];
@@ -28580,15 +28534,15 @@ function scopeResolution(definition, selection) {
28580
28534
  }
28581
28535
  function scopeTargetKeys(definition, selection) {
28582
28536
  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);
28537
+ if (resolution.market !== void 0) return sortedUnique(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey));
28538
+ if (selection.scope === "project") return definition.targets.map((target) => target.id).sort(compareText);
28585
28539
  if (selection.scope === "property") return [resolution.option.id];
28586
28540
  if (selection.scope === "group") {
28587
28541
  const group = definition.groups.find((candidate) => candidate.id === resolution.option.id);
28588
28542
  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);
28543
+ return [...group.targetKeys].sort(compareText);
28590
28544
  }
28591
- return [...new Set(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey))].sort(compareText14);
28545
+ return sortedUnique(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey));
28592
28546
  }
28593
28547
  function observationIndex(run) {
28594
28548
  const index2 = /* @__PURE__ */ new Map();
@@ -28624,16 +28578,16 @@ function candidatesFor(run, selection, queryClass) {
28624
28578
  bySlot.set(slot.id, list);
28625
28579
  }
28626
28580
  }
28627
- const provider = selection.provider === void 0 ? void 0 : normalizeText3(selection.provider);
28628
- const model = selection.model === void 0 ? void 0 : normalizeText3(selection.model);
28581
+ const provider = selection.provider === void 0 ? void 0 : normalizeIdentityText(selection.provider);
28582
+ const model = selection.model === void 0 ? void 0 : normalizeIdentityText(selection.model);
28629
28583
  const candidates = [];
28630
28584
  for (const [slotId, edges] of bySlot) {
28631
28585
  const slot = slotsById.get(slotId);
28632
28586
  if (!slot) throw new Error(`Frozen slot ${slotId} disappeared during reconstruction`);
28633
28587
  const observation = observations.get(slot.id) ?? null;
28634
- if (provider !== void 0 && normalizeText3(slot.provider) !== provider) continue;
28588
+ if (provider !== void 0 && normalizeIdentityText(slot.provider) !== provider) continue;
28635
28589
  if (!locationMatches(slot.location, selection.location)) continue;
28636
- if (model !== void 0 && (observation === null || observation.model === null || normalizeText3(observation.model) !== model)) continue;
28590
+ if (model !== void 0 && (observation === null || observation.model === null || normalizeIdentityText(observation.model) !== model)) continue;
28637
28591
  candidates.push({ run, slot, edges, observation });
28638
28592
  }
28639
28593
  return { resolution, candidates };
@@ -28702,7 +28656,7 @@ function candidateKey(candidate) {
28702
28656
  return `${candidate.run.id}\0${candidate.slot.id}`;
28703
28657
  }
28704
28658
  function selectedTargetKeys(candidates) {
28705
- return [...new Set(candidates.flatMap((candidate) => candidate.edges.map((edge) => edge.targetKey)))].sort(compareText14);
28659
+ return sortedUnique(candidates.flatMap((candidate) => candidate.edges.map((edge) => edge.targetKey)));
28706
28660
  }
28707
28661
  function targetMetrics(candidates, targets, targetKey) {
28708
28662
  const own = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey));
@@ -28773,7 +28727,7 @@ function summary2(candidates, definition, populationTargetKeys = selectedTargetK
28773
28727
  }
28774
28728
  function queryRows(candidates, definition, selection) {
28775
28729
  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))));
28730
+ 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
28731
  const groups = /* @__PURE__ */ new Map();
28778
28732
  for (const candidate of narrowed) {
28779
28733
  const model = candidate.observation?.model ?? null;
@@ -28786,7 +28740,7 @@ function queryRows(candidates, definition, selection) {
28786
28740
  const first = rows[0];
28787
28741
  const value = coverageSummary(rows, definition, targets);
28788
28742
  return {
28789
- sourceSnapshotIds: [...new Set(rows.flatMap((row) => row.observation ? [row.observation.answerId] : []))].sort(compareText14),
28743
+ sourceSnapshotIds: sortedUnique(rows.flatMap((row) => row.observation ? [row.observation.answerId] : [])),
28790
28744
  queryKey: first.slot.queryKey,
28791
28745
  queryId: first.slot.queryId,
28792
28746
  query: first.slot.query,
@@ -28795,14 +28749,14 @@ function queryRows(candidates, definition, selection) {
28795
28749
  location: first.slot.location,
28796
28750
  targetKeys: selectedTargetKeys(rows),
28797
28751
  ...(() => {
28798
- const marketKeys = [...new Set(rows.flatMap((row) => row.edges.flatMap((edge) => edge.marketKeys)))].sort(compareText14);
28752
+ const marketKeys = sortedUnique(rows.flatMap((row) => row.edges.flatMap((edge) => edge.marketKeys)));
28799
28753
  return marketKeys.length === 0 ? {} : { marketKeys };
28800
28754
  })(),
28801
28755
  answerCount: value.answerCount,
28802
28756
  mentionCoverage: value.mentionCoverage,
28803
28757
  citationCoverage: value.citationCoverage
28804
28758
  };
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));
28759
+ }).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
28760
  }
28807
28761
  function evidenceRows(candidates, definition, selection) {
28808
28762
  if (selection.queryKey === void 0 && selection.queryId === void 0) return [];
@@ -28830,10 +28784,10 @@ function evidenceRows(candidates, definition, selection) {
28830
28784
  sources: [...observation.sources],
28831
28785
  observedCompetitors: [...observation.observedCompetitorNames]
28832
28786
  };
28833
- }).sort((left, right) => compareText14(left.createdAt, right.createdAt) || compareText14(left.answerId, right.answerId));
28787
+ }).sort((left, right) => compareText(left.createdAt, right.createdAt) || compareText(left.answerId, right.answerId));
28834
28788
  }
28835
28789
  function competitorRows(candidates) {
28836
- const domains = [...new Set(candidates.flatMap((candidate) => candidate.edges.flatMap((edge) => edge.competitorDomains)))].sort(compareText14);
28790
+ const domains = sortedUnique(candidates.flatMap((candidate) => candidate.edges.flatMap((edge) => edge.competitorDomains)));
28837
28791
  return domains.map((domain) => {
28838
28792
  const eligible = candidates.map((candidate) => {
28839
28793
  const edges = candidate.edges.filter((edge) => edge.competitorDomains.includes(domain));
@@ -28864,7 +28818,7 @@ function observedCompetitorRows(candidates) {
28864
28818
  answersByName.set(name, answers);
28865
28819
  }
28866
28820
  }
28867
- return [...answersByName].map(([name, answers]) => ({ name, answerCount: answers.size })).sort((left, right) => compareText14(left.name, right.name));
28821
+ return [...answersByName].map(([name, answers]) => ({ name, answerCount: answers.size })).sort((left, right) => compareText(left.name, right.name));
28868
28822
  }
28869
28823
  function narrowedToTargetKeys(candidate, targetKeys) {
28870
28824
  const edges = candidate.edges.filter((edge) => targetKeys.has(edge.targetKey));
@@ -28883,7 +28837,7 @@ function breakdown(candidates, definition, populationTargetKeys = selectedTarget
28883
28837
  queryCount: new Set(own.map((candidate) => candidate.slot.queryKey)).size,
28884
28838
  ...metrics
28885
28839
  };
28886
- }).sort((left, right) => compareText14(left.label, right.label) || compareText14(left.id, right.id));
28840
+ }).sort((left, right) => compareText(left.label, right.label) || compareText(left.id, right.id));
28887
28841
  const populationTargetSet = new Set(populationTargetKeys);
28888
28842
  const groups = definition.groups.map((group) => ({ group, targetKeys: group.targetKeys.filter((targetKey) => populationTargetSet.has(targetKey)) })).filter(({ targetKeys }) => targetKeys.length > 0).map(({ group, targetKeys }) => {
28889
28843
  const groupTargetKeys = new Set(targetKeys);
@@ -28897,7 +28851,7 @@ function breakdown(candidates, definition, populationTargetKeys = selectedTarget
28897
28851
  mentionCoverage: metrics,
28898
28852
  citationCoverage: citations
28899
28853
  };
28900
- }).sort((left, right) => compareText14(left.label, right.label) || compareText14(left.id, right.id));
28854
+ }).sort((left, right) => compareText(left.label, right.label) || compareText(left.id, right.id));
28901
28855
  return { properties, groups };
28902
28856
  }
28903
28857
  function fingerprint(value) {
@@ -28974,16 +28928,16 @@ function filterOptions(definition, selection, run) {
28974
28928
  const resolution = scopeResolution(definition, selection);
28975
28929
  const slots = definition.slots.filter((slot) => definition.edges.some((edge) => edge.executionId === slot.executionId && resolution.edgeIds.has(edge.id)));
28976
28930
  const observations = run ? observationIndex(run) : /* @__PURE__ */ new Map();
28977
- const providers = [...new Set(slots.map((slot) => slot.provider))].sort(compareText14);
28931
+ const providers = sortedUnique(slots.map((slot) => slot.provider));
28978
28932
  const models = [...new Map(slots.flatMap((slot) => {
28979
28933
  const observation = observations.get(slot.id);
28980
28934
  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));
28935
+ })).values()].sort((left, right) => compareText(left.provider, right.provider) || compareText(left.model, right.model));
28982
28936
  const locations = [{ kind: "all" }];
28983
28937
  const seen2 = /* @__PURE__ */ new Set(["all"]);
28984
28938
  for (const slot of slots) {
28985
28939
  const value = slot.location === null ? { kind: "none" } : { kind: "exact", value: slot.location };
28986
- const key = value.kind === "exact" ? `exact:${normalizeText3(value.value)}` : value.kind;
28940
+ const key = value.kind === "exact" ? `exact:${normalizeIdentityText(value.value)}` : value.kind;
28987
28941
  if (!seen2.has(key)) {
28988
28942
  seen2.add(key);
28989
28943
  locations.push(value);
@@ -29074,7 +29028,7 @@ function emptyPopulation(queryClass, definition, input) {
29074
29028
  };
29075
29029
  }
29076
29030
  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));
29031
+ 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
29032
  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
29033
  const definition = selectedRun?.definition ?? input.activeDefinition;
29080
29034
  const selectedResolution = scopeResolution(definition, input.selection);
@@ -29208,9 +29162,6 @@ function visibilityComparisonPopulation(run, selection) {
29208
29162
  }
29209
29163
 
29210
29164
  // ../api-routes/src/visibility-report.ts
29211
- function normalizedText3(value) {
29212
- return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase("en");
29213
- }
29214
29165
  function observedCompetitorNames2(values) {
29215
29166
  const names = [];
29216
29167
  const seen2 = /* @__PURE__ */ new Set();
@@ -29294,7 +29245,7 @@ function competitorSignals(snapshot, matchers, preparedAnswerText = preparedAnsw
29294
29245
  try {
29295
29246
  return normalizeMeasurementHost(value);
29296
29247
  } catch {
29297
- return normalizedText3(value);
29248
+ return normalizeIdentityText(value);
29298
29249
  }
29299
29250
  }));
29300
29251
  const cited = [...matchers.keys()].filter((domain) => citedDomains.has(normalizeMeasurementHost(domain)));
@@ -29462,7 +29413,7 @@ function stableSimpleDefinitionId(definition) {
29462
29413
  return `simple:${createHash8("sha256").update(JSON.stringify(source)).digest("hex")}`;
29463
29414
  }
29464
29415
  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"));
29416
+ const engines = [...definition.engines].map((engine) => ({ provider: normalizeIdentityText(engine.provider), requestedModel: engine.requestedModel })).sort((left, right) => left.provider.localeCompare(right.provider, "en"));
29466
29417
  return `simple-model:${createHash8("sha256").update(JSON.stringify(engines)).digest("hex")}`;
29467
29418
  }
29468
29419
  function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
@@ -29479,21 +29430,21 @@ function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
29479
29430
  matches.push(query);
29480
29431
  queriesByText.set(query.queryText, matches);
29481
29432
  }
29482
- const enginesByProvider = new Map(definition.engines.map((engine) => [normalizedText3(engine.provider), engine]));
29433
+ const enginesByProvider = new Map(definition.engines.map((engine) => [normalizeIdentityText(engine.provider), engine]));
29483
29434
  const competitors2 = definition.competitors === void 0 ? null : frozenCompetitorMatchers(definition.competitors);
29484
29435
  const slots = definition.queries.flatMap((query) => definition.engines.map((engine) => ({
29485
- id: `slot:simple:${query.queryId}:${normalizedText3(engine.provider)}`,
29436
+ id: `slot:simple:${query.queryId}:${normalizeIdentityText(engine.provider)}`,
29486
29437
  executionId: `simple:${query.queryId}`,
29487
29438
  queryKey: query.queryId,
29488
29439
  queryId: query.queryId,
29489
29440
  query: query.queryText,
29490
- provider: normalizedText3(engine.provider),
29441
+ provider: normalizeIdentityText(engine.provider),
29491
29442
  location: definition.location?.label ?? null
29492
29443
  })));
29493
29444
  const slotKeys = new Set(slots.map((slot) => slot.id));
29494
29445
  const observations = [];
29495
29446
  for (const snapshot of snapshots) {
29496
- const provider = normalizedText3(snapshot.provider);
29447
+ const provider = normalizeIdentityText(snapshot.provider);
29497
29448
  const engine = enginesByProvider.get(provider);
29498
29449
  if (!engine) throw new Error(`Frozen simple definition has no provider ${snapshot.provider} for stored snapshot ${snapshot.id}`);
29499
29450
  const query = snapshot.queryId === null ? (() => {
@@ -29568,7 +29519,7 @@ function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
29568
29519
  queryKey: snapshot.queryId ?? `legacy:${snapshot.queryText ?? snapshot.id}`,
29569
29520
  queryId: snapshot.queryId,
29570
29521
  query: snapshot.queryText ?? "",
29571
- provider: normalizedText3(snapshot.provider),
29522
+ provider: normalizeIdentityText(snapshot.provider),
29572
29523
  location: snapshot.location
29573
29524
  }));
29574
29525
  return {
@@ -29937,17 +29888,11 @@ import { and as and29, desc as desc16, eq as eq37, inArray as inArray18, isNotNu
29937
29888
  var SOURCE_LIMIT = 100;
29938
29889
  var MAX_REVIEW_AGE_MS = 15 * 60 * 1e3;
29939
29890
  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
29891
  function unique2(values) {
29947
29892
  return [...new Set(values)];
29948
29893
  }
29949
- function stableNewQueryId(projectId, normalizedText4) {
29950
- return `qtc_${sha256Hex(`${projectId}\0${normalizedText4}`).slice(0, 32)}`;
29894
+ function stableNewQueryId(projectId, normalizedText) {
29895
+ return `qtc_${sha256Hex(`${projectId}\0${normalizedText}`).slice(0, 32)}`;
29951
29896
  }
29952
29897
  function providerNames(project, opts) {
29953
29898
  return resolveRunProviderSelection({
@@ -29981,7 +29926,7 @@ function simpleDispatchEngines(project, opts) {
29981
29926
  // override/configuration, just as it is in JobRunner, which also resolves
29982
29927
  // a retired override to the id that answers now.
29983
29928
  requestedModel: resolvedOverride(provider, overrides[provider]) ?? summaries.get(provider)?.model ?? null
29984
- })).sort((left, right) => compareText15(left.provider, right.provider));
29929
+ })).sort((left, right) => compareText(left.provider, right.provider));
29985
29930
  }
29986
29931
  function resolvedOverride(provider, model) {
29987
29932
  return model === void 0 ? void 0 : resolveProviderModel(provider, model);
@@ -29992,12 +29937,12 @@ function simpleDefaultLocation(project) {
29992
29937
  }
29993
29938
  function contextKey(context) {
29994
29939
  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 });
29940
+ return canonicalJson({ providers: [...context.providers].sort(compareText), models: context.models, location });
29996
29941
  }
29997
29942
  function uniqueContexts(contexts) {
29998
29943
  const byKey = /* @__PURE__ */ new Map();
29999
29944
  for (const context of contexts) byKey.set(contextKey(context), context);
30000
- return [...byKey.values()].sort((left, right) => compareText15(contextKey(left), contextKey(right)));
29945
+ return [...byKey.values()].sort((left, right) => compareText(contextKey(left), contextKey(right)));
30001
29946
  }
30002
29947
  function defaultContexts(project, opts, mode) {
30003
29948
  const providers = providerNames(project, opts);
@@ -30028,7 +29973,7 @@ function workspaceFingerprint(project, active, queryRows2) {
30028
29973
  defaultLocation: project.defaultLocation
30029
29974
  },
30030
29975
  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))
29976
+ queries: queryRows2.map((row) => ({ id: row.id, query: row.query, provenance: row.provenance })).sort((left, right) => compareText(left.id, right.id))
30032
29977
  }))}`;
30033
29978
  }
30034
29979
  function readWorkspace(db, project) {
@@ -30065,9 +30010,9 @@ function simpleExecutionSignature(input) {
30065
30010
  return canonicalJson({
30066
30011
  identity: {
30067
30012
  displayName: input.identity.displayName,
30068
- aliases: [...input.identity.aliases].sort(compareText15),
30013
+ aliases: [...input.identity.aliases].sort(compareText),
30069
30014
  canonicalDomain: input.identity.canonicalDomain,
30070
- ownedDomains: [...input.identity.ownedDomains].sort(compareText15)
30015
+ ownedDomains: [...input.identity.ownedDomains].sort(compareText)
30071
30016
  },
30072
30017
  country: input.country,
30073
30018
  language: input.language,
@@ -30075,7 +30020,7 @@ function simpleExecutionSignature(input) {
30075
30020
  engines: input.engines.map((engine) => ({
30076
30021
  provider: engine.provider.trim().toLocaleLowerCase("en"),
30077
30022
  requestedModel: engine.requestedModel
30078
- })).sort((left, right) => compareText15(left.provider, right.provider))
30023
+ })).sort((left, right) => compareText(left.provider, right.provider))
30079
30024
  });
30080
30025
  }
30081
30026
  function currentSimpleExecutionSignature(project, opts) {
@@ -30338,7 +30283,7 @@ function measuredAtByQuery(db, state, opts) {
30338
30283
  for (const [queryId, signatures] of activeSignatures) {
30339
30284
  const measured = measurements.get(queryId);
30340
30285
  if (!measured || [...signatures].some((signature) => !measured.has(signature))) continue;
30341
- const latest = [...measured.values()].sort(compareText15).at(-1);
30286
+ const latest = [...measured.values()].sort(compareText).at(-1);
30342
30287
  if (latest) result.set(queryId, latest);
30343
30288
  }
30344
30289
  return result;
@@ -30353,7 +30298,7 @@ function marketKeysByEdge(plan) {
30353
30298
  else result.set(key, [scope.stableKey]);
30354
30299
  }
30355
30300
  }
30356
- for (const keys of result.values()) keys.sort(compareText15);
30301
+ for (const keys of result.values()) keys.sort(compareText);
30357
30302
  return result;
30358
30303
  }
30359
30304
  function trackedRows(db, state, queryRows2, plan, opts) {
@@ -30362,12 +30307,12 @@ function trackedRows(db, state, queryRows2, plan, opts) {
30362
30307
  return [...queryRows2.values()].map((row) => ({
30363
30308
  queryId: row.id,
30364
30309
  queryText: row.query,
30365
- normalizedText: normalizeText4(row.query),
30310
+ normalizedText: normalizeIdentityText(row.query),
30366
30311
  provenance: parseQueryProvenance(row.provenance),
30367
30312
  state: measured.has(row.id) ? "tracked" : "awaiting-sweep",
30368
30313
  lastMeasuredAt: measured.get(row.id) ?? null,
30369
30314
  assignments: []
30370
- })).sort((left, right) => compareText15(left.normalizedText, right.normalizedText) || compareText15(left.queryId, right.queryId));
30315
+ })).sort((left, right) => compareText(left.normalizedText, right.normalizedText) || compareText(left.queryId, right.queryId));
30371
30316
  }
30372
30317
  const snapshots = new Map(plan.querySnapshots.map((snapshot) => [snapshot.queryId, snapshot]));
30373
30318
  const allIds = unique2([...queryRows2.keys(), ...snapshots.keys()]);
@@ -30405,7 +30350,7 @@ function trackedRows(db, state, queryRows2, plan, opts) {
30405
30350
  } else {
30406
30351
  assignments.set(assignment.targetKey, {
30407
30352
  targetKey: assignment.targetKey,
30408
- groupKeys: [...groupsByTarget.get(assignment.targetKey) ?? []].sort(compareText15),
30353
+ groupKeys: [...groupsByTarget.get(assignment.targetKey) ?? []].sort(compareText),
30409
30354
  marketKeys: [...marketsByEdge.get(edgeKey2) ?? []],
30410
30355
  queryClass: assignment.queryClass,
30411
30356
  classificationSource: source,
@@ -30416,17 +30361,17 @@ function trackedRows(db, state, queryRows2, plan, opts) {
30416
30361
  return {
30417
30362
  queryId,
30418
30363
  queryText,
30419
- normalizedText: normalizeText4(queryText),
30364
+ normalizedText: normalizeIdentityText(queryText),
30420
30365
  provenance: snapshot ? provenanceFromPlan(snapshot.provenance) : parseQueryProvenance(row?.provenance ?? null),
30421
30366
  state: measured.has(queryId) ? "tracked" : "awaiting-sweep",
30422
30367
  lastMeasuredAt: measured.get(queryId) ?? null,
30423
30368
  assignments: [...assignments.values()].map((assignment) => ({
30424
30369
  ...assignment,
30425
- marketKeys: unique2(assignment.marketKeys).sort(compareText15),
30370
+ marketKeys: unique2(assignment.marketKeys).sort(compareText),
30426
30371
  contexts: uniqueContexts(assignment.contexts)
30427
- })).sort((left, right) => compareText15(left.targetKey, right.targetKey))
30372
+ })).sort((left, right) => compareText(left.targetKey, right.targetKey))
30428
30373
  };
30429
- }).sort((left, right) => compareText15(left.normalizedText, right.normalizedText) || compareText15(left.queryId, right.queryId));
30374
+ }).sort((left, right) => compareText(left.normalizedText, right.normalizedText) || compareText(left.queryId, right.queryId));
30430
30375
  }
30431
30376
  function savedSources(db, projectId) {
30432
30377
  const research = db.select({
@@ -30509,9 +30454,9 @@ function resolveAudience(plan, input) {
30509
30454
  throw validationError("Select at least one Property, group, or market for a portfolio query.");
30510
30455
  }
30511
30456
  return {
30512
- targetKeys: [...targetKeys].sort(compareText15),
30513
- groupKeys: groupKeys.sort(compareText15),
30514
- marketKeys: marketKeys.sort(compareText15)
30457
+ targetKeys: [...targetKeys].sort(compareText),
30458
+ groupKeys: groupKeys.sort(compareText),
30459
+ marketKeys: marketKeys.sort(compareText)
30515
30460
  };
30516
30461
  }
30517
30462
  function contextsFromInput(project, contexts) {
@@ -30522,7 +30467,7 @@ function contextsFromInput(project, contexts) {
30522
30467
  if (context.location !== null && !location) {
30523
30468
  throw validationError(`Execution context names a location the project does not configure: ${context.location}`);
30524
30469
  }
30525
- const providers = unique2(context.providers.map((provider) => provider.trim().toLocaleLowerCase("en")).filter(Boolean)).sort(compareText15);
30470
+ const providers = unique2(context.providers.map((provider) => provider.trim().toLocaleLowerCase("en")).filter(Boolean)).sort(compareText);
30526
30471
  for (const provider of Object.keys(context.models)) {
30527
30472
  if (!providers.includes(provider.trim().toLocaleLowerCase("en"))) {
30528
30473
  throw validationError(`Model "${provider}" is not selected by this execution context.`);
@@ -30747,23 +30692,23 @@ function edgeKey(edge) {
30747
30692
  function queryIdsByNormalized(state) {
30748
30693
  const result = /* @__PURE__ */ new Map();
30749
30694
  for (const row of state.queryRows) {
30750
- const key = normalizeText4(row.query);
30695
+ const key = normalizeIdentityText(row.query);
30751
30696
  const ids = result.get(key);
30752
30697
  if (ids) ids.push(row.id);
30753
30698
  else result.set(key, [row.id]);
30754
30699
  }
30755
30700
  for (const snapshot of state.active?.plan.querySnapshots ?? []) {
30756
- const key = normalizeText4(snapshot.queryText);
30701
+ const key = normalizeIdentityText(snapshot.queryText);
30757
30702
  const ids = result.get(key);
30758
30703
  if (ids) ids.push(snapshot.queryId);
30759
30704
  else result.set(key, [snapshot.queryId]);
30760
30705
  }
30761
- for (const ids of result.values()) ids.sort(compareText15);
30706
+ for (const ids of result.values()) ids.sort(compareText);
30762
30707
  return result;
30763
30708
  }
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));
30709
+ function preferredQueryId(state, normalizedText) {
30710
+ const candidates = queryIdsByNormalized(state).get(normalizedText) ?? [];
30711
+ const bound = new Set((state.active?.plan.querySnapshots ?? []).filter((snapshot) => normalizeIdentityText(snapshot.queryText) === normalizedText).map((snapshot) => snapshot.queryId));
30767
30712
  return candidates.find((candidate) => bound.has(candidate)) ?? candidates.at(0) ?? null;
30768
30713
  }
30769
30714
  function rowsEqual(before, after) {
@@ -30785,7 +30730,7 @@ function changeRows(ids, rows, plan, fallbackPlan = null) {
30785
30730
  queryText,
30786
30731
  assignmentCount: (plan ?? fallbackPlan)?.assignments.filter((assignment) => assignment.queryId === queryId).length ?? 0
30787
30732
  }];
30788
- }).sort((left, right) => compareText15(left.queryText, right.queryText) || compareText15(left.queryId, right.queryId));
30733
+ }).sort((left, right) => compareText(left.queryText, right.queryText) || compareText(left.queryId, right.queryId));
30789
30734
  }
30790
30735
  function planSnapshot(plan, queryId) {
30791
30736
  return plan?.querySnapshots.find((snapshot) => snapshot.queryId === queryId) ?? null;
@@ -30793,26 +30738,26 @@ function planSnapshot(plan, queryId) {
30793
30738
  function displayIds(rows, plan) {
30794
30739
  return /* @__PURE__ */ new Set([...rows.keys(), ...plan?.querySnapshots.map((snapshot) => snapshot.queryId) ?? []]);
30795
30740
  }
30796
- function candidateIdsForText(state, plan, rows, normalizedText4) {
30741
+ function candidateIdsForText(state, plan, rows, normalizedText) {
30797
30742
  const ids = /* @__PURE__ */ new Set();
30798
30743
  for (const row of rows.values()) {
30799
- if (normalizeText4(row.query) === normalizedText4) ids.add(row.id);
30744
+ if (normalizeIdentityText(row.query) === normalizedText) ids.add(row.id);
30800
30745
  }
30801
30746
  for (const snapshot of plan?.querySnapshots ?? []) {
30802
- if (normalizeText4(snapshot.queryText) === normalizedText4) ids.add(snapshot.queryId);
30747
+ if (normalizeIdentityText(snapshot.queryText) === normalizedText) ids.add(snapshot.queryId);
30803
30748
  }
30804
- const activeId = preferredQueryId(state, normalizedText4);
30749
+ const activeId = preferredQueryId(state, normalizedText);
30805
30750
  return [...ids].sort((left, right) => {
30806
30751
  if (left === activeId) return -1;
30807
30752
  if (right === activeId) return 1;
30808
- return compareText15(left, right);
30753
+ return compareText(left, right);
30809
30754
  });
30810
30755
  }
30811
30756
  function resolveMutationQueryId(state, plan, rows, removal) {
30812
30757
  if (removal.queryId !== void 0) {
30813
30758
  return rows.has(removal.queryId) || planSnapshot(plan, removal.queryId) !== null ? removal.queryId : null;
30814
30759
  }
30815
- const ids = candidateIdsForText(state, plan, rows, normalizeText4(removal.queryText));
30760
+ const ids = candidateIdsForText(state, plan, rows, normalizeIdentityText(removal.queryText));
30816
30761
  return ids[0] ?? null;
30817
30762
  }
30818
30763
  function queryTextForId(state, plan, rows, queryId, fallback) {
@@ -30909,7 +30854,7 @@ function simpleWorkload(project, rows, opts) {
30909
30854
  const result = /* @__PURE__ */ new Map();
30910
30855
  for (const context of defaultContexts(project, opts, "simple")) {
30911
30856
  for (const row of rows.values()) {
30912
- result.set(`${normalizeText4(row.query)}\0${contextKey(context)}`, context.providers.length);
30857
+ result.set(`${normalizeIdentityText(row.query)}\0${contextKey(context)}`, context.providers.length);
30913
30858
  }
30914
30859
  }
30915
30860
  return result;
@@ -30979,7 +30924,7 @@ function mutationPreviewToken(candidate, mutation, reviewedAt) {
30979
30924
  mutation: { additions: mutation.additions, removals: mutation.removals, ...mutation.edits ? { edits: mutation.edits } : {} },
30980
30925
  reviewedAt,
30981
30926
  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))
30927
+ queries: [...candidate.queryRows.values()].map((row) => ({ id: row.id, query: row.query, provenance: row.provenance })).sort((left, right) => compareText(left.id, right.id))
30983
30928
  }))}`;
30984
30929
  }
30985
30930
  function assertReviewedAt(reviewedAt) {
@@ -30996,7 +30941,7 @@ function assertSimpleAddition(addition) {
30996
30941
  }
30997
30942
  }
30998
30943
  function addSimpleSource(state, rows, source, reusedQueryIds) {
30999
- const normalized = normalizeText4(source.queryText);
30944
+ const normalized = normalizeIdentityText(source.queryText);
31000
30945
  const existingId = candidateIdsForText(state, null, rows, normalized).at(0) ?? preferredQueryId(state, normalized) ?? stableNewQueryId(state.project.id, normalized);
31001
30946
  const existing = rows.get(existingId);
31002
30947
  if (existing) {
@@ -31026,7 +30971,7 @@ function buildSimpleCandidate(db, state, mutation, opts, manualCapturedAt) {
31026
30971
  }
31027
30972
  const existing = rows.get(edit.queryId);
31028
30973
  if (!existing) throw notFound("Tracked query", edit.queryId);
31029
- if (edit.text === void 0 || normalizeText4(edit.text) === normalizeText4(existing.query)) continue;
30974
+ if (edit.text === void 0 || normalizeIdentityText(edit.text) === normalizeIdentityText(existing.query)) continue;
31030
30975
  rows.delete(edit.queryId);
31031
30976
  mutatedQueryIds.add(edit.queryId);
31032
30977
  addSimpleSource(state, rows, {
@@ -31070,7 +31015,7 @@ function buildSimpleCandidate(db, state, mutation, opts, manualCapturedAt) {
31070
31015
  };
31071
31016
  }
31072
31017
  function addAdvancedSource(state, plan, rows, addition, source, reusedQueryIds, mutatedQueryIds) {
31073
- const normalized = normalizeText4(source.queryText);
31018
+ const normalized = normalizeIdentityText(source.queryText);
31074
31019
  const queryId = candidateIdsForText(state, plan, rows, normalized).at(0) ?? preferredQueryId(state, normalized) ?? stableNewQueryId(state.project.id, normalized);
31075
31020
  const knownBefore = state.queryRows.some((row) => row.id === queryId) || planSnapshot(state.active?.plan ?? null, queryId) !== null;
31076
31021
  const queryText = queryTextForId(state, plan, rows, queryId, source.queryText.trim());
@@ -31146,7 +31091,7 @@ function applyAdvancedEdit(state, plan, rows, edit, capturedAt, reusedQueryIds,
31146
31091
  if (!snapshot && !row) throw notFound("Tracked query", edit.queryId);
31147
31092
  const oldText = snapshot?.queryText ?? row.query;
31148
31093
  const text2 = edit.text ?? oldText;
31149
- const textChanged = normalizeText4(text2) !== normalizeText4(oldText);
31094
+ const textChanged = normalizeIdentityText(text2) !== normalizeIdentityText(oldText);
31150
31095
  const audience = edit.audience === void 0 ? null : resolveAudience(requireAdvanced(state).plan, edit.audience);
31151
31096
  const targetKeys = audience ? new Set(audience.targetKeys) : null;
31152
31097
  const marketKeys = new Set(audience?.marketKeys ?? []);
@@ -31180,7 +31125,7 @@ function applyAdvancedEdit(state, plan, rows, edit, capturedAt, reusedQueryIds,
31180
31125
  }
31181
31126
  return plan;
31182
31127
  }
31183
- const newQueryId = candidateIdsForText(state, plan, rows, normalizeText4(text2)).at(0) ?? preferredQueryId(state, normalizeText4(text2)) ?? stableNewQueryId(state.project.id, normalizeText4(text2));
31128
+ const newQueryId = candidateIdsForText(state, plan, rows, normalizeIdentityText(text2)).at(0) ?? preferredQueryId(state, normalizeIdentityText(text2)) ?? stableNewQueryId(state.project.id, normalizeIdentityText(text2));
31184
31129
  const queryText = queryTextForId(state, plan, rows, newQueryId, text2.trim());
31185
31130
  const provenance = provenanceForId(state, plan, rows, newQueryId, { source: "manual", sourceId: null, capturedAt });
31186
31131
  if (!rows.has(newQueryId)) rows.set(newQueryId, { id: newQueryId, query: queryText, provenance: serializeQueryProvenance(provenance) });
@@ -31281,7 +31226,7 @@ function writeSimpleCandidate(tx, candidate, now) {
31281
31226
  for (const queryId of candidate.createdQueryIds) {
31282
31227
  const planned = candidate.queryRows.get(queryId);
31283
31228
  if (!planned) continue;
31284
- const row = persisted.find((candidateRow) => normalizeText4(candidateRow.query) === normalizeText4(planned.query));
31229
+ const row = persisted.find((candidateRow) => normalizeIdentityText(candidateRow.query) === normalizeIdentityText(planned.query));
31285
31230
  if (!row) continue;
31286
31231
  if (row.id !== planned.id) tx.update(queries).set({ id: planned.id }).where(eq37(queries.id, row.id)).run();
31287
31232
  if (planned.provenance) tx.update(queries).set({ provenance: planned.provenance }).where(eq37(queries.id, planned.id)).run();
@@ -31616,7 +31561,7 @@ import crypto24 from "crypto";
31616
31561
  import dns2 from "dns/promises";
31617
31562
  import http2 from "http";
31618
31563
  import https2 from "https";
31619
- import net2 from "net";
31564
+ import net3 from "net";
31620
31565
  var REQUEST_TIMEOUT_MS = 1e4;
31621
31566
  async function resolveWebhookTarget(raw, options = {}) {
31622
31567
  let parsed;
@@ -31631,13 +31576,13 @@ async function resolveWebhookTarget(raw, options = {}) {
31631
31576
  if (parsed.username || parsed.password) {
31632
31577
  return { ok: false, message: '"url" must not include credentials' };
31633
31578
  }
31634
- const lookupHost = stripIpv6Brackets2(parsed.hostname);
31579
+ const lookupHost = stripIpv6Brackets(parsed.hostname);
31635
31580
  if (!lookupHost) {
31636
31581
  return { ok: false, message: '"url" must include a hostname' };
31637
31582
  }
31638
- const addresses = await resolveHostAddresses(lookupHost);
31583
+ const addresses = await resolveHostAddresses(lookupHost, options.resolveAddresses);
31639
31584
  if (addresses.length === 0) {
31640
- return { ok: false, message: '"url" hostname could not be resolved' };
31585
+ return { ok: false, message: '"url" hostname could not be resolved', unresolved: true };
31641
31586
  }
31642
31587
  const blocked = addresses.find((entry) => isBlockedAddress(entry.address, options));
31643
31588
  if (blocked) {
@@ -31648,7 +31593,8 @@ async function resolveWebhookTarget(raw, options = {}) {
31648
31593
  target: {
31649
31594
  url: parsed,
31650
31595
  address: addresses[0].address,
31651
- family: addresses[0].family
31596
+ family: addresses[0].family,
31597
+ addresses
31652
31598
  }
31653
31599
  };
31654
31600
  }
@@ -31668,16 +31614,23 @@ async function deliverWebhook(target, payload, webhookSecret) {
31668
31614
  }
31669
31615
  return await new Promise((resolve) => {
31670
31616
  const requestOptions = {
31671
- family: target.family,
31617
+ // A connection of its own: a shared agent pools by hostname, so it could
31618
+ // hand this request a socket some other caller dialed unchecked.
31619
+ agent: false,
31620
+ // Dial the checked addresses, falling back from one to the next, so a
31621
+ // receiver that listens on only one of them (`localhost` checks as
31622
+ // 127.0.0.1 and ::1) still gets the delivery.
31623
+ autoSelectFamily: true,
31624
+ lookup: pinnedLookup(checkedAddresses(target)),
31672
31625
  headers,
31673
- hostname: target.address,
31626
+ hostname: stripIpv6Brackets(target.url.hostname),
31674
31627
  method: "POST",
31675
31628
  path: path8,
31676
31629
  port,
31677
31630
  timeout: REQUEST_TIMEOUT_MS
31678
31631
  };
31679
31632
  if (isHttps) {
31680
- requestOptions.servername = stripIpv6Brackets2(target.url.hostname);
31633
+ requestOptions.servername = stripIpv6Brackets(target.url.hostname);
31681
31634
  }
31682
31635
  const request = (isHttps ? https2.request : http2.request)(requestOptions, (response) => {
31683
31636
  response.resume();
@@ -31694,11 +31647,27 @@ async function deliverWebhook(target, payload, webhookSecret) {
31694
31647
  request.end(body);
31695
31648
  });
31696
31649
  }
31697
- async function resolveHostAddresses(hostname) {
31698
- const family = net2.isIP(hostname);
31650
+ function checkedAddresses(target) {
31651
+ return target.addresses ?? [{ address: target.address, family: target.family }];
31652
+ }
31653
+ function pinnedLookup(addresses) {
31654
+ return (_hostname, options, callback) => {
31655
+ if (options.all) callback(null, addresses.map(({ address, family }) => ({ address, family })));
31656
+ else callback(null, addresses[0].address, addresses[0].family);
31657
+ };
31658
+ }
31659
+ var LOCALHOST_ADDRESSES = [{ address: "127.0.0.1", family: 4 }, { address: "::1", family: 6 }];
31660
+ function isLocalhostName(hostname) {
31661
+ const name = hostname.toLowerCase().replace(/\.$/, "");
31662
+ return name === "localhost" || name.endsWith(".localhost");
31663
+ }
31664
+ async function resolveHostAddresses(hostname, resolveAddresses) {
31665
+ const family = net3.isIP(hostname);
31699
31666
  if (family === 4 || family === 6) {
31700
31667
  return [{ address: hostname, family }];
31701
31668
  }
31669
+ if (isLocalhostName(hostname)) return LOCALHOST_ADDRESSES;
31670
+ if (resolveAddresses) return await resolveAddresses(hostname);
31702
31671
  try {
31703
31672
  const [ipv4, ipv6] = await Promise.allSettled([dns2.resolve4(hostname), dns2.resolve6(hostname)]);
31704
31673
  const unique3 = /* @__PURE__ */ new Map();
@@ -31718,65 +31687,7 @@ async function resolveHostAddresses(hostname) {
31718
31687
  }
31719
31688
  }
31720
31689
  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;
31690
+ return blockedAddressReason(address, { allowLoopback: options.allowLoopback }) !== null;
31780
31691
  }
31781
31692
 
31782
31693
  // ../api-routes/src/apply.ts
@@ -31895,7 +31806,7 @@ async function applyRoutes(app, opts) {
31895
31806
  const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
31896
31807
  if (existing) {
31897
31808
  const prevAliases = existing.aliases;
31898
- aliasesChanged = !aliasArraysEqual2(prevAliases, nextAliases);
31809
+ aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
31899
31810
  }
31900
31811
  if (existing) {
31901
31812
  projectId = existing.id;
@@ -32126,13 +32037,6 @@ async function applyRoutes(app, opts) {
32126
32037
  });
32127
32038
  });
32128
32039
  }
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
32040
 
32137
32041
  // ../api-routes/src/history.ts
32138
32042
  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 +36038,8 @@ function readVisibilityCompare(db, projectName, query) {
36134
36038
  if (scope === "project" && filters.scopeKey !== void 0) throw validationError("scopeKey is not valid for project scope.");
36135
36039
  if (scope === "market" && filters.marketKey !== void 0) throw validationError("marketKey is not valid for market scope.");
36136
36040
  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);
36041
+ const providerKey2 = filters.provider === void 0 ? void 0 : normalizeIdentityText(filters.provider);
36042
+ const locationKey2 = filters.location === void 0 ? void 0 : normalizeIdentityText(filters.location);
36139
36043
  const scoped = scope !== "project" || filters.marketKey !== void 0;
36140
36044
  let advancedComparison;
36141
36045
  if (activeMeasurementPlan(db, project.id) !== null) {
@@ -36186,7 +36090,7 @@ function readVisibilityCompare(db, projectName, query) {
36186
36090
  }).from(querySnapshots).where(inArray23(querySnapshots.runId, runIds)).all() : [];
36187
36091
  return {
36188
36092
  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)))
36093
+ 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
36094
  };
36191
36095
  };
36192
36096
  const fromMonth = loadMonth(fromBounds);
@@ -38513,6 +38417,7 @@ var SCHEMA_TABLE = {
38513
38417
  IndexingRequestResponseDto: indexingRequestResponseDtoSchema,
38514
38418
  IntegrationSettingsSummaryDto: integrationSettingsSummaryDtoSchema,
38515
38419
  KeywordDto: keywordDtoSchema,
38420
+ KeywordGenerateResponse: keywordGenerateResponseSchema,
38516
38421
  LatestProjectRunDto: latestProjectRunDtoSchema,
38517
38422
  RunAdmissionDto: runAdmissionDtoSchema,
38518
38423
  LocationContext: locationContextSchema,
@@ -38589,13 +38494,16 @@ var SCHEMA_TABLE = {
38589
38494
  OrganicEvidenceDto: organicEvidenceDtoSchema,
38590
38495
  ProjectCreateRequest: projectCreateRequestSchema,
38591
38496
  ProjectDto: projectDtoSchema,
38497
+ ProjectDefaultLocationResponse: projectDefaultLocationResponseSchema,
38592
38498
  ProjectUpsertRequest: projectUpsertRequestSchema,
38593
38499
  ProjectConfig: projectConfigSchema,
38500
+ ProjectConfigExport: projectConfigExportSchema,
38594
38501
  ProjectOverviewDto: projectOverviewDtoSchema,
38595
38502
  TrafficAnalyticsResponse: trafficAnalyticsResponseSchema,
38596
38503
  ProjectSearchResponseDto: projectSearchResponseSchema,
38597
38504
  ProviderSummaryEntryDto: providerSummaryEntryDtoSchema,
38598
38505
  QueryDto: queryDtoSchema,
38506
+ QueryGenerateResponse: queryGenerateResponseSchema,
38599
38507
  ReferralAssessment: referralAssessmentSchema,
38600
38508
  ResultsExportDto: resultsExportDtoSchema,
38601
38509
  RunCompletenessDto: runCompletenessDtoSchema,
@@ -38658,7 +38566,7 @@ var SCHEMA_TABLE = {
38658
38566
  WordpressManualAssistDto: wordpressManualAssistDtoSchema,
38659
38567
  WordpressOnboardResultDto: wordpressOnboardResultDtoSchema,
38660
38568
  WordpressPageDetailDto: wordpressPageDetailDtoSchema,
38661
- WordpressPageSummaryDto: wordpressPageSummaryDtoSchema,
38569
+ WordpressPageListDto: wordpressPageListDtoSchema,
38662
38570
  WordpressSchemaBlockDto: wordpressSchemaBlockDtoSchema,
38663
38571
  WordpressSchemaDeployResultDto: wordpressSchemaDeployResultDtoSchema,
38664
38572
  WordpressSchemaStatusResultDto: wordpressSchemaStatusResultDtoSchema,
@@ -38678,10 +38586,27 @@ var SCHEMA_TABLE = {
38678
38586
  var SHARED_ENUM_REFS = [
38679
38587
  { schema: "ScheduleDto", property: "kind", component: "SchedulableRunKind" }
38680
38588
  ];
38589
+ function addNullToNullableEnums(node) {
38590
+ if (Array.isArray(node)) {
38591
+ for (const item2 of node) addNullToNullableEnums(item2);
38592
+ return;
38593
+ }
38594
+ if (node === null || typeof node !== "object") return;
38595
+ const record = node;
38596
+ if (record.nullable === true && Array.isArray(record.enum)) {
38597
+ const values = record.enum;
38598
+ if (!values.includes(null)) record.enum = [...values, null];
38599
+ }
38600
+ for (const value of Object.values(record)) addNullToNullableEnums(value);
38601
+ }
38602
+ var INPUT_FORM_SCHEMAS = /* @__PURE__ */ new Set(["ProjectConfig"]);
38681
38603
  function buildComponentSchemas() {
38682
38604
  const out = {};
38683
38605
  for (const [name, schema] of Object.entries(SCHEMA_TABLE)) {
38684
- out[name] = z.toJSONSchema(schema, { target: "openapi-3.0" });
38606
+ const io = INPUT_FORM_SCHEMAS.has(name) ? "input" : "output";
38607
+ const json = z.toJSONSchema(schema, { target: "openapi-3.0", io });
38608
+ addNullToNullableEnums(json);
38609
+ out[name] = json;
38685
38610
  }
38686
38611
  for (const { schema, property, component } of SHARED_ENUM_REFS) {
38687
38612
  const properties = out[schema]?.properties;
@@ -40428,7 +40353,7 @@ var routeCatalog = [
40428
40353
  }
40429
40354
  },
40430
40355
  responses: {
40431
- 200: jsonResponse("Default location updated.", "ProjectDto"),
40356
+ 200: jsonResponse("Default location updated.", "ProjectDefaultLocationResponse"),
40432
40357
  400: errorResponse("Invalid location."),
40433
40358
  404: errorResponse("Project not found.")
40434
40359
  }
@@ -40440,7 +40365,7 @@ var routeCatalog = [
40440
40365
  tags: ["projects"],
40441
40366
  parameters: [nameParameter],
40442
40367
  responses: {
40443
- 200: jsonResponse("Project configuration returned.", "ProjectConfig"),
40368
+ 200: jsonResponse("Project configuration returned.", "ProjectConfigExport"),
40444
40369
  404: errorResponse("Project not found.")
40445
40370
  }
40446
40371
  },
@@ -40643,7 +40568,7 @@ var routeCatalog = [
40643
40568
  }
40644
40569
  },
40645
40570
  responses: {
40646
- 200: rawJsonResponse("Query suggestions returned.", { type: "object", properties: { suggestions: { type: "array", items: { type: "string" } } } }),
40571
+ 200: jsonResponse("Query suggestions returned.", "QueryGenerateResponse"),
40647
40572
  501: errorResponse("Query generation is not available.")
40648
40573
  }
40649
40574
  },
@@ -40752,8 +40677,7 @@ var routeCatalog = [
40752
40677
  }
40753
40678
  },
40754
40679
  responses: {
40755
- // TODO: Add `KeywordGenerateResponse` Zod schema (`{ suggestions: string[] }`) in contracts.
40756
- 200: rawJsonResponse("Legacy keyword suggestions returned.", looseObjectSchema),
40680
+ 200: jsonResponse("Legacy keyword suggestions returned.", "KeywordGenerateResponse"),
40757
40681
  501: errorResponse("Legacy keyword generation is not available.")
40758
40682
  }
40759
40683
  },
@@ -44089,7 +44013,7 @@ var routeCatalog = [
44089
44013
  tags: ["wordpress"],
44090
44014
  parameters: [nameParameter, wordpressEnvQueryParameter],
44091
44015
  responses: {
44092
- 200: jsonArrayResponse("WordPress pages returned.", "WordpressPageSummaryDto"),
44016
+ 200: jsonResponse("WordPress pages returned.", "WordpressPageListDto"),
44093
44017
  400: errorResponse("Invalid environment or missing connection."),
44094
44018
  404: errorResponse("Project not found.")
44095
44019
  }
@@ -47812,18 +47736,18 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
47812
47736
  const parsed = JSON.parse(body);
47813
47737
  if (parsed.error) detail = parsed.error;
47814
47738
  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"), "***");
47739
+ const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(code), "g"), "***");
47816
47740
  detail += detail ? `: ${sanitized}` : sanitized;
47817
47741
  }
47818
47742
  } catch {
47819
47743
  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"), "***");
47744
+ detail = truncated.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(code), "g"), "***");
47821
47745
  }
47822
47746
  throw new GoogleAuthError(`Token exchange failed (${res.status}): ${detail}`);
47823
47747
  }
47824
47748
  return await res.json();
47825
47749
  }
47826
- function escapeRegExp(str) {
47750
+ function escapeRegExp2(str) {
47827
47751
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47828
47752
  }
47829
47753
  async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
@@ -47851,12 +47775,12 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
47851
47775
  const parsed = JSON.parse(body);
47852
47776
  if (parsed.error) detail = parsed.error;
47853
47777
  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"), "***");
47778
+ const sanitized = parsed.error_description.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(currentRefreshToken), "g"), "***");
47855
47779
  detail += detail ? `: ${sanitized}` : sanitized;
47856
47780
  }
47857
47781
  } catch {
47858
47782
  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"), "***");
47783
+ detail = truncated.replace(new RegExp(escapeRegExp2(clientId), "g"), "***").replace(new RegExp(escapeRegExp2(clientSecret), "g"), "***").replace(new RegExp(escapeRegExp2(currentRefreshToken), "g"), "***");
47860
47784
  }
47861
47785
  throw new GoogleAuthError(`Token refresh failed (${res.status}): ${detail}`);
47862
47786
  }
@@ -47946,7 +47870,7 @@ function gscClientLog(level, action, ctx) {
47946
47870
  const stream = level === "error" ? process.stderr : process.stdout;
47947
47871
  stream.write(JSON.stringify(entry) + "\n");
47948
47872
  }
47949
- function escapeRegExp2(str) {
47873
+ function escapeRegExp3(str) {
47950
47874
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47951
47875
  }
47952
47876
  async function gscFetchResponse(accessToken, url, opts) {
@@ -47975,7 +47899,7 @@ async function gscFetchResponse(accessToken, url, opts) {
47975
47899
  const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
47976
47900
  let sanitizedDetail = detail;
47977
47901
  if (accessToken) {
47978
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp2(accessToken), "g"), "***");
47902
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
47979
47903
  }
47980
47904
  throw new GoogleApiError(`GSC API error (${res.status}): ${sanitizedDetail}`, res.status);
47981
47905
  }
@@ -48263,15 +48187,12 @@ async function getAccessToken(clientEmail, privateKey) {
48263
48187
  const body = await res.text().catch(() => "");
48264
48188
  ga4Log("error", "token.failed", { httpStatus: res.status });
48265
48189
  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"), "***");
48190
+ const sanitizedDetail = detail.replace(new RegExp(escapeRegExp(clientEmail), "g"), "***").replace(new RegExp(escapeRegExp(privateKey.slice(0, 32)), "g"), "***");
48267
48191
  throw new GA4ApiError(`Failed to get access token: ${sanitizedDetail}`, res.status);
48268
48192
  }
48269
48193
  const data = await res.json();
48270
48194
  return data.access_token;
48271
48195
  }
48272
- function escapeRegExp3(str) {
48273
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
48274
- }
48275
48196
  var gaInFlight = 0;
48276
48197
  var gaWaitQueue = [];
48277
48198
  async function acquireGa4Slot() {
@@ -48377,7 +48298,7 @@ async function runReport(accessToken, propertyId, request) {
48377
48298
  }
48378
48299
  let sanitizedDetail = detail;
48379
48300
  if (accessToken) {
48380
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48301
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48381
48302
  }
48382
48303
  ga4Log("error", "report.auth-failed", { propertyId, httpStatus: res.status });
48383
48304
  throw new GA4ApiError(
@@ -48397,7 +48318,7 @@ async function runReport(accessToken, propertyId, request) {
48397
48318
  const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
48398
48319
  let sanitizedDetail = detail;
48399
48320
  if (accessToken) {
48400
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48321
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48401
48322
  }
48402
48323
  throw new GA4ApiError(`GA4 API error (${res.status}): ${sanitizedDetail}`, res.status, retryAfterSeconds);
48403
48324
  }
@@ -48421,7 +48342,7 @@ async function batchRunReports(accessToken, propertyId, requests) {
48421
48342
  ga4Log("error", "batch-report.auth-failed", { propertyId, httpStatus: res.status });
48422
48343
  let sanitizedDetail = body;
48423
48344
  if (accessToken) {
48424
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48345
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48425
48346
  }
48426
48347
  throw new GA4ApiError(
48427
48348
  `GA4 API authentication failed \u2014 check service account permissions. ${sanitizedDetail}`,
@@ -48440,7 +48361,7 @@ async function batchRunReports(accessToken, propertyId, requests) {
48440
48361
  const detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
48441
48362
  let sanitizedDetail = detail;
48442
48363
  if (accessToken) {
48443
- sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48364
+ sanitizedDetail = sanitizedDetail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48444
48365
  }
48445
48366
  throw new GA4ApiError(`GA4 API error (${res.status}): ${sanitizedDetail}`, res.status, retryAfterSeconds);
48446
48367
  }
@@ -48742,7 +48663,7 @@ async function listProperties(accessToken) {
48742
48663
  } catch {
48743
48664
  if (raw.length < 200) detail = ` ${raw}`;
48744
48665
  }
48745
- const sanitized = detail.replace(new RegExp(escapeRegExp3(accessToken), "g"), "***");
48666
+ const sanitized = detail.replace(new RegExp(escapeRegExp(accessToken), "g"), "***");
48746
48667
  const retryAfterSeconds = res.status === 429 || res.status >= 500 || res.status === 403 && quotaReason ? parseRetryAfter(res.headers.get("retry-after")) : void 0;
48747
48668
  ga4Log("error", "admin.account-summaries-failed", {
48748
48669
  httpStatus: res.status,
@@ -49137,7 +49058,7 @@ async function fetchSearchLandingWindow(accessToken, propertyId, window, options
49137
49058
  const seen2 = /* @__PURE__ */ new Set();
49138
49059
  const rows = [];
49139
49060
  for (const row of rawRows) {
49140
- const landingPage = row.dimensionValues[0]?.value || NOT_SET_LANDING_PAGE;
49061
+ const landingPage = row.dimensionValues[0]?.value ?? NOT_SET_LANDING_PAGE;
49141
49062
  if (seen2.has(landingPage)) continue;
49142
49063
  seen2.add(landingPage);
49143
49064
  rows.push({ landingPage, ...parseSearchLandingMetrics(row, index2) });
@@ -58074,9 +57995,6 @@ function bingClientLog(level, action, ctx) {
58074
57995
  const stream = level === "error" ? process.stderr : process.stdout;
58075
57996
  stream.write(JSON.stringify(entry) + "\n");
58076
57997
  }
58077
- function escapeRegExp4(str) {
58078
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58079
- }
58080
57998
  function parseBingErrorCode(body) {
58081
57999
  try {
58082
58000
  const parsed = JSON.parse(body);
@@ -58113,7 +58031,7 @@ async function bingFetchOnce(apiKey, endpoint, opts) {
58113
58031
  if (!res.ok) {
58114
58032
  const body = await res.text();
58115
58033
  let detail = body.length <= 500 ? body : `${body.slice(0, 500)}... [truncated]`;
58116
- detail = detail.replace(new RegExp(escapeRegExp4(apiKey), "g"), "***");
58034
+ detail = detail.replace(new RegExp(escapeRegExp(apiKey), "g"), "***");
58117
58035
  const bingErrorCode = parseBingErrorCode(body);
58118
58036
  const err = new BingApiError(`Bing API error (${res.status}): ${detail}`, res.status, bingErrorCode);
58119
58037
  bingClientLog("error", err.isThrottle ? "http.throttled" : "http.error", {
@@ -58956,7 +58874,7 @@ function findBiggestMover(current, prior) {
58956
58874
  for (const row of current) entry(row.source).current += row.sessions ?? 0;
58957
58875
  for (const row of prior) entry(row.source).prior += row.sessions ?? 0;
58958
58876
  let mover = null;
58959
- const sources = [...bySource.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
58877
+ const sources = [...bySource.keys()].sort(compareText);
58960
58878
  for (const source of sources) {
58961
58879
  const sessions = bySource.get(source);
58962
58880
  const size = Math.abs(sessions.current - sessions.prior);
@@ -60572,7 +60490,7 @@ function buildAuthErrorMessage(res, responseText) {
60572
60490
  async function fetchJson(connection, siteUrl, path8, init) {
60573
60491
  if (siteUrl.startsWith("http:")) {
60574
60492
  }
60575
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path8}`, {
60493
+ const res = await (connection.fetchImpl ?? fetch)(`${normalizeSiteUrl(siteUrl)}${path8}`, {
60576
60494
  ...init,
60577
60495
  headers: {
60578
60496
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -60596,10 +60514,10 @@ async function fetchJson(connection, siteUrl, path8, init) {
60596
60514
  const text2 = await res.text().catch(() => "");
60597
60515
  let detail = text2 || res.statusText;
60598
60516
  if (connection.appPassword) {
60599
- detail = detail.replace(new RegExp(escapeRegExp5(connection.appPassword), "g"), "***");
60517
+ detail = detail.replace(new RegExp(escapeRegExp(connection.appPassword), "g"), "***");
60600
60518
  }
60601
60519
  if (connection.username) {
60602
- detail = detail.replace(new RegExp(escapeRegExp5(connection.username), "g"), "***");
60520
+ detail = detail.replace(new RegExp(escapeRegExp(connection.username), "g"), "***");
60603
60521
  }
60604
60522
  throw new WordpressApiError("UPSTREAM_ERROR", `WordPress API error (${res.status}): ${detail}`, res.status);
60605
60523
  }
@@ -60631,9 +60549,9 @@ async function fetchPageCollectionSummary(connection, siteUrl, options) {
60631
60549
  );
60632
60550
  return response;
60633
60551
  }
60634
- async function fetchText(url) {
60552
+ async function fetchText(connection, url) {
60635
60553
  try {
60636
- const res = await fetch(url, { signal: AbortSignal.timeout(WP_FETCH_TEXT_TIMEOUT_MS) });
60554
+ const res = await (connection.fetchImpl ?? fetch)(url, { signal: AbortSignal.timeout(WP_FETCH_TEXT_TIMEOUT_MS) });
60637
60555
  if (!res.ok) return null;
60638
60556
  return await res.text();
60639
60557
  } catch {
@@ -60775,7 +60693,7 @@ async function verifyWordpressConnection(connection) {
60775
60693
  validateConnection(connection, site.siteUrl);
60776
60694
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
60777
60695
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
60778
- const homeHtml = await fetchText(site.siteUrl);
60696
+ const homeHtml = await fetchText(connection, site.siteUrl);
60779
60697
  return {
60780
60698
  url: site.siteUrl,
60781
60699
  reachable: true,
@@ -60791,7 +60709,7 @@ async function getSiteStatus(connection, env) {
60791
60709
  try {
60792
60710
  const userInfo = await verifyAuthenticatedRestAccess(connection, site.siteUrl);
60793
60711
  const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
60794
- const homeHtml = await fetchText(site.siteUrl);
60712
+ const homeHtml = await fetchText(connection, site.siteUrl);
60795
60713
  const plugins = await listActivePlugins(connection, env);
60796
60714
  return {
60797
60715
  url: site.siteUrl,
@@ -60881,15 +60799,15 @@ async function getPageBySlug(connection, slug, env) {
60881
60799
  }
60882
60800
  return body[0];
60883
60801
  }
60884
- async function fetchRenderedPage(link) {
60802
+ async function fetchRenderedPage(connection, link) {
60885
60803
  if (!link) return null;
60886
- return fetchText(link);
60804
+ return fetchText(connection, link);
60887
60805
  }
60888
60806
  async function getPageDetail(connection, slug, env, plugins) {
60889
60807
  const site = resolveEnvironment(connection, env);
60890
60808
  const resolvedPlugins = plugins === void 0 ? await listActivePlugins(connection, site.env) : plugins;
60891
60809
  const page3 = await getPageBySlug(connection, slug, site.env);
60892
- const html = await fetchRenderedPage(page3.link);
60810
+ const html = await fetchRenderedPage(connection, page3.link);
60893
60811
  const schemaBlocks = html ? extractSchemaBlocks(html) : [];
60894
60812
  const seo = buildSeoState(page3, html, resolvedPlugins);
60895
60813
  return {
@@ -61094,14 +61012,11 @@ var CANONRY_SCHEMA_START = "<!-- canonry:schema:start -->";
61094
61012
  var CANONRY_SCHEMA_END = "<!-- canonry:schema:end -->";
61095
61013
  function stripCanonrySchema(content) {
61096
61014
  const regex = new RegExp(
61097
- `${escapeRegExp5(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp5(CANONRY_SCHEMA_END)}`,
61015
+ `${escapeRegExp(CANONRY_SCHEMA_START)}[\\s\\S]*?${escapeRegExp(CANONRY_SCHEMA_END)}`,
61098
61016
  "g"
61099
61017
  );
61100
61018
  return content.replace(regex, "").replace(/\n{3,}/g, "\n\n").trim();
61101
61019
  }
61102
- function escapeRegExp5(str) {
61103
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61104
- }
61105
61020
  function injectCanonrySchema(content, schemas) {
61106
61021
  if (schemas.length === 0) return content;
61107
61022
  const blocks = schemas.map((schema) => `<script type="application/ld+json">${JSON.stringify(schema).replace(/<\//g, "<\\/")}</script>`).join("\n");
@@ -61206,7 +61121,7 @@ async function getSchemaStatus(connection, env) {
61206
61121
  const thirdPartySchemas = [];
61207
61122
  if (hasCanonryMarker) {
61208
61123
  const markerRegex = new RegExp(
61209
- `${escapeRegExp5(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp5(CANONRY_SCHEMA_END)}`
61124
+ `${escapeRegExp(CANONRY_SCHEMA_START)}([\\s\\S]*?)${escapeRegExp(CANONRY_SCHEMA_END)}`
61210
61125
  );
61211
61126
  const match = markerRegex.exec(rawContent);
61212
61127
  if (match?.[1]) {
@@ -61249,7 +61164,7 @@ async function getLlmsTxt(connection, env) {
61249
61164
  return {
61250
61165
  env: site.env,
61251
61166
  url,
61252
- content: await fetchText(url)
61167
+ content: await fetchText(connection, url)
61253
61168
  };
61254
61169
  }
61255
61170
  async function buildManualLlmsTxtUpdate(connection, content, env) {
@@ -61431,6 +61346,132 @@ function parseEnv(value) {
61431
61346
  return parsed.success ? parsed.data : void 0;
61432
61347
  }
61433
61348
 
61349
+ // ../api-routes/src/guarded-fetch.ts
61350
+ import { Agent, fetch as undiciFetch } from "undici";
61351
+ var GUARDED_FETCH_MAX_REDIRECTS = 5;
61352
+ var MAX_POOLED_AGENTS = 16;
61353
+ var EgressRefusedError = class extends Error {
61354
+ /**
61355
+ * True when the refused target is a redirect the site chose rather than the
61356
+ * URL the caller asked for. Refusing the caller's URL is a verdict on the
61357
+ * caller's input; refusing a redirect is the site's failure.
61358
+ */
61359
+ redirected;
61360
+ constructor(message, redirected) {
61361
+ super(message);
61362
+ this.name = "EgressRefusedError";
61363
+ this.redirected = redirected;
61364
+ }
61365
+ };
61366
+ var EgressFailedError = class extends Error {
61367
+ constructor(message) {
61368
+ super(message);
61369
+ this.name = "EgressFailedError";
61370
+ }
61371
+ };
61372
+ var CROSS_ORIGIN_DROPPED_HEADERS = ["authorization", "cookie", "proxy-authorization"];
61373
+ var BODY_HEADERS = ["content-encoding", "content-language", "content-length", "content-location", "content-type"];
61374
+ function createGuardedFetch(options = {}) {
61375
+ const resolveTarget = options.resolveTarget ?? ((url) => resolveWebhookTarget(url, { allowLoopback: options.allowLoopback }));
61376
+ const agents = /* @__PURE__ */ new Map();
61377
+ function agentFor(target) {
61378
+ const addresses = checkedAddresses(target);
61379
+ const key = `${target.url.origin} ${addresses.map(({ address }) => address).sort().join(" ")}`;
61380
+ const agent = agents.get(key) ?? pinnedAgent(addresses);
61381
+ agents.delete(key);
61382
+ agents.set(key, agent);
61383
+ if (agents.size > MAX_POOLED_AGENTS) {
61384
+ const [oldestKey, oldest] = agents.entries().next().value;
61385
+ agents.delete(oldestKey);
61386
+ oldest.close().catch(() => {
61387
+ });
61388
+ }
61389
+ return agent;
61390
+ }
61391
+ return async (input, init = {}) => {
61392
+ let href = String(input);
61393
+ let method = (init.method ?? "GET").toUpperCase();
61394
+ let body = init.body ?? null;
61395
+ const headers = new Headers(init.headers);
61396
+ const signal = init.signal ?? void 0;
61397
+ let mayRedirectToLoopback = true;
61398
+ for (let redirects = 0; ; redirects += 1) {
61399
+ signal?.throwIfAborted();
61400
+ const check2 = await untilAborted(resolveTarget(href), signal);
61401
+ if (!check2.ok) {
61402
+ const parsed = URL.canParse(href) ? new URL(href) : null;
61403
+ if (check2.unresolved) throw new EgressFailedError(`Could not resolve ${parsed?.hostname || href}`);
61404
+ throw new EgressRefusedError(`Refused to connect to ${parsed?.host || href}: ${check2.message.replace(/^"url" /, "")}`, redirects > 0);
61405
+ }
61406
+ const url = check2.target.url;
61407
+ const onLoopback = checkedAddresses(check2.target).map(({ address }) => isLoopbackAddress(address));
61408
+ if (!mayRedirectToLoopback && onLoopback.includes(true)) {
61409
+ 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);
61410
+ }
61411
+ mayRedirectToLoopback = !onLoopback.includes(false);
61412
+ const response = await requestPinned(check2.target, agentFor(check2.target), { method, headers, body, signal });
61413
+ const location = isLocationRedirectStatus(response.status) ? response.headers.get("location") : null;
61414
+ if (location === null) return response;
61415
+ await response.body?.cancel();
61416
+ if (redirects >= GUARDED_FETCH_MAX_REDIRECTS) {
61417
+ throw new EgressFailedError(`${url.host} redirected more than ${GUARDED_FETCH_MAX_REDIRECTS} times`);
61418
+ }
61419
+ let next;
61420
+ try {
61421
+ next = new URL(location, url);
61422
+ } catch {
61423
+ throw new EgressFailedError(`${url.host} redirected to an invalid Location`);
61424
+ }
61425
+ const status = response.status;
61426
+ if (status === 303 && method !== "GET" && method !== "HEAD" || (status === 301 || status === 302) && method === "POST") {
61427
+ method = "GET";
61428
+ body = null;
61429
+ for (const name of BODY_HEADERS) headers.delete(name);
61430
+ }
61431
+ if (next.origin !== url.origin) {
61432
+ for (const name of CROSS_ORIGIN_DROPPED_HEADERS) headers.delete(name);
61433
+ }
61434
+ href = next.href;
61435
+ }
61436
+ };
61437
+ }
61438
+ async function untilAborted(operation, signal) {
61439
+ if (!signal) return await operation;
61440
+ let onAbort;
61441
+ try {
61442
+ return await Promise.race([
61443
+ operation,
61444
+ new Promise((_resolve, reject) => {
61445
+ onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new DOMException("This operation was aborted", "AbortError"));
61446
+ signal.addEventListener("abort", onAbort, { once: true });
61447
+ })
61448
+ ]);
61449
+ } finally {
61450
+ if (onAbort) signal.removeEventListener("abort", onAbort);
61451
+ }
61452
+ }
61453
+ async function requestPinned(target, dispatcher, init) {
61454
+ const response = await undiciFetch(target.url, {
61455
+ method: init.method,
61456
+ headers: [...init.headers],
61457
+ body: init.body,
61458
+ signal: init.signal,
61459
+ redirect: "manual",
61460
+ dispatcher
61461
+ });
61462
+ return response;
61463
+ }
61464
+ function pinnedAgent(addresses) {
61465
+ return new Agent({
61466
+ // Try the next checked address when one does not answer, so a dual-stack
61467
+ // site whose first address is unreachable (an IPv6-only network, a dead A
61468
+ // record) still loads, as it does through global `fetch`. Every address
61469
+ // passed the policy, so falling back reaches nothing it refused.
61470
+ autoSelectFamily: true,
61471
+ connect: { lookup: pinnedLookup(addresses) }
61472
+ });
61473
+ }
61474
+
61434
61475
  // ../api-routes/src/wordpress.ts
61435
61476
  function parseEnvInput(value, fieldName = "env") {
61436
61477
  const env = parseEnv(value);
@@ -61458,6 +61499,8 @@ async function withWordpressErrorHandling(handler) {
61458
61499
  return await handler();
61459
61500
  } catch (error) {
61460
61501
  if (error instanceof WordpressApiError) throw toAppError(error);
61502
+ if (error instanceof EgressRefusedError && !error.redirected) throw validationError(error.message);
61503
+ if (error instanceof EgressRefusedError || error instanceof EgressFailedError) throw providerError(error.message);
61461
61504
  throw error;
61462
61505
  }
61463
61506
  }
@@ -61467,6 +61510,10 @@ async function wordpressRoutes(app, opts) {
61467
61510
  if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
61468
61511
  throw validationError("WordPress connection storage is not configured for this deployment");
61469
61512
  }
61513
+ const egressFetch = createGuardedFetch({ allowLoopback });
61514
+ function guarded(connection) {
61515
+ return { ...connection, fetchImpl: egressFetch };
61516
+ }
61470
61517
  async function assertWordpressUrlAllowed(rawUrl, field) {
61471
61518
  const check2 = await resolveWebhookTarget(rawUrl, { allowLoopback });
61472
61519
  if (!check2.ok) {
@@ -61478,7 +61525,7 @@ async function wordpressRoutes(app, opts) {
61478
61525
  if (!connection) {
61479
61526
  throw validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
61480
61527
  }
61481
- return connection;
61528
+ return guarded(connection);
61482
61529
  }
61483
61530
  app.post("/projects/:name/wordpress/connect", async (request) => {
61484
61531
  return withWordpressErrorHandling(async () => {
@@ -61506,8 +61553,8 @@ async function wordpressRoutes(app, opts) {
61506
61553
  createdAt: existing?.createdAt ?? now,
61507
61554
  updatedAt: now
61508
61555
  };
61509
- await verifyWordpressConnection(nextConnection);
61510
- const connection = store.upsertConnection(nextConnection);
61556
+ await verifyWordpressConnection(guarded(nextConnection));
61557
+ const connection = guarded(store.upsertConnection(nextConnection));
61511
61558
  const live = await getSiteStatus(connection, "live");
61512
61559
  const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
61513
61560
  writeAuditLog(app.db, {
@@ -61545,8 +61592,8 @@ async function wordpressRoutes(app, opts) {
61545
61592
  });
61546
61593
  app.get("/projects/:name/wordpress/status", async (request) => {
61547
61594
  const project = resolveProject(app.db, request.params.name);
61548
- const connection = opts.wordpressConnectionStore?.getConnection(project.name);
61549
- if (!connection) {
61595
+ const stored = opts.wordpressConnectionStore?.getConnection(project.name);
61596
+ if (!stored) {
61550
61597
  return {
61551
61598
  connected: false,
61552
61599
  projectName: project.name,
@@ -61556,6 +61603,7 @@ async function wordpressRoutes(app, opts) {
61556
61603
  adminUrl: null
61557
61604
  };
61558
61605
  }
61606
+ const connection = guarded(stored);
61559
61607
  const live = await getSiteStatus(connection, "live");
61560
61608
  const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
61561
61609
  return {
@@ -61840,8 +61888,8 @@ async function wordpressRoutes(app, opts) {
61840
61888
  createdAt: existing?.createdAt ?? now,
61841
61889
  updatedAt: now
61842
61890
  };
61843
- await verifyWordpressConnection(nextConnection);
61844
- connection = store.upsertConnection(nextConnection);
61891
+ await verifyWordpressConnection(guarded(nextConnection));
61892
+ connection = guarded(store.upsertConnection(nextConnection));
61845
61893
  writeAuditLog(app.db, {
61846
61894
  projectId: project.id,
61847
61895
  actor: "api",
@@ -62793,7 +62841,6 @@ async function backlinksRoutes(app, opts) {
62793
62841
  import crypto48 from "crypto";
62794
62842
  import { isIP } from "net";
62795
62843
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
62796
- import { Agent as UndiciAgent } from "undici";
62797
62844
  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
62845
 
62799
62846
  // ../api-routes/src/cloudflare-worker-version.ts
@@ -78093,7 +78140,7 @@ async function listWordpressTrafficEvents(options) {
78093
78140
  let skippedEntryCount = 0;
78094
78141
  let hasMore = false;
78095
78142
  const events = [];
78096
- const dispatcher = options.dispatcher;
78143
+ const fetchImpl = options.fetchImpl ?? fetch;
78097
78144
  for (let page3 = 0; page3 < maxPages; page3 += 1) {
78098
78145
  const url = new URL(endpoint);
78099
78146
  url.searchParams.set("limit", String(pageSize));
@@ -78107,7 +78154,7 @@ async function listWordpressTrafficEvents(options) {
78107
78154
  url.searchParams.set("until", options.until);
78108
78155
  }
78109
78156
  url.searchParams.set("_cb", randomUUID4());
78110
- const fetchInit = {
78157
+ const response = await fetchImpl(url, {
78111
78158
  method: "GET",
78112
78159
  headers: {
78113
78160
  Authorization: authHeader,
@@ -78115,11 +78162,7 @@ async function listWordpressTrafficEvents(options) {
78115
78162
  "Cache-Control": "no-cache"
78116
78163
  },
78117
78164
  signal: AbortSignal.timeout(timeoutMs)
78118
- };
78119
- if (dispatcher !== void 0) {
78120
- fetchInit.dispatcher = dispatcher;
78121
- }
78122
- const response = await fetch(url, fetchInit);
78165
+ });
78123
78166
  if (!response.ok) {
78124
78167
  const body2 = await readErrorBody2(response);
78125
78168
  throw new WordpressTrafficApiError(
@@ -80570,28 +80613,8 @@ async function trafficRoutes(app, opts) {
80570
80613
  if (!check2.ok) {
80571
80614
  throw validationError(`WordPress baseUrl rejected: ${check2.message}`);
80572
80615
  }
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
80616
  }
80617
+ const wordpressTrafficFetch = createGuardedFetch({ allowLoopback });
80595
80618
  const vercelMaxPages = opts.defaultVercelMaxPages ?? DEFAULT_VERCEL_MAX_PAGES;
80596
80619
  const vercelSyncDeadlineMs = opts.vercelSyncDeadlineMs ?? DEFAULT_VERCEL_SYNC_DEADLINE_MS;
80597
80620
  const syncWindowMinutes = opts.defaultSyncWindowMinutes ?? DEFAULT_SYNC_WINDOW_MINUTES;
@@ -80743,26 +80766,26 @@ async function trafficRoutes(app, opts) {
80743
80766
  throw validationError(parsed.error.issues.map((i) => i.message).join("; "));
80744
80767
  }
80745
80768
  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}`);
80769
+ await assertWordpressTargetAllowed(baseUrl);
80770
+ try {
80771
+ await pullWordpressEvents({
80772
+ baseUrl,
80773
+ username,
80774
+ applicationPassword,
80775
+ pageSize: 1,
80776
+ maxPages: 1,
80777
+ fetchImpl: wordpressTrafficFetch
80778
+ });
80779
+ } catch (e) {
80780
+ if (e instanceof WordpressTrafficApiError) {
80781
+ throw providerError(
80782
+ `WordPress traffic probe failed (HTTP ${e.status}): ${e.message}${e.body ? ` \u2014 ${e.body}` : ""}`
80783
+ );
80764
80784
  }
80765
- });
80785
+ if (e instanceof EgressRefusedError && !e.redirected) throw validationError(`WordPress baseUrl rejected: ${e.message}`);
80786
+ const msg = describeError(e);
80787
+ throw providerError(`WordPress traffic probe failed: ${msg}`);
80788
+ }
80766
80789
  const now = (/* @__PURE__ */ new Date()).toISOString();
80767
80790
  const existing = credentialStore.getConnection(project.name);
80768
80791
  credentialStore.upsertConnection({
@@ -81835,10 +81858,9 @@ async function trafficRoutes(app, opts) {
81835
81858
  const windowEndIso = windowEnd.toISOString();
81836
81859
  const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
81837
81860
  const wpMaxPages = opts.defaultWordpressMaxPages ?? DEFAULT_WP_MAX_PAGES;
81838
- let pinnedDispatcher;
81839
81861
  try {
81840
81862
  renewWordpressLease();
81841
- pinnedDispatcher = await assertWordpressTargetAllowed(credential.baseUrl);
81863
+ await assertWordpressTargetAllowed(credential.baseUrl);
81842
81864
  } catch (e) {
81843
81865
  const msg = describeError(e);
81844
81866
  markFailed(msg, "PROVIDER_PULL");
@@ -81858,7 +81880,7 @@ async function trafficRoutes(app, opts) {
81858
81880
  maxPages: 1,
81859
81881
  since: windowStartIso,
81860
81882
  until: windowEndIso,
81861
- dispatcher: pinnedDispatcher
81883
+ fetchImpl: wordpressTrafficFetch
81862
81884
  });
81863
81885
  collected.push(...pageResult.events);
81864
81886
  const previousCursor = cursor;
@@ -81876,10 +81898,8 @@ async function trafficRoutes(app, opts) {
81876
81898
  } catch (e) {
81877
81899
  const msg = describeError(e);
81878
81900
  markFailed(msg, "PROVIDER_PULL");
81901
+ if (e instanceof EgressRefusedError && !e.redirected) throw validationError(`WordPress baseUrl rejected: ${msg}`);
81879
81902
  throw providerError(`WordPress pull failed: ${msg}`);
81880
- } finally {
81881
- await pinnedDispatcher.close().catch(() => {
81882
- });
81883
81903
  }
81884
81904
  } else {
81885
81905
  auditAction = "traffic.vercel.synced";
@@ -84383,7 +84403,7 @@ var DATA_FRESHNESS_CHECKS = [gaRecentDataCheck, gscRecentDataCheck];
84383
84403
  import dns3 from "dns/promises";
84384
84404
  import http3 from "http";
84385
84405
  import https3 from "https";
84386
- import net3 from "net";
84406
+ import net4 from "net";
84387
84407
  var SITE_REACHABILITY_USER_AGENT = "Canonry site-liveness (+https://canonry.ai)";
84388
84408
  var DEFAULT_SITE_MAX_REDIRECTS = 10;
84389
84409
  var DEFAULT_TIMEOUT_MS5 = 1e4;
@@ -84408,7 +84428,7 @@ function errorCode(err) {
84408
84428
  return typeof code === "string" ? code : "";
84409
84429
  }
84410
84430
  async function lookupSiteAddresses(hostname, timeoutMs = DNS_TIMEOUT_MS) {
84411
- const literalFamily = net3.isIPv6(hostname) ? 6 : net3.isIPv4(hostname) ? 4 : null;
84431
+ const literalFamily = net4.isIPv6(hostname) ? 6 : net4.isIPv4(hostname) ? 4 : null;
84412
84432
  if (literalFamily) return { kind: "addresses", addresses: [{ address: hostname, family: literalFamily }] };
84413
84433
  let timer;
84414
84434
  const deadline = new Promise((resolve) => {
@@ -86374,7 +86394,10 @@ var WORDPRESS_PUBLISH_CHECKS = [
86374
86394
  };
86375
86395
  }
86376
86396
  try {
86377
- const status = await verifyWordpressConnection(connection);
86397
+ const status = await verifyWordpressConnection({
86398
+ ...connection,
86399
+ fetchImpl: createGuardedFetch({ allowLoopback: ctx.allowLoopbackWebhooks === true })
86400
+ });
86378
86401
  return {
86379
86402
  status: CheckStatuses.ok,
86380
86403
  code: "wordpress.publish.connected",
@@ -87023,6 +87046,7 @@ async function doctorRoutes(app, opts) {
87023
87046
  googleConnectionStore: opts.googleConnectionStore,
87024
87047
  bingConnectionStore: opts.bingConnectionStore,
87025
87048
  wordpressConnectionStore: opts.wordpressConnectionStore,
87049
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
87026
87050
  ga4CredentialStore: opts.ga4CredentialStore,
87027
87051
  adsCredentialStore: opts.adsCredentialStore,
87028
87052
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
@@ -87056,6 +87080,7 @@ async function doctorRoutes(app, opts) {
87056
87080
  googleConnectionStore: opts.googleConnectionStore,
87057
87081
  bingConnectionStore: opts.bingConnectionStore,
87058
87082
  wordpressConnectionStore: opts.wordpressConnectionStore,
87083
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
87059
87084
  ga4CredentialStore: opts.ga4CredentialStore,
87060
87085
  adsCredentialStore: opts.adsCredentialStore,
87061
87086
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
@@ -90964,6 +90989,7 @@ async function apiRoutes(app, opts) {
90964
90989
  googleConnectionStore: opts.googleConnectionStore,
90965
90990
  bingConnectionStore: opts.bingConnectionStore,
90966
90991
  wordpressConnectionStore: opts.wordpressConnectionStore,
90992
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
90967
90993
  ga4CredentialStore: opts.ga4CredentialStore,
90968
90994
  adsCredentialStore: opts.adsCredentialStore,
90969
90995
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
@@ -91028,6 +91054,7 @@ function buildTrafficSourceValidators(opts) {
91028
91054
  if (opts.wordpressTrafficCredentialStore) {
91029
91055
  const store = opts.wordpressTrafficCredentialStore;
91030
91056
  const pullEvents = opts.pullWordpressTrafficEvents ?? listWordpressTrafficEvents;
91057
+ const wordpressTrafficFetch = createGuardedFetch({ allowLoopback: opts.allowLoopbackWebhooks === true });
91031
91058
  validators[TrafficSourceTypes.wordpress] = {
91032
91059
  validateCredentials: async (source) => {
91033
91060
  const record = store.getConnection(source.projectName);
@@ -91045,7 +91072,10 @@ function buildTrafficSourceValidators(opts) {
91045
91072
  username: record.username,
91046
91073
  applicationPassword: record.applicationPassword,
91047
91074
  pageSize: 1,
91048
- maxPages: 1
91075
+ maxPages: 1,
91076
+ // The probe carries Basic-auth credentials: check the stored URL
91077
+ // and every redirect hop as the sync route does.
91078
+ fetchImpl: wordpressTrafficFetch
91049
91079
  });
91050
91080
  return {
91051
91081
  status: CheckStatuses.ok,
@@ -92319,6 +92349,7 @@ export {
92319
92349
  determineCitationState,
92320
92350
  computeCitedCompetitorDomains,
92321
92351
  resolveTrustProxy,
92352
+ hasProxyHeaders,
92322
92353
  USER_SESSION_COOKIE_NAME,
92323
92354
  cookieIsSecure,
92324
92355
  anyUsersExist,
@@ -92331,6 +92362,8 @@ export {
92331
92362
  requireInstanceAdministrator,
92332
92363
  hashApiKey,
92333
92364
  notProbeRun,
92365
+ writeAuditLog,
92366
+ auditFromRequest,
92334
92367
  registerAgentConversationRoutes,
92335
92368
  measurementSlotKey,
92336
92369
  measurementRunSlotState,
@@ -92392,6 +92425,9 @@ export {
92392
92425
  classifyReviewsError,
92393
92426
  getUrlInfo,
92394
92427
  getCrawlIssues,
92428
+ EgressRefusedError,
92429
+ EgressFailedError,
92430
+ createGuardedFetch,
92395
92431
  PLUGIN_DIR,
92396
92432
  DUCKDB_SPEC,
92397
92433
  CC_CACHE_DIR,