@canonry/canonry 4.169.0 → 4.171.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/assets/assets/{AuditHistoryPanel-B3APGWB4.js → AuditHistoryPanel-BpIn6E7p.js} +1 -1
  2. package/assets/assets/{BacklinksPage-C-7KAx5g.js → BacklinksPage-BvOWgx1l.js} +1 -1
  3. package/assets/assets/{HistoryPage-Cf-dJuPA.js → HistoryPage-BXaQyur8.js} +1 -1
  4. package/assets/assets/{MeasurementPropertyPage-C1W-BG81.js → MeasurementPropertyPage-Buj708lc.js} +1 -1
  5. package/assets/assets/{ProjectPage-CnZC8AA7.js → ProjectPage-BwC29w2_.js} +1 -1
  6. package/assets/assets/{RunRow-DJQ4Up9f.js → RunRow-C9hp5yEp.js} +1 -1
  7. package/assets/assets/{RunsPage-BbtbYLgP.js → RunsPage-DmkRH1iu.js} +1 -1
  8. package/assets/assets/{SettingsPage-DG-6IuO1.js → SettingsPage-BIyuj-gL.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-rCrAk98W.js → SiteHealthSection-ZlYR3JtG.js} +3 -3
  10. package/assets/assets/{TrafficPage-CpS4K8eT.js → TrafficPage-PxIetTWH.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-D_zPmdUz.js → TrafficSourceDetailPage-BhF_leby.js} +1 -1
  12. package/assets/assets/{extract-error-message-40mUXcbq.js → extract-error-message-Dd2su-BK.js} +1 -1
  13. package/assets/assets/{index-Nk2MzD2d.js → index-DyjfMyum.js} +21 -21
  14. package/assets/assets/{react-sigma_core.esm.min-BOZ1RPZp.js → react-sigma_core.esm.min-CsNemdas.js} +1 -1
  15. package/assets/index.html +1 -1
  16. package/dist/{chunk-T2UJAYTX.js → chunk-5KGN224W.js} +3407 -137
  17. package/dist/{chunk-K7XXDKYY.js → chunk-HJ7W2LXI.js} +165 -1
  18. package/dist/{chunk-HVBAGSQC.js → chunk-IIFDS3K2.js} +807 -13
  19. package/dist/{chunk-U2X42SKN.js → chunk-O226BLC7.js} +3893 -1157
  20. package/dist/{chunk-ANA3GJSC.js → chunk-RR4KSDOD.js} +2 -2
  21. package/dist/cli.js +690 -111
  22. package/dist/index.d.ts +132 -2
  23. package/dist/index.js +10 -4
  24. package/dist/{intelligence-service-6ZYTEFXX.js → intelligence-service-KT6K4ODY.js} +2 -2
  25. package/dist/mcp.js +37 -4
  26. package/package.json +8 -8
@@ -9,7 +9,7 @@ import {
9
9
  loadConfig,
10
10
  loadConfigRaw,
11
11
  saveConfigPatch
12
- } from "./chunk-HVBAGSQC.js";
12
+ } from "./chunk-IIFDS3K2.js";
13
13
  import {
14
14
  CC_CACHE_DIR,
15
15
  DUCKDB_SPEC,
@@ -30,6 +30,7 @@ import {
30
30
  anyUsersExist,
31
31
  apiKeys,
32
32
  apiRoutes,
33
+ assessConversionTrackingIntegrity,
33
34
  auditLog,
34
35
  backlinkDomains,
35
36
  backlinkSummaries,
@@ -55,6 +56,7 @@ import {
55
56
  downloadFile,
56
57
  dropLegacyCredentialColumns,
57
58
  ensureCurrentQueryBasketRevision,
59
+ exchangeCode,
58
60
  executeDiscovery,
59
61
  extractLegacyCredentials,
60
62
  extractRecommendedCompetitors,
@@ -72,10 +74,13 @@ import {
72
74
  gbpPlaceActions,
73
75
  gbpPlaceDetails,
74
76
  getAttributes,
77
+ getAuthUrl,
75
78
  getCrawlIssues,
76
79
  getLodging,
77
80
  getPlaceDetails,
78
81
  getUrlInfo,
82
+ googleAdsConnections,
83
+ googleAdsRawSnapshots,
79
84
  groupRunsByCreatedAt,
80
85
  gscCoverageSnapshots,
81
86
  gscDailyTotals,
@@ -83,6 +88,8 @@ import {
83
88
  gscQueryDailyTotals,
84
89
  gscSearchData,
85
90
  gscUrlInspections,
91
+ gtmConnections,
92
+ gtmRawSnapshots,
86
93
  hashAttributes,
87
94
  hashLodging,
88
95
  hashPlaceDetails,
@@ -138,7 +145,7 @@ import {
138
145
  siteCrawlSnapshots,
139
146
  toAlertView,
140
147
  usageCounters
141
- } from "./chunk-U2X42SKN.js";
148
+ } from "./chunk-O226BLC7.js";
142
149
  import {
143
150
  AGENT_MEMORY_VALUE_MAX_BYTES,
144
151
  AGENT_PROVIDER_IDS,
@@ -151,6 +158,16 @@ import {
151
158
  CITED_URL_CAPTURE_VERSION,
152
159
  CcReleaseSyncStatuses,
153
160
  DiscoveryCompetitorTypes,
161
+ GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS,
162
+ GOOGLE_ADS_CAMPAIGN_METRICS_MAX_DAYS,
163
+ GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS,
164
+ GoogleAdsCampaignStatuses,
165
+ GoogleAdsConversionActionStatuses,
166
+ GoogleAdsCustomerStatuses,
167
+ GoogleAdsGoalConfigurationLevels,
168
+ GoogleAdsSnapshotKinds,
169
+ GoogleMarketingProviders,
170
+ GtmSnapshotKinds,
154
171
  LLM_CAPABILITIES,
155
172
  LlmCapabilities,
156
173
  MemorySources,
@@ -188,6 +205,9 @@ import {
188
205
  authRequired,
189
206
  bucketOnboardingCount,
190
207
  buildRunErrorFromMessages,
208
+ canonicalizeGtmAccountId,
209
+ canonicalizeGtmContainerId,
210
+ canonicalizeGtmResourceSelection,
191
211
  classifyTemplateLinkEdge,
192
212
  cloudflareQueueNameSchema,
193
213
  computeBacklinkSummaryMetrics,
@@ -196,6 +216,7 @@ import {
196
216
  createTemplateLinkDetectionTally,
197
217
  createTemplateLinkPairIndex,
198
218
  deriveCitedUrlCandidates,
219
+ deriveGoogleAdsEffectiveGoalGraph,
199
220
  deriveIndexCoverage,
200
221
  deriveSiteHealthState,
201
222
  describeError,
@@ -209,6 +230,19 @@ import {
209
230
  forbidden,
210
231
  formatIsoDateInTimeZone,
211
232
  frameAncestorsHeaderValue,
233
+ googleAdsAccessibleCustomerDtoSchema,
234
+ googleAdsAccessibleCustomersResponseSchema,
235
+ googleAdsCampaignMetricsQuerySchema,
236
+ googleAdsRawSnapshotDtoSchema,
237
+ gtmAccountsResponseSchema,
238
+ gtmContainerListResponseSchema,
239
+ gtmGoogleAdsConversionIdMappingDtoSchema,
240
+ gtmGoogleAdsConversionLabelMappingDtoSchema,
241
+ gtmGoogleAdsConversionValueMappingDtoSchema,
242
+ gtmGoogleAdsCurrencyMappingDtoSchema,
243
+ gtmGoogleAdsTransactionIdMappingDtoSchema,
244
+ gtmRawSnapshotDtoSchema,
245
+ gtmWorkspaceListResponseSchema,
212
246
  hostMatchesAnyDomain,
213
247
  hostMatchesDomain,
214
248
  hostOf,
@@ -233,6 +267,7 @@ import {
233
267
  providerSupportsLocationContext,
234
268
  registrableDomain,
235
269
  resolveSnapshotRequestQueries,
270
+ retryAfterDelayMs,
236
271
  serializeForInlineScript,
237
272
  serializeRunError,
238
273
  shiftIsoCalendarDate,
@@ -247,7 +282,7 @@ import {
247
282
  validationError,
248
283
  winnabilityClassLabel,
249
284
  withRetry
250
- } from "./chunk-K7XXDKYY.js";
285
+ } from "./chunk-HJ7W2LXI.js";
251
286
 
252
287
  // src/telemetry.ts
253
288
  import crypto from "crypto";
@@ -576,13 +611,2720 @@ function checkLatestVersionForServer(opts) {
576
611
  return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
577
612
  }
578
613
 
614
+ // src/google-marketing-runtime.ts
615
+ import crypto3 from "crypto";
616
+
617
+ // ../integration-google-ads/src/constants.ts
618
+ var GOOGLE_ADS_API_VERSION = "v25";
619
+ var GOOGLE_ADS_API_HOST = "https://googleads.googleapis.com";
620
+ var GOOGLE_ADS_API_BASE = `${GOOGLE_ADS_API_HOST}/${GOOGLE_ADS_API_VERSION}`;
621
+ var GOOGLE_ADS_OAUTH_SCOPE = "https://www.googleapis.com/auth/adwords";
622
+ var GOOGLE_ADS_REQUEST_TIMEOUT_MS = 3e4;
623
+ var GOOGLE_ADS_MAX_RETRIES = 3;
624
+ var GOOGLE_ADS_RETRY_BASE_DELAY_MS = 1e3;
625
+ var GOOGLE_ADS_RETRY_MAX_DELAY_MS = 3e4;
626
+ var GOOGLE_ADS_MAX_RESULT_ROWS = 1e4;
627
+ var GOOGLE_ADS_MAX_DAILY_METRICS_DAYS = 90;
628
+ var GOOGLE_ADS_MAX_CAMPAIGN_FILTER_IDS = 200;
629
+ var GOOGLE_ADS_DEFAULT_ACCESSIBLE_CUSTOMER_DETAILS = 20;
630
+ var GOOGLE_ADS_MAX_ACCESSIBLE_CUSTOMER_DETAILS = 100;
631
+ var GOOGLE_ADS_CUSTOMER_DETAIL_CONCURRENCY = 5;
632
+ var GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENTS = 100;
633
+ var GOOGLE_ADS_MAX_CUSTOMER_CLIENTS = 1e3;
634
+ var GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENT_DEPTH = 1;
635
+ var GOOGLE_ADS_MAX_CUSTOMER_CLIENT_DEPTH = 10;
636
+
637
+ // ../integration-google-ads/src/goal-semantics.ts
638
+ function addInclusion(included, action, inclusion) {
639
+ const existing = included.get(action.resourceName);
640
+ if (existing) {
641
+ existing.includedBy.push(inclusion);
642
+ return;
643
+ }
644
+ included.set(action.resourceName, { conversionAction: action, includedBy: [inclusion] });
645
+ }
646
+ function resolveEffectiveCampaignGoalInputs(inputs) {
647
+ const actions = inputs.conversionActions.map((row) => row.conversionAction);
648
+ const actionsByResource = new Map(actions.map((action) => [action.resourceName, action]));
649
+ const customGoalsByResource = new Map(
650
+ inputs.customGoals.map((row) => [row.customConversionGoal.resourceName, row.customConversionGoal])
651
+ );
652
+ return inputs.campaignConfigs.map((row) => {
653
+ const config = row.conversionGoalCampaignConfig;
654
+ let categoryGoalSource = "UNKNOWN";
655
+ let categoryOriginGoals = [];
656
+ if (config.goalConfigLevel === "CUSTOMER") {
657
+ categoryGoalSource = "CUSTOMER";
658
+ categoryOriginGoals = inputs.customerGoals.map(({ customerConversionGoal: goal }) => ({
659
+ resourceName: goal.resourceName,
660
+ source: "CUSTOMER",
661
+ category: goal.category,
662
+ origin: goal.origin,
663
+ biddable: goal.biddable === true
664
+ }));
665
+ } else if (config.goalConfigLevel === "CAMPAIGN") {
666
+ categoryGoalSource = "CAMPAIGN";
667
+ categoryOriginGoals = inputs.campaignGoals.filter(({ campaignConversionGoal: goal }) => goal.campaign === config.campaign).map(({ campaignConversionGoal: goal }) => ({
668
+ resourceName: goal.resourceName,
669
+ source: "CAMPAIGN",
670
+ category: goal.category,
671
+ origin: goal.origin,
672
+ biddable: goal.biddable === true
673
+ }));
674
+ }
675
+ const included = /* @__PURE__ */ new Map();
676
+ for (const goal of categoryOriginGoals) {
677
+ if (!goal.biddable) continue;
678
+ for (const action of actions) {
679
+ if (action.primaryForGoal !== false && action.category === goal.category && action.origin === goal.origin) {
680
+ addInclusion(included, action, {
681
+ source: goal.source === "CUSTOMER" ? "CUSTOMER_GOAL" : "CAMPAIGN_GOAL",
682
+ goalResourceName: goal.resourceName,
683
+ category: goal.category,
684
+ origin: goal.origin
685
+ });
686
+ }
687
+ }
688
+ }
689
+ let customGoal = null;
690
+ let missingCustomGoalResourceName;
691
+ const missingConversionActionResourceNames = [];
692
+ if (config.goalConfigLevel === "CAMPAIGN" && config.customConversionGoal) {
693
+ customGoal = customGoalsByResource.get(config.customConversionGoal) ?? null;
694
+ if (!customGoal) {
695
+ missingCustomGoalResourceName = config.customConversionGoal;
696
+ } else {
697
+ for (const resourceName of customGoal.conversionActions ?? []) {
698
+ const action = actionsByResource.get(resourceName);
699
+ if (!action) {
700
+ missingConversionActionResourceNames.push(resourceName);
701
+ continue;
702
+ }
703
+ addInclusion(included, action, {
704
+ source: "CUSTOM_GOAL",
705
+ goalResourceName: customGoal.resourceName
706
+ });
707
+ }
708
+ }
709
+ }
710
+ return {
711
+ campaign: row.campaign,
712
+ config,
713
+ categoryGoalSource,
714
+ categoryOriginGoals,
715
+ customGoal,
716
+ conversionActions: [...included.values()],
717
+ ...missingCustomGoalResourceName ? { missingCustomGoalResourceName } : {},
718
+ missingConversionActionResourceNames
719
+ };
720
+ });
721
+ }
722
+
723
+ // ../integration-google-ads/src/types.ts
724
+ var GoogleAdsApiError = class extends Error {
725
+ constructor(message, status, providerStatus, requestId, retryAfter) {
726
+ super(message);
727
+ this.status = status;
728
+ this.providerStatus = providerStatus;
729
+ this.requestId = requestId;
730
+ this.retryAfter = retryAfter;
731
+ }
732
+ status;
733
+ providerStatus;
734
+ requestId;
735
+ retryAfter;
736
+ name = "GoogleAdsApiError";
737
+ };
738
+
739
+ // ../integration-google-ads/src/queries.ts
740
+ var DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
741
+ function selectQuery(fields, resource, clauses) {
742
+ return [
743
+ "SELECT",
744
+ ...fields.map((field, index) => ` ${field}${index === fields.length - 1 ? "" : ","}`),
745
+ `FROM ${resource}`,
746
+ ...clauses
747
+ ].join("\n");
748
+ }
749
+ function validatedLimit(limit) {
750
+ const value = limit ?? GOOGLE_ADS_MAX_RESULT_ROWS;
751
+ if (!Number.isInteger(value) || value < 1 || value > GOOGLE_ADS_MAX_RESULT_ROWS) {
752
+ throw new GoogleAdsApiError(
753
+ `Limit must be an integer between 1 and ${GOOGLE_ADS_MAX_RESULT_ROWS}`,
754
+ 400
755
+ );
756
+ }
757
+ return value;
758
+ }
759
+ function normalizeGoogleAdsCustomerId(value, label = "Customer ID") {
760
+ if (typeof value !== "string" || value.trim() === "") {
761
+ throw new GoogleAdsApiError(`${label} is required`, 400);
762
+ }
763
+ const normalized = value.trim().replaceAll("-", "");
764
+ if (!/^\d{10}$/.test(normalized)) {
765
+ throw new GoogleAdsApiError(`${label} must contain exactly 10 digits`, 400);
766
+ }
767
+ return normalized;
768
+ }
769
+ function normalizeGoogleAdsCustomerClientsOptions(options = {}) {
770
+ const maxDepth = options.maxDepth ?? GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENT_DEPTH;
771
+ if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > GOOGLE_ADS_MAX_CUSTOMER_CLIENT_DEPTH) {
772
+ throw new GoogleAdsApiError(
773
+ `Customer-client depth must be an integer between 0 and ${GOOGLE_ADS_MAX_CUSTOMER_CLIENT_DEPTH}`,
774
+ 400
775
+ );
776
+ }
777
+ const maxCustomers = options.maxCustomers ?? GOOGLE_ADS_DEFAULT_CUSTOMER_CLIENTS;
778
+ if (!Number.isInteger(maxCustomers) || maxCustomers < 1 || maxCustomers > GOOGLE_ADS_MAX_CUSTOMER_CLIENTS) {
779
+ throw new GoogleAdsApiError(
780
+ `Customer-client limit must be an integer between 1 and ${GOOGLE_ADS_MAX_CUSTOMER_CLIENTS}`,
781
+ 400
782
+ );
783
+ }
784
+ return { maxDepth, maxCustomers };
785
+ }
786
+ function buildCustomerDetailsQuery() {
787
+ return selectQuery([
788
+ "customer.resource_name",
789
+ "customer.id",
790
+ "customer.descriptive_name",
791
+ "customer.currency_code",
792
+ "customer.time_zone",
793
+ "customer.manager",
794
+ "customer.test_account",
795
+ "customer.status",
796
+ "customer.auto_tagging_enabled",
797
+ "customer.conversion_tracking_setting.conversion_tracking_id",
798
+ "customer.conversion_tracking_setting.conversion_tracking_status",
799
+ "customer.conversion_tracking_setting.cross_account_conversion_tracking_id",
800
+ "customer.conversion_tracking_setting.google_ads_conversion_customer"
801
+ ], "customer", ["LIMIT 1"]);
802
+ }
803
+ function buildCustomerClientsQuery(options = {}) {
804
+ const { maxDepth, maxCustomers } = normalizeGoogleAdsCustomerClientsOptions(options);
805
+ return selectQuery([
806
+ "customer_client.resource_name",
807
+ "customer_client.client_customer",
808
+ "customer_client.id",
809
+ "customer_client.descriptive_name",
810
+ "customer_client.currency_code",
811
+ "customer_client.time_zone",
812
+ "customer_client.manager",
813
+ "customer_client.test_account",
814
+ "customer_client.hidden",
815
+ "customer_client.status",
816
+ "customer_client.level"
817
+ ], "customer_client", [
818
+ `WHERE customer_client.level <= ${maxDepth}`,
819
+ "ORDER BY customer_client.level ASC, customer_client.id ASC",
820
+ `LIMIT ${maxCustomers + 1}`
821
+ ]);
822
+ }
823
+ function parseIsoDate(value, label) {
824
+ const match = DATE_PATTERN.exec(value);
825
+ if (!match) {
826
+ throw new GoogleAdsApiError(`${label} must use YYYY-MM-DD format`, 400);
827
+ }
828
+ const year = Number(match[1]);
829
+ const month = Number(match[2]);
830
+ const day = Number(match[3]);
831
+ const timestamp = Date.UTC(year, month - 1, day);
832
+ const parsed = new Date(timestamp);
833
+ if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
834
+ throw new GoogleAdsApiError(`${label} must be a real calendar date`, 400);
835
+ }
836
+ return timestamp;
837
+ }
838
+ function dailyMetricClauses(options) {
839
+ const start = parseIsoDate(options.startDate, "Start date");
840
+ const end = parseIsoDate(options.endDate, "End date");
841
+ if (start > end) {
842
+ throw new GoogleAdsApiError("Start date must be on or before end date", 400);
843
+ }
844
+ const inclusiveDays = Math.floor((end - start) / 864e5) + 1;
845
+ if (inclusiveDays > GOOGLE_ADS_MAX_DAILY_METRICS_DAYS) {
846
+ throw new GoogleAdsApiError(
847
+ `Daily metrics range cannot exceed ${GOOGLE_ADS_MAX_DAILY_METRICS_DAYS} days`,
848
+ 400
849
+ );
850
+ }
851
+ const filters = [
852
+ `segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'`,
853
+ "campaign.status != 'REMOVED'"
854
+ ];
855
+ if (options.campaignIds !== void 0) {
856
+ if (!Array.isArray(options.campaignIds) || options.campaignIds.length === 0) {
857
+ throw new GoogleAdsApiError("Campaign IDs must be a non-empty array when provided", 400);
858
+ }
859
+ if (options.campaignIds.length > GOOGLE_ADS_MAX_CAMPAIGN_FILTER_IDS) {
860
+ throw new GoogleAdsApiError(
861
+ `Campaign ID filter cannot exceed ${GOOGLE_ADS_MAX_CAMPAIGN_FILTER_IDS} IDs`,
862
+ 400
863
+ );
864
+ }
865
+ const normalized = options.campaignIds.map((id) => {
866
+ const value = id.trim();
867
+ if (!/^\d+$/.test(value)) {
868
+ throw new GoogleAdsApiError("Campaign IDs must contain digits only", 400);
869
+ }
870
+ return value;
871
+ });
872
+ const unique = [...new Set(normalized)];
873
+ filters.push(`campaign.id IN (${unique.join(", ")})`);
874
+ }
875
+ return [
876
+ `WHERE ${filters.join("\n AND ")}`,
877
+ "ORDER BY segments.date ASC, campaign.id ASC",
878
+ `LIMIT ${validatedLimit(options.limit)}`
879
+ ];
880
+ }
881
+ function buildCampaignsQuery(options = {}) {
882
+ const clauses = [];
883
+ if (!options.includeRemoved) clauses.push("WHERE campaign.status != 'REMOVED'");
884
+ clauses.push("ORDER BY campaign.id ASC", `LIMIT ${validatedLimit(options.limit)}`);
885
+ return selectQuery([
886
+ "campaign.resource_name",
887
+ "campaign.id",
888
+ "campaign.name",
889
+ "campaign.status",
890
+ "campaign.primary_status",
891
+ "campaign.serving_status",
892
+ "campaign.advertising_channel_type",
893
+ "campaign.advertising_channel_sub_type",
894
+ "campaign.campaign_budget",
895
+ "campaign.bidding_strategy",
896
+ "campaign.bidding_strategy_type",
897
+ "campaign.maximize_conversions.target_cpa_micros",
898
+ "campaign.maximize_conversion_value.target_roas",
899
+ "campaign.target_cpa.target_cpa_micros",
900
+ "campaign.target_roas.target_roas",
901
+ "campaign.target_impression_share.cpc_bid_ceiling_micros",
902
+ "campaign.target_impression_share.location",
903
+ "campaign.target_impression_share.location_fraction_micros",
904
+ "campaign.target_spend.cpc_bid_ceiling_micros",
905
+ "campaign.target_spend.target_spend_micros",
906
+ "campaign_budget.resource_name",
907
+ "campaign_budget.id",
908
+ "campaign_budget.name",
909
+ "campaign_budget.amount_micros",
910
+ "campaign_budget.total_amount_micros",
911
+ "campaign_budget.status",
912
+ "campaign_budget.explicitly_shared",
913
+ "bidding_strategy.resource_name",
914
+ "bidding_strategy.id",
915
+ "bidding_strategy.name",
916
+ "bidding_strategy.status",
917
+ "bidding_strategy.type",
918
+ "bidding_strategy.maximize_conversions.target_cpa_micros",
919
+ "bidding_strategy.maximize_conversion_value.target_roas",
920
+ "bidding_strategy.target_cpa.target_cpa_micros",
921
+ "bidding_strategy.target_roas.target_roas"
922
+ ], "campaign", clauses);
923
+ }
924
+ function buildConversionActionsQuery(options = {}) {
925
+ const clauses = [];
926
+ if (!options.includeRemoved) clauses.push("WHERE conversion_action.status != 'REMOVED'");
927
+ clauses.push("ORDER BY conversion_action.id ASC", `LIMIT ${validatedLimit(options.limit)}`);
928
+ return selectQuery([
929
+ "conversion_action.resource_name",
930
+ "conversion_action.id",
931
+ "conversion_action.name",
932
+ "conversion_action.status",
933
+ "conversion_action.type",
934
+ "conversion_action.category",
935
+ "conversion_action.origin",
936
+ "conversion_action.owner_customer",
937
+ "conversion_action.primary_for_goal",
938
+ "conversion_action.include_in_conversions_metric",
939
+ "conversion_action.counting_type",
940
+ "conversion_action.click_through_lookback_window_days",
941
+ "conversion_action.view_through_lookback_window_days",
942
+ "conversion_action.attribution_model_settings.attribution_model",
943
+ "conversion_action.value_settings.always_use_default_value",
944
+ "conversion_action.value_settings.default_currency_code",
945
+ "conversion_action.value_settings.default_value",
946
+ "conversion_action.google_analytics_4_settings.event_name",
947
+ "conversion_action.google_analytics_4_settings.property_id",
948
+ "conversion_action.google_analytics_4_settings.property_name",
949
+ "conversion_action.tag_snippets"
950
+ ], "conversion_action", clauses);
951
+ }
952
+ function buildCustomerConversionGoalsQuery(limit) {
953
+ return selectQuery([
954
+ "customer_conversion_goal.resource_name",
955
+ "customer_conversion_goal.category",
956
+ "customer_conversion_goal.origin",
957
+ "customer_conversion_goal.biddable"
958
+ ], "customer_conversion_goal", [
959
+ "ORDER BY customer_conversion_goal.category ASC, customer_conversion_goal.origin ASC",
960
+ `LIMIT ${validatedLimit(limit)}`
961
+ ]);
962
+ }
963
+ function buildCampaignConversionGoalsQuery(limit) {
964
+ return selectQuery([
965
+ "campaign_conversion_goal.resource_name",
966
+ "campaign_conversion_goal.campaign",
967
+ "campaign_conversion_goal.category",
968
+ "campaign_conversion_goal.origin",
969
+ "campaign_conversion_goal.biddable",
970
+ "campaign.resource_name",
971
+ "campaign.id",
972
+ "campaign.name",
973
+ "campaign.status"
974
+ ], "campaign_conversion_goal", [
975
+ "WHERE campaign.status != 'REMOVED'",
976
+ "ORDER BY campaign.id ASC, campaign_conversion_goal.category ASC, campaign_conversion_goal.origin ASC",
977
+ `LIMIT ${validatedLimit(limit)}`
978
+ ]);
979
+ }
980
+ function buildCustomConversionGoalsQuery(limit) {
981
+ return selectQuery([
982
+ "custom_conversion_goal.resource_name",
983
+ "custom_conversion_goal.id",
984
+ "custom_conversion_goal.name",
985
+ "custom_conversion_goal.status",
986
+ "custom_conversion_goal.conversion_actions"
987
+ ], "custom_conversion_goal", [
988
+ "ORDER BY custom_conversion_goal.id ASC",
989
+ `LIMIT ${validatedLimit(limit)}`
990
+ ]);
991
+ }
992
+ function buildConversionGoalCampaignConfigsQuery(limit) {
993
+ return selectQuery([
994
+ "conversion_goal_campaign_config.resource_name",
995
+ "conversion_goal_campaign_config.campaign",
996
+ "conversion_goal_campaign_config.custom_conversion_goal",
997
+ "conversion_goal_campaign_config.goal_config_level",
998
+ "campaign.resource_name",
999
+ "campaign.id",
1000
+ "campaign.name",
1001
+ "campaign.status"
1002
+ ], "conversion_goal_campaign_config", [
1003
+ "WHERE campaign.status != 'REMOVED'",
1004
+ "ORDER BY campaign.id ASC",
1005
+ `LIMIT ${validatedLimit(limit)}`
1006
+ ]);
1007
+ }
1008
+ function buildDailyCampaignMetricsQuery(options) {
1009
+ return selectQuery([
1010
+ "segments.date",
1011
+ "campaign.resource_name",
1012
+ "campaign.id",
1013
+ "campaign.name",
1014
+ "campaign.status",
1015
+ "metrics.impressions",
1016
+ "metrics.clicks",
1017
+ "metrics.cost_micros",
1018
+ "metrics.average_cpc",
1019
+ "metrics.ctr",
1020
+ "metrics.conversions",
1021
+ "metrics.conversions_value",
1022
+ "metrics.all_conversions",
1023
+ "metrics.all_conversions_value",
1024
+ "metrics.search_impression_share"
1025
+ ], "campaign", dailyMetricClauses(options));
1026
+ }
1027
+ function buildDailyConversionMetricsQuery(options) {
1028
+ return selectQuery([
1029
+ "segments.date",
1030
+ "segments.conversion_action",
1031
+ "segments.conversion_action_name",
1032
+ "segments.conversion_action_category",
1033
+ "campaign.resource_name",
1034
+ "campaign.id",
1035
+ "campaign.name",
1036
+ "campaign.status",
1037
+ "metrics.conversions",
1038
+ "metrics.conversions_value",
1039
+ "metrics.all_conversions",
1040
+ "metrics.all_conversions_value"
1041
+ ], "campaign", dailyMetricClauses(options));
1042
+ }
1043
+
1044
+ // ../integration-google-ads/src/google-ads-client.ts
1045
+ function escapeRegExp(value) {
1046
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1047
+ }
1048
+ function sanitizeText(value, secrets) {
1049
+ let sanitized = value.replace(/(authorization\s*:\s*bearer\s+)[^\s,"'}]+/gi, "$1***").replace(/((?:developer|access)[_-]?token\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1***");
1050
+ for (const secret of secrets) {
1051
+ if (!secret) continue;
1052
+ sanitized = sanitized.replace(new RegExp(escapeRegExp(secret), "g"), "***");
1053
+ const encoded = encodeURIComponent(secret);
1054
+ if (encoded !== secret) {
1055
+ sanitized = sanitized.replace(new RegExp(escapeRegExp(encoded), "g"), "***");
1056
+ }
1057
+ }
1058
+ return sanitized.length <= 1e3 ? sanitized : `${sanitized.slice(0, 1e3)}... [truncated]`;
1059
+ }
1060
+ function safeRequestId(value) {
1061
+ if (typeof value !== "string") return void 0;
1062
+ const trimmed = value.trim();
1063
+ if (trimmed === "" || trimmed.length > 256 || /[\r\n]/.test(trimmed)) return void 0;
1064
+ return trimmed;
1065
+ }
1066
+ function findRequestId(value, depth = 0) {
1067
+ if (depth > 6 || value === null || typeof value !== "object") return void 0;
1068
+ if (Array.isArray(value)) {
1069
+ for (const item of value) {
1070
+ const found = findRequestId(item, depth + 1);
1071
+ if (found) return found;
1072
+ }
1073
+ return void 0;
1074
+ }
1075
+ const record = value;
1076
+ const direct = safeRequestId(record.requestId) ?? safeRequestId(record.request_id);
1077
+ if (direct) return direct;
1078
+ for (const nested of Object.values(record)) {
1079
+ const found = findRequestId(nested, depth + 1);
1080
+ if (found) return found;
1081
+ }
1082
+ return void 0;
1083
+ }
1084
+ function providerErrorParts(payload) {
1085
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) return {};
1086
+ const envelope = payload;
1087
+ const message = typeof envelope.error?.message === "string" ? envelope.error.message : void 0;
1088
+ const providerStatus = typeof envelope.error?.status === "string" ? envelope.error.status : void 0;
1089
+ const requestId = findRequestId(envelope.error?.details);
1090
+ return { message, providerStatus, requestId };
1091
+ }
1092
+ function validateCredential(value, label) {
1093
+ if (typeof value !== "string" || value.trim() === "") {
1094
+ throw new GoogleAdsApiError(`${label} is required`, 400);
1095
+ }
1096
+ return value.trim();
1097
+ }
1098
+ function validatedAccessibleCustomerLimit(value) {
1099
+ const limit = value ?? GOOGLE_ADS_DEFAULT_ACCESSIBLE_CUSTOMER_DETAILS;
1100
+ if (!Number.isInteger(limit) || limit < 1 || limit > GOOGLE_ADS_MAX_ACCESSIBLE_CUSTOMER_DETAILS) {
1101
+ throw new GoogleAdsApiError(
1102
+ `Accessible customer detail limit must be an integer between 1 and ${GOOGLE_ADS_MAX_ACCESSIBLE_CUSTOMER_DETAILS}`,
1103
+ 400
1104
+ );
1105
+ }
1106
+ return limit;
1107
+ }
1108
+ function customerIdFromResourceName(resourceName) {
1109
+ const match = /^customers\/(\d{10})$/.exec(resourceName);
1110
+ if (!match) {
1111
+ throw new GoogleAdsApiError(
1112
+ "Google Ads API returned an invalid accessible customer resource name",
1113
+ 502
1114
+ );
1115
+ }
1116
+ return match[1];
1117
+ }
1118
+ function isCustomerClientRow(row) {
1119
+ return row.customerClient !== void 0;
1120
+ }
1121
+ var GoogleAdsClient = class {
1122
+ #accessToken;
1123
+ #developerToken;
1124
+ #loginCustomerId;
1125
+ #fetch;
1126
+ #requestTimeoutMs;
1127
+ #retry;
1128
+ constructor(credentials, options = {}) {
1129
+ this.#accessToken = validateCredential(credentials.accessToken, "Access token");
1130
+ this.#developerToken = validateCredential(credentials.developerToken, "Developer token");
1131
+ this.#loginCustomerId = credentials.loginCustomerId === void 0 ? void 0 : normalizeGoogleAdsCustomerId(credentials.loginCustomerId, "Login customer ID");
1132
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
1133
+ this.#requestTimeoutMs = options.requestTimeoutMs ?? GOOGLE_ADS_REQUEST_TIMEOUT_MS;
1134
+ if (!Number.isInteger(this.#requestTimeoutMs) || this.#requestTimeoutMs < 1) {
1135
+ throw new GoogleAdsApiError("Request timeout must be a positive integer in milliseconds", 400);
1136
+ }
1137
+ this.#retry = options.retry ?? {};
1138
+ }
1139
+ async listAccessibleCustomers() {
1140
+ const result = await this.#requestJson(
1141
+ "list-accessible-customers",
1142
+ "/customers:listAccessibleCustomers",
1143
+ { method: "GET" },
1144
+ false
1145
+ );
1146
+ const resourceNames = result.data.resourceNames ?? [];
1147
+ if (!Array.isArray(resourceNames) || resourceNames.some((name) => typeof name !== "string")) {
1148
+ throw new GoogleAdsApiError(
1149
+ "Google Ads API returned an invalid accessible-customer response",
1150
+ 502,
1151
+ void 0,
1152
+ result.metadata.requestId
1153
+ );
1154
+ }
1155
+ return { ...result, data: { resourceNames } };
1156
+ }
1157
+ async getCustomerDetails(customerId) {
1158
+ const result = await this.#searchStream(
1159
+ customerId,
1160
+ "customer-details",
1161
+ buildCustomerDetailsQuery()
1162
+ );
1163
+ if (result.data.length !== 1 || !result.data[0]?.customer) {
1164
+ throw new GoogleAdsApiError(
1165
+ "Google Ads API returned an invalid customer detail response",
1166
+ 502,
1167
+ void 0,
1168
+ result.metadata.requestId
1169
+ );
1170
+ }
1171
+ return { data: result.data[0].customer, metadata: result.metadata };
1172
+ }
1173
+ async listAccessibleCustomerDetails(options = {}) {
1174
+ const maxCustomers = validatedAccessibleCustomerLimit(options.maxCustomers);
1175
+ const accessible = await this.listAccessibleCustomers();
1176
+ const resourceNames = accessible.data.resourceNames.slice(0, maxCustomers);
1177
+ const attempts = await mapWithConcurrency(
1178
+ resourceNames,
1179
+ GOOGLE_ADS_CUSTOMER_DETAIL_CONCURRENCY,
1180
+ async (resourceName) => {
1181
+ let customerId;
1182
+ try {
1183
+ customerId = customerIdFromResourceName(resourceName);
1184
+ const result = await this.getCustomerDetails(customerId);
1185
+ return { customer: result.data, metadata: result.metadata };
1186
+ } catch (error) {
1187
+ const googleAdsError = error instanceof GoogleAdsApiError ? error : void 0;
1188
+ const detail = describeError(error);
1189
+ return {
1190
+ failure: {
1191
+ resourceName,
1192
+ ...customerId ? { customerId } : {},
1193
+ ...googleAdsError?.status !== void 0 ? { status: googleAdsError.status } : {},
1194
+ ...googleAdsError?.providerStatus ? { providerStatus: googleAdsError.providerStatus } : {},
1195
+ ...googleAdsError?.requestId ? { requestId: googleAdsError.requestId } : {},
1196
+ message: sanitizeText(detail, this.#secrets())
1197
+ },
1198
+ ...customerId ? {
1199
+ metadata: {
1200
+ apiVersion: GOOGLE_ADS_API_VERSION,
1201
+ operation: "customer-details",
1202
+ ...googleAdsError?.requestId ? { requestId: googleAdsError.requestId } : {}
1203
+ }
1204
+ } : {}
1205
+ };
1206
+ }
1207
+ }
1208
+ );
1209
+ const customers = attempts.flatMap((attempt) => attempt.customer ? [attempt.customer] : []);
1210
+ const failures = attempts.flatMap((attempt) => attempt.failure ? [attempt.failure] : []);
1211
+ const omitted = accessible.data.resourceNames.length - resourceNames.length;
1212
+ return {
1213
+ data: {
1214
+ customers,
1215
+ failures,
1216
+ totalAccessible: accessible.data.resourceNames.length,
1217
+ attempted: resourceNames.length,
1218
+ returned: customers.length,
1219
+ truncated: omitted > 0,
1220
+ omitted
1221
+ },
1222
+ metadata: {
1223
+ apiVersion: GOOGLE_ADS_API_VERSION,
1224
+ requests: [
1225
+ accessible.metadata,
1226
+ ...attempts.flatMap((attempt) => attempt.metadata ? [attempt.metadata] : [])
1227
+ ]
1228
+ }
1229
+ };
1230
+ }
1231
+ async getCustomerClients(customerId, options = {}) {
1232
+ const normalizedOptions = normalizeGoogleAdsCustomerClientsOptions(options);
1233
+ const result = await this.#searchStream(
1234
+ customerId,
1235
+ "customer-clients",
1236
+ buildCustomerClientsQuery(normalizedOptions)
1237
+ );
1238
+ if (!result.data.every(isCustomerClientRow)) {
1239
+ throw new GoogleAdsApiError(
1240
+ "Google Ads API returned an invalid customer-client response",
1241
+ 502,
1242
+ void 0,
1243
+ result.metadata.requestId
1244
+ );
1245
+ }
1246
+ const truncated = result.data.length > normalizedOptions.maxCustomers;
1247
+ const customerClients = result.data.slice(0, normalizedOptions.maxCustomers).map((row) => row.customerClient);
1248
+ return {
1249
+ data: {
1250
+ customerClients,
1251
+ maxDepth: normalizedOptions.maxDepth,
1252
+ returned: customerClients.length,
1253
+ truncated
1254
+ },
1255
+ metadata: result.metadata
1256
+ };
1257
+ }
1258
+ async getCampaigns(customerId, options = {}) {
1259
+ return this.#searchStream(customerId, "campaigns", buildCampaignsQuery(options));
1260
+ }
1261
+ async getConversionActions(customerId, options = {}) {
1262
+ return this.#searchStream(customerId, "conversion-actions", buildConversionActionsQuery(options));
1263
+ }
1264
+ async getConversionGoals(customerId) {
1265
+ const normalizedCustomerId = normalizeGoogleAdsCustomerId(customerId);
1266
+ const [conversionActions, customerGoals, campaignGoals, customGoals, campaignConfigs] = await Promise.all([
1267
+ this.#searchStream(
1268
+ normalizedCustomerId,
1269
+ "conversion-actions",
1270
+ buildConversionActionsQuery({ includeRemoved: true })
1271
+ ),
1272
+ this.#searchStream(
1273
+ normalizedCustomerId,
1274
+ "customer-conversion-goals",
1275
+ buildCustomerConversionGoalsQuery()
1276
+ ),
1277
+ this.#searchStream(
1278
+ normalizedCustomerId,
1279
+ "campaign-conversion-goals",
1280
+ buildCampaignConversionGoalsQuery()
1281
+ ),
1282
+ this.#searchStream(
1283
+ normalizedCustomerId,
1284
+ "custom-conversion-goals",
1285
+ buildCustomConversionGoalsQuery()
1286
+ ),
1287
+ this.#searchStream(
1288
+ normalizedCustomerId,
1289
+ "conversion-goal-campaign-configs",
1290
+ buildConversionGoalCampaignConfigsQuery()
1291
+ )
1292
+ ]);
1293
+ const raw = {
1294
+ conversionActions: conversionActions.data,
1295
+ customerGoals: customerGoals.data,
1296
+ campaignGoals: campaignGoals.data,
1297
+ customGoals: customGoals.data,
1298
+ campaignConfigs: campaignConfigs.data
1299
+ };
1300
+ return {
1301
+ data: {
1302
+ ...raw,
1303
+ // A result exactly at the cap may have more rows upstream. Treat it
1304
+ // as incomplete rather than silently deriving absent campaign goals.
1305
+ campaignGoalsComplete: campaignGoals.data.length < GOOGLE_ADS_MAX_RESULT_ROWS,
1306
+ effectiveCampaignGoalInputs: resolveEffectiveCampaignGoalInputs(raw)
1307
+ },
1308
+ metadata: {
1309
+ apiVersion: GOOGLE_ADS_API_VERSION,
1310
+ requests: [
1311
+ conversionActions.metadata,
1312
+ customerGoals.metadata,
1313
+ campaignGoals.metadata,
1314
+ customGoals.metadata,
1315
+ campaignConfigs.metadata
1316
+ ]
1317
+ }
1318
+ };
1319
+ }
1320
+ async getDailyCampaignMetrics(customerId, options) {
1321
+ return this.#searchStream(
1322
+ customerId,
1323
+ "daily-campaign-metrics",
1324
+ buildDailyCampaignMetricsQuery(options)
1325
+ );
1326
+ }
1327
+ async getDailyConversionMetrics(customerId, options) {
1328
+ return this.#searchStream(
1329
+ customerId,
1330
+ "daily-conversion-metrics",
1331
+ buildDailyConversionMetricsQuery(options)
1332
+ );
1333
+ }
1334
+ async #searchStream(customerId, operation, query) {
1335
+ const normalizedCustomerId = normalizeGoogleAdsCustomerId(customerId);
1336
+ const result = await this.#requestJson(
1337
+ operation,
1338
+ `/customers/${normalizedCustomerId}/googleAds:searchStream`,
1339
+ {
1340
+ method: "POST",
1341
+ body: JSON.stringify({ query })
1342
+ },
1343
+ true
1344
+ );
1345
+ if (!Array.isArray(result.data)) {
1346
+ throw new GoogleAdsApiError(
1347
+ "Google Ads API returned an invalid searchStream response",
1348
+ 502,
1349
+ void 0,
1350
+ result.metadata.requestId
1351
+ );
1352
+ }
1353
+ const rows = [];
1354
+ let requestId = result.metadata.requestId;
1355
+ for (const chunk of result.data) {
1356
+ if (Array.isArray(chunk.results)) rows.push(...chunk.results);
1357
+ requestId ??= safeRequestId(chunk.requestId);
1358
+ }
1359
+ return {
1360
+ data: rows,
1361
+ metadata: {
1362
+ ...result.metadata,
1363
+ ...requestId ? { requestId } : {}
1364
+ }
1365
+ };
1366
+ }
1367
+ async #requestJson(operation, path9, init, includeLoginCustomerId) {
1368
+ const maxRetries = this.#retry.maxRetries ?? GOOGLE_ADS_MAX_RETRIES;
1369
+ const baseDelayMs = this.#retry.baseDelayMs ?? GOOGLE_ADS_RETRY_BASE_DELAY_MS;
1370
+ const maxDelayMs = this.#retry.maxDelayMs ?? GOOGLE_ADS_RETRY_MAX_DELAY_MS;
1371
+ return withRetry(
1372
+ () => this.#requestJsonOnce(operation, path9, init, includeLoginCustomerId),
1373
+ {
1374
+ maxRetries,
1375
+ baseDelayMs,
1376
+ maxDelayMs,
1377
+ jitter: this.#retry.jitter,
1378
+ sleep: this.#retry.sleep,
1379
+ isRetryable: isRetryableHttpError,
1380
+ computeDelayMs: (_attempt, error, defaultMs) => retryAfterDelayMs(error) ?? defaultMs
1381
+ }
1382
+ );
1383
+ }
1384
+ async #requestJsonOnce(operation, path9, init, includeLoginCustomerId) {
1385
+ const headers = {
1386
+ Authorization: `Bearer ${this.#accessToken}`,
1387
+ "Content-Type": "application/json",
1388
+ "developer-token": this.#developerToken
1389
+ };
1390
+ if (includeLoginCustomerId && this.#loginCustomerId) {
1391
+ headers["login-customer-id"] = this.#loginCustomerId;
1392
+ }
1393
+ let response;
1394
+ try {
1395
+ response = await this.#fetch(`${GOOGLE_ADS_API_BASE}${path9}`, {
1396
+ ...init,
1397
+ headers,
1398
+ signal: AbortSignal.timeout(this.#requestTimeoutMs)
1399
+ });
1400
+ } catch (error) {
1401
+ const detail = describeError(error);
1402
+ throw new GoogleAdsApiError(
1403
+ `Google Ads API request failed: ${sanitizeText(detail, this.#secrets())}`
1404
+ );
1405
+ }
1406
+ const headerRequestId = safeRequestId(response.headers.get("request-id")) ?? safeRequestId(response.headers.get("google-ads-request-id"));
1407
+ const retryAfter = response.headers.get("retry-after") ?? void 0;
1408
+ let body;
1409
+ try {
1410
+ body = await response.text();
1411
+ } catch (error) {
1412
+ const detail = describeError(error);
1413
+ throw new GoogleAdsApiError(
1414
+ `Google Ads API response failed: ${sanitizeText(detail, this.#secrets())}`,
1415
+ void 0,
1416
+ void 0,
1417
+ headerRequestId
1418
+ );
1419
+ }
1420
+ let payload;
1421
+ try {
1422
+ payload = body.trim() === "" ? null : JSON.parse(body);
1423
+ } catch {
1424
+ throw new GoogleAdsApiError(
1425
+ "Google Ads API returned invalid JSON",
1426
+ 502,
1427
+ void 0,
1428
+ headerRequestId,
1429
+ retryAfter
1430
+ );
1431
+ }
1432
+ if (!response.ok) {
1433
+ const provider = providerErrorParts(payload);
1434
+ const requestId = headerRequestId ?? provider.requestId;
1435
+ const rawDetail = provider.message ?? (body.trim() || response.statusText);
1436
+ const detail = sanitizeText(rawDetail, this.#secrets());
1437
+ throw new GoogleAdsApiError(
1438
+ `Google Ads API error (${response.status}): ${detail}`,
1439
+ response.status,
1440
+ provider.providerStatus,
1441
+ requestId,
1442
+ retryAfter
1443
+ );
1444
+ }
1445
+ if (payload === null) {
1446
+ throw new GoogleAdsApiError(
1447
+ "Google Ads API returned an empty response",
1448
+ 502,
1449
+ void 0,
1450
+ headerRequestId
1451
+ );
1452
+ }
1453
+ return {
1454
+ data: payload,
1455
+ metadata: {
1456
+ apiVersion: GOOGLE_ADS_API_VERSION,
1457
+ operation,
1458
+ ...headerRequestId ? { requestId: headerRequestId } : {}
1459
+ }
1460
+ };
1461
+ }
1462
+ #secrets() {
1463
+ return [this.#accessToken, this.#developerToken];
1464
+ }
1465
+ };
1466
+
1467
+ // ../integration-google-tag-manager/src/constants.ts
1468
+ var GTM_API_BASE = "https://tagmanager.googleapis.com/tagmanager/v2";
1469
+ var GTM_READONLY_SCOPE = "https://www.googleapis.com/auth/tagmanager.readonly";
1470
+ var GTM_REQUEST_TIMEOUT_MS = 3e4;
1471
+ var GTM_MAX_PAGES = 200;
1472
+ var GTM_SNAPSHOT_SCHEMA_VERSION = "gtm-container-snapshot-v1";
1473
+ var GTM_COMPARISON_SCHEMA_VERSION = "gtm-container-comparison-v1";
1474
+ var GTM_GOOGLE_ADS_CONVERSION_RECOGNIZER_VERSION = "google-ads-conversion-v1";
1475
+ var GTM_GOOGLE_ADS_CONVERSION_ASSESSMENT_SCHEMA_VERSION = "gtm-google-ads-conversion-assessment-v1";
1476
+
1477
+ // ../integration-google-tag-manager/src/checksum.ts
1478
+ import crypto2 from "crypto";
1479
+ function stableStringify(value) {
1480
+ if (value === null) return "null";
1481
+ if (typeof value === "string") return JSON.stringify(value);
1482
+ if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
1483
+ if (typeof value !== "object") throw new TypeError("Value is not JSON-serializable");
1484
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
1485
+ const object = value;
1486
+ const members = Object.keys(object).filter((key) => object[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`);
1487
+ return `{${members.join(",")}}`;
1488
+ }
1489
+ function checksumJson(value) {
1490
+ return crypto2.createHash("sha256").update(stableStringify(value)).digest("hex");
1491
+ }
1492
+
1493
+ // ../integration-google-tag-manager/src/types.ts
1494
+ var GtmApiError = class extends Error {
1495
+ status;
1496
+ providerStatus;
1497
+ reason;
1498
+ retryAfter;
1499
+ requestId;
1500
+ constructor(message, status, options = {}) {
1501
+ super(message);
1502
+ this.name = "GtmApiError";
1503
+ this.status = status;
1504
+ this.providerStatus = options.providerStatus ?? null;
1505
+ this.reason = options.reason ?? null;
1506
+ this.retryAfter = options.retryAfter ?? null;
1507
+ this.requestId = options.requestId ?? null;
1508
+ }
1509
+ };
1510
+
1511
+ // ../integration-google-tag-manager/src/http.ts
1512
+ function safeMessage(body, status, accessToken) {
1513
+ const payload = body;
1514
+ const providerMessage = payload?.error?.message;
1515
+ const message = typeof providerMessage === "string" ? providerMessage : `Tag Manager request failed (HTTP ${status})`;
1516
+ const redacted = accessToken ? message.replaceAll(accessToken, "[redacted]") : message;
1517
+ return redacted.replace(/[\r\n\t]+/g, " ").slice(0, 500);
1518
+ }
1519
+ function providerReason(body) {
1520
+ const payload = body;
1521
+ for (const detail of payload?.error?.details ?? []) {
1522
+ if (typeof detail.reason === "string") return detail.reason;
1523
+ }
1524
+ return null;
1525
+ }
1526
+ async function fetchOnce(url, accessToken, options) {
1527
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1528
+ const controller = new AbortController();
1529
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? GTM_REQUEST_TIMEOUT_MS);
1530
+ try {
1531
+ const response = await fetchImpl(url, {
1532
+ method: "GET",
1533
+ headers: {
1534
+ authorization: `Bearer ${accessToken}`,
1535
+ accept: "application/json"
1536
+ },
1537
+ signal: controller.signal
1538
+ });
1539
+ const text = await response.text();
1540
+ let body = void 0;
1541
+ let invalidJson = false;
1542
+ if (text) {
1543
+ try {
1544
+ body = JSON.parse(text);
1545
+ } catch {
1546
+ invalidJson = true;
1547
+ }
1548
+ }
1549
+ if (!response.ok) {
1550
+ const payload = body;
1551
+ throw new GtmApiError(safeMessage(body, response.status, accessToken), response.status, {
1552
+ providerStatus: payload?.error?.status ?? null,
1553
+ reason: providerReason(body),
1554
+ retryAfter: response.headers.get("retry-after"),
1555
+ requestId: response.headers.get("x-request-id")
1556
+ });
1557
+ }
1558
+ if (invalidJson) {
1559
+ throw new GtmApiError("Tag Manager returned invalid JSON", 502, {
1560
+ reason: "INVALID_PROVIDER_RESPONSE",
1561
+ retryAfter: response.headers.get("retry-after"),
1562
+ requestId: response.headers.get("x-request-id")
1563
+ });
1564
+ }
1565
+ return body;
1566
+ } finally {
1567
+ clearTimeout(timeout);
1568
+ }
1569
+ }
1570
+ async function gtmFetchGet(url, accessToken, options) {
1571
+ return withRetry(() => fetchOnce(url, accessToken, options), {
1572
+ maxRetries: options.retry?.maxRetries ?? 3,
1573
+ baseDelayMs: options.retry?.baseDelayMs ?? 1e3,
1574
+ jitter: true,
1575
+ isRetryable: isRetryableHttpError,
1576
+ computeDelayMs: (_attempt, error, defaultMs) => retryAfterDelayMs(error) ?? defaultMs,
1577
+ sleep: options.retry?.sleep
1578
+ });
1579
+ }
1580
+
1581
+ // ../integration-google-tag-manager/src/snapshot.ts
1582
+ var COMPARISON_METADATA_KEYS = /* @__PURE__ */ new Set([
1583
+ "accountId",
1584
+ "containerId",
1585
+ "workspaceId",
1586
+ "path",
1587
+ "fingerprint",
1588
+ "tagManagerUrl"
1589
+ ]);
1590
+ function comparisonValue(raw) {
1591
+ return Object.fromEntries(
1592
+ Object.entries(raw).filter(([key]) => !COMPARISON_METADATA_KEYS.has(key))
1593
+ );
1594
+ }
1595
+ function resourceId(kind, raw) {
1596
+ const explicit = kind === "tag" ? raw.tagId : kind === "trigger" ? raw.triggerId : kind === "variable" ? raw.variableId : kind === "folder" ? raw.folderId : raw.type;
1597
+ if (explicit) return explicit;
1598
+ if (raw.path) {
1599
+ const finalSegment = raw.path.split("/").filter(Boolean).at(-1);
1600
+ if (finalSegment) return finalSegment;
1601
+ }
1602
+ if (raw.name) return `name:${raw.name}`;
1603
+ return `anonymous:${checksumJson(raw)}`;
1604
+ }
1605
+ function snapshotEntity(kind, raw) {
1606
+ return {
1607
+ kind,
1608
+ id: resourceId(kind, raw),
1609
+ name: raw.name ?? null,
1610
+ fingerprint: "fingerprint" in raw && typeof raw.fingerprint === "string" ? raw.fingerprint : null,
1611
+ checksum: checksumJson(raw),
1612
+ contentChecksum: checksumJson(comparisonValue(raw)),
1613
+ raw
1614
+ };
1615
+ }
1616
+ function snapshotEntities(lists) {
1617
+ const entities = {
1618
+ tags: lists.tags.map((raw) => snapshotEntity("tag", raw)),
1619
+ triggers: lists.triggers.map((raw) => snapshotEntity("trigger", raw)),
1620
+ variables: lists.variables.map((raw) => snapshotEntity("variable", raw)),
1621
+ folders: lists.folders.map((raw) => snapshotEntity("folder", raw)),
1622
+ builtInVariables: lists.builtInVariables.map((raw) => snapshotEntity("builtInVariable", raw))
1623
+ };
1624
+ entities.tags.sort((left, right) => left.id.localeCompare(right.id));
1625
+ entities.triggers.sort((left, right) => left.id.localeCompare(right.id));
1626
+ entities.variables.sort((left, right) => left.id.localeCompare(right.id));
1627
+ entities.folders.sort((left, right) => left.id.localeCompare(right.id));
1628
+ entities.builtInVariables.sort((left, right) => left.id.localeCompare(right.id));
1629
+ assertUniqueIdentities(entities);
1630
+ return entities;
1631
+ }
1632
+ function allEntities(sets) {
1633
+ return [
1634
+ ...sets.tags,
1635
+ ...sets.triggers,
1636
+ ...sets.variables,
1637
+ ...sets.folders,
1638
+ ...sets.builtInVariables
1639
+ ];
1640
+ }
1641
+ function assertUniqueIdentities(sets) {
1642
+ const seen = /* @__PURE__ */ new Set();
1643
+ for (const entity of allEntities(sets)) {
1644
+ const identity = `${entity.kind}:${entity.id}`;
1645
+ if (seen.has(identity)) {
1646
+ throw new GtmApiError(`Tag Manager returned duplicate ${entity.kind} identity`, 502, {
1647
+ reason: "DUPLICATE_ENTITY_IDENTITY"
1648
+ });
1649
+ }
1650
+ seen.add(identity);
1651
+ }
1652
+ }
1653
+ function snapshotChecksum(accountId, containerId, entities) {
1654
+ return checksumJson({
1655
+ accountId,
1656
+ containerId,
1657
+ entities: allEntities(entities).map((entity) => ({
1658
+ kind: entity.kind,
1659
+ id: entity.id,
1660
+ contentChecksum: entity.contentChecksum
1661
+ }))
1662
+ });
1663
+ }
1664
+ function buildLiveSnapshot(version) {
1665
+ const entities = snapshotEntities({
1666
+ tags: version.tag ?? [],
1667
+ triggers: version.trigger ?? [],
1668
+ variables: version.variable ?? [],
1669
+ folders: version.folder ?? [],
1670
+ builtInVariables: version.builtInVariable ?? []
1671
+ });
1672
+ const accountId = version.accountId ?? null;
1673
+ const containerId = version.containerId ?? null;
1674
+ return {
1675
+ schemaVersion: GTM_SNAPSHOT_SCHEMA_VERSION,
1676
+ source: "live",
1677
+ identity: {
1678
+ accountId,
1679
+ containerId,
1680
+ containerVersionId: version.containerVersionId ?? null,
1681
+ workspaceId: null,
1682
+ path: version.path ?? null,
1683
+ fingerprint: version.fingerprint ?? null
1684
+ },
1685
+ entities,
1686
+ checksum: snapshotChecksum(accountId, containerId, entities),
1687
+ raw: { containerVersion: version }
1688
+ };
1689
+ }
1690
+ function buildWorkspaceSnapshot(workspace, status, lists) {
1691
+ const entities = snapshotEntities(lists);
1692
+ const accountId = workspace.accountId ?? null;
1693
+ const containerId = workspace.containerId ?? null;
1694
+ return {
1695
+ schemaVersion: GTM_SNAPSHOT_SCHEMA_VERSION,
1696
+ source: "workspace",
1697
+ identity: {
1698
+ accountId,
1699
+ containerId,
1700
+ containerVersionId: null,
1701
+ workspaceId: workspace.workspaceId ?? null,
1702
+ path: workspace.path ?? null,
1703
+ fingerprint: workspace.fingerprint ?? null
1704
+ },
1705
+ entities,
1706
+ checksum: snapshotChecksum(accountId, containerId, entities),
1707
+ raw: { workspace, workspaceStatus: status }
1708
+ };
1709
+ }
1710
+ function fingerprintMatches(live, draft) {
1711
+ if (!live?.fingerprint || !draft?.fingerprint) return null;
1712
+ return live.fingerprint === draft.fingerprint;
1713
+ }
1714
+ function compareContainerSnapshots(live, draft) {
1715
+ if (live.source !== "live" || draft.source !== "workspace") {
1716
+ throw new GtmApiError("Comparison requires one live snapshot and one workspace snapshot", 400, {
1717
+ reason: "INVALID_SNAPSHOT_PAIR"
1718
+ });
1719
+ }
1720
+ if (live.identity.accountId !== draft.identity.accountId || live.identity.containerId !== draft.identity.containerId) {
1721
+ throw new GtmApiError("Live and workspace snapshots belong to different containers", 400, {
1722
+ reason: "CONTAINER_IDENTITY_MISMATCH"
1723
+ });
1724
+ }
1725
+ const liveByIdentity = new Map(allEntities(live.entities).map((entity) => [`${entity.kind}:${entity.id}`, entity]));
1726
+ const draftByIdentity = new Map(allEntities(draft.entities).map((entity) => [`${entity.kind}:${entity.id}`, entity]));
1727
+ const identities = [.../* @__PURE__ */ new Set([...liveByIdentity.keys(), ...draftByIdentity.keys()])].sort();
1728
+ const changes = identities.map((identity) => {
1729
+ const liveEntity = liveByIdentity.get(identity);
1730
+ const draftEntity = draftByIdentity.get(identity);
1731
+ const contentMatches = Boolean(
1732
+ liveEntity && draftEntity && liveEntity.contentChecksum === draftEntity.contentChecksum
1733
+ );
1734
+ const status = !liveEntity ? "added" : !draftEntity ? "removed" : contentMatches ? "unchanged" : "changed";
1735
+ const entity = draftEntity ?? liveEntity;
1736
+ if (!entity) throw new Error("Entity comparison invariant failed");
1737
+ return {
1738
+ kind: entity.kind,
1739
+ id: entity.id,
1740
+ name: entity.name,
1741
+ status,
1742
+ contentMatches,
1743
+ providerFingerprintMatches: fingerprintMatches(liveEntity, draftEntity),
1744
+ live: liveEntity ?? null,
1745
+ draft: draftEntity ?? null
1746
+ };
1747
+ });
1748
+ const workspaceStatus = draft.raw.workspaceStatus ?? {};
1749
+ const workspaceChanges = workspaceStatus.workspaceChange ?? [];
1750
+ const mergeConflicts = workspaceStatus.mergeConflict ?? [];
1751
+ const hasConflicts = mergeConflicts.length > 0;
1752
+ const hasUnpublishedChanges = hasConflicts || workspaceChanges.length > 0 || changes.some((change) => change.status !== "unchanged");
1753
+ return {
1754
+ schemaVersion: GTM_COMPARISON_SCHEMA_VERSION,
1755
+ state: hasConflicts ? "conflicted" : hasUnpublishedChanges ? "unpublished-changes" : "in-sync",
1756
+ hasUnpublishedChanges,
1757
+ hasConflicts,
1758
+ liveChecksum: live.checksum,
1759
+ draftChecksum: draft.checksum,
1760
+ changes,
1761
+ workspaceChanges,
1762
+ mergeConflicts
1763
+ };
1764
+ }
1765
+
1766
+ // ../integration-google-tag-manager/src/client.ts
1767
+ var GTM_ID_SEGMENT = String.raw`[\w-]+`;
1768
+ var PATH_PATTERNS = {
1769
+ account: new RegExp(`^accounts/${GTM_ID_SEGMENT}$`),
1770
+ container: new RegExp(`^accounts/${GTM_ID_SEGMENT}/containers/${GTM_ID_SEGMENT}$`),
1771
+ workspace: new RegExp(
1772
+ `^accounts/${GTM_ID_SEGMENT}/containers/${GTM_ID_SEGMENT}/workspaces/${GTM_ID_SEGMENT}$`
1773
+ )
1774
+ };
1775
+ function assertAccessToken(accessToken) {
1776
+ if (typeof accessToken !== "string" || accessToken.trim() === "") {
1777
+ throw new GtmApiError("OAuth access token is required", 400, { reason: "INVALID_ACCESS_TOKEN" });
1778
+ }
1779
+ }
1780
+ function assertPath(path9, kind) {
1781
+ if (!PATH_PATTERNS[kind].test(path9)) {
1782
+ throw new GtmApiError(`Invalid Tag Manager ${kind} resource path`, 400, {
1783
+ reason: "INVALID_RESOURCE_PATH"
1784
+ });
1785
+ }
1786
+ }
1787
+ function apiUrl(path9, pageToken) {
1788
+ const url = new URL(`${GTM_API_BASE}/${path9}`);
1789
+ if (pageToken !== void 0) url.searchParams.set("pageToken", pageToken);
1790
+ return url.toString();
1791
+ }
1792
+ async function listCollection(accessToken, path9, responseKey, options) {
1793
+ const collected = [];
1794
+ const seenTokens = /* @__PURE__ */ new Set();
1795
+ let pageToken;
1796
+ for (let page = 0; page < GTM_MAX_PAGES; page++) {
1797
+ const response = await gtmFetchGet(apiUrl(path9, pageToken), accessToken, options);
1798
+ const values = response[responseKey];
1799
+ if (values !== void 0 && !Array.isArray(values)) {
1800
+ throw new GtmApiError(`Tag Manager returned an invalid ${responseKey} list`, 502, {
1801
+ reason: "INVALID_PROVIDER_RESPONSE"
1802
+ });
1803
+ }
1804
+ collected.push(...values ?? []);
1805
+ const nextPageToken = response.nextPageToken;
1806
+ if (nextPageToken === void 0 || nextPageToken === "") return collected;
1807
+ if (typeof nextPageToken !== "string" || seenTokens.has(nextPageToken)) {
1808
+ throw new GtmApiError("Tag Manager returned an invalid pagination token", 502, {
1809
+ reason: "INVALID_PROVIDER_PAGINATION"
1810
+ });
1811
+ }
1812
+ seenTokens.add(nextPageToken);
1813
+ pageToken = nextPageToken;
1814
+ }
1815
+ throw new GtmApiError("Tag Manager pagination exceeded the safety limit", 502, {
1816
+ reason: "PAGINATION_LIMIT_EXCEEDED"
1817
+ });
1818
+ }
1819
+ function createGoogleTagManagerClient(accessToken, options = {}) {
1820
+ assertAccessToken(accessToken);
1821
+ const listAccounts = () => listCollection(accessToken, "accounts", "account", options);
1822
+ const listContainers = async (accountPath2) => {
1823
+ assertPath(accountPath2, "account");
1824
+ return listCollection(accessToken, `${accountPath2}/containers`, "container", options);
1825
+ };
1826
+ const listWorkspaces = async (containerPath2) => {
1827
+ assertPath(containerPath2, "container");
1828
+ return listCollection(accessToken, `${containerPath2}/workspaces`, "workspace", options);
1829
+ };
1830
+ const getLiveContainerVersion = async (containerPath2) => {
1831
+ assertPath(containerPath2, "container");
1832
+ return gtmFetchGet(
1833
+ `${GTM_API_BASE}/${containerPath2}/versions:live`,
1834
+ accessToken,
1835
+ options
1836
+ );
1837
+ };
1838
+ const getLiveSnapshot = async (containerPath2) => buildLiveSnapshot(await getLiveContainerVersion(containerPath2));
1839
+ const getWorkspaceSnapshot = async (workspacePath2) => {
1840
+ assertPath(workspacePath2, "workspace");
1841
+ const [workspace, status, tags, triggers, variables, folders, builtInVariables] = await Promise.all([
1842
+ gtmFetchGet(`${GTM_API_BASE}/${workspacePath2}`, accessToken, options),
1843
+ gtmFetchGet(`${GTM_API_BASE}/${workspacePath2}/status`, accessToken, options),
1844
+ listCollection(accessToken, `${workspacePath2}/tags`, "tag", options),
1845
+ listCollection(accessToken, `${workspacePath2}/triggers`, "trigger", options),
1846
+ listCollection(accessToken, `${workspacePath2}/variables`, "variable", options),
1847
+ listCollection(accessToken, `${workspacePath2}/folders`, "folder", options),
1848
+ listCollection(
1849
+ accessToken,
1850
+ `${workspacePath2}/built_in_variables`,
1851
+ "builtInVariable",
1852
+ options
1853
+ )
1854
+ ]);
1855
+ return buildWorkspaceSnapshot(workspace, status, { tags, triggers, variables, folders, builtInVariables });
1856
+ };
1857
+ const compareLiveAndWorkspace = async (containerPath2, workspacePath2) => {
1858
+ const [live, workspace] = await Promise.all([
1859
+ getLiveSnapshot(containerPath2),
1860
+ getWorkspaceSnapshot(workspacePath2)
1861
+ ]);
1862
+ return compareContainerSnapshots(live, workspace);
1863
+ };
1864
+ return {
1865
+ listAccounts,
1866
+ listContainers,
1867
+ listWorkspaces,
1868
+ getLiveContainerVersion,
1869
+ getLiveSnapshot,
1870
+ getWorkspaceSnapshot,
1871
+ compareLiveAndWorkspace
1872
+ };
1873
+ }
1874
+
1875
+ // ../integration-google-tag-manager/src/recognizer.ts
1876
+ var GOOGLE_ADS_CONVERSION_TAG_TYPE = "awct";
1877
+ var URL_TRIGGER_TYPES = /* @__PURE__ */ new Set([
1878
+ "pageview",
1879
+ "domReady",
1880
+ "windowLoaded",
1881
+ "init",
1882
+ "consentInit",
1883
+ "serverPageview",
1884
+ "historyChange"
1885
+ ]);
1886
+ function unknownReason(tagType) {
1887
+ if (tagType === "html") return "custom-html";
1888
+ if (tagType === "cvt" || tagType?.startsWith("cvt_")) return "custom-template";
1889
+ return "unsupported-tag-type";
1890
+ }
1891
+ function parametersByKey(parameters) {
1892
+ const byKey = /* @__PURE__ */ new Map();
1893
+ for (const parameter of parameters ?? []) {
1894
+ if (!parameter.key) continue;
1895
+ const values = byKey.get(parameter.key) ?? [];
1896
+ values.push(parameter);
1897
+ byKey.set(parameter.key, values);
1898
+ }
1899
+ return byKey;
1900
+ }
1901
+ function scalarMapping(byKey, key) {
1902
+ const parameters = byKey.get(key);
1903
+ if (parameters?.length !== 1) return null;
1904
+ const parameter = parameters[0];
1905
+ if (typeof parameter.value !== "string" || parameter.list || parameter.map) return null;
1906
+ const trimmed = parameter.value.trim();
1907
+ const hasVariable = parameter.value.includes("{{") && parameter.value.includes("}}");
1908
+ const onlyVariable = trimmed.startsWith("{{") && trimmed.endsWith("}}");
1909
+ return {
1910
+ parameterKey: key,
1911
+ value: parameter.value,
1912
+ source: onlyVariable ? "variable" : hasVariable ? "mixed" : "literal"
1913
+ };
1914
+ }
1915
+ function conditions(trigger) {
1916
+ return [
1917
+ ...trigger.customEventFilter ?? [],
1918
+ ...trigger.filter ?? [],
1919
+ ...trigger.autoEventFilter ?? []
1920
+ ];
1921
+ }
1922
+ function conditionParameter(condition, key) {
1923
+ const parameter = condition.parameter?.find((candidate) => candidate.key === key);
1924
+ return typeof parameter?.value === "string" ? parameter.value : null;
1925
+ }
1926
+ function normalizedVariableName(value) {
1927
+ const trimmed = value.trim();
1928
+ const unwrapped = trimmed.startsWith("{{") && trimmed.endsWith("}}") ? trimmed.slice(2, -2) : trimmed;
1929
+ return unwrapped.trim().toLowerCase();
1930
+ }
1931
+ function isEventVariable(value) {
1932
+ const normalized = normalizedVariableName(value);
1933
+ return normalized === "_event" || normalized === "event" || normalized === "event name";
1934
+ }
1935
+ function isHostnameVariable(value) {
1936
+ const normalized = normalizedVariableName(value);
1937
+ return normalized === "page hostname" || normalized === "_hostname";
1938
+ }
1939
+ function customEventNames(trigger) {
1940
+ const names = [];
1941
+ for (const condition of trigger.customEventFilter ?? []) {
1942
+ const left = conditionParameter(condition, "arg0");
1943
+ const right = conditionParameter(condition, "arg1");
1944
+ if (left && right && isEventVariable(left)) names.push(right);
1945
+ }
1946
+ return names;
1947
+ }
1948
+ function filterMatchesHostname(operator, filterValue, hostname) {
1949
+ const expected = hostname.toLowerCase();
1950
+ const value = filterValue.toLowerCase();
1951
+ if (operator === "equals") return expected === value;
1952
+ if (operator === "contains") return expected.includes(value);
1953
+ if (operator === "startsWith") return expected.startsWith(value);
1954
+ if (operator === "endsWith") return expected.endsWith(value);
1955
+ if (operator === "matchRegex" || operator === "urlMatches") {
1956
+ try {
1957
+ return new RegExp(filterValue, "i").test(hostname);
1958
+ } catch {
1959
+ return false;
1960
+ }
1961
+ }
1962
+ return false;
1963
+ }
1964
+ function hostnameFilters(trigger, expectedHostname) {
1965
+ const filters = [];
1966
+ for (const condition of conditions(trigger)) {
1967
+ const left = conditionParameter(condition, "arg0");
1968
+ const right = conditionParameter(condition, "arg1");
1969
+ if (!left || !right || !isHostnameVariable(left)) continue;
1970
+ const negated = conditionParameter(condition, "negate") === "true";
1971
+ filters.push({
1972
+ operator: condition.type ?? "conditionTypeUnspecified",
1973
+ value: right,
1974
+ negated,
1975
+ matchesExpectedHostname: expectedHostname === void 0 ? null : !negated && filterMatchesHostname(condition.type ?? "", right, expectedHostname)
1976
+ });
1977
+ }
1978
+ return filters;
1979
+ }
1980
+ function triggerStrategy(triggers) {
1981
+ if (triggers.length === 0) return "none";
1982
+ const categories = new Set(triggers.map((trigger) => {
1983
+ if (trigger.type === "customEvent") return "custom-event";
1984
+ if (trigger.type && URL_TRIGGER_TYPES.has(trigger.type)) return "url-based";
1985
+ return "unknown";
1986
+ }));
1987
+ if (categories.size > 1) return "mixed";
1988
+ return [...categories][0];
1989
+ }
1990
+ function unknownAssessment(tag, reason, triggers, options) {
1991
+ const firingTriggerIds = [...tag.firingTriggerId ?? []];
1992
+ const triggerById = /* @__PURE__ */ new Map();
1993
+ for (const trigger of triggers) {
1994
+ if (typeof trigger.triggerId === "string") triggerById.set(trigger.triggerId, trigger);
1995
+ }
1996
+ const resolved = firingTriggerIds.map((id) => triggerById.get(id)).filter((trigger) => trigger !== void 0);
1997
+ return {
1998
+ schemaVersion: GTM_GOOGLE_ADS_CONVERSION_ASSESSMENT_SCHEMA_VERSION,
1999
+ recognizerVersion: GTM_GOOGLE_ADS_CONVERSION_RECOGNIZER_VERSION,
2000
+ recognition: { status: "unknown", reason },
2001
+ review: { status: "needs-review", reasons: ["tag-unknown"] },
2002
+ tag: {
2003
+ id: tag.tagId ?? null,
2004
+ name: tag.name ?? null,
2005
+ type: tag.type ?? null,
2006
+ checksum: checksumJson(tag)
2007
+ },
2008
+ conversion: null,
2009
+ trigger: {
2010
+ strategy: triggerStrategy(resolved),
2011
+ firingTriggerIds,
2012
+ resolvedTriggerIds: resolved.flatMap((trigger) => trigger.triggerId ? [trigger.triggerId] : []),
2013
+ unresolvedTriggerIds: firingTriggerIds.filter((id) => !triggerById.has(id)),
2014
+ customEventNames: [...new Set(resolved.flatMap(customEventNames))],
2015
+ hostnameFilters: resolved.flatMap((trigger) => hostnameFilters(trigger, options.expectedHostname))
2016
+ }
2017
+ };
2018
+ }
2019
+ function recognizeGoogleAdsConversionTag(tag, triggers, options = {}) {
2020
+ if (tag.type !== GOOGLE_ADS_CONVERSION_TAG_TYPE) {
2021
+ return unknownAssessment(tag, unknownReason(tag.type), triggers, options);
2022
+ }
2023
+ const byKey = parametersByKey(tag.parameter);
2024
+ const conversionId = scalarMapping(byKey, "conversionId");
2025
+ const conversionLabel = scalarMapping(byKey, "conversionLabel");
2026
+ if (!conversionId || !conversionLabel) {
2027
+ return unknownAssessment(tag, "malformed-google-ads-conversion-tag", triggers, options);
2028
+ }
2029
+ const conversionValue = scalarMapping(byKey, "conversionValue");
2030
+ const transactionId = scalarMapping(byKey, "orderId");
2031
+ const currency = scalarMapping(byKey, "currencyCode");
2032
+ const firingTriggerIds = [...tag.firingTriggerId ?? []];
2033
+ const triggerById = /* @__PURE__ */ new Map();
2034
+ for (const trigger of triggers) {
2035
+ if (typeof trigger.triggerId === "string") triggerById.set(trigger.triggerId, trigger);
2036
+ }
2037
+ const resolved = firingTriggerIds.map((id) => triggerById.get(id)).filter((trigger) => trigger !== void 0);
2038
+ const unresolvedTriggerIds = firingTriggerIds.filter((id) => !triggerById.has(id));
2039
+ const strategy = triggerStrategy(resolved);
2040
+ const eventNames = [...new Set(resolved.flatMap(customEventNames))];
2041
+ const hostFilters = resolved.flatMap((trigger) => hostnameFilters(trigger, options.expectedHostname));
2042
+ const reasons = [];
2043
+ if (tag.paused) reasons.push("tag-paused");
2044
+ if (firingTriggerIds.length === 0) reasons.push("no-firing-trigger");
2045
+ if (unresolvedTriggerIds.length > 0) reasons.push("unresolved-firing-trigger");
2046
+ if (strategy === "url-based") reasons.push("url-based-trigger");
2047
+ if (strategy === "mixed") reasons.push("mixed-trigger-strategies");
2048
+ if (strategy === "custom-event" && eventNames.length === 0) reasons.push("missing-custom-event-name");
2049
+ if (strategy === "custom-event" && options.expectedEventName && !eventNames.includes(options.expectedEventName)) reasons.push("unexpected-custom-event-name");
2050
+ if (hostFilters.length === 0) reasons.push("missing-hostname-filter");
2051
+ if (options.expectedHostname && hostFilters.length > 0 && !hostFilters.some((filter) => filter.matchesExpectedHostname)) reasons.push("unexpected-hostname-filter");
2052
+ if (!conversionValue) reasons.push("missing-conversion-value");
2053
+ if (!transactionId) reasons.push("missing-transaction-id");
2054
+ return {
2055
+ schemaVersion: GTM_GOOGLE_ADS_CONVERSION_ASSESSMENT_SCHEMA_VERSION,
2056
+ recognizerVersion: GTM_GOOGLE_ADS_CONVERSION_RECOGNIZER_VERSION,
2057
+ recognition: { status: "recognized", kind: "google-ads-conversion" },
2058
+ review: { status: reasons.length === 0 ? "pass" : "needs-review", reasons },
2059
+ tag: {
2060
+ id: tag.tagId ?? null,
2061
+ name: tag.name ?? null,
2062
+ type: tag.type ?? null,
2063
+ checksum: checksumJson(tag)
2064
+ },
2065
+ conversion: {
2066
+ id: conversionId,
2067
+ label: conversionLabel,
2068
+ value: conversionValue,
2069
+ transactionId,
2070
+ currency
2071
+ },
2072
+ trigger: {
2073
+ strategy,
2074
+ firingTriggerIds,
2075
+ resolvedTriggerIds: resolved.flatMap((trigger) => trigger.triggerId ? [trigger.triggerId] : []),
2076
+ unresolvedTriggerIds,
2077
+ customEventNames: eventNames,
2078
+ hostnameFilters: hostFilters
2079
+ }
2080
+ };
2081
+ }
2082
+
2083
+ // ../integration-google-tag-manager/src/dto-adapter.ts
2084
+ function required(value, label) {
2085
+ if (!value) {
2086
+ throw new GtmApiError(`Tag Manager response is missing ${label}`, 502, {
2087
+ reason: "INVALID_PROVIDER_RESPONSE"
2088
+ });
2089
+ }
2090
+ return value;
2091
+ }
2092
+ function finalPathSegment(path9) {
2093
+ return path9?.split("/").filter(Boolean).at(-1);
2094
+ }
2095
+ function uniqueStrings(values) {
2096
+ return [...new Set(values.filter((value) => typeof value === "string"))].sort();
2097
+ }
2098
+ function parameterKeys(parameters) {
2099
+ return uniqueStrings((parameters ?? []).map((parameter) => parameter.key));
2100
+ }
2101
+ function parameterValues(parameters) {
2102
+ return (parameters ?? []).flatMap((parameter) => [
2103
+ ...typeof parameter.value === "string" ? [parameter.value] : [],
2104
+ ...parameterValues(parameter.list),
2105
+ ...parameterValues(parameter.map)
2106
+ ]);
2107
+ }
2108
+ function referencedVariableIds(tag, variableIdsByName) {
2109
+ const ids = [];
2110
+ for (const value of parameterValues(tag.parameter)) {
2111
+ for (const match of value.matchAll(/\{\{([^{}]+)\}\}/g)) {
2112
+ const name = match[1].trim();
2113
+ if (name) ids.push(...variableIdsByName.get(name) ?? []);
2114
+ }
2115
+ }
2116
+ return uniqueStrings(ids);
2117
+ }
2118
+ function conditionParameter2(condition, key) {
2119
+ const parameter = condition.parameter?.find((candidate) => candidate.key === key);
2120
+ return typeof parameter?.value === "string" ? parameter.value : null;
2121
+ }
2122
+ function normalizedVariableName2(value) {
2123
+ const trimmed = value.trim();
2124
+ const unwrapped = trimmed.startsWith("{{") && trimmed.endsWith("}}") ? trimmed.slice(2, -2) : trimmed;
2125
+ return unwrapped.trim().toLowerCase();
2126
+ }
2127
+ function safeCustomEventNames(trigger) {
2128
+ const names = [];
2129
+ for (const condition of trigger.customEventFilter ?? []) {
2130
+ const left = conditionParameter2(condition, "arg0");
2131
+ const right = conditionParameter2(condition, "arg1");
2132
+ if (left && right) {
2133
+ const variableName = normalizedVariableName2(left);
2134
+ if (variableName === "_event" || variableName === "event" || variableName === "event name") {
2135
+ names.push(right);
2136
+ }
2137
+ }
2138
+ }
2139
+ return uniqueStrings(names);
2140
+ }
2141
+ function conditionKeys(conditions2) {
2142
+ return uniqueStrings((conditions2 ?? []).flatMap((condition) => (condition.parameter ?? []).map((parameter) => parameter.key)));
2143
+ }
2144
+ function triggerConditions(trigger) {
2145
+ return [
2146
+ ...trigger.customEventFilter ?? [],
2147
+ ...trigger.filter ?? [],
2148
+ ...trigger.autoEventFilter ?? []
2149
+ ];
2150
+ }
2151
+ function triggerPredicate(trigger) {
2152
+ if (!trigger.triggerId) return null;
2153
+ const eventPredicates = [];
2154
+ const hostnamePredicates = [];
2155
+ let unsupportedConditionCount = 0;
2156
+ for (const condition of triggerConditions(trigger)) {
2157
+ const left = conditionParameter2(condition, "arg0");
2158
+ const right = conditionParameter2(condition, "arg1");
2159
+ const operator = condition.type?.trim() ?? "";
2160
+ if (!left || right === null || !operator || operator.length > 64 || right.length > 2048) {
2161
+ unsupportedConditionCount += 1;
2162
+ continue;
2163
+ }
2164
+ const predicate = {
2165
+ operator,
2166
+ value: right,
2167
+ negated: conditionParameter2(condition, "negate")?.toLowerCase() === "true",
2168
+ ignoreCase: (conditionParameter2(condition, "ignore_case") ?? conditionParameter2(condition, "ignoreCase"))?.toLowerCase() === "true"
2169
+ };
2170
+ const variableName = normalizedVariableName2(left);
2171
+ if (variableName === "_event" || variableName === "event" || variableName === "event name") {
2172
+ eventPredicates.push(predicate);
2173
+ } else if (variableName === "page hostname" || variableName === "_hostname") {
2174
+ hostnamePredicates.push(predicate);
2175
+ } else {
2176
+ unsupportedConditionCount += 1;
2177
+ }
2178
+ }
2179
+ return {
2180
+ triggerId: trigger.triggerId,
2181
+ triggerType: trigger.type ?? "",
2182
+ eventPredicates,
2183
+ hostnamePredicates,
2184
+ unsupportedConditionCount
2185
+ };
2186
+ }
2187
+ function toGtmAccountDto(account) {
2188
+ return {
2189
+ id: required(account.accountId ?? finalPathSegment(account.path), "account id"),
2190
+ path: account.path ?? "",
2191
+ name: account.name ?? "",
2192
+ shareData: typeof account.shareData === "boolean" ? account.shareData : null
2193
+ };
2194
+ }
2195
+ function toGtmContainerDto(container) {
2196
+ return {
2197
+ accountId: required(container.accountId, "container account id"),
2198
+ id: required(container.containerId ?? finalPathSegment(container.path), "container id"),
2199
+ path: container.path ?? "",
2200
+ name: container.name ?? "",
2201
+ publicId: container.publicId ?? null,
2202
+ domainName: container.domainName?.join(", ") ?? null,
2203
+ usageContexts: [...container.usageContext ?? []]
2204
+ };
2205
+ }
2206
+ function toGtmWorkspaceDto(workspace) {
2207
+ return {
2208
+ accountId: required(workspace.accountId, "workspace account id"),
2209
+ containerId: required(workspace.containerId, "workspace container id"),
2210
+ id: required(workspace.workspaceId ?? finalPathSegment(workspace.path), "workspace id"),
2211
+ path: workspace.path ?? "",
2212
+ name: workspace.name ?? "",
2213
+ description: workspace.description ?? null,
2214
+ fingerprint: workspace.fingerprint ?? null
2215
+ };
2216
+ }
2217
+ function toGtmContainerVersionDto(version) {
2218
+ return {
2219
+ accountId: required(version.accountId, "container version account id"),
2220
+ containerId: required(version.containerId, "container version container id"),
2221
+ id: required(version.containerVersionId ?? finalPathSegment(version.path), "container version id"),
2222
+ path: version.path ?? "",
2223
+ name: version.name ?? "",
2224
+ description: version.description ?? null,
2225
+ fingerprint: version.fingerprint ?? null,
2226
+ deleted: version.deleted ?? false
2227
+ };
2228
+ }
2229
+ function toGtmTagDto(tag, fallbackId, variableIdsByName) {
2230
+ return {
2231
+ id: required(tag.tagId ?? fallbackId, "tag id"),
2232
+ name: tag.name ?? "",
2233
+ type: tag.type ?? "",
2234
+ paused: tag.paused ?? false,
2235
+ firingTriggerIds: uniqueStrings(tag.firingTriggerId ?? []),
2236
+ blockingTriggerIds: uniqueStrings(tag.blockingTriggerId ?? []),
2237
+ referencedVariableIds: referencedVariableIds(tag, variableIdsByName),
2238
+ parameterKeys: parameterKeys(tag.parameter),
2239
+ fingerprint: tag.fingerprint ?? null
2240
+ };
2241
+ }
2242
+ function toGtmTriggerDto(trigger, fallbackId) {
2243
+ return {
2244
+ id: required(trigger.triggerId ?? fallbackId, "trigger id"),
2245
+ name: trigger.name ?? "",
2246
+ type: trigger.type ?? "",
2247
+ customEventNames: safeCustomEventNames(trigger),
2248
+ filterKeys: uniqueStrings([
2249
+ ...conditionKeys(trigger.customEventFilter),
2250
+ ...conditionKeys(trigger.filter)
2251
+ ]),
2252
+ autoEventFilterKeys: conditionKeys(trigger.autoEventFilter),
2253
+ fingerprint: trigger.fingerprint ?? null
2254
+ };
2255
+ }
2256
+ function dataLayerVariableName(variable) {
2257
+ if (variable.type !== "v") return null;
2258
+ const name = variable.parameter?.find((parameter) => parameter.key === "name")?.value;
2259
+ return typeof name === "string" && name.trim() ? name : null;
2260
+ }
2261
+ function toGtmVariableDto(variable, fallbackId) {
2262
+ return {
2263
+ id: required(variable.variableId ?? fallbackId, "variable id"),
2264
+ name: variable.name ?? "",
2265
+ type: variable.type ?? "",
2266
+ dataLayerVariableName: dataLayerVariableName(variable),
2267
+ parameterKeys: parameterKeys(variable.parameter),
2268
+ fingerprint: variable.fingerprint ?? null
2269
+ };
2270
+ }
2271
+ function absentMapping() {
2272
+ return { source: "absent", literal: null, variableRef: null };
2273
+ }
2274
+ function unknownMapping() {
2275
+ return { source: "unknown", literal: null, variableRef: null };
2276
+ }
2277
+ function normalizedVariableReference(value) {
2278
+ const trimmed = value.trim();
2279
+ if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
2280
+ const inner = trimmed.slice(2, -2);
2281
+ const normalized = inner.trim();
2282
+ if (!normalized || normalized.length > 128 || inner.includes("{{") || inner.includes("}}") || !/^\w[\w ./-]*$/.test(normalized)) return null;
2283
+ return `{{${normalized}}}`;
2284
+ }
2285
+ function normalizedLiteral(field, value) {
2286
+ const trimmed = value.trim();
2287
+ if (!trimmed) return null;
2288
+ if (field === "conversionId") {
2289
+ const match = /^(?:AW-)?(\d{1,20})$/i.exec(trimmed);
2290
+ return match ? `AW-${match[1]}` : null;
2291
+ }
2292
+ if (field === "conversionLabel") {
2293
+ return /^[\w-]{1,128}$/.test(trimmed) ? trimmed : null;
2294
+ }
2295
+ if (field === "value") {
2296
+ if (!/^(?:0|[1-9]\d{0,11})(?:\.\d{1,6})?$/.test(trimmed)) return null;
2297
+ const [whole, fractional = ""] = trimmed.split(".");
2298
+ const normalizedFractional = fractional.replace(/0+$/, "");
2299
+ return normalizedFractional ? `${whole}.${normalizedFractional}` : whole;
2300
+ }
2301
+ if (field === "currency") {
2302
+ return /^[a-z]{3}$/i.test(trimmed) ? trimmed.toUpperCase() : null;
2303
+ }
2304
+ return null;
2305
+ }
2306
+ function fieldMapping(field, value, source) {
2307
+ if (source === "variable") {
2308
+ const variableRef = normalizedVariableReference(value);
2309
+ return variableRef ? { source: "variable-ref", literal: null, variableRef } : unknownMapping();
2310
+ }
2311
+ if (source !== "literal") return unknownMapping();
2312
+ const literal = normalizedLiteral(field, value);
2313
+ return literal ? { source: "literal", literal, variableRef: null } : unknownMapping();
2314
+ }
2315
+ function toFieldMapping(mapping, field) {
2316
+ return mapping ? fieldMapping(field, mapping.value, mapping.source) : absentMapping();
2317
+ }
2318
+ function safeTagFieldMapping(tag, key, field) {
2319
+ const parameters = (tag.parameter ?? []).filter((parameter) => parameter.key === key);
2320
+ if (parameters.length === 0) return absentMapping();
2321
+ if (parameters.length !== 1) return unknownMapping();
2322
+ const value = parameters[0]?.value;
2323
+ if (typeof value !== "string" || parameters[0]?.list || parameters[0]?.map) return unknownMapping();
2324
+ const variableRef = normalizedVariableReference(value);
2325
+ if (variableRef) return { source: "variable-ref", literal: null, variableRef };
2326
+ if (value.includes("{{") || value.includes("}}") || !value.trim()) return unknownMapping();
2327
+ return fieldMapping(field, value, "literal");
2328
+ }
2329
+ function triggerStrategy2(assessment, triggers) {
2330
+ if (assessment.trigger.unresolvedTriggerIds.length > 0) return "unknown";
2331
+ if (assessment.trigger.strategy === "custom-event") return "custom-event";
2332
+ if (assessment.trigger.strategy !== "url-based") return "unknown";
2333
+ const firingIds = new Set(assessment.trigger.firingTriggerIds);
2334
+ const resolved = triggers.filter((trigger) => trigger.triggerId && firingIds.has(trigger.triggerId));
2335
+ const hasFilters = resolved.some((trigger) => (trigger.customEventFilter?.length ?? 0) > 0 || (trigger.filter?.length ?? 0) > 0 || (trigger.autoEventFilter?.length ?? 0) > 0);
2336
+ return hasFilters ? "filtered" : "all-pages";
2337
+ }
2338
+ function mapReviewReasons(assessment, tag, fields) {
2339
+ const reasons = /* @__PURE__ */ new Set();
2340
+ if (assessment.recognition.status === "unknown") {
2341
+ if (assessment.recognition.reason === "custom-html") reasons.add("custom-html-opaque");
2342
+ else if (assessment.recognition.reason === "custom-template") reasons.add("unsupported-tag-type");
2343
+ else if (assessment.recognition.reason === "unsupported-tag-type") reasons.add("not-google-ads-tag");
2344
+ else {
2345
+ reasons.add("conversion-id-unresolved");
2346
+ reasons.add("conversion-label-unresolved");
2347
+ }
2348
+ }
2349
+ for (const reason of assessment.review.reasons) {
2350
+ if (reason === "no-firing-trigger" || reason === "unresolved-firing-trigger" || reason === "url-based-trigger" || reason === "mixed-trigger-strategies" || reason === "missing-custom-event-name" || reason === "unexpected-custom-event-name") reasons.add("trigger-unresolved");
2351
+ if (reason === "missing-hostname-filter" || reason === "unexpected-hostname-filter") {
2352
+ reasons.add("hostname-filter-unresolved");
2353
+ }
2354
+ if (reason === "missing-conversion-value") reasons.add("value-mapping-missing");
2355
+ if (reason === "missing-transaction-id") reasons.add("transaction-id-mapping-missing");
2356
+ }
2357
+ if (tag.type === "awct") {
2358
+ if (fields.conversionId.source === "absent" || fields.conversionId.source === "unknown") {
2359
+ reasons.add("conversion-id-unresolved");
2360
+ }
2361
+ if (fields.conversionLabel.source === "absent" || fields.conversionLabel.source === "unknown") {
2362
+ reasons.add("conversion-label-unresolved");
2363
+ }
2364
+ if (fields.value.source === "absent" || fields.value.source === "unknown") {
2365
+ reasons.add("value-mapping-missing");
2366
+ }
2367
+ if (fields.transactionId.source === "absent" || fields.transactionId.source === "unknown") {
2368
+ reasons.add("transaction-id-mapping-missing");
2369
+ }
2370
+ if (fields.currency.source === "absent" || fields.currency.source === "unknown") {
2371
+ reasons.add("currency-mapping-missing");
2372
+ }
2373
+ }
2374
+ return [...reasons];
2375
+ }
2376
+ function toGtmGoogleAdsTagAssessmentDto(tag, triggers, options = {}, fallbackTagId) {
2377
+ const assessment = recognizeGoogleAdsConversionTag(tag, triggers, options);
2378
+ const knownConversion = assessment.conversion;
2379
+ const knownTag = tag.type === "awct";
2380
+ const fields = {
2381
+ conversionId: gtmGoogleAdsConversionIdMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.id, "conversionId") : knownTag ? safeTagFieldMapping(tag, "conversionId", "conversionId") : unknownMapping()),
2382
+ conversionLabel: gtmGoogleAdsConversionLabelMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.label, "conversionLabel") : knownTag ? safeTagFieldMapping(tag, "conversionLabel", "conversionLabel") : unknownMapping()),
2383
+ value: gtmGoogleAdsConversionValueMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.value, "value") : knownTag ? safeTagFieldMapping(tag, "conversionValue", "value") : unknownMapping()),
2384
+ transactionId: gtmGoogleAdsTransactionIdMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.transactionId, "transactionId") : knownTag ? safeTagFieldMapping(tag, "orderId", "transactionId") : unknownMapping()),
2385
+ currency: gtmGoogleAdsCurrencyMappingDtoSchema.parse(knownConversion ? toFieldMapping(knownConversion.currency, "currency") : knownTag ? safeTagFieldMapping(tag, "currencyCode", "currency") : unknownMapping())
2386
+ };
2387
+ return {
2388
+ tagId: required(tag.tagId ?? fallbackTagId, "Google Ads assessment tag id"),
2389
+ tagType: tag.type ?? "",
2390
+ recognition: assessment.recognition.status,
2391
+ recognitionReason: assessment.recognition.status === "unknown" ? assessment.recognition.reason : null,
2392
+ ...fields,
2393
+ triggerStrategy: triggerStrategy2(assessment, triggers),
2394
+ triggerIds: uniqueStrings(assessment.trigger.firingTriggerIds),
2395
+ triggerPredicates: triggers.filter((trigger) => trigger.triggerId && assessment.trigger.firingTriggerIds.includes(trigger.triggerId)).map(triggerPredicate).filter((predicate) => predicate !== null).sort((left, right) => left.triggerId.localeCompare(right.triggerId)),
2396
+ reviewReasons: mapReviewReasons(assessment, tag, fields)
2397
+ };
2398
+ }
2399
+ function toGtmConfigurationGraphDto(snapshot, options = {}) {
2400
+ const accountId = required(snapshot.identity.accountId, "snapshot account id");
2401
+ const containerId = required(snapshot.identity.containerId, "snapshot container id");
2402
+ const triggers = snapshot.entities.triggers.map((entity) => entity.raw);
2403
+ const variableIdsByName = /* @__PURE__ */ new Map();
2404
+ for (const entity of snapshot.entities.variables) {
2405
+ const name = entity.raw.name?.trim();
2406
+ if (!name) continue;
2407
+ const id = required(entity.raw.variableId ?? entity.id, "variable id");
2408
+ variableIdsByName.set(name, [...variableIdsByName.get(name) ?? [], id]);
2409
+ }
2410
+ return {
2411
+ accountId,
2412
+ containerId,
2413
+ workspaceId: snapshot.source === "workspace" ? required(snapshot.identity.workspaceId, "snapshot workspace id") : null,
2414
+ tags: snapshot.entities.tags.map((entity) => toGtmTagDto(entity.raw, entity.id, variableIdsByName)),
2415
+ triggers: snapshot.entities.triggers.map((entity) => toGtmTriggerDto(entity.raw, entity.id)),
2416
+ variables: snapshot.entities.variables.map((entity) => toGtmVariableDto(entity.raw, entity.id)),
2417
+ googleAdsTagAssessments: snapshot.entities.tags.map((entity) => toGtmGoogleAdsTagAssessmentDto(entity.raw, triggers, options, entity.id))
2418
+ };
2419
+ }
2420
+ function fetchedAt(options) {
2421
+ return options.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2422
+ }
2423
+ function toGtmLiveContainerGraphDto(snapshot, options = {}) {
2424
+ if (snapshot.source !== "live") {
2425
+ throw new GtmApiError("Live graph adapter requires a live snapshot", 400, {
2426
+ reason: "INVALID_SNAPSHOT_SOURCE"
2427
+ });
2428
+ }
2429
+ const version = snapshot.raw.containerVersion;
2430
+ if (!version) {
2431
+ throw new GtmApiError("Live snapshot is missing its container version", 502, {
2432
+ reason: "INVALID_PROVIDER_RESPONSE"
2433
+ });
2434
+ }
2435
+ return {
2436
+ source: "live",
2437
+ version: toGtmContainerVersionDto(version),
2438
+ graph: toGtmConfigurationGraphDto(snapshot, options),
2439
+ fetchedAt: fetchedAt(options)
2440
+ };
2441
+ }
2442
+ function toGtmDraftWorkspaceGraphDto(snapshot, options = {}) {
2443
+ if (snapshot.source !== "workspace") {
2444
+ throw new GtmApiError("Draft graph adapter requires a workspace snapshot", 400, {
2445
+ reason: "INVALID_SNAPSHOT_SOURCE"
2446
+ });
2447
+ }
2448
+ const workspace = snapshot.raw.workspace;
2449
+ if (!workspace) {
2450
+ throw new GtmApiError("Workspace snapshot is missing its workspace", 502, {
2451
+ reason: "INVALID_PROVIDER_RESPONSE"
2452
+ });
2453
+ }
2454
+ return {
2455
+ source: "draft",
2456
+ workspace: toGtmWorkspaceDto(workspace),
2457
+ graph: toGtmConfigurationGraphDto(snapshot, options),
2458
+ conflictCount: snapshot.raw.workspaceStatus?.mergeConflict?.length ?? 0,
2459
+ fetchedAt: fetchedAt(options)
2460
+ };
2461
+ }
2462
+
2463
+ // src/google-ads-config.ts
2464
+ function ensureConnections(config) {
2465
+ if (!config.googleAds) config.googleAds = {};
2466
+ if (!config.googleAds.connections) config.googleAds.connections = [];
2467
+ return config.googleAds.connections;
2468
+ }
2469
+ function getGoogleAdsAuthConfig(config) {
2470
+ return {
2471
+ clientId: config.googleAds?.clientId ?? config.google?.clientId,
2472
+ clientSecret: config.googleAds?.clientSecret ?? config.google?.clientSecret,
2473
+ developerToken: config.googleAds?.developerToken
2474
+ };
2475
+ }
2476
+ function setGoogleAdsAuthConfig(config, patch) {
2477
+ if (!config.googleAds) config.googleAds = {};
2478
+ if (patch.developerToken !== void 0) config.googleAds.developerToken = patch.developerToken;
2479
+ if (patch.clientId !== void 0) config.googleAds.clientId = patch.clientId;
2480
+ if (patch.clientSecret !== void 0) config.googleAds.clientSecret = patch.clientSecret;
2481
+ }
2482
+ function getGoogleAdsConnection(config, projectId) {
2483
+ return config.googleAds?.connections?.find((entry) => entry.projectId === projectId);
2484
+ }
2485
+ function upsertGoogleAdsConnection(config, connection) {
2486
+ const connections = ensureConnections(config);
2487
+ const index = connections.findIndex((entry) => entry.projectId === connection.projectId);
2488
+ if (index === -1) connections.push(connection);
2489
+ else connections[index] = connection;
2490
+ return connection;
2491
+ }
2492
+ function removeGoogleAdsConnection(config, projectId) {
2493
+ const connections = config.googleAds?.connections;
2494
+ if (!connections?.length) return false;
2495
+ const next = connections.filter((entry) => entry.projectId !== projectId);
2496
+ if (next.length === connections.length) return false;
2497
+ if (!config.googleAds) return false;
2498
+ config.googleAds.connections = next;
2499
+ return true;
2500
+ }
2501
+ function removeLegacyGoogleAdsConnections(config) {
2502
+ const connections = config.googleAds?.connections;
2503
+ if (!connections?.length) return 0;
2504
+ const next = connections.filter((entry) => typeof entry.projectId === "string" && entry.projectId.trim().length > 0);
2505
+ const removed = connections.length - next.length;
2506
+ if (removed > 0 && config.googleAds) config.googleAds.connections = next;
2507
+ return removed;
2508
+ }
2509
+ function removeOrphanedGoogleAdsConnections(config, projectIds) {
2510
+ const connections = config.googleAds?.connections;
2511
+ if (!connections?.length) return 0;
2512
+ const next = connections.filter((entry) => projectIds.has(entry.projectId));
2513
+ const removed = connections.length - next.length;
2514
+ if (removed > 0 && config.googleAds) config.googleAds.connections = next;
2515
+ return removed;
2516
+ }
2517
+
2518
+ // src/gtm-config.ts
2519
+ function ensureConnections2(config) {
2520
+ if (!config.gtm) config.gtm = {};
2521
+ if (!config.gtm.connections) config.gtm.connections = [];
2522
+ return config.gtm.connections;
2523
+ }
2524
+ function getGtmAuthConfig(config) {
2525
+ return {
2526
+ clientId: config.gtm?.clientId ?? config.google?.clientId,
2527
+ clientSecret: config.gtm?.clientSecret ?? config.google?.clientSecret
2528
+ };
2529
+ }
2530
+ function getGtmConnection(config, projectId) {
2531
+ return config.gtm?.connections?.find((entry) => entry.projectId === projectId);
2532
+ }
2533
+ function upsertGtmConnection(config, connection) {
2534
+ const connections = ensureConnections2(config);
2535
+ const index = connections.findIndex((entry) => entry.projectId === connection.projectId);
2536
+ if (index === -1) connections.push(connection);
2537
+ else connections[index] = connection;
2538
+ return connection;
2539
+ }
2540
+ function removeGtmConnection(config, projectId) {
2541
+ const connections = config.gtm?.connections;
2542
+ if (!connections?.length) return false;
2543
+ const next = connections.filter((entry) => entry.projectId !== projectId);
2544
+ if (next.length === connections.length) return false;
2545
+ if (!config.gtm) return false;
2546
+ config.gtm.connections = next;
2547
+ return true;
2548
+ }
2549
+ function removeLegacyGtmConnections(config) {
2550
+ const connections = config.gtm?.connections;
2551
+ if (!connections?.length) return 0;
2552
+ const next = connections.filter((entry) => typeof entry.projectId === "string" && entry.projectId.trim().length > 0);
2553
+ const removed = connections.length - next.length;
2554
+ if (removed > 0 && config.gtm) config.gtm.connections = next;
2555
+ return removed;
2556
+ }
2557
+ function removeOrphanedGtmConnections(config, projectIds) {
2558
+ const connections = config.gtm?.connections;
2559
+ if (!connections?.length) return 0;
2560
+ const next = connections.filter((entry) => projectIds.has(entry.projectId));
2561
+ const removed = connections.length - next.length;
2562
+ if (removed > 0 && config.gtm) config.gtm.connections = next;
2563
+ return removed;
2564
+ }
2565
+
2566
+ // src/google-marketing-runtime.ts
2567
+ var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
2568
+ var DEFAULT_LIST_LIMIT = 100;
2569
+ var MAX_LIST_LIMIT = 1e3;
2570
+ var GoogleMarketingRuntimeError = class extends Error {
2571
+ constructor(message, code) {
2572
+ super(message);
2573
+ this.code = code;
2574
+ }
2575
+ code;
2576
+ name = "GoogleMarketingRuntimeError";
2577
+ };
2578
+ function credentialGeneration(connection) {
2579
+ return crypto3.createHash("sha256").update(JSON.stringify({
2580
+ projectId: connection.projectId,
2581
+ credentialGeneration: connection.credentialGeneration ?? null,
2582
+ accessToken: connection.accessToken ?? null,
2583
+ refreshToken: connection.refreshToken ?? null,
2584
+ tokenExpiresAt: connection.tokenExpiresAt ?? null,
2585
+ scopes: connection.scopes ?? [],
2586
+ createdAt: connection.createdAt,
2587
+ updatedAt: connection.updatedAt
2588
+ })).digest("base64url");
2589
+ }
2590
+ function nonEmpty(value, label) {
2591
+ if (typeof value !== "string" || value.trim() === "") {
2592
+ throw new GoogleMarketingRuntimeError(`${label} is required`, "INVALID_INPUT");
2593
+ }
2594
+ return value.trim();
2595
+ }
2596
+ function validateProject(project) {
2597
+ return {
2598
+ id: nonEmpty(project.id, "Project ID"),
2599
+ name: nonEmpty(project.name, "Project name")
2600
+ };
2601
+ }
2602
+ function validatedListLimit(value) {
2603
+ const limit = value ?? DEFAULT_LIST_LIMIT;
2604
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
2605
+ throw new GoogleMarketingRuntimeError(
2606
+ `List limit must be an integer between 1 and ${MAX_LIST_LIMIT}`,
2607
+ "INVALID_INPUT"
2608
+ );
2609
+ }
2610
+ return limit;
2611
+ }
2612
+ function resourceId2(value, label) {
2613
+ const id = value.split("/").filter(Boolean).at(-1);
2614
+ if (!id) throw new GoogleMarketingRuntimeError(`${label} is invalid`, "INVALID_INPUT");
2615
+ return id;
2616
+ }
2617
+ function canonicalGtmAccountId(value) {
2618
+ const accountId = canonicalizeGtmAccountId(value);
2619
+ if (!accountId) {
2620
+ throw new GoogleMarketingRuntimeError("GTM account ID is invalid", "INVALID_INPUT");
2621
+ }
2622
+ return accountId;
2623
+ }
2624
+ function canonicalGtmContainerId(value, accountId) {
2625
+ const containerId = canonicalizeGtmContainerId(value, accountId);
2626
+ if (!containerId) {
2627
+ throw new GoogleMarketingRuntimeError("GTM container ID is invalid or belongs to another account", "INVALID_INPUT");
2628
+ }
2629
+ return containerId;
2630
+ }
2631
+ function accountPath(accountId) {
2632
+ return `accounts/${canonicalGtmAccountId(accountId)}`;
2633
+ }
2634
+ function containerPath(accountId, containerId) {
2635
+ const canonicalAccountId = canonicalGtmAccountId(accountId);
2636
+ return `accounts/${canonicalAccountId}/containers/${canonicalGtmContainerId(containerId, canonicalAccountId)}`;
2637
+ }
2638
+ function workspacePath(accountId, containerId, workspaceId) {
2639
+ const selection = canonicalizeGtmResourceSelection({ accountId, containerId, workspaceId });
2640
+ if (!selection?.workspaceId) {
2641
+ throw new GoogleMarketingRuntimeError("GTM workspace ID is invalid or belongs to another container", "INVALID_INPUT");
2642
+ }
2643
+ return `accounts/${selection.accountId}/containers/${selection.containerId}/workspaces/${selection.workspaceId}`;
2644
+ }
2645
+ function normalizedCustomerStatus(status) {
2646
+ switch (status?.toUpperCase()) {
2647
+ case "ENABLED":
2648
+ return GoogleAdsCustomerStatuses.enabled;
2649
+ case "SUSPENDED":
2650
+ return GoogleAdsCustomerStatuses.suspended;
2651
+ case "CLOSED":
2652
+ return GoogleAdsCustomerStatuses.closed;
2653
+ case "CANCELED":
2654
+ return GoogleAdsCustomerStatuses.canceled;
2655
+ case "UNSPECIFIED":
2656
+ return GoogleAdsCustomerStatuses.unspecified;
2657
+ default:
2658
+ return GoogleAdsCustomerStatuses.unknown;
2659
+ }
2660
+ }
2661
+ function accessibleCustomerDto(customer) {
2662
+ return googleAdsAccessibleCustomerDtoSchema.parse({
2663
+ resourceName: customer.resourceName,
2664
+ customerId: customer.id,
2665
+ parentCustomerId: null,
2666
+ descriptiveName: customer.descriptiveName ?? null,
2667
+ currencyCode: customer.currencyCode ?? null,
2668
+ timeZone: customer.timeZone ?? null,
2669
+ manager: customer.manager ?? false,
2670
+ hidden: false,
2671
+ testAccount: customer.testAccount ?? false,
2672
+ level: 0,
2673
+ status: normalizedCustomerStatus(customer.status)
2674
+ });
2675
+ }
2676
+ function accessibleCustomerClientDto(customer, loginCustomerId) {
2677
+ const level = Number(customer.level ?? "0");
2678
+ if (!Number.isSafeInteger(level) || level < 0) {
2679
+ throw new GoogleMarketingRuntimeError(
2680
+ "Google Ads returned an invalid customer hierarchy level",
2681
+ "INVALID_INPUT"
2682
+ );
2683
+ }
2684
+ return googleAdsAccessibleCustomerDtoSchema.parse({
2685
+ resourceName: customer.clientCustomer || `customers/${customer.id}`,
2686
+ customerId: customer.id,
2687
+ // Google Ads exposes the selected login-manager root and hierarchy depth,
2688
+ // but not a direct parent edge in this query. Retain the root as the
2689
+ // account context required to read any discovered child honestly.
2690
+ parentCustomerId: customer.id === loginCustomerId ? null : loginCustomerId,
2691
+ descriptiveName: customer.descriptiveName ?? null,
2692
+ currencyCode: customer.currencyCode ?? null,
2693
+ timeZone: customer.timeZone ?? null,
2694
+ manager: customer.manager ?? false,
2695
+ hidden: customer.hidden ?? false,
2696
+ testAccount: customer.testAccount ?? false,
2697
+ level,
2698
+ status: normalizedCustomerStatus(customer.status)
2699
+ });
2700
+ }
2701
+ function campaignStatus(status) {
2702
+ switch (status.toUpperCase()) {
2703
+ case "ENABLED":
2704
+ return GoogleAdsCampaignStatuses.enabled;
2705
+ case "PAUSED":
2706
+ return GoogleAdsCampaignStatuses.paused;
2707
+ case "REMOVED":
2708
+ return GoogleAdsCampaignStatuses.removed;
2709
+ default:
2710
+ return GoogleAdsCampaignStatuses.unknown;
2711
+ }
2712
+ }
2713
+ function conversionActionStatus(status) {
2714
+ switch (status.toUpperCase()) {
2715
+ case "ENABLED":
2716
+ return GoogleAdsConversionActionStatuses.enabled;
2717
+ case "HIDDEN":
2718
+ return GoogleAdsConversionActionStatuses.hidden;
2719
+ case "REMOVED":
2720
+ return GoogleAdsConversionActionStatuses.removed;
2721
+ default:
2722
+ return GoogleAdsConversionActionStatuses.unknown;
2723
+ }
2724
+ }
2725
+ function campaignDto(campaign) {
2726
+ return {
2727
+ id: campaign.id,
2728
+ resourceName: campaign.resourceName,
2729
+ name: campaign.name,
2730
+ status: campaignStatus(campaign.status),
2731
+ advertisingChannelType: campaign.advertisingChannelType ?? null,
2732
+ biddingStrategyType: campaign.biddingStrategyType ?? null
2733
+ };
2734
+ }
2735
+ function conversionActionDto(action) {
2736
+ return {
2737
+ id: action.id,
2738
+ resourceName: action.resourceName,
2739
+ name: action.name,
2740
+ status: conversionActionStatus(action.status),
2741
+ category: action.category,
2742
+ origin: action.origin,
2743
+ primaryForGoal: action.primaryForGoal ?? false,
2744
+ includeInConversionsMetric: action.includeInConversionsMetric ?? false
2745
+ };
2746
+ }
2747
+ function inventoryDto(customerId, fetchedAt2, campaigns, goals) {
2748
+ return {
2749
+ customerId,
2750
+ fetchedAt: fetchedAt2,
2751
+ campaigns: campaigns.map(campaignDto),
2752
+ conversionActions: goals.conversionActions.map((row) => conversionActionDto(row.conversionAction)),
2753
+ customerConversionGoals: goals.customerGoals.map((row) => ({
2754
+ category: row.customerConversionGoal.category,
2755
+ origin: row.customerConversionGoal.origin,
2756
+ biddable: row.customerConversionGoal.biddable ?? false
2757
+ })),
2758
+ campaignConversionGoals: goals.campaignGoals.map((row) => ({
2759
+ campaignId: row.campaign.id,
2760
+ category: row.campaignConversionGoal.category,
2761
+ origin: row.campaignConversionGoal.origin,
2762
+ biddable: row.campaignConversionGoal.biddable ?? false
2763
+ })),
2764
+ campaignConversionGoalsComplete: goals.campaignGoalsComplete === true,
2765
+ customConversionGoals: goals.customGoals.map((row) => ({
2766
+ id: row.customConversionGoal.id,
2767
+ name: row.customConversionGoal.name,
2768
+ conversionActionIds: (row.customConversionGoal.conversionActions ?? []).map((name) => resourceId2(name, "Conversion action resource name"))
2769
+ })),
2770
+ campaignGoalConfigurations: goals.campaignConfigs.map((row) => {
2771
+ const level = row.conversionGoalCampaignConfig.goalConfigLevel;
2772
+ if (level !== "CUSTOMER" && level !== "CAMPAIGN") {
2773
+ throw new GoogleMarketingRuntimeError(
2774
+ "Google Ads returned an unsupported campaign goal configuration level",
2775
+ "INVALID_INPUT"
2776
+ );
2777
+ }
2778
+ return {
2779
+ campaignId: row.campaign.id,
2780
+ goalConfigLevel: level === "CAMPAIGN" ? GoogleAdsGoalConfigurationLevels.campaign : GoogleAdsGoalConfigurationLevels.customer,
2781
+ customGoalId: row.conversionGoalCampaignConfig.customConversionGoal ? resourceId2(
2782
+ row.conversionGoalCampaignConfig.customConversionGoal,
2783
+ "Custom conversion goal resource name"
2784
+ ) : null
2785
+ };
2786
+ })
2787
+ };
2788
+ }
2789
+ function nonnegativeInteger(value, label) {
2790
+ const parsed = Number(value ?? "0");
2791
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
2792
+ throw new GoogleMarketingRuntimeError(`Google Ads returned an invalid ${label}`, "INVALID_INPUT");
2793
+ }
2794
+ return parsed;
2795
+ }
2796
+ function conversionValueMicros(metrics) {
2797
+ if (metrics.conversionsValue === void 0) return null;
2798
+ const value = Math.round(metrics.conversionsValue * 1e6);
2799
+ if (!Number.isSafeInteger(value) || value < 0) {
2800
+ throw new GoogleMarketingRuntimeError(
2801
+ "Google Ads returned an invalid conversion value",
2802
+ "INVALID_INPUT"
2803
+ );
2804
+ }
2805
+ return value;
2806
+ }
2807
+ function campaignMetricDto(row) {
2808
+ return {
2809
+ campaignId: row.campaign.id,
2810
+ date: row.segments.date,
2811
+ impressions: nonnegativeInteger(row.metrics.impressions, "impression count"),
2812
+ clicks: nonnegativeInteger(row.metrics.clicks, "click count"),
2813
+ costMicros: nonnegativeInteger(row.metrics.costMicros, "cost"),
2814
+ conversions: row.metrics.conversions ?? 0,
2815
+ conversionValueMicros: conversionValueMicros(row.metrics)
2816
+ };
2817
+ }
2818
+ function defaultMetricsQuery(now, campaigns, timeZone) {
2819
+ const campaignIds = campaigns.map((campaign) => campaign.id).sort((left, right) => left.localeCompare(right));
2820
+ const selected = campaignIds.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_CAMPAIGNS);
2821
+ if (selected.length === 0) return { query: null, inventoryTruncated: false };
2822
+ const customerTimeZone = typeof timeZone === "string" && timeZone.trim() !== "" ? timeZone : "UTC";
2823
+ const endDate = formatIsoDateInTimeZone(now.toISOString(), customerTimeZone);
2824
+ return {
2825
+ query: googleAdsCampaignMetricsQuerySchema.parse({
2826
+ campaignIds: selected,
2827
+ startDate: shiftIsoCalendarDate(endDate, -(GOOGLE_ADS_CAMPAIGN_METRICS_MAX_DAYS - 1)),
2828
+ endDate
2829
+ }),
2830
+ inventoryTruncated: campaignIds.length > selected.length
2831
+ };
2832
+ }
2833
+ function countRedactedProviderValues(value) {
2834
+ if (value === null || typeof value !== "object") return 0;
2835
+ if (Array.isArray(value)) {
2836
+ let total = 0;
2837
+ for (const item of value) total += countRedactedProviderValues(item);
2838
+ return total;
2839
+ }
2840
+ return Object.entries(value).reduce((total, [key, item]) => {
2841
+ const own = key === "value" || key === "globalSiteTag" || key === "eventSnippet" ? 1 : 0;
2842
+ return total + own + countRedactedProviderValues(item);
2843
+ }, 0);
2844
+ }
2845
+ function rawMetadata(value) {
2846
+ return {
2847
+ // The provider clients intentionally do not expose exact response bytes.
2848
+ // A hash of normalized objects would not be an honest raw-body checksum.
2849
+ rawPayloadSha256: null,
2850
+ rawPayloadBytes: null,
2851
+ redactedFieldCount: countRedactedProviderValues(value)
2852
+ };
2853
+ }
2854
+ var ConfigCredentialStore = class {
2855
+ #config;
2856
+ #saveConfigPatch;
2857
+ #now;
2858
+ #refresh;
2859
+ #refreshes = /* @__PURE__ */ new Map();
2860
+ constructor(options) {
2861
+ this.#config = options.config;
2862
+ this.#saveConfigPatch = options.saveConfigPatch;
2863
+ this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
2864
+ this.#refresh = options.refreshAccessToken ?? refreshAccessToken;
2865
+ }
2866
+ async getCredential(project, provider) {
2867
+ const normalizedProject = validateProject(project);
2868
+ const accessToken = await this.#accessToken(normalizedProject, provider);
2869
+ return { accessToken };
2870
+ }
2871
+ async #accessToken(project, provider) {
2872
+ const connection = provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsConnection(this.#config, project.id) : getGtmConnection(this.#config, project.id);
2873
+ if (!connection) {
2874
+ throw new GoogleMarketingRuntimeError(
2875
+ `${provider === GoogleMarketingProviders["google-ads"] ? "Google Ads" : "GTM"} is not connected for this project`,
2876
+ "CONNECTION_NOT_FOUND"
2877
+ );
2878
+ }
2879
+ if (this.#hasFreshAccessToken(connection)) return connection.accessToken;
2880
+ const key = `${provider}\0${project.id}\0${credentialGeneration(connection)}`;
2881
+ const pending = this.#refreshes.get(key);
2882
+ if (pending) return pending;
2883
+ const refresh = this.#refreshConnection(project, provider, connection);
2884
+ this.#refreshes.set(key, refresh);
2885
+ try {
2886
+ return await refresh;
2887
+ } finally {
2888
+ if (this.#refreshes.get(key) === refresh) this.#refreshes.delete(key);
2889
+ }
2890
+ }
2891
+ #hasFreshAccessToken(connection) {
2892
+ if (!connection.accessToken || !connection.tokenExpiresAt) return false;
2893
+ const expiresAt = Date.parse(connection.tokenExpiresAt);
2894
+ return Number.isFinite(expiresAt) && this.#now().getTime() < expiresAt - TOKEN_REFRESH_SKEW_MS;
2895
+ }
2896
+ #connection(provider, projectId) {
2897
+ return provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsConnection(this.#config, projectId) : getGtmConnection(this.#config, projectId);
2898
+ }
2899
+ #connectionHasGeneration(provider, projectId, expectedGeneration) {
2900
+ const current = this.#connection(provider, projectId);
2901
+ return current !== void 0 && credentialGeneration(current) === expectedGeneration;
2902
+ }
2903
+ #restoreConnection(provider, connection) {
2904
+ if (provider === GoogleMarketingProviders["google-ads"]) {
2905
+ upsertGoogleAdsConnection(this.#config, connection);
2906
+ } else {
2907
+ upsertGtmConnection(this.#config, connection);
2908
+ }
2909
+ }
2910
+ async #refreshConnection(project, provider, connection) {
2911
+ if (!connection.refreshToken) {
2912
+ throw new GoogleMarketingRuntimeError(
2913
+ "Google OAuth access has expired and no refresh token is available",
2914
+ "TOKEN_REFRESH_REQUIRED"
2915
+ );
2916
+ }
2917
+ const auth = provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsAuthConfig(this.#config) : getGtmAuthConfig(this.#config);
2918
+ if (!auth.clientId || !auth.clientSecret) {
2919
+ throw new GoogleMarketingRuntimeError(
2920
+ "Google OAuth client credentials are not configured",
2921
+ "NOT_CONFIGURED"
2922
+ );
2923
+ }
2924
+ const generationBeforeRefresh = credentialGeneration(connection);
2925
+ const tokens = await this.#refresh(auth.clientId, auth.clientSecret, connection.refreshToken);
2926
+ if (typeof tokens.access_token !== "string" || tokens.access_token.trim() === "" || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0) {
2927
+ throw new GoogleMarketingRuntimeError(
2928
+ "Google OAuth returned an invalid token response",
2929
+ "INVALID_TOKEN_RESPONSE"
2930
+ );
2931
+ }
2932
+ const now = this.#now();
2933
+ const patch = {
2934
+ accessToken: tokens.access_token,
2935
+ refreshToken: tokens.refresh_token ?? connection.refreshToken,
2936
+ tokenExpiresAt: new Date(now.getTime() + tokens.expires_in * 1e3).toISOString(),
2937
+ ...tokens.scope ? { scopes: tokens.scope.split(/\s+/).filter(Boolean) } : {},
2938
+ updatedAt: now.toISOString()
2939
+ };
2940
+ if (!this.#connectionHasGeneration(provider, project.id, generationBeforeRefresh)) {
2941
+ throw new GoogleMarketingRuntimeError(
2942
+ `${provider === GoogleMarketingProviders["google-ads"] ? "Google Ads" : "GTM"} connection changed during token refresh`,
2943
+ "CONNECTION_NOT_FOUND"
2944
+ );
2945
+ }
2946
+ const refreshedConnection = { ...connection, ...patch };
2947
+ const refreshedGeneration = credentialGeneration(refreshedConnection);
2948
+ this.#restoreConnection(provider, refreshedConnection);
2949
+ try {
2950
+ await this.#saveConfigPatch(
2951
+ provider === GoogleMarketingProviders["google-ads"] ? { googleAds: this.#config.googleAds } : { gtm: this.#config.gtm }
2952
+ );
2953
+ } catch (error) {
2954
+ if (this.#connectionHasGeneration(provider, project.id, refreshedGeneration)) {
2955
+ this.#restoreConnection(provider, connection);
2956
+ }
2957
+ throw error;
2958
+ }
2959
+ return tokens.access_token;
2960
+ }
2961
+ };
2962
+ var DefaultGoogleMarketingRuntime = class {
2963
+ #config;
2964
+ #credentials;
2965
+ #env;
2966
+ #fetch;
2967
+ #now;
2968
+ #randomUUID;
2969
+ constructor(options) {
2970
+ this.#config = options.config;
2971
+ this.#credentials = new ConfigCredentialStore(options);
2972
+ this.#env = options.env ?? process.env;
2973
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
2974
+ this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
2975
+ this.#randomUUID = options.randomUUID ?? crypto3.randomUUID;
2976
+ }
2977
+ async listGoogleAdsCustomers(project, options = {}) {
2978
+ const normalizedProject = validateProject(project);
2979
+ const client = await this.#adsClient(normalizedProject, options.loginCustomerId);
2980
+ const result = await client.listAccessibleCustomerDetails({ maxCustomers: options.maxCustomers });
2981
+ const fetchedAt2 = this.#now().toISOString();
2982
+ const customersById = new Map(
2983
+ result.data.customers.map((customer) => {
2984
+ const dto = accessibleCustomerDto(customer);
2985
+ return [dto.customerId, dto];
2986
+ })
2987
+ );
2988
+ let hierarchyTruncated = false;
2989
+ for (const root of result.data.customers) {
2990
+ if (!root.manager) continue;
2991
+ const remaining = GOOGLE_ADS_MAX_CUSTOMER_CLIENTS - customersById.size;
2992
+ if (remaining <= 0) {
2993
+ hierarchyTruncated = true;
2994
+ break;
2995
+ }
2996
+ try {
2997
+ const hierarchy = await client.getCustomerClients(root.id, { maxCustomers: remaining });
2998
+ hierarchyTruncated ||= hierarchy.data.truncated;
2999
+ for (const customer of hierarchy.data.customerClients) {
3000
+ const dto = accessibleCustomerClientDto(customer, root.id);
3001
+ const existing = customersById.get(dto.customerId);
3002
+ if (!existing || dto.level < existing.level) customersById.set(dto.customerId, dto);
3003
+ }
3004
+ } catch {
3005
+ hierarchyTruncated = true;
3006
+ }
3007
+ }
3008
+ const customers = [...customersById.values()].sort((left, right) => left.level - right.level || left.customerId.localeCompare(right.customerId));
3009
+ return googleAdsAccessibleCustomersResponseSchema.parse({
3010
+ customers,
3011
+ totalAccessible: customers.length,
3012
+ truncated: result.data.truncated || result.data.failures.length > 0 || hierarchyTruncated,
3013
+ selection: {
3014
+ loginCustomerId: options.loginCustomerId ? normalizeGoogleAdsCustomerId(options.loginCustomerId, "Login customer ID") : null,
3015
+ customerId: null,
3016
+ selectedAt: null
3017
+ },
3018
+ fetchedAt: fetchedAt2
3019
+ });
3020
+ }
3021
+ async getGoogleAdsCustomer(project, input) {
3022
+ const normalizedProject = validateProject(project);
3023
+ const client = await this.#adsClient(normalizedProject, input.loginCustomerId);
3024
+ const result = await client.getCustomerDetails(input.customerId);
3025
+ return accessibleCustomerDto(result.data);
3026
+ }
3027
+ async listGtmAccounts(project, options = {}) {
3028
+ const limit = validatedListLimit(options.maxResults);
3029
+ const client = await this.#gtmClient(validateProject(project));
3030
+ const accounts = (await client.listAccounts()).map(toGtmAccountDto);
3031
+ return gtmAccountsResponseSchema.parse({
3032
+ accounts: accounts.slice(0, limit),
3033
+ totalAccessible: accounts.length,
3034
+ truncated: accounts.length > limit,
3035
+ fetchedAt: this.#now().toISOString()
3036
+ });
3037
+ }
3038
+ async listGtmContainers(project, accountId, options = {}) {
3039
+ const limit = validatedListLimit(options.maxResults);
3040
+ const normalizedAccountId = canonicalGtmAccountId(accountId);
3041
+ const client = await this.#gtmClient(validateProject(project));
3042
+ const containers = (await client.listContainers(accountPath(normalizedAccountId))).map(toGtmContainerDto);
3043
+ return gtmContainerListResponseSchema.parse({
3044
+ accountId: normalizedAccountId,
3045
+ containers: containers.slice(0, limit),
3046
+ totalAccessible: containers.length,
3047
+ truncated: containers.length > limit,
3048
+ fetchedAt: this.#now().toISOString()
3049
+ });
3050
+ }
3051
+ async listGtmWorkspaces(project, accountId, containerId, options = {}) {
3052
+ const limit = validatedListLimit(options.maxResults);
3053
+ const normalizedAccountId = canonicalGtmAccountId(accountId);
3054
+ const normalizedContainerId = canonicalGtmContainerId(containerId, normalizedAccountId);
3055
+ const client = await this.#gtmClient(validateProject(project));
3056
+ const workspaces = (await client.listWorkspaces(
3057
+ containerPath(normalizedAccountId, normalizedContainerId)
3058
+ )).map(toGtmWorkspaceDto);
3059
+ return gtmWorkspaceListResponseSchema.parse({
3060
+ accountId: normalizedAccountId,
3061
+ containerId: normalizedContainerId,
3062
+ workspaces: workspaces.slice(0, limit),
3063
+ totalAccessible: workspaces.length,
3064
+ truncated: workspaces.length > limit,
3065
+ fetchedAt: this.#now().toISOString()
3066
+ });
3067
+ }
3068
+ async syncGoogleAds(input) {
3069
+ const project = validateProject(input.project);
3070
+ const context = this.#snapshotContext(project, input.connectionId, input.runId);
3071
+ const customerId = normalizeGoogleAdsCustomerId(input.selection.customerId);
3072
+ const suppliedMetricsQuery = input.metricsQuery ? googleAdsCampaignMetricsQuerySchema.parse(input.metricsQuery) : void 0;
3073
+ const client = await this.#adsClient(project, input.selection.loginCustomerId);
3074
+ const [customerResult, campaignResult, goalResult] = await Promise.all([
3075
+ client.getCustomerDetails(customerId),
3076
+ client.getCampaigns(customerId, { limit: GOOGLE_ADS_MAX_RESULT_ROWS }),
3077
+ client.getConversionGoals(customerId)
3078
+ ]);
3079
+ const capturedAt = this.#now().toISOString();
3080
+ const accessibleCustomersData = googleAdsAccessibleCustomersResponseSchema.parse({
3081
+ customers: [accessibleCustomerDto(customerResult.data)],
3082
+ totalAccessible: 1,
3083
+ truncated: false,
3084
+ selection: {
3085
+ loginCustomerId: input.selection.loginCustomerId ? normalizeGoogleAdsCustomerId(input.selection.loginCustomerId, "Login customer ID") : null,
3086
+ customerId,
3087
+ selectedAt: input.selection.selectedAt ?? null
3088
+ },
3089
+ fetchedAt: capturedAt
3090
+ });
3091
+ const accessibleCustomersPayload = {
3092
+ kind: GoogleAdsSnapshotKinds["accessible-customers"],
3093
+ data: accessibleCustomersData
3094
+ };
3095
+ const accessibleCustomersSnapshot = this.#googleAdsSnapshot(
3096
+ context,
3097
+ accessibleCustomersPayload,
3098
+ customerId,
3099
+ customerResult.data,
3100
+ capturedAt
3101
+ );
3102
+ const inventory = inventoryDto(
3103
+ customerId,
3104
+ capturedAt,
3105
+ campaignResult.data.map((row) => row.campaign),
3106
+ goalResult.data
3107
+ );
3108
+ const inventoryPayload = {
3109
+ kind: GoogleAdsSnapshotKinds.inventory,
3110
+ data: inventory
3111
+ };
3112
+ const inventoryRaw = {
3113
+ campaigns: campaignResult.data,
3114
+ conversionGoals: goalResult.data
3115
+ };
3116
+ const inventorySnapshot = this.#googleAdsSnapshot(
3117
+ context,
3118
+ inventoryPayload,
3119
+ customerId,
3120
+ inventoryRaw,
3121
+ capturedAt
3122
+ );
3123
+ const effectiveGoalGraph = deriveGoogleAdsEffectiveGoalGraph(inventory);
3124
+ const defaults = defaultMetricsQuery(this.#now(), inventory.campaigns, customerResult.data.timeZone);
3125
+ const metricsQuery = suppliedMetricsQuery ?? defaults.query;
3126
+ if (!metricsQuery) {
3127
+ return {
3128
+ accessibleCustomers: accessibleCustomersSnapshot,
3129
+ inventory: inventorySnapshot,
3130
+ metrics: null,
3131
+ effectiveGoalGraph
3132
+ };
3133
+ }
3134
+ const metricResult = await client.getDailyCampaignMetrics(customerId, {
3135
+ ...metricsQuery,
3136
+ limit: GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS + 1
3137
+ });
3138
+ const metricRows = metricResult.data.map(campaignMetricDto);
3139
+ const response = {
3140
+ query: metricsQuery,
3141
+ rows: metricRows.slice(0, GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS),
3142
+ truncated: metricRows.length > GOOGLE_ADS_CAMPAIGN_METRICS_MAX_ROWS || suppliedMetricsQuery === void 0 && defaults.inventoryTruncated,
3143
+ fetchedAt: this.#now().toISOString()
3144
+ };
3145
+ const metricsPayload = {
3146
+ kind: GoogleAdsSnapshotKinds["campaign-metrics"],
3147
+ data: response
3148
+ };
3149
+ const metricsSnapshot = this.#googleAdsSnapshot(
3150
+ context,
3151
+ metricsPayload,
3152
+ customerId,
3153
+ metricResult.data,
3154
+ response.fetchedAt
3155
+ );
3156
+ return {
3157
+ accessibleCustomers: accessibleCustomersSnapshot,
3158
+ inventory: inventorySnapshot,
3159
+ metrics: metricsSnapshot,
3160
+ effectiveGoalGraph
3161
+ };
3162
+ }
3163
+ async syncGtm(input) {
3164
+ const project = validateProject(input.project);
3165
+ const context = this.#snapshotContext(project, input.connectionId, input.runId);
3166
+ const selected = canonicalizeGtmResourceSelection({
3167
+ accountId: input.selection.accountId,
3168
+ containerId: input.selection.containerId,
3169
+ ...input.selection.workspaceId === void 0 || input.selection.workspaceId === null ? {} : { workspaceId: input.selection.workspaceId }
3170
+ });
3171
+ if (!selected) {
3172
+ throw new GoogleMarketingRuntimeError("GTM resource selection is invalid", "INVALID_INPUT");
3173
+ }
3174
+ const selectedAccountId = selected.accountId;
3175
+ const selectedContainerId = selected.containerId;
3176
+ const selectedWorkspaceId = selected.workspaceId ?? null;
3177
+ const client = await this.#gtmClient(project);
3178
+ const accounts = await client.listAccounts();
3179
+ const accountDtos = accounts.map(toGtmAccountDto);
3180
+ const account = accountDtos.find((candidate) => candidate.id === selectedAccountId);
3181
+ if (!account) {
3182
+ throw new GoogleMarketingRuntimeError("Selected GTM account was not found", "RESOURCE_NOT_FOUND");
3183
+ }
3184
+ const selectedContainerPath = containerPath(selectedAccountId, selectedContainerId);
3185
+ const [containers, workspaces] = await Promise.all([
3186
+ client.listContainers(accountPath(selectedAccountId)),
3187
+ client.listWorkspaces(selectedContainerPath)
3188
+ ]);
3189
+ const containerDtos = containers.map(toGtmContainerDto);
3190
+ const container = containerDtos.find((candidate) => candidate.id === selectedContainerId);
3191
+ if (!container) {
3192
+ throw new GoogleMarketingRuntimeError("Selected GTM container was not found", "RESOURCE_NOT_FOUND");
3193
+ }
3194
+ const workspaceDtos = workspaces.map(toGtmWorkspaceDto);
3195
+ if (workspaceDtos.length > MAX_LIST_LIMIT) {
3196
+ throw new GoogleMarketingRuntimeError(
3197
+ `GTM workspace inventory exceeds the ${MAX_LIST_LIMIT}-workspace safety limit`,
3198
+ "INVALID_INPUT"
3199
+ );
3200
+ }
3201
+ const selectedWorkspace = selectedWorkspaceId ? workspaceDtos.find((candidate) => candidate.id === selectedWorkspaceId) : void 0;
3202
+ if (selectedWorkspaceId && !selectedWorkspace) {
3203
+ throw new GoogleMarketingRuntimeError("Selected GTM workspace was not found", "RESOURCE_NOT_FOUND");
3204
+ }
3205
+ const snapshotReads = [
3206
+ client.getLiveSnapshot(selectedContainerPath)
3207
+ ];
3208
+ if (selectedWorkspaceId) {
3209
+ snapshotReads.push(client.getWorkspaceSnapshot(
3210
+ workspacePath(selectedAccountId, selectedContainerId, selectedWorkspaceId)
3211
+ ));
3212
+ }
3213
+ const snapshots = await Promise.all(snapshotReads);
3214
+ const capturedAt = this.#now().toISOString();
3215
+ const adapterOptions = {
3216
+ fetchedAt: capturedAt,
3217
+ ...input.expectedEventName ? { expectedEventName: input.expectedEventName } : {},
3218
+ ...input.expectedHostname ? { expectedHostname: input.expectedHostname } : {}
3219
+ };
3220
+ const live = toGtmLiveContainerGraphDto(snapshots[0], adapterOptions);
3221
+ const draft = snapshots[1] ? toGtmDraftWorkspaceGraphDto(snapshots[1], adapterOptions) : null;
3222
+ const payload = {
3223
+ kind: GtmSnapshotKinds.container,
3224
+ data: {
3225
+ account,
3226
+ container,
3227
+ workspaces: workspaceDtos,
3228
+ live,
3229
+ draft,
3230
+ fetchedAt: capturedAt
3231
+ }
3232
+ };
3233
+ const raw = { accounts, containers, workspaces, snapshots };
3234
+ return this.#gtmSnapshot(
3235
+ context,
3236
+ payload,
3237
+ selectedAccountId,
3238
+ selectedContainerId,
3239
+ selectedWorkspaceId,
3240
+ raw,
3241
+ capturedAt
3242
+ );
3243
+ }
3244
+ async #adsClient(project, loginCustomerId) {
3245
+ const { accessToken } = await this.#credentials.getCredential(
3246
+ project,
3247
+ GoogleMarketingProviders["google-ads"]
3248
+ );
3249
+ const auth = getGoogleAdsAuthConfig(this.#config);
3250
+ const developerToken = this.#env.GOOGLE_ADS_DEVELOPER_TOKEN?.trim() || auth.developerToken?.trim();
3251
+ if (!developerToken) {
3252
+ throw new GoogleMarketingRuntimeError(
3253
+ "Google Ads developer token is not configured",
3254
+ "NOT_CONFIGURED"
3255
+ );
3256
+ }
3257
+ return new GoogleAdsClient({
3258
+ accessToken,
3259
+ developerToken,
3260
+ ...loginCustomerId ? { loginCustomerId } : {}
3261
+ }, { fetch: this.#fetch });
3262
+ }
3263
+ async #gtmClient(project) {
3264
+ const { accessToken } = await this.#credentials.getCredential(
3265
+ project,
3266
+ GoogleMarketingProviders.gtm
3267
+ );
3268
+ return createGoogleTagManagerClient(accessToken, { fetch: this.#fetch });
3269
+ }
3270
+ #snapshotContext(project, connectionId, runId) {
3271
+ return {
3272
+ project,
3273
+ connectionId: nonEmpty(connectionId, "Connection ID"),
3274
+ runId: nonEmpty(runId, "Run ID")
3275
+ };
3276
+ }
3277
+ #googleAdsSnapshot(context, payload, customerId, raw, capturedAt) {
3278
+ return googleAdsRawSnapshotDtoSchema.parse({
3279
+ metadata: {
3280
+ id: this.#randomUUID(),
3281
+ projectId: context.project.id,
3282
+ connectionId: context.connectionId,
3283
+ runId: context.runId,
3284
+ kind: payload.kind,
3285
+ customerId,
3286
+ payloadChecksum: checksumJson(payload),
3287
+ ...rawMetadata(raw),
3288
+ capturedAt,
3289
+ createdAt: capturedAt
3290
+ },
3291
+ payload
3292
+ });
3293
+ }
3294
+ #gtmSnapshot(context, payload, accountId, containerId, workspaceId, raw, capturedAt) {
3295
+ return gtmRawSnapshotDtoSchema.parse({
3296
+ metadata: {
3297
+ id: this.#randomUUID(),
3298
+ projectId: context.project.id,
3299
+ connectionId: context.connectionId,
3300
+ runId: context.runId,
3301
+ kind: payload.kind,
3302
+ accountId,
3303
+ containerId,
3304
+ workspaceId,
3305
+ payloadChecksum: checksumJson(payload),
3306
+ ...rawMetadata(raw),
3307
+ capturedAt,
3308
+ createdAt: capturedAt
3309
+ },
3310
+ payload
3311
+ });
3312
+ }
3313
+ };
3314
+ function createGoogleMarketingCredentialStore(options) {
3315
+ return new ConfigCredentialStore(options);
3316
+ }
3317
+ function createGoogleMarketingRuntime(options) {
3318
+ return new DefaultGoogleMarketingRuntime(options);
3319
+ }
3320
+
579
3321
  // src/server.ts
580
3322
  import { createRequire as createRequire4 } from "module";
581
- import crypto24 from "crypto";
3323
+ import crypto26 from "crypto";
582
3324
  import fs7 from "fs";
583
3325
  import path8 from "path";
584
3326
  import { fileURLToPath as fileURLToPath2 } from "url";
585
- import { and as and18, eq as eq24 } from "drizzle-orm";
3327
+ import { and as and19, eq as eq25 } from "drizzle-orm";
586
3328
  import Fastify from "fastify";
587
3329
  import os4 from "os";
588
3330
 
@@ -2977,7 +5719,7 @@ function getPlacesConfig(config) {
2977
5719
  }
2978
5720
 
2979
5721
  // src/google-config.ts
2980
- function ensureConnections(config) {
5722
+ function ensureConnections3(config) {
2981
5723
  if (!config.google) config.google = {};
2982
5724
  if (!config.google.connections) config.google.connections = [];
2983
5725
  return config.google.connections;
@@ -3006,7 +5748,7 @@ function getGoogleConnection(config, domain, connectionType) {
3006
5748
  return (config.google?.connections ?? []).find((connection) => connection.domain === domain && connection.connectionType === connectionType);
3007
5749
  }
3008
5750
  function upsertGoogleConnection(config, connection) {
3009
- const connections = ensureConnections(config);
5751
+ const connections = ensureConnections3(config);
3010
5752
  const index = connections.findIndex((entry) => entry.domain === connection.domain && entry.connectionType === connection.connectionType);
3011
5753
  const normalized = {
3012
5754
  ...connection,
@@ -3051,7 +5793,7 @@ function removeGoogleConnection(config, domain, connectionType) {
3051
5793
  }
3052
5794
 
3053
5795
  // src/ga4-config.ts
3054
- function ensureConnections2(config) {
5796
+ function ensureConnections4(config) {
3055
5797
  if (!config.ga4) config.ga4 = {};
3056
5798
  if (!config.ga4.connections) config.ga4.connections = [];
3057
5799
  return config.ga4.connections;
@@ -3060,7 +5802,7 @@ function getGa4Connection(config, projectName) {
3060
5802
  return (config.ga4?.connections ?? []).find((c) => c.projectName === projectName);
3061
5803
  }
3062
5804
  function upsertGa4Connection(config, connection) {
3063
- const connections = ensureConnections2(config);
5805
+ const connections = ensureConnections4(config);
3064
5806
  const index = connections.findIndex((c) => c.projectName === connection.projectName);
3065
5807
  if (index === -1) {
3066
5808
  connections.push(connection);
@@ -3083,7 +5825,7 @@ function removeGa4Connection(config, projectName) {
3083
5825
  }
3084
5826
 
3085
5827
  // src/cloud-run-config.ts
3086
- function ensureConnections3(config) {
5828
+ function ensureConnections5(config) {
3087
5829
  if (!config.cloudRun) config.cloudRun = {};
3088
5830
  if (!config.cloudRun.connections) config.cloudRun.connections = [];
3089
5831
  return config.cloudRun.connections;
@@ -3092,7 +5834,7 @@ function getCloudRunConnection(config, projectName) {
3092
5834
  return (config.cloudRun?.connections ?? []).find((c) => c.projectName === projectName);
3093
5835
  }
3094
5836
  function upsertCloudRunConnection(config, connection) {
3095
- const connections = ensureConnections3(config);
5837
+ const connections = ensureConnections5(config);
3096
5838
  const index = connections.findIndex((c) => c.projectName === connection.projectName);
3097
5839
  if (index === -1) {
3098
5840
  connections.push(connection);
@@ -3115,7 +5857,7 @@ function removeCloudRunConnection(config, projectName) {
3115
5857
  }
3116
5858
 
3117
5859
  // src/wordpress-traffic-config.ts
3118
- function ensureConnections4(config) {
5860
+ function ensureConnections6(config) {
3119
5861
  if (!config.wordpressTraffic) config.wordpressTraffic = {};
3120
5862
  if (!config.wordpressTraffic.connections) config.wordpressTraffic.connections = [];
3121
5863
  return config.wordpressTraffic.connections;
@@ -3124,7 +5866,7 @@ function getWordpressTrafficConnection(config, projectName) {
3124
5866
  return (config.wordpressTraffic?.connections ?? []).find((c) => c.projectName === projectName);
3125
5867
  }
3126
5868
  function upsertWordpressTrafficConnection(config, connection) {
3127
- const connections = ensureConnections4(config);
5869
+ const connections = ensureConnections6(config);
3128
5870
  const index = connections.findIndex((c) => c.projectName === connection.projectName);
3129
5871
  if (index === -1) {
3130
5872
  connections.push(connection);
@@ -3147,7 +5889,7 @@ function removeWordpressTrafficConnection(config, projectName) {
3147
5889
  }
3148
5890
 
3149
5891
  // src/vercel-traffic-config.ts
3150
- function ensureConnections5(config) {
5892
+ function ensureConnections7(config) {
3151
5893
  if (!config.vercelTraffic) config.vercelTraffic = {};
3152
5894
  if (!config.vercelTraffic.connections) config.vercelTraffic.connections = [];
3153
5895
  return config.vercelTraffic.connections;
@@ -3156,7 +5898,7 @@ function getVercelTrafficConnection(config, projectName) {
3156
5898
  return (config.vercelTraffic?.connections ?? []).find((c) => c.projectName === projectName);
3157
5899
  }
3158
5900
  function upsertVercelTrafficConnection(config, connection) {
3159
- const connections = ensureConnections5(config);
5901
+ const connections = ensureConnections7(config);
3160
5902
  const index = connections.findIndex((c) => c.projectName === connection.projectName);
3161
5903
  if (index === -1) {
3162
5904
  connections.push(connection);
@@ -3183,7 +5925,7 @@ function unsupportedDeliveryMode(deliveryMode) {
3183
5925
  const label = typeof deliveryMode === "string" ? ` "${deliveryMode}"` : "";
3184
5926
  return new Error(`Unsupported Cloudflare traffic delivery mode${label}`);
3185
5927
  }
3186
- function ensureConnections6(config) {
5928
+ function ensureConnections8(config) {
3187
5929
  if (!config.cloudflareTraffic) config.cloudflareTraffic = {};
3188
5930
  if (!config.cloudflareTraffic.connections) config.cloudflareTraffic.connections = [];
3189
5931
  return config.cloudflareTraffic.connections;
@@ -3213,7 +5955,7 @@ function getCloudflareTrafficConnectionBySourceId(config, sourceId) {
3213
5955
  }
3214
5956
  function upsertCloudflareTrafficConnection(config, connection) {
3215
5957
  normalizeCloudflareTrafficConnection(connection);
3216
- const connections = ensureConnections6(config);
5958
+ const connections = ensureConnections8(config);
3217
5959
  const index = connections.findIndex((c) => c.sourceId === connection.sourceId);
3218
5960
  if (index === -1) {
3219
5961
  connections.push(connection);
@@ -3252,7 +5994,7 @@ function buildCloudflareIngestUrlTemplate(config) {
3252
5994
  }
3253
5995
 
3254
5996
  // src/wordpress-config.ts
3255
- function ensureConnections7(config) {
5997
+ function ensureConnections9(config) {
3256
5998
  if (!config.wordpress) config.wordpress = {};
3257
5999
  if (!config.wordpress.connections) config.wordpress.connections = [];
3258
6000
  return config.wordpress.connections;
@@ -3269,7 +6011,7 @@ function getWordpressConnection(config, projectName) {
3269
6011
  return (config.wordpress?.connections ?? []).find((connection) => connection.projectName === projectName);
3270
6012
  }
3271
6013
  function upsertWordpressConnection(config, connection) {
3272
- const connections = ensureConnections7(config);
6014
+ const connections = ensureConnections9(config);
3273
6015
  const normalized = normalizeConnection(connection);
3274
6016
  const index = connections.findIndex((entry) => entry.projectName === connection.projectName);
3275
6017
  if (index === -1) {
@@ -3303,14 +6045,14 @@ function removeWordpressConnection(config, projectName) {
3303
6045
  }
3304
6046
 
3305
6047
  // src/job-runner.ts
3306
- import crypto4 from "crypto";
6048
+ import crypto6 from "crypto";
3307
6049
  import fs2 from "fs";
3308
6050
  import path3 from "path";
3309
6051
  import os3 from "os";
3310
6052
  import { and as and2, eq as eq2, inArray, ne, sql as sql2 } from "drizzle-orm";
3311
6053
 
3312
6054
  // src/run-telemetry.ts
3313
- import crypto2 from "crypto";
6055
+ import crypto4 from "crypto";
3314
6056
  function extractRegistrableHost(input) {
3315
6057
  if (!input) return null;
3316
6058
  const trimmed = input.trim();
@@ -3330,7 +6072,7 @@ function extractRegistrableHost(input) {
3330
6072
  function hashDomain(input) {
3331
6073
  const host = extractRegistrableHost(input);
3332
6074
  if (!host) return null;
3333
- return crypto2.createHash("sha256").update(host).digest("hex");
6075
+ return crypto4.createHash("sha256").update(host).digest("hex");
3334
6076
  }
3335
6077
  function buildRunCompletedProps(input) {
3336
6078
  const totalMs = input.phases?.total_ms ?? Date.now() - input.startTime;
@@ -3415,7 +6157,7 @@ function getSharedProviderExecutionGate(providerName, maxConcurrency, maxPerMinu
3415
6157
  }
3416
6158
 
3417
6159
  // src/usage-quota.ts
3418
- import crypto3 from "crypto";
6160
+ import crypto5 from "crypto";
3419
6161
  import { and, eq, lte, sql } from "drizzle-orm";
3420
6162
  function getCurrentUsageDay() {
3421
6163
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -3432,7 +6174,7 @@ function reserveDailyQueryQuota(db, input) {
3432
6174
  )).run();
3433
6175
  if (reserveExisting().changes === 1) return { reserved: true, used: 0 };
3434
6176
  const inserted = tx.insert(usageCounters).values({
3435
- id: crypto3.randomUUID(),
6177
+ id: crypto5.randomUUID(),
3436
6178
  scope: input.scope,
3437
6179
  period: input.period,
3438
6180
  metric: "queries",
@@ -3846,7 +6588,7 @@ var JobRunner = class {
3846
6588
  );
3847
6589
  let screenshotRelPath = null;
3848
6590
  if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
3849
- const snapshotId = crypto4.randomUUID();
6591
+ const snapshotId = crypto6.randomUUID();
3850
6592
  const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
3851
6593
  if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
3852
6594
  const destPath = path3.join(screenshotDir, `${snapshotId}.png`);
@@ -3889,7 +6631,7 @@ var JobRunner = class {
3889
6631
  }).run();
3890
6632
  } else {
3891
6633
  this.db.insert(querySnapshots).values({
3892
- id: crypto4.randomUUID(),
6634
+ id: crypto6.randomUUID(),
3893
6635
  runId,
3894
6636
  queryId: q.id,
3895
6637
  queryText: q.query,
@@ -4001,7 +6743,7 @@ var JobRunner = class {
4001
6743
  competitorDomains,
4002
6744
  allBrandNames
4003
6745
  );
4004
- const snapshotId = crypto4.randomUUID();
6746
+ const snapshotId = crypto6.randomUUID();
4005
6747
  let screenshotRelPath = null;
4006
6748
  if (raw.screenshotPath && fs2.existsSync(raw.screenshotPath)) {
4007
6749
  const screenshotDir = path3.join(os3.homedir(), ".canonry", "screenshots", runId);
@@ -4237,7 +6979,7 @@ var JobRunner = class {
4237
6979
  const now = (/* @__PURE__ */ new Date()).toISOString();
4238
6980
  const period = now.slice(0, 10);
4239
6981
  this.db.insert(usageCounters).values({
4240
- id: crypto4.randomUUID(),
6982
+ id: crypto6.randomUUID(),
4241
6983
  scope,
4242
6984
  period,
4243
6985
  metric,
@@ -4339,11 +7081,11 @@ function maybeShowActivationNotice(io = {}) {
4339
7081
  }
4340
7082
 
4341
7083
  // src/gsc-sync.ts
4342
- import crypto6 from "crypto";
7084
+ import crypto8 from "crypto";
4343
7085
  import { eq as eq4, and as and4, sql as sql3 } from "drizzle-orm";
4344
7086
 
4345
7087
  // src/gsc-coverage-snapshot.ts
4346
- import crypto5 from "crypto";
7088
+ import crypto7 from "crypto";
4347
7089
  import { and as and3, desc, eq as eq3, gte } from "drizzle-orm";
4348
7090
  var COVERAGE_IMPRESSION_WINDOW_DAYS = 30;
4349
7091
  function pageKey(raw) {
@@ -4405,7 +7147,7 @@ function writeCoverageSnapshot(db, projectId, runId, opts = {}) {
4405
7147
  const snapshotDate = now.toISOString().split("T")[0];
4406
7148
  db.delete(gscCoverageSnapshots).where(and3(eq3(gscCoverageSnapshots.projectId, projectId), eq3(gscCoverageSnapshots.date, snapshotDate))).run();
4407
7149
  db.insert(gscCoverageSnapshots).values({
4408
- id: crypto5.randomUUID(),
7150
+ id: crypto7.randomUUID(),
4409
7151
  projectId,
4410
7152
  syncRunId: runId,
4411
7153
  date: snapshotDate,
@@ -4484,7 +7226,7 @@ async function executeGscSync(db, runId, projectId, opts) {
4484
7226
  for (const row of batch) {
4485
7227
  const [query, page, country, device, date] = row.keys;
4486
7228
  db.insert(gscSearchData).values({
4487
- id: crypto6.randomUUID(),
7229
+ id: crypto8.randomUUID(),
4488
7230
  projectId,
4489
7231
  syncRunId: runId,
4490
7232
  date: date ?? "",
@@ -4516,7 +7258,7 @@ async function executeGscSync(db, runId, projectId, opts) {
4516
7258
  for (const row of totalRows) {
4517
7259
  const [date] = row.keys;
4518
7260
  db.insert(gscDailyTotals).values({
4519
- id: crypto6.randomUUID(),
7261
+ id: crypto8.randomUUID(),
4520
7262
  projectId,
4521
7263
  date: date ?? "",
4522
7264
  clicks: row.clicks,
@@ -4565,7 +7307,7 @@ async function executeGscSync(db, runId, projectId, opts) {
4565
7307
  const [date, query] = row.keys;
4566
7308
  if (!date || !query) continue;
4567
7309
  db.insert(gscQueryDailyTotals).values({
4568
- id: crypto6.randomUUID(),
7310
+ id: crypto8.randomUUID(),
4569
7311
  projectId,
4570
7312
  date,
4571
7313
  query,
@@ -4590,7 +7332,7 @@ async function executeGscSync(db, runId, projectId, opts) {
4590
7332
  }
4591
7333
 
4592
7334
  // src/gbp-sync.ts
4593
- import crypto7 from "crypto";
7335
+ import crypto9 from "crypto";
4594
7336
  import { eq as eq5, and as and5, desc as desc2, inArray as inArray2, lt } from "drizzle-orm";
4595
7337
  var MS_PER_DAY = 864e5;
4596
7338
  var log3 = createLogger("GbpSync");
@@ -4715,7 +7457,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
4715
7457
  tx.delete(gbpDailyMetrics).where(and5(eq5(gbpDailyMetrics.projectId, projectId), eq5(gbpDailyMetrics.locationName, loc.locationName))).run();
4716
7458
  for (const row of metricRows) {
4717
7459
  tx.insert(gbpDailyMetrics).values({
4718
- id: crypto7.randomUUID(),
7460
+ id: crypto9.randomUUID(),
4719
7461
  projectId,
4720
7462
  locationName: loc.locationName,
4721
7463
  date: row.date,
@@ -4727,7 +7469,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
4727
7469
  tx.delete(gbpKeywordImpressions).where(and5(eq5(gbpKeywordImpressions.projectId, projectId), eq5(gbpKeywordImpressions.locationName, loc.locationName))).run();
4728
7470
  for (const row of keywordRows) {
4729
7471
  tx.insert(gbpKeywordImpressions).values({
4730
- id: crypto7.randomUUID(),
7472
+ id: crypto9.randomUUID(),
4731
7473
  projectId,
4732
7474
  locationName: loc.locationName,
4733
7475
  periodStart: monthKey(keywordsStart),
@@ -4748,7 +7490,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
4748
7490
  for (const { month, rows } of monthlyKeywordResults) {
4749
7491
  for (const row of rows) {
4750
7492
  tx.insert(gbpKeywordMonthly).values({
4751
- id: crypto7.randomUUID(),
7493
+ id: crypto9.randomUUID(),
4752
7494
  projectId,
4753
7495
  locationName: loc.locationName,
4754
7496
  month,
@@ -4769,7 +7511,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
4769
7511
  tx.delete(gbpPlaceActions).where(and5(eq5(gbpPlaceActions.projectId, projectId), eq5(gbpPlaceActions.locationName, loc.locationName))).run();
4770
7512
  for (const row of placeActionRows) {
4771
7513
  tx.insert(gbpPlaceActions).values({
4772
- id: crypto7.randomUUID(),
7514
+ id: crypto9.randomUUID(),
4773
7515
  projectId,
4774
7516
  locationName: loc.locationName,
4775
7517
  placeActionLinkName: row.placeActionLinkName,
@@ -4782,7 +7524,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
4782
7524
  }
4783
7525
  if (lodging !== null && lodgingChanged) {
4784
7526
  tx.insert(gbpLodgingSnapshots).values({
4785
- id: crypto7.randomUUID(),
7527
+ id: crypto9.randomUUID(),
4786
7528
  projectId,
4787
7529
  locationName: loc.locationName,
4788
7530
  contentHash: lodgingHash,
@@ -4796,7 +7538,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
4796
7538
  }
4797
7539
  if (attributesChanged) {
4798
7540
  tx.insert(gbpAttributesSnapshots).values({
4799
- id: crypto7.randomUUID(),
7541
+ id: crypto9.randomUUID(),
4800
7542
  projectId,
4801
7543
  locationName: loc.locationName,
4802
7544
  contentHash: attributesHash,
@@ -4810,7 +7552,7 @@ async function executeGbpSync(db, runId, projectId, opts) {
4810
7552
  }
4811
7553
  if (placeToWrite) {
4812
7554
  tx.insert(gbpPlaceDetails).values({
4813
- id: crypto7.randomUUID(),
7555
+ id: crypto9.randomUUID(),
4814
7556
  projectId,
4815
7557
  locationName: loc.locationName,
4816
7558
  placeId: placeToWrite.placeId,
@@ -4897,7 +7639,7 @@ async function refreshSelectedLocationProfiles(db, projectId, accessToken, locat
4897
7639
  }
4898
7640
 
4899
7641
  // src/ads-sync.ts
4900
- import crypto8 from "crypto";
7642
+ import crypto10 from "crypto";
4901
7643
  import { eq as eq6 } from "drizzle-orm";
4902
7644
 
4903
7645
  // ../integration-openai-ads/src/constants.ts
@@ -5388,7 +8130,7 @@ async function getAdGroupInsights(apiKey, adGroupId, opts) {
5388
8130
  }
5389
8131
 
5390
8132
  // src/ads-config.ts
5391
- function ensureConnections8(config) {
8133
+ function ensureConnections10(config) {
5392
8134
  if (!config.openaiAds) config.openaiAds = {};
5393
8135
  if (!config.openaiAds.connections) config.openaiAds.connections = [];
5394
8136
  return config.openaiAds.connections;
@@ -5397,7 +8139,7 @@ function getOpenAiAdsConnection(config, projectName) {
5397
8139
  return (config.openaiAds?.connections ?? []).find((c) => c.projectName === projectName);
5398
8140
  }
5399
8141
  function upsertOpenAiAdsConnection(config, connection) {
5400
- const connections = ensureConnections8(config);
8142
+ const connections = ensureConnections10(config);
5401
8143
  const index = connections.findIndex((c) => c.projectName === connection.projectName);
5402
8144
  if (index === -1) {
5403
8145
  connections.push(connection);
@@ -5625,7 +8367,7 @@ async function executeAdsSync(db, runId, projectId, opts) {
5625
8367
  }
5626
8368
  for (const upsert of insightUpserts) {
5627
8369
  tx.insert(adsInsightsDaily).values({
5628
- id: crypto8.randomUUID(),
8370
+ id: crypto10.randomUUID(),
5629
8371
  projectId,
5630
8372
  level: upsert.level,
5631
8373
  entityId: upsert.entityId,
@@ -5692,7 +8434,7 @@ async function executeAdsSync(db, runId, projectId, opts) {
5692
8434
  }
5693
8435
 
5694
8436
  // src/gsc-inspect-sitemap.ts
5695
- import crypto10 from "crypto";
8437
+ import crypto12 from "crypto";
5696
8438
  import { eq as eq7 } from "drizzle-orm";
5697
8439
 
5698
8440
  // src/sitemap-parser.ts
@@ -5792,7 +8534,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
5792
8534
  }
5793
8535
 
5794
8536
  // src/inspect-rate-gate.ts
5795
- import crypto9 from "crypto";
8537
+ import crypto11 from "crypto";
5796
8538
  var gates = /* @__PURE__ */ new Map();
5797
8539
  function sharedRateGate(key) {
5798
8540
  let entry = gates.get(key);
@@ -5811,7 +8553,7 @@ function sharedRateGate(key) {
5811
8553
  };
5812
8554
  }
5813
8555
  function credentialGateKey(prefix, secret) {
5814
- const digest = crypto9.createHash("sha256").update(secret).digest("hex").slice(0, 12);
8556
+ const digest = crypto11.createHash("sha256").update(secret).digest("hex").slice(0, 12);
5815
8557
  return `${prefix}:${digest}`;
5816
8558
  }
5817
8559
 
@@ -5966,7 +8708,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
5966
8708
  const rich = ir.richResultsResult;
5967
8709
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
5968
8710
  db.insert(gscUrlInspections).values({
5969
- id: crypto10.randomUUID(),
8711
+ id: crypto12.randomUUID(),
5970
8712
  projectId,
5971
8713
  syncRunId: runId,
5972
8714
  url: pageUrl,
@@ -6024,7 +8766,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
6024
8766
  }
6025
8767
 
6026
8768
  // src/bing-inspect-sitemap.ts
6027
- import crypto11 from "crypto";
8769
+ import crypto13 from "crypto";
6028
8770
  import { eq as eq8, desc as desc3 } from "drizzle-orm";
6029
8771
  var log7 = createLogger("BingInspectSitemap");
6030
8772
  var BING_INSPECT_SPACING_MS = 2e3;
@@ -6129,7 +8871,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
6129
8871
  derivedInIndex = false;
6130
8872
  }
6131
8873
  db.insert(bingUrlInspections).values({
6132
- id: crypto11.randomUUID(),
8874
+ id: crypto13.randomUUID(),
6133
8875
  projectId,
6134
8876
  url: pageUrl,
6135
8877
  httpCode,
@@ -6213,7 +8955,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
6213
8955
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
6214
8956
  const snapNow = (/* @__PURE__ */ new Date()).toISOString();
6215
8957
  db.insert(bingCoverageSnapshots).values({
6216
- id: crypto11.randomUUID(),
8958
+ id: crypto13.randomUUID(),
6217
8959
  projectId,
6218
8960
  syncRunId: runId,
6219
8961
  date: snapshotDate,
@@ -6254,7 +8996,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
6254
8996
  }
6255
8997
 
6256
8998
  // src/coverage-refresh.ts
6257
- import crypto12 from "crypto";
8999
+ import crypto14 from "crypto";
6258
9000
  import { and as and6, desc as desc4, eq as eq9, inArray as inArray3 } from "drizzle-orm";
6259
9001
  var log8 = createLogger("CoverageRefresh");
6260
9002
  var COVERAGE_REFRESH_MIN_INTERVAL_MS = 60 * 60 * 1e3;
@@ -6304,7 +9046,7 @@ async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps
6304
9046
  return null;
6305
9047
  }
6306
9048
  }
6307
- const runId = crypto12.randomUUID();
9049
+ const runId = crypto14.randomUUID();
6308
9050
  db.insert(runs).values({
6309
9051
  id: runId,
6310
9052
  projectId,
@@ -6327,7 +9069,7 @@ async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps
6327
9069
  }
6328
9070
 
6329
9071
  // src/commoncrawl-sync.ts
6330
- import crypto13 from "crypto";
9072
+ import crypto15 from "crypto";
6331
9073
  import path4 from "path";
6332
9074
  import { and as and7, eq as eq10, sql as sql4 } from "drizzle-orm";
6333
9075
  var log9 = createLogger("CommonCrawlSync");
@@ -6402,7 +9144,7 @@ async function executeReleaseSync(db, syncId, opts) {
6402
9144
  if (!projectIds) continue;
6403
9145
  for (const projectId of projectIds) {
6404
9146
  expanded.push({
6405
- id: crypto13.randomUUID(),
9147
+ id: crypto15.randomUUID(),
6406
9148
  projectId,
6407
9149
  releaseSyncId: syncId,
6408
9150
  release,
@@ -6422,7 +9164,7 @@ async function executeReleaseSync(db, syncId, opts) {
6422
9164
  const projectRows = rowsByProject.get(p.id) ?? [];
6423
9165
  const summary = computeSummary(projectRows);
6424
9166
  tx.insert(backlinkSummaries).values({
6425
- id: crypto13.randomUUID(),
9167
+ id: crypto15.randomUUID(),
6426
9168
  projectId: p.id,
6427
9169
  releaseSyncId: syncId,
6428
9170
  source: BacklinkSources.commoncrawl,
@@ -6519,7 +9261,7 @@ function computeSummary(rows) {
6519
9261
  }
6520
9262
 
6521
9263
  // src/backlink-extract.ts
6522
- import crypto14 from "crypto";
9264
+ import crypto16 from "crypto";
6523
9265
  import fs3 from "fs";
6524
9266
  import { and as and8, desc as desc5, eq as eq11 } from "drizzle-orm";
6525
9267
  var log10 = createLogger("BacklinkExtract");
@@ -6575,7 +9317,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
6575
9317
  ).run();
6576
9318
  if (rows.length > 0) {
6577
9319
  const values = rows.map((r) => ({
6578
- id: crypto14.randomUUID(),
9320
+ id: crypto16.randomUUID(),
6579
9321
  projectId,
6580
9322
  releaseSyncId: syncId,
6581
9323
  source: BacklinkSources.commoncrawl,
@@ -6589,7 +9331,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
6589
9331
  }
6590
9332
  const summary = computeSummary2(rows);
6591
9333
  tx.insert(backlinkSummaries).values({
6592
- id: crypto14.randomUUID(),
9334
+ id: crypto16.randomUUID(),
6593
9335
  projectId,
6594
9336
  releaseSyncId: syncId,
6595
9337
  source: BacklinkSources.commoncrawl,
@@ -6632,7 +9374,7 @@ function computeSummary2(rows) {
6632
9374
  }
6633
9375
 
6634
9376
  // src/discovery-run.ts
6635
- import crypto15 from "crypto";
9377
+ import crypto17 from "crypto";
6636
9378
  import { and as and9, eq as eq12 } from "drizzle-orm";
6637
9379
  var log11 = createLogger("DiscoveryRun");
6638
9380
  var DEFAULT_SEED_COUNT = 30;
@@ -6987,7 +9729,7 @@ function writeDiscoveryInsight(db, input) {
6987
9729
  eq12(insights.dismissed, false)
6988
9730
  )).run();
6989
9731
  tx.insert(insights).values({
6990
- id: crypto15.randomUUID(),
9732
+ id: crypto17.randomUUID(),
6991
9733
  projectId: input.projectId,
6992
9734
  runId: input.runId,
6993
9735
  type: "discovery.basket-divergence",
@@ -7023,7 +9765,7 @@ function buildDiscoveryInsightTitle(input) {
7023
9765
  }
7024
9766
 
7025
9767
  // src/execute-site-audit.ts
7026
- import crypto17 from "crypto";
9768
+ import crypto19 from "crypto";
7027
9769
  import { and as and12, eq as eq15, sql as sql6 } from "drizzle-orm";
7028
9770
  import { runSiteCrawl } from "@canonry/aeo-audit";
7029
9771
 
@@ -7212,7 +9954,7 @@ async function requestPinnedSiteAuditRoot(target, options) {
7212
9954
  }
7213
9955
 
7214
9956
  // src/site-crawl-graph-layout.ts
7215
- import crypto16 from "crypto";
9957
+ import crypto18 from "crypto";
7216
9958
  import { createRequire as createRequire3 } from "module";
7217
9959
  import { Worker } from "worker_threads";
7218
9960
  import { and as and10, asc, count, desc as desc6, eq as eq13, isNull, ne as ne2, or, sql as sql5 } from "drizzle-orm";
@@ -7289,7 +10031,7 @@ function noverlapMaxIterations(nodeCount) {
7289
10031
  }
7290
10032
  var COMPONENT_PACKING_SPACING_FACTOR = 3;
7291
10033
  function siteCrawlGraphLayoutSettingsFingerprint(settings) {
7292
- return crypto16.createHash("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 8);
10034
+ return crypto18.createHash("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 8);
7293
10035
  }
7294
10036
  var SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM = "site-health-fa2-v5";
7295
10037
  var SITE_CRAWL_GRAPH_LAYOUT_VERSION = `${SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM}-${siteCrawlGraphLayoutSettingsFingerprint({
@@ -7786,7 +10528,7 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
7786
10528
  eq13(siteCrawlGraphLayouts.attemptId, scope.attemptId)
7787
10529
  )).run();
7788
10530
  tx.insert(siteCrawlGraphLayouts).values({
7789
- id: crypto16.randomUUID(),
10531
+ id: crypto18.randomUUID(),
7790
10532
  projectId: scope.projectId,
7791
10533
  runId: scope.runId,
7792
10534
  attemptId: scope.attemptId,
@@ -7806,7 +10548,7 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
7806
10548
  const batchSize = 250;
7807
10549
  for (let offset = 0; offset < layout.nodes.length; offset += batchSize) {
7808
10550
  tx.insert(siteCrawlGraphNodes).values(layout.nodes.slice(offset, offset + batchSize).map((node) => ({
7809
- id: crypto16.randomUUID(),
10551
+ id: crypto18.randomUUID(),
7810
10552
  projectId: scope.projectId,
7811
10553
  runId: scope.runId,
7812
10554
  attemptId: scope.attemptId,
@@ -7819,7 +10561,7 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
7819
10561
  }
7820
10562
  for (let offset = 0; offset < layout.edges.length; offset += batchSize) {
7821
10563
  tx.insert(siteCrawlGraphEdges).values(layout.edges.slice(offset, offset + batchSize).map((edge) => ({
7822
- id: crypto16.randomUUID(),
10564
+ id: crypto18.randomUUID(),
7823
10565
  projectId: scope.projectId,
7824
10566
  runId: scope.runId,
7825
10567
  attemptId: scope.attemptId,
@@ -8108,7 +10850,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8108
10850
  const claim = db.update(runs).set({ status: "running", startedAt }).where(and12(eq15(runs.id, runId), eq15(runs.projectId, projectId), eq15(runs.status, "queued"))).run();
8109
10851
  if (claim.changes === 0) return;
8110
10852
  const attemptNumber = (db.select({ value: sql6`coalesce(max(${siteCrawlAttempts.attemptNumber}), 0)` }).from(siteCrawlAttempts).where(eq15(siteCrawlAttempts.runId, runId)).get()?.value ?? 0) + 1;
8111
- const attemptId = crypto17.randomUUID();
10853
+ const attemptId = crypto19.randomUUID();
8112
10854
  db.insert(siteCrawlAttempts).values({
8113
10855
  id: attemptId,
8114
10856
  projectId,
@@ -8186,7 +10928,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8186
10928
  nodeKey: page.key
8187
10929
  });
8188
10930
  tx.insert(siteCrawlPages).values({
8189
- id: crypto17.randomUUID(),
10931
+ id: crypto19.randomUUID(),
8190
10932
  projectId,
8191
10933
  runId,
8192
10934
  attemptId,
@@ -8277,7 +11019,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8277
11019
  if (isSelfLink(edge.from, edge.to)) return;
8278
11020
  observedEdges.set(edge.key, edge);
8279
11021
  tx.insert(siteCrawlEdges).values({
8280
- id: crypto17.randomUUID(),
11022
+ id: crypto19.randomUUID(),
8281
11023
  projectId,
8282
11024
  runId,
8283
11025
  attemptId,
@@ -8347,7 +11089,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8347
11089
  return;
8348
11090
  }
8349
11091
  tx.insert(siteCrawlEventReceipts).values({
8350
- id: crypto17.randomUUID(),
11092
+ id: crypto19.randomUUID(),
8351
11093
  projectId,
8352
11094
  runId,
8353
11095
  attemptId,
@@ -8481,7 +11223,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8481
11223
  if (claim2.changes === 0) return false;
8482
11224
  if (hasAuditedPages) {
8483
11225
  tx.insert(siteAuditSnapshots).values({
8484
- id: crypto17.randomUUID(),
11226
+ id: crypto19.randomUUID(),
8485
11227
  projectId,
8486
11228
  runId,
8487
11229
  sitemapUrl: opts.sitemapUrl ?? new URL("/sitemap.xml", crawlRootUrl).href,
@@ -8499,7 +11241,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8499
11241
  for (const page of observedPages.values()) {
8500
11242
  if (!isLegacyScorecardPage(page)) continue;
8501
11243
  tx.insert(siteAuditPages).values({
8502
- id: crypto17.randomUUID(),
11244
+ id: crypto19.randomUUID(),
8503
11245
  projectId,
8504
11246
  runId,
8505
11247
  url: page.finalUrl ?? page.requestedUrl,
@@ -8512,7 +11254,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8512
11254
  }
8513
11255
  }
8514
11256
  tx.insert(siteCrawlSnapshots).values({
8515
- id: crypto17.randomUUID(),
11257
+ id: crypto19.randomUUID(),
8516
11258
  projectId,
8517
11259
  runId,
8518
11260
  attemptId,
@@ -8556,7 +11298,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8556
11298
  if (opts.checkDeadLinks) {
8557
11299
  for (const finding of deadLinkFindings) {
8558
11300
  tx.insert(siteCrawlFindings).values({
8559
- id: crypto17.randomUUID(),
11301
+ id: crypto19.randomUUID(),
8560
11302
  projectId,
8561
11303
  runId,
8562
11304
  attemptId,
@@ -9087,7 +11829,7 @@ function readStoredGroundingSources(rawResponse) {
9087
11829
  return result;
9088
11830
  }
9089
11831
  async function backfillInsightsCommand(project, opts) {
9090
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-6ZYTEFXX.js");
11832
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-KT6K4ODY.js");
9091
11833
  const config = loadConfig();
9092
11834
  const db = createClient(config.database);
9093
11835
  migrate(db);
@@ -9663,7 +12405,7 @@ var ProviderRegistry = class {
9663
12405
  };
9664
12406
 
9665
12407
  // src/scheduler.ts
9666
- import crypto18 from "crypto";
12408
+ import crypto20 from "crypto";
9667
12409
  import cron from "node-cron";
9668
12410
  import { and as and14, eq as eq17, inArray as inArray6, notExists, sql as sql8 } from "drizzle-orm";
9669
12411
  var log13 = createLogger("Scheduler");
@@ -9706,7 +12448,7 @@ var Scheduler = class {
9706
12448
  const now = (/* @__PURE__ */ new Date()).toISOString();
9707
12449
  for (const project of projectsWithoutHealth) {
9708
12450
  this.db.insert(schedules).values({
9709
- id: crypto18.randomUUID(),
12451
+ id: crypto20.randomUUID(),
9710
12452
  projectId: project.id,
9711
12453
  kind: SchedulableRunKinds.doctor,
9712
12454
  cronExpr: DEFAULT_HEALTH_CRON,
@@ -9865,7 +12607,7 @@ var Scheduler = class {
9865
12607
  log13.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
9866
12608
  return;
9867
12609
  }
9868
- const runId2 = crypto18.randomUUID();
12610
+ const runId2 = crypto20.randomUUID();
9869
12611
  this.db.insert(runs).values({
9870
12612
  id: runId2,
9871
12613
  projectId,
@@ -9898,7 +12640,7 @@ var Scheduler = class {
9898
12640
  this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq17(schedules.id, currentSchedule.id)).run();
9899
12641
  return;
9900
12642
  }
9901
- const runId2 = crypto18.randomUUID();
12643
+ const runId2 = crypto20.randomUUID();
9902
12644
  this.db.insert(runs).values({
9903
12645
  id: runId2,
9904
12646
  projectId,
@@ -9973,7 +12715,7 @@ var Scheduler = class {
9973
12715
  this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq17(schedules.id, currentSchedule.id)).run();
9974
12716
  return;
9975
12717
  }
9976
- const runId2 = crypto18.randomUUID();
12718
+ const runId2 = crypto20.randomUUID();
9977
12719
  const effectiveRequest = normalizeSiteAuditRunRequest({});
9978
12720
  this.db.transaction((tx) => {
9979
12721
  tx.insert(runs).values({
@@ -10054,7 +12796,11 @@ async function refreshAllIntegrations(client, projectName) {
10054
12796
  { name: "bing", run: () => client.bingInspectSitemap(projectName, {}) },
10055
12797
  { name: "ga", run: () => client.gaSync(projectName, { days: 30 }) },
10056
12798
  { name: "gbp", run: () => client.triggerGbpSync(projectName, {}) },
10057
- { name: "ads", run: () => client.triggerAdsSync(projectName) }
12799
+ // `ads` remains the OpenAI / ChatGPT Ads integration. For scheduled
12800
+ // refreshes, Google providers have explicit names and fan out only here.
12801
+ { name: "ads", run: () => client.triggerAdsSync(projectName) },
12802
+ { name: "google-ads", run: () => client.triggerGoogleAdsSync(projectName) },
12803
+ { name: "gtm", run: () => client.triggerGtmSync(projectName) }
10058
12804
  ];
10059
12805
  const results = await Promise.allSettled(integrations.map((i) => i.run()));
10060
12806
  results.forEach((result, idx) => {
@@ -10071,7 +12817,7 @@ async function refreshAllIntegrations(client, projectName) {
10071
12817
 
10072
12818
  // src/notifier.ts
10073
12819
  import { eq as eq18, desc as desc7, and as and15, inArray as inArray7, or as or2 } from "drizzle-orm";
10074
- import crypto19 from "crypto";
12820
+ import crypto21 from "crypto";
10075
12821
  var log15 = createLogger("Notifier");
10076
12822
  var Notifier = class {
10077
12823
  db;
@@ -10397,7 +13143,7 @@ var Notifier = class {
10397
13143
  }
10398
13144
  logDelivery(projectId, notificationId, event, status, error) {
10399
13145
  this.db.insert(auditLog).values({
10400
- id: crypto19.randomUUID(),
13146
+ id: crypto21.randomUUID(),
10401
13147
  projectId,
10402
13148
  actor: "scheduler",
10403
13149
  action: `notification.${status}`,
@@ -10559,7 +13305,7 @@ function analysisResultFromInsights(insights2) {
10559
13305
  }
10560
13306
 
10561
13307
  // src/agent/session-registry.ts
10562
- import crypto23 from "crypto";
13308
+ import crypto25 from "crypto";
10563
13309
  import { eq as eq21 } from "drizzle-orm";
10564
13310
 
10565
13311
  // src/agent/session.ts
@@ -10993,12 +13739,12 @@ function stripProjectFromJsonSchema(jsonSchema) {
10993
13739
  return { schema: jsonSchema, hadProject: false };
10994
13740
  }
10995
13741
  const { project: _project, ...remainingProps } = properties;
10996
- const required = Array.isArray(obj.required) ? obj.required.filter((name) => name !== "project") : obj.required;
13742
+ const required2 = Array.isArray(obj.required) ? obj.required.filter((name) => name !== "project") : obj.required;
10997
13743
  const stripped = { ...obj, properties: remainingProps };
10998
- if (required === void 0) {
13744
+ if (required2 === void 0) {
10999
13745
  delete stripped.required;
11000
13746
  } else {
11001
- stripped.required = required;
13747
+ stripped.required = required2;
11002
13748
  }
11003
13749
  return { schema: stripped, hadProject: true };
11004
13750
  }
@@ -11303,7 +14049,7 @@ function buildAeroStateTools(ctx, opts = {}) {
11303
14049
  }
11304
14050
 
11305
14051
  // src/agent/llm-usage.ts
11306
- import crypto20 from "crypto";
14052
+ import crypto22 from "crypto";
11307
14053
  var AeroLlmUsageFeatures = {
11308
14054
  turn: "aero.turn"
11309
14055
  };
@@ -11322,7 +14068,7 @@ function recordLlmUsageEvent(args) {
11322
14068
  const usage = args.message.usage;
11323
14069
  const now = (/* @__PURE__ */ new Date()).toISOString();
11324
14070
  args.db.insert(llmUsageEvents).values({
11325
- id: crypto20.randomUUID(),
14071
+ id: crypto22.randomUUID(),
11326
14072
  projectId: args.projectId,
11327
14073
  runId: args.runId,
11328
14074
  agentSessionId: args.agentSessionId,
@@ -11398,7 +14144,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
11398
14144
  }
11399
14145
 
11400
14146
  // src/agent/tool-usage.ts
11401
- import crypto21 from "crypto";
14147
+ import crypto23 from "crypto";
11402
14148
  var AeroToolEventStatuses = {
11403
14149
  success: "success",
11404
14150
  error: "error"
@@ -11461,7 +14207,7 @@ function createAeroToolUsageHooks(args) {
11461
14207
  function recordAgentToolEvent(args) {
11462
14208
  try {
11463
14209
  args.db.insert(agentToolEvents).values({
11464
- id: crypto21.randomUUID(),
14210
+ id: crypto23.randomUUID(),
11465
14211
  projectId: args.projectId,
11466
14212
  agentSessionId: args.agentSessionId,
11467
14213
  toolCallId: args.toolCallId,
@@ -11688,7 +14434,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
11688
14434
  }
11689
14435
 
11690
14436
  // src/agent/memory-store.ts
11691
- import crypto22 from "crypto";
14437
+ import crypto24 from "crypto";
11692
14438
  import { and as and16, desc as desc8, eq as eq20, like, sql as sql9 } from "drizzle-orm";
11693
14439
  var COMPACTION_KEY_PREFIX = "compaction:";
11694
14440
  var COMPACTION_NOTES_PER_SESSION = 3;
@@ -11717,7 +14463,7 @@ function upsertMemoryEntry(db, args) {
11717
14463
  throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
11718
14464
  }
11719
14465
  const now = (/* @__PURE__ */ new Date()).toISOString();
11720
- const id = crypto22.randomUUID();
14466
+ const id = crypto24.randomUUID();
11721
14467
  db.insert(agentMemory).values({
11722
14468
  id,
11723
14469
  projectId: args.projectId,
@@ -11754,7 +14500,7 @@ function writeCompactionNote(db, args) {
11754
14500
  }
11755
14501
  const now = (/* @__PURE__ */ new Date()).toISOString();
11756
14502
  const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
11757
- const id = crypto22.randomUUID();
14503
+ const id = crypto24.randomUUID();
11758
14504
  let inserted;
11759
14505
  db.transaction((tx) => {
11760
14506
  tx.insert(agentMemory).values({
@@ -12033,7 +14779,7 @@ var SessionRegistry = class {
12033
14779
  }
12034
14780
  const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
12035
14781
  const systemPrompt = loadAeroSystemPrompt();
12036
- const sessionId = crypto23.randomUUID();
14782
+ const sessionId = crypto25.randomUUID();
12037
14783
  const agent = createAeroSession({
12038
14784
  projectName,
12039
14785
  client: this.opts.client,
@@ -12412,7 +15158,7 @@ ${lines.join("\n")}
12412
15158
  insertRow(params) {
12413
15159
  const now = (/* @__PURE__ */ new Date()).toISOString();
12414
15160
  this.opts.db.insert(agentSessions).values({
12415
- id: params.id ?? crypto23.randomUUID(),
15161
+ id: params.id ?? crypto25.randomUUID(),
12416
15162
  projectId: params.projectId,
12417
15163
  systemPrompt: params.systemPrompt,
12418
15164
  modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
@@ -12953,7 +15699,7 @@ var SnapshotService = class {
12953
15699
  });
12954
15700
  const enrichedResults = applyBatchAssessment(queryResults, batchAssessment);
12955
15701
  const summary = buildSnapshotSummary(companyName, profile.queries, providers, enrichedResults, audit, batchAssessment);
12956
- const reportCompetitors = uniqueStrings([
15702
+ const reportCompetitors = uniqueStrings2([
12957
15703
  ...manualCompetitors,
12958
15704
  ...summary.topCompetitors.map((entry) => entry.name)
12959
15705
  ]);
@@ -13005,8 +15751,8 @@ var SnapshotService = class {
13005
15751
  return {
13006
15752
  industry: parsed.industry?.trim() || "Unknown",
13007
15753
  summary: parsed.summary?.trim() || ctx.audit.summary,
13008
- services: uniqueStrings(parsed.services ?? []).slice(0, 6),
13009
- categoryTerms: uniqueStrings(parsed.categoryTerms ?? []).slice(0, 8),
15754
+ services: uniqueStrings2(parsed.services ?? []).slice(0, 6),
15755
+ categoryTerms: uniqueStrings2(parsed.categoryTerms ?? []).slice(0, 8),
13010
15756
  queries: parsedQueries
13011
15757
  };
13012
15758
  } catch (err) {
@@ -13075,9 +15821,9 @@ var SnapshotService = class {
13075
15821
  accuracyNotes: null,
13076
15822
  incorrectClaims: [],
13077
15823
  recommendedCompetitors: preliminaryCompetitors,
13078
- citedDomains: uniqueStrings(normalized.citedDomains),
15824
+ citedDomains: uniqueStrings2(normalized.citedDomains),
13079
15825
  groundingSources: normalized.groundingSources,
13080
- searchQueries: uniqueStrings(normalized.searchQueries),
15826
+ searchQueries: uniqueStrings2(normalized.searchQueries),
13081
15827
  answerText: normalized.answerText,
13082
15828
  error: null
13083
15829
  };
@@ -13143,14 +15889,14 @@ var SnapshotService = class {
13143
15889
  mentioned: assessment.mentioned,
13144
15890
  describedAccurately: assessment.describedAccurately,
13145
15891
  accuracyNotes: assessment.accuracyNotes ?? null,
13146
- incorrectClaims: uniqueStrings(assessment.incorrectClaims ?? []).slice(0, 5),
15892
+ incorrectClaims: uniqueStrings2(assessment.incorrectClaims ?? []).slice(0, 5),
13147
15893
  ...hasReviewedCompetitors ? {
13148
- recommendedCompetitors: uniqueStrings(assessment.recommendedCompetitors ?? []).slice(0, 10)
15894
+ recommendedCompetitors: uniqueStrings2(assessment.recommendedCompetitors ?? []).slice(0, 10)
13149
15895
  } : {}
13150
15896
  };
13151
15897
  }),
13152
- whatThisMeans: uniqueStrings(parsed.whatThisMeans ?? []).slice(0, 4),
13153
- recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
15898
+ whatThisMeans: uniqueStrings2(parsed.whatThisMeans ?? []).slice(0, 4),
15899
+ recommendedActions: uniqueStrings2(parsed.recommendedActions ?? []).slice(0, 4)
13154
15900
  };
13155
15901
  } catch (err) {
13156
15902
  log19.warn("response.analysis-failed", {
@@ -13245,13 +15991,13 @@ function applyBatchAssessment(queryResults, batchAssessment) {
13245
15991
  };
13246
15992
  }
13247
15993
  const reviewedCompetitors = assessment.recommendedCompetitors;
13248
- const recommendedCompetitors = reviewedCompetitors !== void 0 ? uniqueStrings(reviewedCompetitors) : result.recommendedCompetitors;
15994
+ const recommendedCompetitors = reviewedCompetitors !== void 0 ? uniqueStrings2(reviewedCompetitors) : result.recommendedCompetitors;
13249
15995
  return {
13250
15996
  ...result,
13251
15997
  mentioned: result.mentioned || assessment.mentioned === true,
13252
15998
  describedAccurately: assessment.describedAccurately ?? (result.mentioned ? "unknown" : "not-mentioned"),
13253
15999
  accuracyNotes: assessment.accuracyNotes ?? result.accuracyNotes ?? null,
13254
- incorrectClaims: uniqueStrings([
16000
+ incorrectClaims: uniqueStrings2([
13255
16001
  ...result.incorrectClaims,
13256
16002
  ...assessment.incorrectClaims ?? []
13257
16003
  ]),
@@ -13281,7 +16027,7 @@ function buildSnapshotSummary(companyName, queries2, providers, queryResults, au
13281
16027
  const whatThisMeans = batchAssessment.whatThisMeans.length > 0 ? batchAssessment.whatThisMeans : [
13282
16028
  defaultMeaning
13283
16029
  ];
13284
- const combinedWhatThisMeans = uniqueStrings([...whatThisMeans, ...failureNote]).slice(0, 5);
16030
+ const combinedWhatThisMeans = uniqueStrings2([...whatThisMeans, ...failureNote]).slice(0, 5);
13285
16031
  const recommendedActions = batchAssessment.recommendedActions.length > 0 ? batchAssessment.recommendedActions : buildFallbackRecommendedActions(audit);
13286
16032
  return {
13287
16033
  totalQueries: queries2.length,
@@ -13324,7 +16070,7 @@ function buildFallbackRecommendedActions(audit) {
13324
16070
  "Add machine-readable trust signals such as schema, FAQs, and llms.txt support.",
13325
16071
  "Build comparison and proof content that makes the category fit unmistakable."
13326
16072
  ];
13327
- return uniqueStrings([...weakestFactors, ...defaults]).slice(0, 4);
16073
+ return uniqueStrings2([...weakestFactors, ...defaults]).slice(0, 4);
13328
16074
  }
13329
16075
  function citesTargetDomain(citedDomains, groundingSources, targetDomain) {
13330
16076
  for (const domain of citedDomains) {
@@ -13389,11 +16135,11 @@ function parseJsonObject(input) {
13389
16135
  }
13390
16136
  function normalizeStringList(values) {
13391
16137
  const items = values.flatMap((value) => value.split(","));
13392
- return uniqueStrings(
16138
+ return uniqueStrings2(
13393
16139
  items.map((value) => value.trim()).filter(Boolean)
13394
16140
  );
13395
16141
  }
13396
- function uniqueStrings(values) {
16142
+ function uniqueStrings2(values) {
13397
16143
  if (!Array.isArray(values)) return [];
13398
16144
  return [...new Set(
13399
16145
  values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
@@ -13528,6 +16274,253 @@ function finalizeResearchRun(db, runId, fatalError) {
13528
16274
  }).where(eq23(researchRuns.id, runId)).run();
13529
16275
  }
13530
16276
 
16277
+ // src/google-marketing-sync.ts
16278
+ import { and as and18, eq as eq24, isNull as isNull3 } from "drizzle-orm";
16279
+ function safeErrorMessage(error) {
16280
+ const raw = describeError(error);
16281
+ return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
16282
+ }
16283
+ function googleAdsSnapshotValues(snapshot) {
16284
+ return {
16285
+ ...snapshot.metadata,
16286
+ payload: snapshot.payload
16287
+ };
16288
+ }
16289
+ function gtmSnapshotValues(snapshot) {
16290
+ return {
16291
+ ...snapshot.metadata,
16292
+ payload: snapshot.payload
16293
+ };
16294
+ }
16295
+ function assertSnapshotContext(metadata, expected) {
16296
+ if (metadata.projectId !== expected.projectId || metadata.connectionId !== expected.connectionId || metadata.runId !== expected.runId) {
16297
+ throw new Error("Google Marketing runtime returned snapshot metadata for a different run context.");
16298
+ }
16299
+ }
16300
+ function requireSyncRun(db, runId, projectId, kind) {
16301
+ const run = db.select().from(runs).where(and18(
16302
+ eq24(runs.id, runId),
16303
+ eq24(runs.projectId, projectId),
16304
+ eq24(runs.kind, kind)
16305
+ )).get();
16306
+ if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
16307
+ if (run.status !== RunStatuses.queued) {
16308
+ throw new Error(`Run ${runId} is ${run.status}; only queued Google Marketing runs can start.`);
16309
+ }
16310
+ return run;
16311
+ }
16312
+ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
16313
+ db.update(runs).set({
16314
+ status: RunStatuses.failed,
16315
+ error: serializeRunError({ message: safeErrorMessage(error) }),
16316
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
16317
+ }).where(and18(
16318
+ eq24(runs.id, runId),
16319
+ eq24(runs.projectId, projectId),
16320
+ eq24(runs.kind, kind),
16321
+ eq24(runs.status, expectedStatus)
16322
+ )).run();
16323
+ }
16324
+ function claimSyncRun(db, runId, projectId, kind, startedAt) {
16325
+ const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and18(
16326
+ eq24(runs.id, runId),
16327
+ eq24(runs.projectId, projectId),
16328
+ eq24(runs.kind, kind),
16329
+ eq24(runs.status, RunStatuses.queued)
16330
+ )).run();
16331
+ return claim.changes === 1;
16332
+ }
16333
+ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
16334
+ let claimed = false;
16335
+ try {
16336
+ requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
16337
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
16338
+ if (!project) throw new Error(`Project ${projectId} was not found.`);
16339
+ const connection = db.select().from(googleAdsConnections).where(eq24(googleAdsConnections.projectId, projectId)).get();
16340
+ if (!connection?.selectedCustomerId) {
16341
+ throw new Error("Google Ads needs a selected customer before it can sync.");
16342
+ }
16343
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
16344
+ if (!claimSyncRun(db, runId, projectId, RunKinds["google-ads-sync"], startedAt)) {
16345
+ throw new Error(`Run ${runId} could not be claimed for Google Ads sync.`);
16346
+ }
16347
+ claimed = true;
16348
+ const result = await runtime.syncGoogleAds({
16349
+ project: { id: project.id, name: project.name },
16350
+ connectionId: connection.id,
16351
+ runId,
16352
+ selection: {
16353
+ customerId: connection.selectedCustomerId,
16354
+ loginCustomerId: connection.selectedLoginCustomerId,
16355
+ selectedAt: connection.lastValidatedAt
16356
+ }
16357
+ });
16358
+ const expected = { projectId, connectionId: connection.id, runId };
16359
+ assertSnapshotContext(result.accessibleCustomers.metadata, expected);
16360
+ assertSnapshotContext(result.inventory.metadata, expected);
16361
+ if (result.metrics) assertSnapshotContext(result.metrics.metadata, expected);
16362
+ if (result.accessibleCustomers.payload.kind !== GoogleAdsSnapshotKinds["accessible-customers"]) {
16363
+ throw new Error("Google Marketing runtime returned the wrong customer snapshot kind.");
16364
+ }
16365
+ if (result.inventory.payload.kind !== GoogleAdsSnapshotKinds.inventory) {
16366
+ throw new Error("Google Marketing runtime returned the wrong inventory snapshot kind.");
16367
+ }
16368
+ if (result.metrics && result.metrics.payload.kind !== GoogleAdsSnapshotKinds["campaign-metrics"]) {
16369
+ throw new Error("Google Marketing runtime returned the wrong metrics snapshot kind.");
16370
+ }
16371
+ if (result.accessibleCustomers.metadata.customerId !== connection.selectedCustomerId || result.inventory.metadata.customerId !== connection.selectedCustomerId || result.inventory.payload.data.customerId !== connection.selectedCustomerId || result.metrics && result.metrics.metadata.customerId !== connection.selectedCustomerId) {
16372
+ throw new Error("Google Ads returned evidence for a different customer.");
16373
+ }
16374
+ const observedSelection = result.accessibleCustomers.payload.data.selection;
16375
+ if (observedSelection.customerId !== connection.selectedCustomerId || observedSelection.loginCustomerId !== connection.selectedLoginCustomerId) {
16376
+ throw new Error("Google Ads returned evidence for a different customer selection.");
16377
+ }
16378
+ const selectedCustomer = result.accessibleCustomers.payload.data.customers.find(
16379
+ (customer) => customer.customerId === connection.selectedCustomerId
16380
+ );
16381
+ if (!selectedCustomer) {
16382
+ throw new Error("Google Ads did not return the selected customer in its validated snapshot.");
16383
+ }
16384
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
16385
+ db.transaction((tx) => {
16386
+ const terminalClaim = tx.update(runs).set({
16387
+ status: RunStatuses.completed,
16388
+ finishedAt,
16389
+ error: null
16390
+ }).where(and18(
16391
+ eq24(runs.id, runId),
16392
+ eq24(runs.projectId, projectId),
16393
+ eq24(runs.kind, RunKinds["google-ads-sync"]),
16394
+ eq24(runs.status, RunStatuses.running)
16395
+ )).run();
16396
+ if (terminalClaim.changes !== 1) {
16397
+ throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
16398
+ }
16399
+ const connectionClaim = tx.update(googleAdsConnections).set({
16400
+ selectedCustomerName: selectedCustomer.descriptiveName,
16401
+ selectedCustomerCurrencyCode: selectedCustomer.currencyCode,
16402
+ selectedCustomerTimeZone: selectedCustomer.timeZone,
16403
+ selectedCustomerStatus: selectedCustomer.status,
16404
+ lastValidatedAt: result.accessibleCustomers.metadata.capturedAt,
16405
+ lastCustomerSnapshotId: result.accessibleCustomers.metadata.id,
16406
+ lastInventorySnapshotAt: result.inventory.metadata.capturedAt,
16407
+ lastInventorySnapshotId: result.inventory.metadata.id,
16408
+ lastMetricsSnapshotAt: result.metrics?.metadata.capturedAt ?? connection.lastMetricsSnapshotAt,
16409
+ lastMetricsSnapshotId: result.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
16410
+ updatedAt: finishedAt
16411
+ }).where(and18(
16412
+ eq24(googleAdsConnections.id, connection.id),
16413
+ eq24(googleAdsConnections.projectId, projectId),
16414
+ eq24(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
16415
+ connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) : eq24(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
16416
+ eq24(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
16417
+ )).run();
16418
+ if (connectionClaim.changes !== 1) {
16419
+ throw new Error("Google Ads selection changed while its evidence was being collected.");
16420
+ }
16421
+ tx.insert(googleAdsRawSnapshots).values([
16422
+ googleAdsSnapshotValues(result.accessibleCustomers),
16423
+ googleAdsSnapshotValues(result.inventory),
16424
+ ...result.metrics ? [googleAdsSnapshotValues(result.metrics)] : []
16425
+ ]).run();
16426
+ });
16427
+ } catch (error) {
16428
+ markFailed(
16429
+ db,
16430
+ runId,
16431
+ projectId,
16432
+ RunKinds["google-ads-sync"],
16433
+ claimed ? RunStatuses.running : RunStatuses.queued,
16434
+ error
16435
+ );
16436
+ throw error;
16437
+ }
16438
+ }
16439
+ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
16440
+ let claimed = false;
16441
+ try {
16442
+ requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
16443
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
16444
+ if (!project) throw new Error(`Project ${projectId} was not found.`);
16445
+ const connection = db.select().from(gtmConnections).where(eq24(gtmConnections.projectId, projectId)).get();
16446
+ if (!connection?.selectedAccountId || !connection.selectedContainerId) {
16447
+ throw new Error("GTM needs a selected account and container before it can sync.");
16448
+ }
16449
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
16450
+ if (!claimSyncRun(db, runId, projectId, RunKinds["gtm-sync"], startedAt)) {
16451
+ throw new Error(`Run ${runId} could not be claimed for GTM sync.`);
16452
+ }
16453
+ claimed = true;
16454
+ const snapshot = await runtime.syncGtm({
16455
+ project: { id: project.id, name: project.name },
16456
+ connectionId: connection.id,
16457
+ runId,
16458
+ selection: {
16459
+ accountId: connection.selectedAccountId,
16460
+ containerId: connection.selectedContainerId,
16461
+ workspaceId: connection.selectedWorkspaceId
16462
+ },
16463
+ expectedHostname: hostOf(project.canonicalDomain) ?? project.canonicalDomain
16464
+ });
16465
+ assertSnapshotContext(snapshot.metadata, { projectId, connectionId: connection.id, runId });
16466
+ if (snapshot.payload.kind !== "container") {
16467
+ throw new Error("Google Marketing runtime returned the wrong GTM snapshot kind.");
16468
+ }
16469
+ const graph = snapshot.payload.data;
16470
+ if (snapshot.metadata.accountId !== connection.selectedAccountId || snapshot.metadata.containerId !== connection.selectedContainerId || snapshot.metadata.workspaceId !== connection.selectedWorkspaceId) {
16471
+ throw new Error("GTM returned evidence for a different account, container, or workspace.");
16472
+ }
16473
+ const selectedWorkspace = connection.selectedWorkspaceId ? graph.workspaces.find((workspace) => workspace.id === connection.selectedWorkspaceId) : null;
16474
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
16475
+ db.transaction((tx) => {
16476
+ const terminalClaim = tx.update(runs).set({
16477
+ status: RunStatuses.completed,
16478
+ finishedAt,
16479
+ error: null
16480
+ }).where(and18(
16481
+ eq24(runs.id, runId),
16482
+ eq24(runs.projectId, projectId),
16483
+ eq24(runs.kind, RunKinds["gtm-sync"]),
16484
+ eq24(runs.status, RunStatuses.running)
16485
+ )).run();
16486
+ if (terminalClaim.changes !== 1) {
16487
+ throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
16488
+ }
16489
+ const connectionClaim = tx.update(gtmConnections).set({
16490
+ selectedAccountName: graph.account.name,
16491
+ selectedContainerName: graph.container.name,
16492
+ selectedContainerPublicId: graph.container.publicId,
16493
+ selectedWorkspaceName: selectedWorkspace?.name ?? null,
16494
+ lastValidatedAt: snapshot.metadata.capturedAt,
16495
+ lastSnapshotAt: snapshot.metadata.capturedAt,
16496
+ lastSnapshotId: snapshot.metadata.id,
16497
+ updatedAt: finishedAt
16498
+ }).where(and18(
16499
+ eq24(gtmConnections.id, connection.id),
16500
+ eq24(gtmConnections.projectId, projectId),
16501
+ eq24(gtmConnections.selectedAccountId, connection.selectedAccountId),
16502
+ eq24(gtmConnections.selectedContainerId, connection.selectedContainerId),
16503
+ connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) : eq24(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
16504
+ eq24(gtmConnections.selectionGeneration, connection.selectionGeneration)
16505
+ )).run();
16506
+ if (connectionClaim.changes !== 1) {
16507
+ throw new Error("GTM selection changed while its evidence was being collected.");
16508
+ }
16509
+ tx.insert(gtmRawSnapshots).values(gtmSnapshotValues(snapshot)).run();
16510
+ });
16511
+ } catch (error) {
16512
+ markFailed(
16513
+ db,
16514
+ runId,
16515
+ projectId,
16516
+ RunKinds["gtm-sync"],
16517
+ claimed ? RunStatuses.running : RunStatuses.queued,
16518
+ error
16519
+ );
16520
+ throw error;
16521
+ }
16522
+ }
16523
+
13531
16524
  // src/server.ts
13532
16525
  var _require3 = createRequire4(import.meta.url);
13533
16526
  var { version: PKG_VERSION2 } = _require3("../package.json");
@@ -13595,14 +16588,14 @@ function summarizeProviderConfig(config) {
13595
16588
  };
13596
16589
  }
13597
16590
  function hashApiKey(key) {
13598
- return crypto24.createHash("sha256").update(key).digest("hex");
16591
+ return crypto26.createHash("sha256").update(key).digest("hex");
13599
16592
  }
13600
16593
  var DASHBOARD_SCRYPT_KEYLEN = 64;
13601
16594
  var DASHBOARD_SCRYPT_COST = 1 << 15;
13602
16595
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
13603
16596
  function hashDashboardPassword(password) {
13604
- const salt = crypto24.randomBytes(16);
13605
- const derived = crypto24.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
16597
+ const salt = crypto26.randomBytes(16);
16598
+ const derived = crypto26.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
13606
16599
  N: DASHBOARD_SCRYPT_COST,
13607
16600
  maxmem: DASHBOARD_SCRYPT_MAXMEM
13608
16601
  });
@@ -13623,14 +16616,14 @@ function verifyDashboardPassword(password, storedHash) {
13623
16616
  } catch {
13624
16617
  return { ok: false, needsRehash: false };
13625
16618
  }
13626
- const derived = crypto24.scryptSync(password, salt, expected.length, {
16619
+ const derived = crypto26.scryptSync(password, salt, expected.length, {
13627
16620
  N: DASHBOARD_SCRYPT_COST,
13628
16621
  maxmem: DASHBOARD_SCRYPT_MAXMEM
13629
16622
  });
13630
16623
  if (derived.length !== expected.length)
13631
16624
  return { ok: false, needsRehash: false };
13632
16625
  return {
13633
- ok: crypto24.timingSafeEqual(derived, expected),
16626
+ ok: crypto26.timingSafeEqual(derived, expected),
13634
16627
  needsRehash: false
13635
16628
  };
13636
16629
  }
@@ -13639,7 +16632,7 @@ function verifyDashboardPassword(password, storedHash) {
13639
16632
  const expected = Buffer.from(storedHash, "hex");
13640
16633
  if (candidate.length !== expected.length)
13641
16634
  return { ok: false, needsRehash: false };
13642
- const ok = crypto24.timingSafeEqual(candidate, expected);
16635
+ const ok = crypto26.timingSafeEqual(candidate, expected);
13643
16636
  return { ok, needsRehash: ok };
13644
16637
  }
13645
16638
  return { ok: false, needsRehash: false };
@@ -13748,6 +16741,126 @@ function resolveGooglePublicUrl(config, basePath) {
13748
16741
  return void 0;
13749
16742
  }
13750
16743
  }
16744
+ function cloneGoogleAdsConfig(config) {
16745
+ if (!config) return void 0;
16746
+ return {
16747
+ ...config,
16748
+ ...config.connections === void 0 ? {} : {
16749
+ connections: config.connections.map((connection) => ({
16750
+ ...connection,
16751
+ ...connection.scopes === void 0 ? {} : { scopes: [...connection.scopes] }
16752
+ }))
16753
+ }
16754
+ };
16755
+ }
16756
+ function cloneGtmConfig(config) {
16757
+ if (!config) return void 0;
16758
+ return {
16759
+ ...config,
16760
+ ...config.connections === void 0 ? {} : {
16761
+ connections: config.connections.map((connection) => ({
16762
+ ...connection,
16763
+ ...connection.scopes === void 0 ? {} : { scopes: [...connection.scopes] }
16764
+ }))
16765
+ }
16766
+ };
16767
+ }
16768
+ function createGoogleMarketingConfigCredentialStore(input) {
16769
+ const persistConfigPatch = input.saveConfigPatch ?? saveConfigPatch;
16770
+ const env = input.env ?? process.env;
16771
+ const randomUUID = input.randomUUID ?? crypto26.randomUUID;
16772
+ const snapshot = () => ({
16773
+ googleAds: cloneGoogleAdsConfig(input.config.googleAds),
16774
+ gtm: cloneGtmConfig(input.config.gtm)
16775
+ });
16776
+ const restore = (previous) => {
16777
+ input.config.googleAds = previous.googleAds;
16778
+ input.config.gtm = previous.gtm;
16779
+ };
16780
+ return {
16781
+ hasGoogleAdsDeveloperToken: () => Boolean(
16782
+ env.GOOGLE_ADS_DEVELOPER_TOKEN?.trim() || getGoogleAdsAuthConfig(input.config).developerToken?.trim()
16783
+ ),
16784
+ get: (project, provider) => {
16785
+ if (provider === GoogleMarketingProviders["google-ads"]) {
16786
+ const connection2 = getGoogleAdsConnection(input.config, project.id);
16787
+ if (!connection2) return void 0;
16788
+ return {
16789
+ accessToken: connection2.accessToken ?? null,
16790
+ refreshToken: connection2.refreshToken ?? null,
16791
+ expiresAt: connection2.tokenExpiresAt ?? null,
16792
+ scopes: connection2.scopes ?? [],
16793
+ developerToken: getGoogleAdsAuthConfig(input.config).developerToken ?? null,
16794
+ createdAt: connection2.createdAt,
16795
+ updatedAt: connection2.updatedAt
16796
+ };
16797
+ }
16798
+ const connection = getGtmConnection(input.config, project.id);
16799
+ if (!connection) return void 0;
16800
+ return {
16801
+ accessToken: connection.accessToken ?? null,
16802
+ refreshToken: connection.refreshToken ?? null,
16803
+ expiresAt: connection.tokenExpiresAt ?? null,
16804
+ scopes: connection.scopes ?? [],
16805
+ createdAt: connection.createdAt,
16806
+ updatedAt: connection.updatedAt
16807
+ };
16808
+ },
16809
+ upsert: (project, provider, credential) => {
16810
+ const previous = snapshot();
16811
+ try {
16812
+ if (provider === GoogleMarketingProviders["google-ads"]) {
16813
+ if (credential.developerToken) {
16814
+ setGoogleAdsAuthConfig(input.config, { developerToken: credential.developerToken });
16815
+ }
16816
+ upsertGoogleAdsConnection(input.config, {
16817
+ projectId: project.id,
16818
+ projectName: project.name,
16819
+ credentialGeneration: randomUUID(),
16820
+ ...credential.accessToken ? { accessToken: credential.accessToken } : {},
16821
+ refreshToken: credential.refreshToken ?? null,
16822
+ tokenExpiresAt: credential.expiresAt ?? null,
16823
+ scopes: credential.scopes,
16824
+ createdAt: credential.createdAt,
16825
+ updatedAt: credential.updatedAt
16826
+ });
16827
+ } else {
16828
+ upsertGtmConnection(input.config, {
16829
+ projectId: project.id,
16830
+ projectName: project.name,
16831
+ credentialGeneration: randomUUID(),
16832
+ ...credential.accessToken ? { accessToken: credential.accessToken } : {},
16833
+ refreshToken: credential.refreshToken ?? null,
16834
+ tokenExpiresAt: credential.expiresAt ?? null,
16835
+ scopes: credential.scopes,
16836
+ createdAt: credential.createdAt,
16837
+ updatedAt: credential.updatedAt
16838
+ });
16839
+ }
16840
+ persistConfigPatch(input.config);
16841
+ } catch (error) {
16842
+ restore(previous);
16843
+ throw error;
16844
+ }
16845
+ return () => {
16846
+ restore(previous);
16847
+ persistConfigPatch(input.config);
16848
+ };
16849
+ },
16850
+ delete: (project, provider) => {
16851
+ const previous = snapshot();
16852
+ const removed = provider === GoogleMarketingProviders["google-ads"] ? removeGoogleAdsConnection(input.config, project.id) : removeGtmConnection(input.config, project.id);
16853
+ if (!removed) return false;
16854
+ try {
16855
+ persistConfigPatch(input.config);
16856
+ } catch (error) {
16857
+ restore(previous);
16858
+ throw error;
16859
+ }
16860
+ return true;
16861
+ }
16862
+ };
16863
+ }
13751
16864
  async function createServer(opts) {
13752
16865
  const logger = opts.logger === false ? false : process.stdout.isTTY ? {
13753
16866
  transport: {
@@ -13847,7 +16960,7 @@ async function createServer(opts) {
13847
16960
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
13848
16961
  async (ctx) => {
13849
16962
  if (!sessionRegistry) return;
13850
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq24(projects.id, ctx.projectId)).get();
16963
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq25(projects.id, ctx.projectId)).get();
13851
16964
  if (!project) return;
13852
16965
  let content;
13853
16966
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -13870,6 +16983,99 @@ async function createServer(opts) {
13870
16983
  );
13871
16984
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
13872
16985
  const snapshotService = new SnapshotService(registry);
16986
+ const liveProjectIds = new Set(
16987
+ opts.db.select({ id: projects.id }).from(projects).all().map((project) => project.id)
16988
+ );
16989
+ const removedGoogleMarketingCredentials = removeLegacyGoogleAdsConnections(opts.config) + removeLegacyGtmConnections(opts.config) + removeOrphanedGoogleAdsConnections(opts.config, liveProjectIds) + removeOrphanedGtmConnections(opts.config, liveProjectIds);
16990
+ if (removedGoogleMarketingCredentials > 0) {
16991
+ saveConfigPatch({
16992
+ googleAds: opts.config.googleAds,
16993
+ gtm: opts.config.gtm
16994
+ });
16995
+ }
16996
+ const googleMarketingRuntime = createGoogleMarketingRuntime({
16997
+ config: opts.config,
16998
+ saveConfigPatch
16999
+ });
17000
+ const googleMarketingCredentialStore = createGoogleMarketingConfigCredentialStore({
17001
+ config: opts.config
17002
+ });
17003
+ const prepareGoogleMarketingCredentialDelete = (projectId) => {
17004
+ const previousGoogleAdsConnections = opts.config.googleAds?.connections;
17005
+ const previousGtmConnections = opts.config.gtm?.connections;
17006
+ const restore = () => {
17007
+ if (opts.config.googleAds) opts.config.googleAds.connections = previousGoogleAdsConnections;
17008
+ if (opts.config.gtm) opts.config.gtm.connections = previousGtmConnections;
17009
+ };
17010
+ const removedGoogleAds = removeGoogleAdsConnection(opts.config, projectId);
17011
+ const removedGtm = removeGtmConnection(opts.config, projectId);
17012
+ if (!removedGoogleAds && !removedGtm) return void 0;
17013
+ try {
17014
+ saveConfigPatch({
17015
+ googleAds: opts.config.googleAds,
17016
+ gtm: opts.config.gtm
17017
+ });
17018
+ } catch (error) {
17019
+ restore();
17020
+ throw error;
17021
+ }
17022
+ return () => {
17023
+ restore();
17024
+ try {
17025
+ saveConfigPatch({
17026
+ googleAds: opts.config.googleAds,
17027
+ gtm: opts.config.gtm
17028
+ });
17029
+ } catch (error) {
17030
+ removeGoogleAdsConnection(opts.config, projectId);
17031
+ removeGtmConnection(opts.config, projectId);
17032
+ throw error;
17033
+ }
17034
+ };
17035
+ };
17036
+ const googleMarketingOAuth = {
17037
+ authorizationUrl: (input) => {
17038
+ const auth = input.provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsAuthConfig(opts.config) : getGtmAuthConfig(opts.config);
17039
+ if (!auth.clientId || !auth.clientSecret) {
17040
+ throw new Error("Google OAuth client credentials are not configured.");
17041
+ }
17042
+ return getAuthUrl(auth.clientId, input.redirectUri, [...input.scopes], input.state);
17043
+ },
17044
+ exchangeCode: async (input) => {
17045
+ const auth = input.provider === GoogleMarketingProviders["google-ads"] ? getGoogleAdsAuthConfig(opts.config) : getGtmAuthConfig(opts.config);
17046
+ if (!auth.clientId || !auth.clientSecret) {
17047
+ throw new Error("Google OAuth client credentials are not configured.");
17048
+ }
17049
+ const tokens = await exchangeCode(
17050
+ auth.clientId,
17051
+ auth.clientSecret,
17052
+ input.code,
17053
+ input.redirectUri
17054
+ );
17055
+ return {
17056
+ accessToken: tokens.access_token,
17057
+ refreshToken: tokens.refresh_token ?? null,
17058
+ expiresAt: Number.isFinite(tokens.expires_in) && tokens.expires_in > 0 ? new Date(Date.now() + tokens.expires_in * 1e3).toISOString() : null,
17059
+ scopes: tokens.scope?.split(/\s+/).filter(Boolean) ?? [input.provider === GoogleMarketingProviders["google-ads"] ? GOOGLE_ADS_OAUTH_SCOPE : GTM_READONLY_SCOPE]
17060
+ };
17061
+ }
17062
+ };
17063
+ const googleMarketingLiveReader = {
17064
+ listGoogleAdsCustomers: (project) => googleMarketingRuntime.listGoogleAdsCustomers(project),
17065
+ listGtmAccounts: (project) => googleMarketingRuntime.listGtmAccounts(project),
17066
+ listGtmContainers: (project, accountId) => googleMarketingRuntime.listGtmContainers(project, accountId),
17067
+ listGtmWorkspaces: (project, accountId, containerId) => googleMarketingRuntime.listGtmWorkspaces(project, accountId, containerId)
17068
+ };
17069
+ const runGoogleAdsMarketingSync = (runId, projectId) => {
17070
+ executeGoogleAdsMarketingSync(opts.db, googleMarketingRuntime, runId, projectId).then(() => runCoordinator.onRunCompleted(runId, projectId)).catch(() => {
17071
+ app.log.error({ runId, projectId }, "Google Ads sync failed");
17072
+ });
17073
+ };
17074
+ const runGtmMarketingSync = (runId, projectId) => {
17075
+ executeGtmMarketingSync(opts.db, googleMarketingRuntime, runId, projectId).then(() => runCoordinator.onRunCompleted(runId, projectId)).catch(() => {
17076
+ app.log.error({ runId, projectId }, "GTM sync failed");
17077
+ });
17078
+ };
13873
17079
  const orphanedOpenClawDir = path8.join(os4.homedir(), ".openclaw-aero");
13874
17080
  if (fs7.existsSync(orphanedOpenClawDir)) {
13875
17081
  app.log.warn(
@@ -14197,7 +17403,7 @@ async function createServer(opts) {
14197
17403
  void (async () => {
14198
17404
  try {
14199
17405
  const report = await aeroClient.runDoctor({ project: projectName });
14200
- const project = opts.db.select().from(projects).where(eq24(projects.name, projectName)).get();
17406
+ const project = opts.db.select().from(projects).where(eq25(projects.name, projectName)).get();
14201
17407
  if (!project) {
14202
17408
  app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
14203
17409
  return;
@@ -14225,9 +17431,9 @@ async function createServer(opts) {
14225
17431
  });
14226
17432
  if (!probed) return;
14227
17433
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
14228
- and18(
14229
- eq24(ccReleaseSyncs.release, probed.release),
14230
- eq24(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
17434
+ and19(
17435
+ eq25(ccReleaseSyncs.release, probed.release),
17436
+ eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
14231
17437
  )
14232
17438
  ).limit(1).get();
14233
17439
  if (alreadySynced) {
@@ -14408,7 +17614,7 @@ async function createServer(opts) {
14408
17614
  return removed;
14409
17615
  }
14410
17616
  };
14411
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto24.randomBytes(32).toString("hex");
17617
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto26.randomBytes(32).toString("hex");
14412
17618
  const googleConnectionStore = {
14413
17619
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
14414
17620
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -14484,11 +17690,11 @@ async function createServer(opts) {
14484
17690
  const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
14485
17691
  if (opts.config.apiKey) {
14486
17692
  const keyHash = hashApiKey(opts.config.apiKey);
14487
- const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
17693
+ const existing = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, keyHash)).get();
14488
17694
  if (!existing) {
14489
17695
  const prefix = opts.config.apiKey.slice(0, 12);
14490
17696
  opts.db.insert(apiKeys).values({
14491
- id: `key_${crypto24.randomBytes(8).toString("hex")}`,
17697
+ id: `key_${crypto26.randomBytes(8).toString("hex")}`,
14492
17698
  name: "default",
14493
17699
  keyHash,
14494
17700
  keyPrefix: prefix,
@@ -14512,7 +17718,7 @@ async function createServer(opts) {
14512
17718
  };
14513
17719
  const createSession = (apiKeyId) => {
14514
17720
  pruneExpiredSessions();
14515
- const sessionId = crypto24.randomBytes(32).toString("hex");
17721
+ const sessionId = crypto26.randomBytes(32).toString("hex");
14516
17722
  sessions.set(sessionId, {
14517
17723
  apiKeyId,
14518
17724
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -14536,7 +17742,7 @@ async function createServer(opts) {
14536
17742
  };
14537
17743
  const getDefaultApiKey = () => {
14538
17744
  if (!opts.config.apiKey) return void 0;
14539
- return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17745
+ return opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
14540
17746
  };
14541
17747
  const createPasswordSession = (reply) => {
14542
17748
  const key = getDefaultApiKey();
@@ -14560,7 +17766,7 @@ async function createServer(opts) {
14560
17766
  if (!header) return false;
14561
17767
  const parts = header.split(" ");
14562
17768
  if (parts.length !== 2 || parts[0] !== "Bearer") return false;
14563
- const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(parts[1]))).get();
17769
+ const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(parts[1]))).get();
14564
17770
  return Boolean(key && !key.revokedAt);
14565
17771
  };
14566
17772
  const namedAccountsInUse = () => anyUsersExist(opts.db);
@@ -14650,12 +17856,12 @@ async function createServer(opts) {
14650
17856
  return reply.send({ authenticated: true });
14651
17857
  }
14652
17858
  if (apiKey) {
14653
- const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
17859
+ const key = opts.db.select().from(apiKeys).where(eq25(apiKeys.keyHash, hashApiKey(apiKey))).get();
14654
17860
  if (!key || key.revokedAt) {
14655
17861
  const err2 = authInvalid();
14656
17862
  return reply.status(err2.statusCode).send(err2.toJSON());
14657
17863
  }
14658
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
17864
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, key.id)).run();
14659
17865
  const sessionId = createSession(key.id);
14660
17866
  reply.header(
14661
17867
  "set-cookie",
@@ -14797,9 +18003,61 @@ async function createServer(opts) {
14797
18003
  // Resolved fresh each call so a key added at runtime (settings API) shows
14798
18004
  // up immediately in the `config.agent-providers` doctor check.
14799
18005
  getAgentProviderSummary: () => buildAgentProvidersResponse(opts.config).providers,
18006
+ getGoogleMarketingDoctorInput: (ctx) => {
18007
+ if (!ctx.project) return null;
18008
+ const googleAds = opts.db.select().from(googleAdsConnections).where(eq25(googleAdsConnections.projectId, ctx.project.id)).get();
18009
+ const gtm = opts.db.select().from(gtmConnections).where(eq25(gtmConnections.projectId, ctx.project.id)).get();
18010
+ const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
18011
+ const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
18012
+ const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
18013
+ const gtmConnected = Boolean(gtmCredential?.accessToken);
18014
+ return {
18015
+ googleAds: googleAds && googleAdsConnected ? {
18016
+ credentialsPresent: Boolean(
18017
+ googleAdsCredential?.accessToken || googleAdsCredential?.refreshToken
18018
+ ),
18019
+ grantedScopes: googleAds.scopes,
18020
+ selectedLoginCustomerId: googleAds.selectedLoginCustomerId,
18021
+ selectedCustomerId: googleAds.selectedCustomerId,
18022
+ latestSnapshotAt: [
18023
+ googleAds.lastInventorySnapshotAt,
18024
+ googleAds.lastMetricsSnapshotAt
18025
+ ].filter((value) => Boolean(value)).sort().at(-1) ?? null
18026
+ } : null,
18027
+ gtm: gtm && gtmConnected ? {
18028
+ credentialsPresent: Boolean(gtmCredential?.accessToken || gtmCredential?.refreshToken),
18029
+ grantedScopes: gtm.scopes,
18030
+ selectedAccountId: gtm.selectedAccountId,
18031
+ selectedContainerId: gtm.selectedContainerId,
18032
+ selectedWorkspaceId: gtm.selectedWorkspaceId,
18033
+ latestSnapshotAt: gtm.lastSnapshotAt
18034
+ } : null
18035
+ };
18036
+ },
14800
18037
  googleConnectionStore,
14801
18038
  googleStateSecret,
14802
18039
  publicUrl: googlePublicUrl,
18040
+ googleMarketingCredentialStore,
18041
+ googleMarketingOAuth,
18042
+ googleMarketingOAuthScopes: {
18043
+ [GoogleMarketingProviders["google-ads"]]: [GOOGLE_ADS_OAUTH_SCOPE],
18044
+ [GoogleMarketingProviders.gtm]: [GTM_READONLY_SCOPE]
18045
+ },
18046
+ googleMarketingLiveReader,
18047
+ assessConversionTrackingIntegrity: ({ contract, googleAdsSnapshot, gtmSnapshot }) => {
18048
+ const googleAdsInventory = googleAdsSnapshot?.payload.kind === "inventory" ? googleAdsSnapshot.payload.data : null;
18049
+ const gtmLiveGraph = gtmSnapshot?.payload.kind === "container" ? gtmSnapshot.payload.data.live : gtmSnapshot?.payload.kind === "live" ? gtmSnapshot.payload.data : null;
18050
+ return assessConversionTrackingIntegrity({
18051
+ contract,
18052
+ googleAdsInventory,
18053
+ googleAdsEvidenceId: googleAdsSnapshot?.metadata.id,
18054
+ gtmLiveGraph,
18055
+ gtmEvidenceId: gtmSnapshot?.metadata.id,
18056
+ evaluatedAt: (/* @__PURE__ */ new Date()).toISOString()
18057
+ });
18058
+ },
18059
+ onGoogleAdsSyncRequested: runGoogleAdsMarketingSync,
18060
+ onGtmSyncRequested: runGtmMarketingSync,
14803
18061
  onGscSyncRequested: (runId, projectId, syncOpts) => {
14804
18062
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
14805
18063
  if (!googleClientId || !googleClientSecret) {
@@ -14872,7 +18130,7 @@ async function createServer(opts) {
14872
18130
  deps: {
14873
18131
  enqueueAutoExtract: ({ projectId, release: r }) => {
14874
18132
  const now = (/* @__PURE__ */ new Date()).toISOString();
14875
- const runId = crypto24.randomUUID();
18133
+ const runId = crypto26.randomUUID();
14876
18134
  opts.db.insert(runs).values({
14877
18135
  id: runId,
14878
18136
  projectId,
@@ -14997,7 +18255,7 @@ async function createServer(opts) {
14997
18255
  ...inspectOpts,
14998
18256
  config: opts.config
14999
18257
  }).then(() => {
15000
- const finished = opts.db.select({ status: runs.status }).from(runs).where(eq24(runs.id, runId)).get();
18258
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq25(runs.id, runId)).get();
15001
18259
  if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
15002
18260
  return maybeRefreshGscCoverage(
15003
18261
  opts.db,
@@ -15107,7 +18365,7 @@ async function createServer(opts) {
15107
18365
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
15108
18366
  opts.db.insert(auditLog).values(
15109
18367
  targetProjectIds.map((projectId) => ({
15110
- id: crypto24.randomUUID(),
18368
+ id: crypto26.randomUUID(),
15111
18369
  projectId,
15112
18370
  actor: "api",
15113
18371
  action: existing ? "provider.updated" : "provider.created",
@@ -15152,8 +18410,17 @@ async function createServer(opts) {
15152
18410
  if (action === "upsert") scheduler.upsert(projectId, kind);
15153
18411
  if (action === "delete") scheduler.remove(projectId, kind);
15154
18412
  },
18413
+ onProjectDeleting: prepareGoogleMarketingCredentialDelete,
15155
18414
  onProjectDeleted: (projectId) => {
15156
18415
  scheduler.removeAllForProject(projectId);
18416
+ const removedGoogleAds = removeGoogleAdsConnection(opts.config, projectId);
18417
+ const removedGtm = removeGtmConnection(opts.config, projectId);
18418
+ if (removedGoogleAds || removedGtm) {
18419
+ saveConfigPatch({
18420
+ googleAds: opts.config.googleAds,
18421
+ gtm: opts.config.gtm
18422
+ });
18423
+ }
15157
18424
  },
15158
18425
  onAliasesChanged: (projectId, projectName) => {
15159
18426
  setImmediate(() => {
@@ -15378,7 +18645,7 @@ async function createServer(opts) {
15378
18645
  }
15379
18646
  checkLatestVersionForServer();
15380
18647
  scheduler.start();
15381
- for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq24(researchRuns.status, ResearchRunStatuses.queued)).all()) {
18648
+ for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq25(researchRuns.status, ResearchRunStatuses.queued)).all()) {
15382
18649
  dispatchResearchRun(run.id, run.projectId);
15383
18650
  }
15384
18651
  app.addHook("onClose", async () => {
@@ -15469,5 +18736,8 @@ export {
15469
18736
  coerceAgentProvider,
15470
18737
  AERO_TOOL_PROFILES,
15471
18738
  isAeroToolProfile,
18739
+ GoogleMarketingRuntimeError,
18740
+ createGoogleMarketingCredentialStore,
18741
+ createGoogleMarketingRuntime,
15472
18742
  createServer
15473
18743
  };