@jadenrazo/cloudcost-mcp 1.0.1 → 1.2.1

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.
@@ -8,10 +8,11 @@ import {
8
8
  getGcpSqlPricing,
9
9
  getGcpStoragePricing,
10
10
  getInstanceMap,
11
+ getPricingMetadata,
11
12
  getRegionMappings,
12
13
  getRegionPriceMultipliers,
13
14
  getStorageMap
14
- } from "./chunk-MNFT5YKN.js";
15
+ } from "./chunk-LWTVE7UY.js";
15
16
 
16
17
  // src/config.ts
17
18
  import { readFileSync, existsSync } from "fs";
@@ -39,9 +40,39 @@ var DEFAULT_CONFIG = {
39
40
  },
40
41
  budget: {
41
42
  warn_percentage: 80
43
+ },
44
+ guardrail: {
45
+ warn_ratio: 0.8
42
46
  }
43
47
  };
44
48
 
49
+ // src/logger.ts
50
+ var LEVEL_ORDER = {
51
+ debug: 0,
52
+ info: 1,
53
+ warn: 2,
54
+ error: 3
55
+ };
56
+ var currentLevel = "info";
57
+ function setLogLevel(level) {
58
+ currentLevel = level;
59
+ }
60
+ function shouldLog(level) {
61
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
62
+ }
63
+ function log(level, message, data) {
64
+ if (!shouldLog(level)) return;
65
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
66
+ const entry = data ? `[${timestamp}] ${level.toUpperCase()}: ${message} ${JSON.stringify(data)}` : `[${timestamp}] ${level.toUpperCase()}: ${message}`;
67
+ process.stderr.write(entry + "\n");
68
+ }
69
+ var logger = {
70
+ debug: (msg, data) => log("debug", msg, data),
71
+ info: (msg, data) => log("info", msg, data),
72
+ warn: (msg, data) => log("warn", msg, data),
73
+ error: (msg, data) => log("error", msg, data)
74
+ };
75
+
45
76
  // src/config.ts
46
77
  function getConfigDir() {
47
78
  return join(homedir(), ".cloudcost");
@@ -51,7 +82,11 @@ function loadFileConfig() {
51
82
  if (!existsSync(configPath)) return {};
52
83
  try {
53
84
  return JSON.parse(readFileSync(configPath, "utf-8"));
54
- } catch {
85
+ } catch (err) {
86
+ logger.warn("Ignoring malformed config file; using defaults", {
87
+ path: configPath,
88
+ error: err instanceof Error ? err.message : String(err)
89
+ });
55
90
  return {};
56
91
  }
57
92
  }
@@ -101,6 +136,23 @@ function loadEnvConfig() {
101
136
  ...warnPct !== void 0 && !isNaN(warnPct) && { warn_percentage: warnPct }
102
137
  };
103
138
  }
139
+ if (env.CLOUDCOST_GUARDRAIL_MAX_MONTHLY || env.CLOUDCOST_GUARDRAIL_MAX_PER_RESOURCE || env.CLOUDCOST_GUARDRAIL_WARN_RATIO) {
140
+ const parsePositive = (raw) => {
141
+ if (!raw) return void 0;
142
+ const v = parseFloat(raw);
143
+ return Number.isFinite(v) && v > 0 ? v : void 0;
144
+ };
145
+ const maxMonthly = parsePositive(env.CLOUDCOST_GUARDRAIL_MAX_MONTHLY);
146
+ const maxPerResource = parsePositive(env.CLOUDCOST_GUARDRAIL_MAX_PER_RESOURCE);
147
+ const warnRatioRaw = env.CLOUDCOST_GUARDRAIL_WARN_RATIO ? parseFloat(env.CLOUDCOST_GUARDRAIL_WARN_RATIO) : NaN;
148
+ const warnRatio = Number.isFinite(warnRatioRaw) && warnRatioRaw > 0 && warnRatioRaw <= 1 ? warnRatioRaw : void 0;
149
+ config.guardrail = {
150
+ ...DEFAULT_CONFIG.guardrail,
151
+ ...maxMonthly !== void 0 && { max_monthly: maxMonthly },
152
+ ...maxPerResource !== void 0 && { max_per_resource: maxPerResource },
153
+ ...warnRatio !== void 0 && { warn_ratio: warnRatio }
154
+ };
155
+ }
104
156
  return config;
105
157
  }
106
158
  function loadConfig() {
@@ -133,43 +185,94 @@ function loadConfig() {
133
185
  ...DEFAULT_CONFIG.budget,
134
186
  ...fileConfig.budget,
135
187
  ...envConfig.budget
188
+ },
189
+ guardrail: {
190
+ ...DEFAULT_CONFIG.guardrail,
191
+ ...fileConfig.guardrail,
192
+ ...envConfig.guardrail
136
193
  }
137
194
  };
138
195
  }
139
196
 
140
- // src/pricing/cache.ts
141
- import Database from "better-sqlite3";
142
- import { mkdirSync } from "fs";
143
- import { dirname } from "path";
144
-
145
- // src/logger.ts
146
- var LEVEL_ORDER = {
147
- debug: 0,
148
- info: 1,
149
- warn: 2,
150
- error: 3
197
+ // src/currency.ts
198
+ var SUPPORTED_CURRENCIES = [
199
+ "USD",
200
+ "EUR",
201
+ "GBP",
202
+ "JPY",
203
+ "CAD",
204
+ "AUD",
205
+ "INR",
206
+ "BRL"
207
+ ];
208
+ var EXCHANGE_RATES_AS_OF = "2026-03";
209
+ var EXCHANGE_RATES = {
210
+ USD: 1,
211
+ EUR: 0.92,
212
+ GBP: 0.79,
213
+ JPY: 149.5,
214
+ CAD: 1.36,
215
+ AUD: 1.53,
216
+ INR: 83.1,
217
+ BRL: 4.97
151
218
  };
152
- var currentLevel = "info";
153
- function setLogLevel(level) {
154
- currentLevel = level;
219
+ function convertCurrency(amountUsd, targetCurrency) {
220
+ const rate = EXCHANGE_RATES[targetCurrency];
221
+ if (rate === void 0 || rate === 1) {
222
+ return amountUsd;
223
+ }
224
+ return amountUsd * rate;
155
225
  }
156
- function shouldLog(level) {
157
- return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
226
+ function getExchangeRateInfo(currency) {
227
+ return {
228
+ rate: EXCHANGE_RATES[currency] ?? 1,
229
+ rate_as_of: EXCHANGE_RATES_AS_OF
230
+ };
158
231
  }
159
- function log(level, message, data) {
160
- if (!shouldLog(level)) return;
161
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
162
- const entry = data ? `[${timestamp}] ${level.toUpperCase()}: ${message} ${JSON.stringify(data)}` : `[${timestamp}] ${level.toUpperCase()}: ${message}`;
163
- process.stderr.write(entry + "\n");
232
+ function convertLineItem(item, currency) {
233
+ return {
234
+ ...item,
235
+ unit_price: convertCurrency(item.unit_price, currency),
236
+ monthly_cost: convertCurrency(item.monthly_cost, currency)
237
+ };
238
+ }
239
+ function convertCostEstimate(estimate, currency) {
240
+ return {
241
+ ...estimate,
242
+ monthly_cost: convertCurrency(estimate.monthly_cost, currency),
243
+ yearly_cost: convertCurrency(estimate.yearly_cost, currency),
244
+ currency,
245
+ breakdown: estimate.breakdown.map((item) => convertLineItem(item, currency))
246
+ };
247
+ }
248
+ function convertBreakdownCurrency(breakdown, currency) {
249
+ const original_usd = {
250
+ total_monthly: breakdown.total_monthly,
251
+ total_yearly: breakdown.total_yearly
252
+ };
253
+ const convertedByService = {};
254
+ for (const [service, amount] of Object.entries(breakdown.by_service)) {
255
+ convertedByService[service] = convertCurrency(amount, currency);
256
+ }
257
+ return {
258
+ ...breakdown,
259
+ total_monthly: convertCurrency(breakdown.total_monthly, currency),
260
+ total_yearly: convertCurrency(breakdown.total_yearly, currency),
261
+ currency,
262
+ by_service: convertedByService,
263
+ by_resource: breakdown.by_resource.map((est) => convertCostEstimate(est, currency)),
264
+ ...breakdown.estimated_egress_monthly !== void 0 && {
265
+ estimated_egress_monthly: convertCurrency(breakdown.estimated_egress_monthly, currency)
266
+ },
267
+ original_usd,
268
+ exchange_rate: getExchangeRateInfo(currency)
269
+ };
164
270
  }
165
- var logger = {
166
- debug: (msg, data) => log("debug", msg, data),
167
- info: (msg, data) => log("info", msg, data),
168
- warn: (msg, data) => log("warn", msg, data),
169
- error: (msg, data) => log("error", msg, data)
170
- };
171
271
 
172
272
  // src/pricing/cache.ts
273
+ import Database from "better-sqlite3";
274
+ import { mkdirSync } from "fs";
275
+ import { dirname } from "path";
173
276
  var SCHEMA = `
174
277
  CREATE TABLE IF NOT EXISTS pricing_cache (
175
278
  key TEXT PRIMARY KEY,
@@ -525,6 +628,15 @@ var PricingCache = class {
525
628
  }
526
629
  };
527
630
 
631
+ // src/pricing/effective-date.ts
632
+ function resolveEffectiveDate(effectiveDate) {
633
+ if (effectiveDate) {
634
+ const t = new Date(effectiveDate);
635
+ if (!Number.isNaN(t.getTime())) return t.toISOString();
636
+ }
637
+ return (/* @__PURE__ */ new Date()).toISOString();
638
+ }
639
+
528
640
  // src/pricing/aws/aws-normalizer.ts
529
641
  function extractUsdPrice(priceDimensions) {
530
642
  for (const dim of Object.values(priceDimensions)) {
@@ -562,94 +674,131 @@ function extractFromTerms(onDemandTerms, defaultUnit) {
562
674
  }
563
675
  return { price: 0, unit: defaultUnit };
564
676
  }
565
- function resolveEffectiveDate(effectiveDate) {
566
- if (effectiveDate) {
567
- const t = new Date(effectiveDate);
568
- if (!Number.isNaN(t.getTime())) return t.toISOString();
569
- }
570
- return (/* @__PURE__ */ new Date()).toISOString();
571
- }
572
- function normalizeAwsCompute(rawProduct, rawPrice, region, effectiveDate) {
573
- const attrs = rawProduct?.attributes ?? {};
677
+ function normalize(rawPrice, region, effectiveDate, spec) {
574
678
  const terms = rawPrice?.terms?.OnDemand ?? {};
575
- const { price, unit } = extractFromTerms(terms, "Hrs");
679
+ const { price, unit } = extractFromTerms(terms, spec.defaultUnit);
576
680
  return {
577
681
  provider: "aws",
578
- service: "ec2",
579
- resource_type: attrs.instanceType ?? "unknown",
682
+ service: spec.service,
683
+ resource_type: spec.resourceType,
580
684
  region,
581
685
  unit,
582
686
  price_per_unit: price,
583
687
  currency: "USD",
688
+ description: spec.description,
689
+ attributes: {
690
+ ...spec.attributes,
691
+ pricing_source: "live"
692
+ },
693
+ effective_date: resolveEffectiveDate(effectiveDate)
694
+ };
695
+ }
696
+ function normalizeAwsCompute(rawProduct, rawPrice, region, effectiveDate) {
697
+ const attrs = rawProduct?.attributes ?? {};
698
+ return normalize(rawPrice, region, effectiveDate, {
699
+ service: "ec2",
700
+ defaultUnit: "Hrs",
701
+ resourceType: attrs.instanceType ?? "unknown",
584
702
  description: attrs.instanceType ? `AWS EC2 ${attrs.instanceType} (${attrs.operatingSystem ?? "Linux"})` : void 0,
585
703
  attributes: {
586
704
  instance_type: attrs.instanceType ?? "",
587
705
  vcpu: attrs.vcpu ?? "",
588
706
  memory: attrs.memory ?? "",
589
707
  operating_system: attrs.operatingSystem ?? "Linux",
590
- tenancy: attrs.tenancy ?? "Shared",
591
- pricing_source: "live"
592
- },
593
- effective_date: resolveEffectiveDate(effectiveDate)
594
- };
708
+ tenancy: attrs.tenancy ?? "Shared"
709
+ }
710
+ });
595
711
  }
596
712
  function normalizeAwsDatabase(rawProduct, rawPrice, region, effectiveDate) {
597
713
  const attrs = rawProduct?.attributes ?? {};
598
- const terms = rawPrice?.terms?.OnDemand ?? {};
599
- const { price, unit } = extractFromTerms(terms, "Hrs");
600
- return {
601
- provider: "aws",
714
+ return normalize(rawPrice, region, effectiveDate, {
602
715
  service: "rds",
603
- resource_type: attrs.instanceType ?? "unknown",
604
- region,
605
- unit,
606
- price_per_unit: price,
607
- currency: "USD",
716
+ defaultUnit: "Hrs",
717
+ resourceType: attrs.instanceType ?? "unknown",
608
718
  description: attrs.instanceType ? `AWS RDS ${attrs.instanceType} (${attrs.databaseEngine ?? "MySQL"})` : void 0,
609
719
  attributes: {
610
720
  instance_type: attrs.instanceType ?? "",
611
721
  database_engine: attrs.databaseEngine ?? "",
612
722
  deployment_option: attrs.deploymentOption ?? "Single-AZ",
613
723
  vcpu: attrs.vcpu ?? "",
614
- memory: attrs.memory ?? "",
615
- pricing_source: "live"
616
- },
617
- effective_date: resolveEffectiveDate(effectiveDate)
618
- };
724
+ memory: attrs.memory ?? ""
725
+ }
726
+ });
619
727
  }
620
728
  function normalizeAwsStorage(rawProduct, rawPrice, region, effectiveDate) {
621
729
  const attrs = rawProduct?.attributes ?? {};
622
- const terms = rawPrice?.terms?.OnDemand ?? {};
623
- const { price, unit } = extractFromTerms(terms, "GB-Mo");
624
730
  const volumeType = attrs.volumeApiName ?? attrs.volumeType ?? "gp3";
625
- return {
626
- provider: "aws",
731
+ return normalize(rawPrice, region, effectiveDate, {
627
732
  service: "ebs",
628
- resource_type: volumeType,
629
- region,
630
- unit,
631
- price_per_unit: price,
632
- currency: "USD",
733
+ defaultUnit: "GB-Mo",
734
+ resourceType: volumeType,
633
735
  description: `AWS EBS ${volumeType} volume`,
634
736
  attributes: {
635
737
  volume_type: volumeType,
636
738
  max_iops: attrs.maxIopsvolume ?? "",
637
- max_throughput: attrs.maxThroughputvolume ?? "",
638
- pricing_source: "live"
639
- },
640
- effective_date: resolveEffectiveDate(effectiveDate)
641
- };
739
+ max_throughput: attrs.maxThroughputvolume ?? ""
740
+ }
741
+ });
642
742
  }
643
743
 
644
744
  // src/pricing/fetch-utils.ts
745
+ var DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
746
+ var DEFAULT_TIMEOUT_MS = 6e4;
747
+ function assertUrlAllowed(url, options) {
748
+ let parsed;
749
+ try {
750
+ parsed = new URL(url);
751
+ } catch {
752
+ throw new Error(`fetchWithRetry: invalid URL`);
753
+ }
754
+ const requireHttps = options.requireHttps ?? true;
755
+ if (requireHttps && parsed.protocol !== "https:") {
756
+ throw new Error(`fetchWithRetry: rejected non-https URL (scheme=${parsed.protocol})`);
757
+ }
758
+ if (options.allowedHosts && options.allowedHosts.length > 0) {
759
+ if (!options.allowedHosts.includes(parsed.host)) {
760
+ throw new Error(`fetchWithRetry: host ${parsed.host} not in allowlist`);
761
+ }
762
+ }
763
+ return parsed;
764
+ }
765
+ function capResponseBody(res, maxBytes) {
766
+ if (!res.body) return res;
767
+ if (!Number.isFinite(maxBytes)) return res;
768
+ let seen = 0;
769
+ const limiter = new TransformStream({
770
+ transform(chunk, controller) {
771
+ seen += chunk.byteLength;
772
+ if (seen > maxBytes) {
773
+ controller.error(new Error(`fetchWithRetry: response body exceeded ${maxBytes} bytes`));
774
+ return;
775
+ }
776
+ controller.enqueue(chunk);
777
+ }
778
+ });
779
+ const limited = res.body.pipeThrough(limiter);
780
+ return new Response(limited, {
781
+ status: res.status,
782
+ statusText: res.statusText,
783
+ headers: res.headers
784
+ });
785
+ }
786
+ function withDefaultSignal(init, timeoutMs) {
787
+ if (init?.signal) return init;
788
+ return { ...init ?? {}, signal: AbortSignal.timeout(timeoutMs) };
789
+ }
645
790
  async function fetchWithRetry(url, init, options = {}) {
646
791
  const maxRetries = options.maxRetries ?? 3;
647
792
  const baseDelay = options.baseDelay ?? 1e3;
648
793
  const maxDelay = options.maxDelay ?? 1e4;
794
+ const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
795
+ const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
796
+ assertUrlAllowed(url, options);
797
+ const safeInit = withDefaultSignal(init, defaultTimeoutMs);
649
798
  let lastError;
650
799
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
651
800
  try {
652
- const res = await fetch(url, init);
801
+ const res = await fetch(url, safeInit);
653
802
  if (res.status === 429 || res.status >= 500 && res.status < 600) {
654
803
  if (attempt < maxRetries) {
655
804
  const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
@@ -663,7 +812,7 @@ async function fetchWithRetry(url, init, options = {}) {
663
812
  continue;
664
813
  }
665
814
  }
666
- return res;
815
+ return capResponseBody(res, maxResponseBytes);
667
816
  } catch (err) {
668
817
  lastError = err;
669
818
  if (attempt < maxRetries) {
@@ -684,7 +833,8 @@ async function fetchWithRetry(url, init, options = {}) {
684
833
  }
685
834
  }
686
835
  }
687
- throw lastError ?? new Error(`fetchWithRetry: all ${maxRetries} retries failed for ${url}`);
836
+ if (lastError instanceof Error) throw lastError;
837
+ throw new Error(`fetchWithRetry: all ${maxRetries} retries failed for ${url}`);
688
838
  }
689
839
  function sleep(ms) {
690
840
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
@@ -1022,6 +1172,24 @@ var NAT_PER_GB = 0.045;
1022
1172
  var EKS_HOURLY = 0.1;
1023
1173
  var CACHE_TTL = 86400;
1024
1174
  var BULK_PRICING_BASE = "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws";
1175
+ var AWS_PRICING_HOST = "pricing.us-east-1.amazonaws.com";
1176
+ var AWS_REGION_RE = /^[a-z]{2}(?:-(?:gov|iso|isob))?-[a-z]+-\d+$/;
1177
+ var AWS_PRICING_SERVICES = /* @__PURE__ */ new Set([
1178
+ "AmazonEC2",
1179
+ "AmazonRDS",
1180
+ "AmazonElastiCache",
1181
+ "AmazonRedshift"
1182
+ ]);
1183
+ function assertValidAwsRegion(region) {
1184
+ if (typeof region !== "string" || !AWS_REGION_RE.test(region)) {
1185
+ throw new Error(`invalid AWS region: ${JSON.stringify(region).slice(0, 64)}`);
1186
+ }
1187
+ }
1188
+ function assertValidAwsPricingService(service) {
1189
+ if (typeof service !== "string" || !AWS_PRICING_SERVICES.has(service)) {
1190
+ throw new Error(`invalid AWS pricing service: ${JSON.stringify(service).slice(0, 64)}`);
1191
+ }
1192
+ }
1025
1193
  var SIZE_ORDER = [
1026
1194
  "nano",
1027
1195
  "micro",
@@ -1038,11 +1206,12 @@ var SIZE_ORDER = [
1038
1206
  "32xlarge",
1039
1207
  "48xlarge"
1040
1208
  ];
1041
- function regionMultiplier(region) {
1042
- const multipliers = getRegionPriceMultipliers();
1043
- const mult = multipliers.aws[region.toLowerCase()];
1209
+
1210
+ // src/pricing/region-multiplier.ts
1211
+ function regionMultiplier(provider, region) {
1212
+ const mult = getRegionPriceMultipliers()[provider][region.toLowerCase()];
1044
1213
  if (mult === void 0) {
1045
- logger.warn("region-multiplier: unknown AWS region, defaulting to 1.0x", { region });
1214
+ logger.warn("region-multiplier: unknown region, defaulting to 1.0x", { provider, region });
1046
1215
  return 1;
1047
1216
  }
1048
1217
  return mult;
@@ -1051,13 +1220,16 @@ function regionMultiplier(region) {
1051
1220
  // src/pricing/aws/bulk-loader.ts
1052
1221
  var AwsBulkLoader = class {
1053
1222
  cache;
1223
+ /** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
1224
+ ttlSeconds;
1054
1225
  /**
1055
1226
  * Deduplicates concurrent streaming CSV fetches for the same region.
1056
1227
  * Keyed by region; value is the in-flight Promise<boolean>.
1057
1228
  */
1058
1229
  ec2CsvFetchInProgress = /* @__PURE__ */ new Map();
1059
- constructor(cache) {
1230
+ constructor(cache, ttlSeconds = CACHE_TTL) {
1060
1231
  this.cache = cache;
1232
+ this.ttlSeconds = ttlSeconds;
1061
1233
  }
1062
1234
  // -------------------------------------------------------------------------
1063
1235
  // Public API
@@ -1084,7 +1256,7 @@ var AwsBulkLoader = class {
1084
1256
  if (bulk) {
1085
1257
  const result = this.extractEc2Price(bulk, instanceType, region, os);
1086
1258
  if (result) {
1087
- this.cache.set(cacheKey, result, "aws", "ec2", region, CACHE_TTL);
1259
+ this.cache.set(cacheKey, result, "aws", "ec2", region, this.ttlSeconds);
1088
1260
  return result;
1089
1261
  }
1090
1262
  }
@@ -1106,7 +1278,7 @@ var AwsBulkLoader = class {
1106
1278
  if (bulk) {
1107
1279
  const result = this.extractRdsPrice(bulk, instanceClass, region, engine);
1108
1280
  if (result) {
1109
- this.cache.set(cacheKey, result, "aws", "rds", region, CACHE_TTL);
1281
+ this.cache.set(cacheKey, result, "aws", "rds", region, this.ttlSeconds);
1110
1282
  return result;
1111
1283
  }
1112
1284
  }
@@ -1128,7 +1300,7 @@ var AwsBulkLoader = class {
1128
1300
  if (bulk) {
1129
1301
  const result = this.extractEbsPrice(bulk, volumeType, region);
1130
1302
  if (result) {
1131
- this.cache.set(cacheKey, result, "aws", "ebs", region, CACHE_TTL);
1303
+ this.cache.set(cacheKey, result, "aws", "ebs", region, this.ttlSeconds);
1132
1304
  return result;
1133
1305
  }
1134
1306
  }
@@ -1141,9 +1313,9 @@ var AwsBulkLoader = class {
1141
1313
  }
1142
1314
  return this.fallbackStoragePrice(volumeType, region, cacheKey);
1143
1315
  }
1144
- async getLoadBalancerPrice(region) {
1145
- const multiplier = regionMultiplier(region);
1146
- return {
1316
+ getLoadBalancerPrice(region) {
1317
+ const multiplier = regionMultiplier("aws", region);
1318
+ return Promise.resolve({
1147
1319
  provider: "aws",
1148
1320
  service: "elb",
1149
1321
  resource_type: "alb",
@@ -1157,11 +1329,11 @@ var AwsBulkLoader = class {
1157
1329
  pricing_model: "fixed_plus_lcu"
1158
1330
  },
1159
1331
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
1160
- };
1332
+ });
1161
1333
  }
1162
- async getNatGatewayPrice(region) {
1163
- const multiplier = regionMultiplier(region);
1164
- return {
1334
+ getNatGatewayPrice(region) {
1335
+ const multiplier = regionMultiplier("aws", region);
1336
+ return Promise.resolve({
1165
1337
  provider: "aws",
1166
1338
  service: "vpc",
1167
1339
  resource_type: "nat-gateway",
@@ -1174,11 +1346,11 @@ var AwsBulkLoader = class {
1174
1346
  per_gb_price: String(NAT_PER_GB * multiplier)
1175
1347
  },
1176
1348
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
1177
- };
1349
+ });
1178
1350
  }
1179
- async getKubernetesPrice(region) {
1180
- const multiplier = regionMultiplier(region);
1181
- return {
1351
+ getKubernetesPrice(region) {
1352
+ const multiplier = regionMultiplier("aws", region);
1353
+ return Promise.resolve({
1182
1354
  provider: "aws",
1183
1355
  service: "eks",
1184
1356
  resource_type: "cluster",
@@ -1189,7 +1361,7 @@ var AwsBulkLoader = class {
1189
1361
  description: "AWS EKS control plane (per cluster/hour)",
1190
1362
  attributes: {},
1191
1363
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
1192
- };
1364
+ });
1193
1365
  }
1194
1366
  // -------------------------------------------------------------------------
1195
1367
  // Internal helpers – EC2 CSV streaming
@@ -1219,7 +1391,8 @@ var AwsBulkLoader = class {
1219
1391
  return promise;
1220
1392
  }
1221
1393
  async _doFetchAndCacheEc2CsvPrices(region) {
1222
- const url = `${BULK_PRICING_BASE}/AmazonEC2/current/${region}/index.csv`;
1394
+ assertValidAwsRegion(region);
1395
+ const url = `${BULK_PRICING_BASE}/AmazonEC2/current/${encodeURIComponent(region)}/index.csv`;
1223
1396
  logger.debug("Streaming AWS EC2 CSV pricing", { url, region });
1224
1397
  const controller = new AbortController();
1225
1398
  const timeoutId = setTimeout(() => controller.abort(), 12e4);
@@ -1230,7 +1403,11 @@ var AwsBulkLoader = class {
1230
1403
  {
1231
1404
  maxRetries: 1,
1232
1405
  baseDelay: 500,
1233
- maxDelay: 2e3
1406
+ maxDelay: 2e3,
1407
+ allowedHosts: [AWS_PRICING_HOST],
1408
+ // AWS EC2 CSVs run ~267 MiB today; allow headroom for growth while
1409
+ // still hard-bounding at 512 MiB to prevent adversarial OOM.
1410
+ maxResponseBytes: 512 * 1024 * 1024
1234
1411
  }
1235
1412
  );
1236
1413
  if (!res.ok) {
@@ -1351,7 +1528,7 @@ var AwsBulkLoader = class {
1351
1528
  },
1352
1529
  effective_date: effectiveDate
1353
1530
  };
1354
- this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, CACHE_TTL);
1531
+ this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, this.ttlSeconds);
1355
1532
  cachedCount++;
1356
1533
  }
1357
1534
  }
@@ -1388,7 +1565,7 @@ var AwsBulkLoader = class {
1388
1565
  "aws",
1389
1566
  "ec2",
1390
1567
  region,
1391
- CACHE_TTL
1568
+ this.ttlSeconds
1392
1569
  );
1393
1570
  cachedCount++;
1394
1571
  }
@@ -1420,14 +1597,19 @@ var AwsBulkLoader = class {
1420
1597
  // Internal helpers – live API fetch
1421
1598
  // -------------------------------------------------------------------------
1422
1599
  async fetchBulkPricing(service, region) {
1423
- const url = `${BULK_PRICING_BASE}/${service}/current/${region}/index.json`;
1600
+ assertValidAwsPricingService(service);
1601
+ assertValidAwsRegion(region);
1602
+ const url = `${BULK_PRICING_BASE}/${encodeURIComponent(service)}/current/${encodeURIComponent(region)}/index.json`;
1424
1603
  logger.debug("Fetching AWS bulk pricing", { url });
1425
1604
  const res = await fetchWithRetryAndCircuitBreaker(
1426
1605
  url,
1427
1606
  {
1428
1607
  signal: AbortSignal.timeout(3e4)
1429
1608
  },
1430
- { maxRetries: 0 }
1609
+ {
1610
+ maxRetries: 0,
1611
+ allowedHosts: [AWS_PRICING_HOST]
1612
+ }
1431
1613
  );
1432
1614
  if (!res.ok) {
1433
1615
  throw new Error(`HTTP ${res.status} from ${url}`);
@@ -1513,7 +1695,7 @@ var AwsBulkLoader = class {
1513
1695
  logger.warn("No fallback price found for EC2 instance type", { instanceType });
1514
1696
  return null;
1515
1697
  }
1516
- const multiplier = regionMultiplier(region);
1698
+ const multiplier = regionMultiplier("aws", region);
1517
1699
  const result = {
1518
1700
  provider: "aws",
1519
1701
  service: "ec2",
@@ -1531,7 +1713,7 @@ var AwsBulkLoader = class {
1531
1713
  },
1532
1714
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
1533
1715
  };
1534
- this.cache.set(cacheKey, result, "aws", "ec2", region, CACHE_TTL);
1716
+ this.cache.set(cacheKey, result, "aws", "ec2", region, this.ttlSeconds);
1535
1717
  return result;
1536
1718
  }
1537
1719
  fallbackDatabasePrice(instanceClass, region, engine, cacheKey) {
@@ -1540,7 +1722,7 @@ var AwsBulkLoader = class {
1540
1722
  logger.warn("No fallback price found for RDS instance class", { instanceClass });
1541
1723
  return null;
1542
1724
  }
1543
- const multiplier = regionMultiplier(region);
1725
+ const multiplier = regionMultiplier("aws", region);
1544
1726
  const result = {
1545
1727
  provider: "aws",
1546
1728
  service: "rds",
@@ -1558,7 +1740,7 @@ var AwsBulkLoader = class {
1558
1740
  },
1559
1741
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
1560
1742
  };
1561
- this.cache.set(cacheKey, result, "aws", "rds", region, CACHE_TTL);
1743
+ this.cache.set(cacheKey, result, "aws", "rds", region, this.ttlSeconds);
1562
1744
  return result;
1563
1745
  }
1564
1746
  fallbackStoragePrice(volumeType, region, cacheKey) {
@@ -1567,7 +1749,7 @@ var AwsBulkLoader = class {
1567
1749
  logger.warn("No fallback price found for EBS volume type", { volumeType });
1568
1750
  return null;
1569
1751
  }
1570
- const multiplier = regionMultiplier(region);
1752
+ const multiplier = regionMultiplier("aws", region);
1571
1753
  const result = {
1572
1754
  provider: "aws",
1573
1755
  service: "ebs",
@@ -1583,7 +1765,7 @@ var AwsBulkLoader = class {
1583
1765
  },
1584
1766
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
1585
1767
  };
1586
- this.cache.set(cacheKey, result, "aws", "ebs", region, CACHE_TTL);
1768
+ this.cache.set(cacheKey, result, "aws", "ebs", region, this.ttlSeconds);
1587
1769
  return result;
1588
1770
  }
1589
1771
  };
@@ -1593,10 +1775,13 @@ var SPOT_ADVISOR_URL = "https://website.spot.ec2.aws.a2z.com/spot.json";
1593
1775
  var SPOT_ADVISOR_CACHE_KEY = "aws/spot-advisor/v1";
1594
1776
  var AwsSpotClient = class {
1595
1777
  cache;
1778
+ /** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
1779
+ ttlSeconds;
1596
1780
  /** In-flight fetch deduplication. */
1597
1781
  inflight = null;
1598
- constructor(cache) {
1782
+ constructor(cache, ttlSeconds = CACHE_TTL) {
1599
1783
  this.cache = cache;
1784
+ this.ttlSeconds = ttlSeconds;
1600
1785
  }
1601
1786
  /**
1602
1787
  * Get the spot discount factor (0–1) for an instance type in a region.
@@ -1660,7 +1845,7 @@ var AwsSpotClient = class {
1660
1845
  logger.debug("AWS Spot Advisor response missing spot_advisor key");
1661
1846
  return null;
1662
1847
  }
1663
- this.cache.set(SPOT_ADVISOR_CACHE_KEY, doc, "aws", "spot-advisor", "global", CACHE_TTL);
1848
+ this.cache.set(SPOT_ADVISOR_CACHE_KEY, doc, "aws", "spot-advisor", "global", this.ttlSeconds);
1664
1849
  return doc;
1665
1850
  } catch (err) {
1666
1851
  logger.debug("AWS Spot Advisor fetch failed", {
@@ -1710,8 +1895,11 @@ function extractRiRatesFromBulk(bulk, instanceType, os = "Linux") {
1710
1895
  }
1711
1896
  var AwsReservedClient = class {
1712
1897
  cache;
1713
- constructor(cache) {
1898
+ /** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
1899
+ ttlSeconds;
1900
+ constructor(cache, ttlSeconds = CACHE_TTL) {
1714
1901
  this.cache = cache;
1902
+ this.ttlSeconds = ttlSeconds;
1715
1903
  }
1716
1904
  async getRiRates(service, instanceType, region, os = "Linux") {
1717
1905
  if (service === "AmazonEC2") {
@@ -1727,13 +1915,18 @@ var AwsReservedClient = class {
1727
1915
  const cacheKey = `aws/ri/${service.toLowerCase()}/${region.toLowerCase()}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
1728
1916
  const cached = this.cache.get(cacheKey);
1729
1917
  if (cached && Array.isArray(cached.rates)) return cached.rates;
1730
- const url = `${BULK_PRICING_BASE}/${service}/current/${region}/index.json`;
1918
+ assertValidAwsPricingService(service);
1919
+ assertValidAwsRegion(region);
1920
+ const url = `${BULK_PRICING_BASE}/${encodeURIComponent(service)}/current/${encodeURIComponent(region)}/index.json`;
1731
1921
  logger.debug("Fetching AWS bulk pricing for RI rates", { url });
1732
1922
  try {
1733
1923
  const res = await fetchWithRetryAndCircuitBreaker(
1734
1924
  url,
1735
1925
  { signal: AbortSignal.timeout(6e4) },
1736
- { maxRetries: 0 }
1926
+ {
1927
+ maxRetries: 0,
1928
+ allowedHosts: [AWS_PRICING_HOST]
1929
+ }
1737
1930
  );
1738
1931
  if (!res.ok) {
1739
1932
  logger.debug("AWS RI bulk fetch non-OK", { status: res.status });
@@ -1747,7 +1940,7 @@ var AwsReservedClient = class {
1747
1940
  unit: "none",
1748
1941
  rates
1749
1942
  };
1750
- this.cache.set(cacheKey, entry, "aws", "ri", region, CACHE_TTL);
1943
+ this.cache.set(cacheKey, entry, "aws", "ri", region, this.ttlSeconds);
1751
1944
  return rates;
1752
1945
  } catch (err) {
1753
1946
  logger.debug("AWS RI bulk fetch failed, will use static fallback", {
@@ -1809,67 +2002,48 @@ function normalizePayment(purchase) {
1809
2002
  }
1810
2003
 
1811
2004
  // src/pricing/azure/azure-normalizer.ts
1812
- function normalizeAzureCompute(item) {
2005
+ function normalize2(item, spec) {
1813
2006
  return {
1814
2007
  provider: "azure",
1815
- service: "virtual-machines",
2008
+ service: spec.service,
1816
2009
  resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1817
2010
  region: item.armRegionName ?? item.location ?? "",
1818
- unit: item.unitOfMeasure ?? "1 Hour",
2011
+ unit: item.unitOfMeasure ?? spec.defaultUnit,
1819
2012
  price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1820
2013
  currency: item.currencyCode ?? "USD",
1821
- tier: item.tierMinimumUnits !== void 0 ? String(item.tierMinimumUnits) : void 0,
2014
+ tier: spec.includeTier && item.tierMinimumUnits !== void 0 ? String(item.tierMinimumUnits) : void 0,
1822
2015
  description: item.productName ?? void 0,
1823
2016
  attributes: {
1824
2017
  sku_name: item.skuName ?? "",
1825
- arm_sku_name: item.armSkuName ?? "",
1826
- service_name: item.serviceName ?? "Virtual Machines",
2018
+ ...spec.extraAttributes?.(item),
2019
+ service_name: item.serviceName ?? spec.defaultServiceName ?? "",
1827
2020
  product_name: item.productName ?? "",
1828
2021
  meter_name: item.meterName ?? "",
1829
2022
  pricing_source: "live"
1830
2023
  },
1831
- effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
2024
+ effective_date: resolveEffectiveDate(item.effectiveStartDate)
1832
2025
  };
1833
2026
  }
2027
+ function normalizeAzureCompute(item) {
2028
+ return normalize2(item, {
2029
+ service: "virtual-machines",
2030
+ defaultUnit: "1 Hour",
2031
+ includeTier: true,
2032
+ defaultServiceName: "Virtual Machines",
2033
+ extraAttributes: (i) => ({ arm_sku_name: i.armSkuName ?? "" })
2034
+ });
2035
+ }
1834
2036
  function normalizeAzureDatabase(item) {
1835
- return {
1836
- provider: "azure",
2037
+ return normalize2(item, {
1837
2038
  service: "azure-database",
1838
- resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1839
- region: item.armRegionName ?? item.location ?? "",
1840
- unit: item.unitOfMeasure ?? "1 Hour",
1841
- price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1842
- currency: item.currencyCode ?? "USD",
1843
- description: item.productName ?? void 0,
1844
- attributes: {
1845
- sku_name: item.skuName ?? "",
1846
- service_name: item.serviceName ?? "",
1847
- product_name: item.productName ?? "",
1848
- meter_name: item.meterName ?? "",
1849
- pricing_source: "live"
1850
- },
1851
- effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1852
- };
2039
+ defaultUnit: "1 Hour"
2040
+ });
1853
2041
  }
1854
2042
  function normalizeAzureStorage(item) {
1855
- return {
1856
- provider: "azure",
2043
+ return normalize2(item, {
1857
2044
  service: "managed-disks",
1858
- resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1859
- region: item.armRegionName ?? item.location ?? "",
1860
- unit: item.unitOfMeasure ?? "1 GiB/Month",
1861
- price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1862
- currency: item.currencyCode ?? "USD",
1863
- description: item.productName ?? void 0,
1864
- attributes: {
1865
- sku_name: item.skuName ?? "",
1866
- service_name: item.serviceName ?? "",
1867
- product_name: item.productName ?? "",
1868
- meter_name: item.meterName ?? "",
1869
- pricing_source: "live"
1870
- },
1871
- effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1872
- };
2045
+ defaultUnit: "1 GiB/Month"
2046
+ });
1873
2047
  }
1874
2048
 
1875
2049
  // src/pricing/azure/arm-regions.ts
@@ -2081,21 +2255,15 @@ var NAT_PER_GB2 = 0.045;
2081
2255
  var AKS_HOURLY = 0.1;
2082
2256
  var CACHE_TTL2 = 86400;
2083
2257
  var RETAIL_API_BASE = "https://prices.azure.com/api/retail/prices";
2084
- function regionMultiplier2(region) {
2085
- const multipliers = getRegionPriceMultipliers();
2086
- const mult = multipliers.azure[region.toLowerCase()];
2087
- if (mult === void 0) {
2088
- logger.warn("region-multiplier: unknown Azure region, defaulting to 1.0x", { region });
2089
- return 1;
2090
- }
2091
- return mult;
2092
- }
2093
2258
 
2094
2259
  // src/pricing/azure/retail-client.ts
2095
2260
  var AzureRetailClient = class {
2096
2261
  cache;
2097
- constructor(cache) {
2262
+ /** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
2263
+ ttlSeconds;
2264
+ constructor(cache, ttlSeconds = CACHE_TTL2) {
2098
2265
  this.cache = cache;
2266
+ this.ttlSeconds = ttlSeconds;
2099
2267
  }
2100
2268
  // -------------------------------------------------------------------------
2101
2269
  // Public API
@@ -2111,7 +2279,7 @@ var AzureRetailClient = class {
2111
2279
  const match = this.pickVmItem(items, vmSize, os);
2112
2280
  if (match) {
2113
2281
  const result = normalizeAzureCompute(match);
2114
- this.cache.set(cacheKey, result, "azure", "vm", region, CACHE_TTL2);
2282
+ this.cache.set(cacheKey, result, "azure", "vm", region, this.ttlSeconds);
2115
2283
  return result;
2116
2284
  }
2117
2285
  } catch (err) {
@@ -2132,7 +2300,7 @@ var AzureRetailClient = class {
2132
2300
  const serviceName = `Azure Database for ${engine}`;
2133
2301
  const result = await this.queryDatabasePrice(tier, armRegion, serviceName);
2134
2302
  if (result) {
2135
- this.cache.set(cacheKey, result, "azure", "db", region, CACHE_TTL2);
2303
+ this.cache.set(cacheKey, result, "azure", "db", region, this.ttlSeconds);
2136
2304
  return result;
2137
2305
  }
2138
2306
  logger.warn("Azure Retail API returned no matching DB pricing", {
@@ -2214,7 +2382,7 @@ var AzureRetailClient = class {
2214
2382
  if (items.length > 0) {
2215
2383
  items.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
2216
2384
  const result = normalizeAzureStorage(items[0]);
2217
- this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
2385
+ this.cache.set(cacheKey, result, "azure", "storage", region, this.ttlSeconds);
2218
2386
  return result;
2219
2387
  }
2220
2388
  logger.warn("Azure Retail API returned no matching storage pricing", {
@@ -2230,9 +2398,9 @@ var AzureRetailClient = class {
2230
2398
  }
2231
2399
  return this.fallbackStoragePrice(diskType, region, cacheKey);
2232
2400
  }
2233
- async getLoadBalancerPrice(region) {
2234
- const multiplier = regionMultiplier2(region);
2235
- return {
2401
+ getLoadBalancerPrice(region) {
2402
+ const multiplier = regionMultiplier("azure", region);
2403
+ return Promise.resolve({
2236
2404
  provider: "azure",
2237
2405
  service: "load-balancer",
2238
2406
  resource_type: "standard",
@@ -2246,11 +2414,11 @@ var AzureRetailClient = class {
2246
2414
  pricing_model: "fixed_plus_rules"
2247
2415
  },
2248
2416
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2249
- };
2417
+ });
2250
2418
  }
2251
- async getNatGatewayPrice(region) {
2252
- const multiplier = regionMultiplier2(region);
2253
- return {
2419
+ getNatGatewayPrice(region) {
2420
+ const multiplier = regionMultiplier("azure", region);
2421
+ return Promise.resolve({
2254
2422
  provider: "azure",
2255
2423
  service: "nat-gateway",
2256
2424
  resource_type: "nat-gateway",
@@ -2263,11 +2431,11 @@ var AzureRetailClient = class {
2263
2431
  per_gb_price: String(NAT_PER_GB2 * multiplier)
2264
2432
  },
2265
2433
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2266
- };
2434
+ });
2267
2435
  }
2268
- async getKubernetesPrice(region) {
2269
- const multiplier = regionMultiplier2(region);
2270
- return {
2436
+ getKubernetesPrice(region) {
2437
+ const multiplier = regionMultiplier("azure", region);
2438
+ return Promise.resolve({
2271
2439
  provider: "azure",
2272
2440
  service: "aks",
2273
2441
  resource_type: "cluster",
@@ -2278,7 +2446,7 @@ var AzureRetailClient = class {
2278
2446
  description: "Azure Kubernetes Service (uptime SLA tier, per cluster/hour)",
2279
2447
  attributes: {},
2280
2448
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2281
- };
2449
+ });
2282
2450
  }
2283
2451
  /**
2284
2452
  * Fetch the live Spot VM price for a given VM size/region/OS from the Azure
@@ -2310,7 +2478,7 @@ var AzureRetailClient = class {
2310
2478
  const result = normalizeAzureCompute(spotItems[0]);
2311
2479
  result.attributes.pricing_source = "live";
2312
2480
  result.attributes.purchase_option = "spot";
2313
- this.cache.set(cacheKey, result, "azure", "vm-spot", region, CACHE_TTL2);
2481
+ this.cache.set(cacheKey, result, "azure", "vm-spot", region, this.ttlSeconds);
2314
2482
  return result;
2315
2483
  }
2316
2484
  } catch (err) {
@@ -2351,7 +2519,7 @@ var AzureRetailClient = class {
2351
2519
  result.attributes.pricing_source = "live";
2352
2520
  result.attributes.purchase_option = "reservation";
2353
2521
  result.attributes.reservation_term = termLabel;
2354
- this.cache.set(cacheKey, result, "azure", "vm-ri", region, CACHE_TTL2);
2522
+ this.cache.set(cacheKey, result, "azure", "vm-ri", region, this.ttlSeconds);
2355
2523
  return result;
2356
2524
  }
2357
2525
  } catch (err) {
@@ -2432,16 +2600,20 @@ var AzureRetailClient = class {
2432
2600
  pickVmItem(items, vmSize, os) {
2433
2601
  if (items.length === 0) return null;
2434
2602
  const sorted = [...items].sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
2435
- const vmLower = vmSize.toLowerCase();
2603
+ const skuFragment = vmSize.toLowerCase().replace(/^standard_/, "").replace(/_/g, " ");
2436
2604
  const isWindows = os.toLowerCase().includes("windows");
2437
- for (const item of sorted) {
2605
+ const onDemand = sorted.filter((item) => {
2606
+ const meterLower = (item.meterName ?? "").toLowerCase();
2607
+ return !meterLower.includes("low priority") && !meterLower.includes("spot");
2608
+ });
2609
+ for (const item of onDemand) {
2438
2610
  const skuLower = (item.skuName ?? "").toLowerCase();
2439
- const hasWindows = skuLower.includes("windows");
2440
- if (skuLower.includes(vmLower) && (isWindows && hasWindows || !isWindows && !hasWindows)) {
2611
+ const hasWindows = skuLower.includes("windows") || (item.productName ?? "").toLowerCase().includes("windows");
2612
+ if (skuLower.includes(skuFragment) && (isWindows && hasWindows || !isWindows && !hasWindows)) {
2441
2613
  return item;
2442
2614
  }
2443
2615
  }
2444
- return sorted[0];
2616
+ return onDemand[0] ?? sorted[0];
2445
2617
  }
2446
2618
  // -------------------------------------------------------------------------
2447
2619
  // Internal helpers – size interpolation
@@ -2481,7 +2653,7 @@ var AzureRetailClient = class {
2481
2653
  logger.warn("No fallback price found for Azure VM size", { vmSize });
2482
2654
  return null;
2483
2655
  }
2484
- const multiplier = regionMultiplier2(region);
2656
+ const multiplier = regionMultiplier("azure", region);
2485
2657
  const result = {
2486
2658
  provider: "azure",
2487
2659
  service: "virtual-machines",
@@ -2498,7 +2670,7 @@ var AzureRetailClient = class {
2498
2670
  },
2499
2671
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2500
2672
  };
2501
- this.cache.set(cacheKey, result, "azure", "vm", region, CACHE_TTL2);
2673
+ this.cache.set(cacheKey, result, "azure", "vm", region, this.ttlSeconds);
2502
2674
  return result;
2503
2675
  }
2504
2676
  fallbackDatabasePrice(tier, region, engine, cacheKey) {
@@ -2508,7 +2680,7 @@ var AzureRetailClient = class {
2508
2680
  logger.warn("No fallback price found for Azure DB tier", { tier });
2509
2681
  return null;
2510
2682
  }
2511
- const multiplier = regionMultiplier2(region);
2683
+ const multiplier = regionMultiplier("azure", region);
2512
2684
  const result = {
2513
2685
  provider: "azure",
2514
2686
  service: "azure-database",
@@ -2525,7 +2697,7 @@ var AzureRetailClient = class {
2525
2697
  },
2526
2698
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2527
2699
  };
2528
- this.cache.set(cacheKey, result, "azure", "db", region, CACHE_TTL2);
2700
+ this.cache.set(cacheKey, result, "azure", "db", region, this.ttlSeconds);
2529
2701
  return result;
2530
2702
  }
2531
2703
  fallbackStoragePrice(diskType, region, cacheKey) {
@@ -2535,7 +2707,7 @@ var AzureRetailClient = class {
2535
2707
  logger.warn("No fallback price found for Azure disk type", { diskType });
2536
2708
  return null;
2537
2709
  }
2538
- const multiplier = regionMultiplier2(region);
2710
+ const multiplier = regionMultiplier("azure", region);
2539
2711
  const result = {
2540
2712
  provider: "azure",
2541
2713
  service: "managed-disks",
@@ -2551,7 +2723,7 @@ var AzureRetailClient = class {
2551
2723
  },
2552
2724
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2553
2725
  };
2554
- this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
2726
+ this.cache.set(cacheKey, result, "azure", "storage", region, this.ttlSeconds);
2555
2727
  return result;
2556
2728
  }
2557
2729
  };
@@ -2569,73 +2741,59 @@ function parseAzureVmSeries(vmName) {
2569
2741
  }
2570
2742
 
2571
2743
  // src/pricing/gcp/gcp-normalizer.ts
2572
- function normalizeGcpCompute(machineType, hourlyPrice, region) {
2744
+ function normalize3(resourceType, pricePerUnit, region, spec, opts) {
2573
2745
  return {
2574
2746
  provider: "gcp",
2575
- service: "compute-engine",
2576
- resource_type: machineType,
2747
+ service: spec.service,
2748
+ resource_type: resourceType,
2577
2749
  region,
2578
- unit: "h",
2579
- price_per_unit: hourlyPrice,
2750
+ unit: spec.unit,
2751
+ price_per_unit: pricePerUnit,
2580
2752
  currency: "USD",
2581
- description: `GCP Compute Engine ${machineType}`,
2753
+ description: `${spec.descriptionPrefix} ${resourceType}`,
2582
2754
  attributes: {
2583
- machine_type: machineType,
2584
- pricing_source: "bundled"
2755
+ [spec.attrKey]: resourceType,
2756
+ ...opts.skuId !== void 0 ? { sku_id: opts.skuId } : {},
2757
+ pricing_source: opts.source
2585
2758
  },
2586
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
2759
+ effective_date: resolveEffectiveDate(opts.effectiveDate)
2587
2760
  };
2588
2761
  }
2762
+ var COMPUTE_SPEC = {
2763
+ service: "compute-engine",
2764
+ unit: "h",
2765
+ descriptionPrefix: "GCP Compute Engine",
2766
+ attrKey: "machine_type"
2767
+ };
2768
+ var DATABASE_SPEC = {
2769
+ service: "cloud-sql",
2770
+ unit: "h",
2771
+ descriptionPrefix: "GCP Cloud SQL",
2772
+ attrKey: "tier"
2773
+ };
2774
+ var STORAGE_SPEC = {
2775
+ service: "cloud-storage",
2776
+ unit: "GiBy.mo",
2777
+ descriptionPrefix: "GCP Cloud Storage",
2778
+ attrKey: "storage_class"
2779
+ };
2780
+ var DISK_SPEC = {
2781
+ service: "persistent-disk",
2782
+ unit: "GiBy.mo",
2783
+ descriptionPrefix: "GCP Persistent Disk",
2784
+ attrKey: "disk_type"
2785
+ };
2786
+ function normalizeGcpCompute(machineType, hourlyPrice, region) {
2787
+ return normalize3(machineType, hourlyPrice, region, COMPUTE_SPEC, { source: "bundled" });
2788
+ }
2589
2789
  function normalizeGcpDatabase(tier, hourlyPrice, region) {
2590
- return {
2591
- provider: "gcp",
2592
- service: "cloud-sql",
2593
- resource_type: tier,
2594
- region,
2595
- unit: "h",
2596
- price_per_unit: hourlyPrice,
2597
- currency: "USD",
2598
- description: `GCP Cloud SQL ${tier}`,
2599
- attributes: {
2600
- tier,
2601
- pricing_source: "bundled"
2602
- },
2603
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
2604
- };
2790
+ return normalize3(tier, hourlyPrice, region, DATABASE_SPEC, { source: "bundled" });
2605
2791
  }
2606
2792
  function normalizeGcpStorage(storageClass, pricePerGb, region) {
2607
- return {
2608
- provider: "gcp",
2609
- service: "cloud-storage",
2610
- resource_type: storageClass,
2611
- region,
2612
- unit: "GiBy.mo",
2613
- price_per_unit: pricePerGb,
2614
- currency: "USD",
2615
- description: `GCP Cloud Storage ${storageClass}`,
2616
- attributes: {
2617
- storage_class: storageClass,
2618
- pricing_source: "bundled"
2619
- },
2620
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
2621
- };
2793
+ return normalize3(storageClass, pricePerGb, region, STORAGE_SPEC, { source: "bundled" });
2622
2794
  }
2623
2795
  function normalizeGcpDisk(diskType, pricePerGb, region) {
2624
- return {
2625
- provider: "gcp",
2626
- service: "persistent-disk",
2627
- resource_type: diskType,
2628
- region,
2629
- unit: "GiBy.mo",
2630
- price_per_unit: pricePerGb,
2631
- currency: "USD",
2632
- description: `GCP Persistent Disk ${diskType}`,
2633
- attributes: {
2634
- disk_type: diskType,
2635
- pricing_source: "bundled"
2636
- },
2637
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
2638
- };
2796
+ return normalize3(diskType, pricePerGb, region, DISK_SPEC, { source: "bundled" });
2639
2797
  }
2640
2798
 
2641
2799
  // src/pricing/gcp/bundled-loader.ts
@@ -2645,20 +2803,11 @@ var NAT_HOURLY3 = 0.044;
2645
2803
  var NAT_PER_GB3 = 0.045;
2646
2804
  var GKE_STANDARD_HOURLY = 0.1;
2647
2805
  var GKE_AUTOPILOT_VCPU_HOURLY = 0.0445;
2648
- function regionMultiplier3(region) {
2649
- const multipliers = getRegionPriceMultipliers();
2650
- const mult = multipliers.gcp[region.toLowerCase()];
2651
- if (mult === void 0) {
2652
- logger.warn("region-multiplier: unknown GCP region, defaulting to 1.0x", { region });
2653
- return 1;
2654
- }
2655
- return mult;
2656
- }
2657
2806
  var GcpBundledLoader = class {
2658
2807
  // -------------------------------------------------------------------------
2659
2808
  // Public API
2660
2809
  // -------------------------------------------------------------------------
2661
- async getComputePrice(machineType, region) {
2810
+ getComputePrice(machineType, region) {
2662
2811
  try {
2663
2812
  const pricing = getGcpComputePricing();
2664
2813
  const regionPrices = pricing[region];
@@ -2667,7 +2816,7 @@ var GcpBundledLoader = class {
2667
2816
  region,
2668
2817
  machineType
2669
2818
  });
2670
- return null;
2819
+ return Promise.resolve(null);
2671
2820
  }
2672
2821
  const price = regionPrices[machineType];
2673
2822
  if (price === void 0) {
@@ -2675,19 +2824,19 @@ var GcpBundledLoader = class {
2675
2824
  region,
2676
2825
  machineType
2677
2826
  });
2678
- return null;
2827
+ return Promise.resolve(null);
2679
2828
  }
2680
- return normalizeGcpCompute(machineType, price, region);
2829
+ return Promise.resolve(normalizeGcpCompute(machineType, price, region));
2681
2830
  } catch (err) {
2682
2831
  logger.error("GCP compute pricing load error", {
2683
2832
  machineType,
2684
2833
  region,
2685
2834
  err: err instanceof Error ? err.message : String(err)
2686
2835
  });
2687
- return null;
2836
+ return Promise.resolve(null);
2688
2837
  }
2689
2838
  }
2690
- async getDatabasePrice(tier, region) {
2839
+ getDatabasePrice(tier, region) {
2691
2840
  try {
2692
2841
  const pricing = getGcpSqlPricing();
2693
2842
  const regionPrices = pricing[region];
@@ -2696,7 +2845,7 @@ var GcpBundledLoader = class {
2696
2845
  region,
2697
2846
  tier
2698
2847
  });
2699
- return null;
2848
+ return Promise.resolve(null);
2700
2849
  }
2701
2850
  const price = regionPrices[tier];
2702
2851
  if (price === void 0 || tier === "storage_per_gb" || tier === "ha_multiplier") {
@@ -2704,19 +2853,19 @@ var GcpBundledLoader = class {
2704
2853
  region,
2705
2854
  tier
2706
2855
  });
2707
- return null;
2856
+ return Promise.resolve(null);
2708
2857
  }
2709
- return normalizeGcpDatabase(tier, price, region);
2858
+ return Promise.resolve(normalizeGcpDatabase(tier, price, region));
2710
2859
  } catch (err) {
2711
2860
  logger.error("GCP SQL pricing load error", {
2712
2861
  tier,
2713
2862
  region,
2714
2863
  err: err instanceof Error ? err.message : String(err)
2715
2864
  });
2716
- return null;
2865
+ return Promise.resolve(null);
2717
2866
  }
2718
2867
  }
2719
- async getStoragePrice(storageClass, region) {
2868
+ getStoragePrice(storageClass, region) {
2720
2869
  try {
2721
2870
  const pricing = getGcpStoragePricing();
2722
2871
  const regionPrices = pricing[region];
@@ -2725,7 +2874,7 @@ var GcpBundledLoader = class {
2725
2874
  region,
2726
2875
  storageClass
2727
2876
  });
2728
- return null;
2877
+ return Promise.resolve(null);
2729
2878
  }
2730
2879
  const classKey = storageClass.toUpperCase();
2731
2880
  const price = regionPrices[classKey];
@@ -2734,19 +2883,19 @@ var GcpBundledLoader = class {
2734
2883
  region,
2735
2884
  storageClass
2736
2885
  });
2737
- return null;
2886
+ return Promise.resolve(null);
2738
2887
  }
2739
- return normalizeGcpStorage(storageClass.toUpperCase(), price, region);
2888
+ return Promise.resolve(normalizeGcpStorage(storageClass.toUpperCase(), price, region));
2740
2889
  } catch (err) {
2741
2890
  logger.error("GCP Storage pricing load error", {
2742
2891
  storageClass,
2743
2892
  region,
2744
2893
  err: err instanceof Error ? err.message : String(err)
2745
2894
  });
2746
- return null;
2895
+ return Promise.resolve(null);
2747
2896
  }
2748
2897
  }
2749
- async getDiskPrice(diskType, region) {
2898
+ getDiskPrice(diskType, region) {
2750
2899
  try {
2751
2900
  const pricing = getGcpDiskPricing();
2752
2901
  const regionPrices = pricing[region];
@@ -2755,7 +2904,7 @@ var GcpBundledLoader = class {
2755
2904
  region,
2756
2905
  diskType
2757
2906
  });
2758
- return null;
2907
+ return Promise.resolve(null);
2759
2908
  }
2760
2909
  const diskKey = diskType;
2761
2910
  const price = regionPrices[diskKey];
@@ -2764,21 +2913,21 @@ var GcpBundledLoader = class {
2764
2913
  region,
2765
2914
  diskType
2766
2915
  });
2767
- return null;
2916
+ return Promise.resolve(null);
2768
2917
  }
2769
- return normalizeGcpDisk(diskType, price, region);
2918
+ return Promise.resolve(normalizeGcpDisk(diskType, price, region));
2770
2919
  } catch (err) {
2771
2920
  logger.error("GCP Disk pricing load error", {
2772
2921
  diskType,
2773
2922
  region,
2774
2923
  err: err instanceof Error ? err.message : String(err)
2775
2924
  });
2776
- return null;
2925
+ return Promise.resolve(null);
2777
2926
  }
2778
2927
  }
2779
- async getLoadBalancerPrice(region) {
2780
- const multiplier = regionMultiplier3(region);
2781
- return {
2928
+ getLoadBalancerPrice(region) {
2929
+ const multiplier = regionMultiplier("gcp", region);
2930
+ return Promise.resolve({
2782
2931
  provider: "gcp",
2783
2932
  service: "cloud-load-balancing",
2784
2933
  resource_type: "forwarding-rule",
@@ -2791,11 +2940,11 @@ var GcpBundledLoader = class {
2791
2940
  per_gb_price: String(LB_PER_GB * multiplier)
2792
2941
  },
2793
2942
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2794
- };
2943
+ });
2795
2944
  }
2796
- async getNatGatewayPrice(region) {
2797
- const multiplier = regionMultiplier3(region);
2798
- return {
2945
+ getNatGatewayPrice(region) {
2946
+ const multiplier = regionMultiplier("gcp", region);
2947
+ return Promise.resolve({
2799
2948
  provider: "gcp",
2800
2949
  service: "cloud-nat",
2801
2950
  resource_type: "nat-gateway",
@@ -2808,12 +2957,12 @@ var GcpBundledLoader = class {
2808
2957
  per_gb_price: String(NAT_PER_GB3 * multiplier)
2809
2958
  },
2810
2959
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2811
- };
2960
+ });
2812
2961
  }
2813
- async getKubernetesPrice(region, mode = "standard") {
2814
- const multiplier = regionMultiplier3(region);
2962
+ getKubernetesPrice(region, mode = "standard") {
2963
+ const multiplier = regionMultiplier("gcp", region);
2815
2964
  const baseHourly = mode === "autopilot" ? GKE_AUTOPILOT_VCPU_HOURLY : GKE_STANDARD_HOURLY;
2816
- return {
2965
+ return Promise.resolve({
2817
2966
  provider: "gcp",
2818
2967
  service: "gke",
2819
2968
  resource_type: "cluster",
@@ -2830,7 +2979,7 @@ var GcpBundledLoader = class {
2830
2979
  }
2831
2980
  },
2832
2981
  effective_date: (/* @__PURE__ */ new Date()).toISOString()
2833
- };
2982
+ });
2834
2983
  }
2835
2984
  };
2836
2985
 
@@ -2869,8 +3018,11 @@ function skuCoversRegion(sku, region) {
2869
3018
  }
2870
3019
  var CloudBillingClient = class {
2871
3020
  cache;
2872
- constructor(cache) {
3021
+ /** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
3022
+ ttlSeconds;
3023
+ constructor(cache, ttlSeconds = CACHE_TTL3) {
2873
3024
  this.cache = cache;
3025
+ this.ttlSeconds = ttlSeconds;
2874
3026
  }
2875
3027
  // -------------------------------------------------------------------------
2876
3028
  // Public fetch methods
@@ -2888,7 +3040,7 @@ var CloudBillingClient = class {
2888
3040
  const family = this.machineFamily(machineType);
2889
3041
  const result = this.extractComputePrice(skus, machineType, family, region);
2890
3042
  if (result) {
2891
- this.cache.set(cacheKey, result, "gcp", "compute", region, CACHE_TTL3);
3043
+ this.cache.set(cacheKey, result, "gcp", "compute", region, this.ttlSeconds);
2892
3044
  return result;
2893
3045
  }
2894
3046
  } catch (err) {
@@ -2911,7 +3063,7 @@ var CloudBillingClient = class {
2911
3063
  const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_SQL, region);
2912
3064
  const result = this.extractDatabasePrice(skus, tier, region);
2913
3065
  if (result) {
2914
- this.cache.set(cacheKey, result, "gcp", "cloud-sql", region, CACHE_TTL3);
3066
+ this.cache.set(cacheKey, result, "gcp", "cloud-sql", region, this.ttlSeconds);
2915
3067
  return result;
2916
3068
  }
2917
3069
  } catch (err) {
@@ -2935,7 +3087,7 @@ var CloudBillingClient = class {
2935
3087
  const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_STORAGE, region);
2936
3088
  const result = this.extractStoragePrice(skus, storageClass, region);
2937
3089
  if (result) {
2938
- this.cache.set(cacheKey, result, "gcp", "cloud-storage", region, CACHE_TTL3);
3090
+ this.cache.set(cacheKey, result, "gcp", "cloud-storage", region, this.ttlSeconds);
2939
3091
  return result;
2940
3092
  }
2941
3093
  } catch (err) {
@@ -2964,7 +3116,7 @@ var CloudBillingClient = class {
2964
3116
  const skus = await this.fetchSkus(SERVICE_IDS.COMPUTE, region);
2965
3117
  const result = this.extractGpuPrice(skus, accelerator, region);
2966
3118
  if (result) {
2967
- this.cache.set(cacheKey, result, "gcp", "compute-gpu", region, CACHE_TTL3);
3119
+ this.cache.set(cacheKey, result, "gcp", "compute-gpu", region, this.ttlSeconds);
2968
3120
  return result;
2969
3121
  }
2970
3122
  } catch (err) {
@@ -3046,7 +3198,7 @@ var CloudBillingClient = class {
3046
3198
  if (result.term1yr === null && result.term3yr === null) {
3047
3199
  return null;
3048
3200
  }
3049
- this.cache.set(cacheKey, result, "gcp", "compute-cud", region, CACHE_TTL3);
3201
+ this.cache.set(cacheKey, result, "gcp", "compute-cud", region, this.ttlSeconds);
3050
3202
  return result;
3051
3203
  } catch (err) {
3052
3204
  logger.warn("GCP Cloud Billing CUD fetch failed", {
@@ -3071,7 +3223,7 @@ var CloudBillingClient = class {
3071
3223
  const skus = await this.fetchSkus(SERVICE_IDS.GKE, region);
3072
3224
  const result = this.extractAutopilotPodRates(skus, region);
3073
3225
  if (result) {
3074
- this.cache.set(cacheKey, result, "gcp", "gke-autopilot", region, CACHE_TTL3);
3226
+ this.cache.set(cacheKey, result, "gcp", "gke-autopilot", region, this.ttlSeconds);
3075
3227
  return result;
3076
3228
  }
3077
3229
  } catch (err) {
@@ -3350,14 +3502,15 @@ var CloudBillingClient = class {
3350
3502
  };
3351
3503
 
3352
3504
  // src/pricing/pricing-engine.ts
3505
+ var DEFAULT_CACHE_TTL_SECONDS = 86400;
3353
3506
  var AwsProvider = class {
3354
3507
  loader;
3355
3508
  spotClient;
3356
3509
  reservedClient;
3357
- constructor(cache) {
3358
- this.loader = new AwsBulkLoader(cache);
3359
- this.spotClient = new AwsSpotClient(cache);
3360
- this.reservedClient = new AwsReservedClient(cache);
3510
+ constructor(cache, ttlSeconds) {
3511
+ this.loader = new AwsBulkLoader(cache, ttlSeconds);
3512
+ this.spotClient = new AwsSpotClient(cache, ttlSeconds);
3513
+ this.reservedClient = new AwsReservedClient(cache, ttlSeconds);
3361
3514
  }
3362
3515
  getSpotFactor(instanceType, region, os) {
3363
3516
  return this.spotClient.getSpotFactor(
@@ -3390,8 +3543,8 @@ var AwsProvider = class {
3390
3543
  };
3391
3544
  var AzureProvider = class {
3392
3545
  client;
3393
- constructor(cache) {
3394
- this.client = new AzureRetailClient(cache);
3546
+ constructor(cache, ttlSeconds) {
3547
+ this.client = new AzureRetailClient(cache, ttlSeconds);
3395
3548
  }
3396
3549
  getComputePrice(instanceType, region, os) {
3397
3550
  return this.client.getComputePrice(instanceType, region, os);
@@ -3433,9 +3586,9 @@ var AzureProvider = class {
3433
3586
  var GcpProvider = class {
3434
3587
  loader;
3435
3588
  liveClient;
3436
- constructor(cache) {
3589
+ constructor(cache, ttlSeconds) {
3437
3590
  this.loader = new GcpBundledLoader();
3438
- this.liveClient = new CloudBillingClient(cache);
3591
+ this.liveClient = new CloudBillingClient(cache, ttlSeconds);
3439
3592
  }
3440
3593
  async getComputePrice(instanceType, region, _os) {
3441
3594
  try {
@@ -3489,12 +3642,51 @@ var GcpProvider = class {
3489
3642
  return this.loader.getKubernetesPrice(region, mode === "autopilot" ? "autopilot" : "standard");
3490
3643
  }
3491
3644
  };
3645
+ var SERVICE_ALIAS_LOOKUPS = (() => {
3646
+ const groups = [
3647
+ [
3648
+ ["compute", "ec2", "vm", "instance", "virtual-machines"],
3649
+ (p, resourceType, region, attributes) => p.getComputePrice(resourceType, region, attributes.os)
3650
+ ],
3651
+ [
3652
+ ["database", "rds", "sql", "db", "cloud-sql", "azure-database"],
3653
+ (p, resourceType, region, attributes) => p.getDatabasePrice(resourceType, region, attributes.engine)
3654
+ ],
3655
+ [
3656
+ ["storage", "ebs", "disk", "gcs", "managed-disks", "persistent-disk", "cloud-storage"],
3657
+ (p, resourceType, region, attributes) => {
3658
+ const sizeGb = attributes.size_gb ? parseFloat(attributes.size_gb) : void 0;
3659
+ return p.getStoragePrice(resourceType, region, sizeGb);
3660
+ }
3661
+ ],
3662
+ [
3663
+ ["lb", "load-balancer", "alb", "nlb", "elb", "cloud-load-balancing"],
3664
+ (p, resourceType, region) => p.getLoadBalancerPrice(resourceType, region)
3665
+ ],
3666
+ [
3667
+ ["nat", "nat-gateway", "cloud-nat"],
3668
+ (p, _resourceType, region) => p.getNatGatewayPrice(region)
3669
+ ],
3670
+ [
3671
+ ["k8s", "kubernetes", "eks", "aks", "gke"],
3672
+ (p, _resourceType, region, attributes) => p.getKubernetesPrice(region, attributes.mode)
3673
+ ]
3674
+ ];
3675
+ const map = /* @__PURE__ */ new Map();
3676
+ for (const [aliases, lookup] of groups) {
3677
+ for (const alias of aliases) map.set(alias, lookup);
3678
+ }
3679
+ return map;
3680
+ })();
3492
3681
  var PricingEngine = class {
3493
3682
  providers = /* @__PURE__ */ new Map();
3494
- constructor(cache, _config) {
3495
- this.providers.set("aws", new AwsProvider(cache));
3496
- this.providers.set("azure", new AzureProvider(cache));
3497
- this.providers.set("gcp", new GcpProvider(cache));
3683
+ config;
3684
+ constructor(cache, config) {
3685
+ this.config = config;
3686
+ const ttlSeconds = config?.cache?.ttl_seconds ?? DEFAULT_CACHE_TTL_SECONDS;
3687
+ this.providers.set("aws", new AwsProvider(cache, ttlSeconds));
3688
+ this.providers.set("azure", new AzureProvider(cache, ttlSeconds));
3689
+ this.providers.set("gcp", new GcpProvider(cache, ttlSeconds));
3498
3690
  }
3499
3691
  /**
3500
3692
  * Return the PricingProvider for a specific cloud provider.
@@ -3529,24 +3721,9 @@ var PricingEngine = class {
3529
3721
  resourceType,
3530
3722
  region
3531
3723
  });
3532
- if (svc === "compute" || svc === "ec2" || svc === "vm" || svc === "instance" || svc === "virtual-machines") {
3533
- return p.getComputePrice(resourceType, region, attributes.os);
3534
- }
3535
- if (svc === "database" || svc === "rds" || svc === "sql" || svc === "db" || svc === "cloud-sql" || svc === "azure-database") {
3536
- return p.getDatabasePrice(resourceType, region, attributes.engine);
3537
- }
3538
- if (svc === "storage" || svc === "ebs" || svc === "disk" || svc === "gcs" || svc === "managed-disks" || svc === "persistent-disk" || svc === "cloud-storage") {
3539
- const sizeGb = attributes.size_gb ? parseFloat(attributes.size_gb) : void 0;
3540
- return p.getStoragePrice(resourceType, region, sizeGb);
3541
- }
3542
- if (svc === "lb" || svc === "load-balancer" || svc === "alb" || svc === "nlb" || svc === "elb" || svc === "cloud-load-balancing") {
3543
- return p.getLoadBalancerPrice(resourceType, region);
3544
- }
3545
- if (svc === "nat" || svc === "nat-gateway" || svc === "cloud-nat") {
3546
- return p.getNatGatewayPrice(region);
3547
- }
3548
- if (svc === "k8s" || svc === "kubernetes" || svc === "eks" || svc === "aks" || svc === "gke") {
3549
- return p.getKubernetesPrice(region, attributes.mode);
3724
+ const lookup = SERVICE_ALIAS_LOOKUPS.get(svc);
3725
+ if (lookup) {
3726
+ return lookup(p, resourceType, region, attributes);
3550
3727
  }
3551
3728
  logger.warn("PricingEngine: unrecognised service", { service, provider });
3552
3729
  return null;
@@ -3554,10 +3731,10 @@ var PricingEngine = class {
3554
3731
  };
3555
3732
 
3556
3733
  // src/tools/analyze-terraform.ts
3557
- import { z as z2 } from "zod";
3734
+ import { z as z3 } from "zod";
3558
3735
 
3559
3736
  // src/parsers/index.ts
3560
- import { dirname as dirname2 } from "path";
3737
+ import { dirname as dirname2, isAbsolute, normalize as normalize4, sep as sep2 } from "path";
3561
3738
 
3562
3739
  // src/parsers/hcl-parser.ts
3563
3740
  import { parse } from "@cdktf/hcl2json";
@@ -3595,6 +3772,39 @@ async function parseHclToJson(hclContent, filename = "main.tf") {
3595
3772
  }
3596
3773
  }
3597
3774
 
3775
+ // src/parsers/extractors/helpers.ts
3776
+ function firstBlock(block, key) {
3777
+ const nested = block[key];
3778
+ if (Array.isArray(nested) && nested.length > 0) {
3779
+ return nested[0];
3780
+ }
3781
+ return {};
3782
+ }
3783
+ function coerceToString(v) {
3784
+ if (typeof v === "string") return v;
3785
+ if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint" || typeof v === "symbol" || typeof v === "function" || typeof v === "undefined") {
3786
+ return String(v);
3787
+ }
3788
+ return JSON.stringify(v);
3789
+ }
3790
+ function str(v) {
3791
+ return typeof v === "string" && v ? v : void 0;
3792
+ }
3793
+ function num(v) {
3794
+ if (typeof v === "number") return v;
3795
+ if (typeof v === "string") {
3796
+ const n = Number(v);
3797
+ return isNaN(n) ? void 0 : n;
3798
+ }
3799
+ return void 0;
3800
+ }
3801
+ function bool(v) {
3802
+ if (typeof v === "boolean") return v;
3803
+ if (v === "true") return true;
3804
+ if (v === "false") return false;
3805
+ return void 0;
3806
+ }
3807
+
3598
3808
  // src/parsers/variable-resolver.ts
3599
3809
  function resolveVariables(hclJson, tfvarsContent) {
3600
3810
  const defaults = extractVariableDefaults(hclJson);
@@ -3673,7 +3883,8 @@ function substituteInString(str2, variables) {
3673
3883
  return str2.replace(VAR_INLINE_RE, (_match, varName) => {
3674
3884
  if (varName in variables) {
3675
3885
  const v = variables[varName];
3676
- return v === null ? "" : String(v);
3886
+ if (v === null || v === void 0) return "";
3887
+ return coerceToString(v);
3677
3888
  }
3678
3889
  return _match;
3679
3890
  });
@@ -3696,32 +3907,6 @@ function detectProvider(resourceType) {
3696
3907
  );
3697
3908
  }
3698
3909
 
3699
- // src/parsers/extractors/helpers.ts
3700
- function firstBlock(block, key) {
3701
- const nested = block[key];
3702
- if (Array.isArray(nested) && nested.length > 0) {
3703
- return nested[0];
3704
- }
3705
- return {};
3706
- }
3707
- function str(v) {
3708
- return typeof v === "string" && v ? v : void 0;
3709
- }
3710
- function num(v) {
3711
- if (typeof v === "number") return v;
3712
- if (typeof v === "string") {
3713
- const n = Number(v);
3714
- return isNaN(n) ? void 0 : n;
3715
- }
3716
- return void 0;
3717
- }
3718
- function bool(v) {
3719
- if (typeof v === "boolean") return v;
3720
- if (v === "true") return true;
3721
- if (v === "false") return false;
3722
- return void 0;
3723
- }
3724
-
3725
3910
  // src/parsers/extractors/aws-extractor.ts
3726
3911
  var awsExtractors = {
3727
3912
  aws_instance(block) {
@@ -4307,7 +4492,7 @@ function extractTags(block) {
4307
4492
  const result = {};
4308
4493
  for (const [k, v] of Object.entries(tags)) {
4309
4494
  if (typeof v === "string") result[k] = v;
4310
- else if (v !== null && v !== void 0) result[k] = String(v);
4495
+ else if (v !== null && v !== void 0) result[k] = coerceToString(v);
4311
4496
  }
4312
4497
  return result;
4313
4498
  }
@@ -4447,10 +4632,20 @@ function isWithin(child, parent) {
4447
4632
  // src/parsers/safe-json.ts
4448
4633
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
4449
4634
  function safeJsonParse(text) {
4450
- return JSON.parse(text, (key, value) => {
4451
- if (FORBIDDEN_KEYS.has(key)) return void 0;
4635
+ let droppedKeys = 0;
4636
+ const parsed = JSON.parse(text, (key, value) => {
4637
+ if (FORBIDDEN_KEYS.has(key)) {
4638
+ droppedKeys++;
4639
+ return void 0;
4640
+ }
4452
4641
  return value;
4453
4642
  });
4643
+ if (droppedKeys > 0) {
4644
+ logger.warn("safeJsonParse: dropped prototype-pollution keys from payload", {
4645
+ droppedKeys
4646
+ });
4647
+ }
4648
+ return parsed;
4454
4649
  }
4455
4650
 
4456
4651
  // src/parsers/module-resolver.ts
@@ -4461,7 +4656,11 @@ function readTfFiles(dirPath) {
4461
4656
  let entries;
4462
4657
  try {
4463
4658
  entries = readdirSync(dirPath);
4464
- } catch {
4659
+ } catch (err) {
4660
+ logger.warn("Cannot list module directory; skipping", {
4661
+ dirPath: sanitizeForMessage(dirPath),
4662
+ error: sanitizeForMessage(err instanceof Error ? err.message : String(err), 512)
4663
+ });
4465
4664
  return [];
4466
4665
  }
4467
4666
  const files = [];
@@ -4471,7 +4670,11 @@ function readTfFiles(dirPath) {
4471
4670
  try {
4472
4671
  const content = readFileSync2(fullPath, "utf-8");
4473
4672
  files.push({ path: fullPath, content });
4474
- } catch {
4673
+ } catch (err) {
4674
+ logger.warn("Cannot read module file; skipping", {
4675
+ path: sanitizeForMessage(fullPath),
4676
+ error: sanitizeForMessage(err instanceof Error ? err.message : String(err), 512)
4677
+ });
4475
4678
  }
4476
4679
  }
4477
4680
  return files;
@@ -4782,7 +4985,7 @@ function extractAttributes(cfnType, props) {
4782
4985
  return {
4783
4986
  instance_type: asString(props.DBInstanceClass),
4784
4987
  engine: asString(props.Engine),
4785
- storage_size_gb: asNumber(props.AllocatedStorage),
4988
+ storage_size_gb: num(props.AllocatedStorage),
4786
4989
  multi_az: asBoolean(props.MultiAZ)
4787
4990
  };
4788
4991
  case "AWS::S3::Bucket":
@@ -4790,8 +4993,8 @@ function extractAttributes(cfnType, props) {
4790
4993
  case "AWS::EBS::Volume":
4791
4994
  return {
4792
4995
  storage_type: asString(props.VolumeType),
4793
- storage_size_gb: asNumber(props.Size),
4794
- iops: asNumber(props.Iops)
4996
+ storage_size_gb: num(props.Size),
4997
+ iops: num(props.Iops)
4795
4998
  };
4796
4999
  case "AWS::ElasticLoadBalancingV2::LoadBalancer":
4797
5000
  return {
@@ -4803,16 +5006,16 @@ function extractAttributes(cfnType, props) {
4803
5006
  return {};
4804
5007
  case "AWS::Lambda::Function":
4805
5008
  return {
4806
- memory_size: asNumber(props.MemorySize),
4807
- timeout: asNumber(props.Timeout)
5009
+ memory_size: num(props.MemorySize),
5010
+ timeout: num(props.Timeout)
4808
5011
  };
4809
5012
  case "AWS::DynamoDB::Table":
4810
5013
  return {
4811
5014
  billing_mode: asString(props.BillingMode) ?? "PROVISIONED",
4812
- read_capacity: asNumber(
5015
+ read_capacity: num(
4813
5016
  props.ProvisionedThroughput?.ReadCapacityUnits
4814
5017
  ),
4815
- write_capacity: asNumber(
5018
+ write_capacity: num(
4816
5019
  props.ProvisionedThroughput?.WriteCapacityUnits
4817
5020
  )
4818
5021
  };
@@ -4827,7 +5030,7 @@ function extractAttributes(cfnType, props) {
4827
5030
  case "AWS::ElastiCache::CacheCluster":
4828
5031
  return {
4829
5032
  instance_type: asString(props.CacheNodeType),
4830
- node_count: asNumber(props.NumCacheNodes),
5033
+ node_count: num(props.NumCacheNodes),
4831
5034
  engine: asString(props.Engine)
4832
5035
  };
4833
5036
  case "AWS::CloudFront::Distribution":
@@ -4842,7 +5045,8 @@ function extractTags2(props) {
4842
5045
  if (Array.isArray(rawTags)) {
4843
5046
  for (const tag of rawTags) {
4844
5047
  if (tag && typeof tag === "object" && "Key" in tag && "Value" in tag) {
4845
- tags[String(tag.Key)] = String(tag.Value);
5048
+ const { Key, Value } = tag;
5049
+ tags[coerceToString(Key)] = coerceToString(Value);
4846
5050
  }
4847
5051
  }
4848
5052
  }
@@ -4851,14 +5055,6 @@ function extractTags2(props) {
4851
5055
  function asString(value) {
4852
5056
  return typeof value === "string" ? value : void 0;
4853
5057
  }
4854
- function asNumber(value) {
4855
- if (typeof value === "number") return value;
4856
- if (typeof value === "string") {
4857
- const n = Number(value);
4858
- return Number.isNaN(n) ? void 0 : n;
4859
- }
4860
- return void 0;
4861
- }
4862
5058
  function asBoolean(value) {
4863
5059
  if (typeof value === "boolean") return value;
4864
5060
  if (value === "true") return true;
@@ -4878,11 +5074,11 @@ var CloudFormationParser = class {
4878
5074
  return false;
4879
5075
  });
4880
5076
  }
4881
- async parse(files, options) {
5077
+ parse(files, options) {
4882
5078
  const warnings = [];
4883
5079
  const file = files.find((f) => this.extensions.some((ext) => f.path.endsWith(ext)));
4884
5080
  if (!file) {
4885
- return emptyInventory(warnings);
5081
+ return Promise.resolve(emptyInventory(warnings));
4886
5082
  }
4887
5083
  let template;
4888
5084
  try {
@@ -4891,11 +5087,11 @@ var CloudFormationParser = class {
4891
5087
  const msg = err instanceof Error ? err.message : String(err);
4892
5088
  warnings.push(`Failed to parse CloudFormation template ${file.path}: ${msg}`);
4893
5089
  logger.warn("CloudFormation parse error", { path: file.path, error: msg });
4894
- return emptyInventory(warnings);
5090
+ return Promise.resolve(emptyInventory(warnings));
4895
5091
  }
4896
5092
  if (!template || typeof template !== "object" || !template.Resources) {
4897
5093
  warnings.push(`Invalid CloudFormation template: missing Resources section in ${file.path}`);
4898
- return emptyInventory(warnings);
5094
+ return Promise.resolve(emptyInventory(warnings));
4899
5095
  }
4900
5096
  const resolvedParams = resolveParameters(
4901
5097
  template.Parameters ?? {},
@@ -4928,7 +5124,7 @@ var CloudFormationParser = class {
4928
5124
  region,
4929
5125
  warningCount: warnings.length
4930
5126
  });
4931
- return inventory;
5127
+ return Promise.resolve(inventory);
4932
5128
  }
4933
5129
  };
4934
5130
  function emptyInventory(warnings) {
@@ -5034,14 +5230,14 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
5034
5230
  const diskProps = props;
5035
5231
  return {
5036
5232
  sku: resource.sku?.name,
5037
- storage_size_gb: asNumber2(diskProps.diskSizeGB)
5233
+ storage_size_gb: num(diskProps.diskSizeGB)
5038
5234
  };
5039
5235
  }
5040
5236
  case "microsoft.dbforpostgresql/flexibleservers": {
5041
5237
  const storage = props.storage;
5042
5238
  return {
5043
5239
  sku: resource.sku?.name,
5044
- storage_size_gb: asNumber2(storage?.storageSizeGB)
5240
+ storage_size_gb: num(storage?.storageSizeGB)
5045
5241
  };
5046
5242
  }
5047
5243
  case "microsoft.dbformysql/flexibleservers":
@@ -5065,7 +5261,7 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
5065
5261
  const pools = props.agentPoolProfiles;
5066
5262
  const firstPool = pools?.[0];
5067
5263
  return {
5068
- node_count: asNumber2(firstPool?.count),
5264
+ node_count: num(firstPool?.count),
5069
5265
  vm_size: typeof firstPool?.vmSize === "string" ? firstPool.vmSize : void 0
5070
5266
  };
5071
5267
  }
@@ -5099,14 +5295,6 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
5099
5295
  return {};
5100
5296
  }
5101
5297
  }
5102
- function asNumber2(value) {
5103
- if (typeof value === "number") return value;
5104
- if (typeof value === "string") {
5105
- const n = Number(value);
5106
- return Number.isNaN(n) ? void 0 : n;
5107
- }
5108
- return void 0;
5109
- }
5110
5298
 
5111
5299
  // src/parsers/bicep/arm-parser.ts
5112
5300
  var ArmParser = class {
@@ -5118,11 +5306,11 @@ var ArmParser = class {
5118
5306
  return f.content.includes("schema.management.azure.com") || f.content.includes("Microsoft.") && f.content.includes("apiVersion");
5119
5307
  });
5120
5308
  }
5121
- async parse(files, options) {
5309
+ parse(files, options) {
5122
5310
  const warnings = [];
5123
5311
  const file = files.find((f) => f.path.endsWith(".json"));
5124
5312
  if (!file) {
5125
- return emptyInventory2(warnings);
5313
+ return Promise.resolve(emptyInventory2(warnings));
5126
5314
  }
5127
5315
  let template;
5128
5316
  try {
@@ -5131,11 +5319,11 @@ var ArmParser = class {
5131
5319
  const msg = err instanceof Error ? err.message : String(err);
5132
5320
  warnings.push(`Failed to parse ARM template ${file.path}: ${msg}`);
5133
5321
  logger.warn("ARM template parse error", { path: file.path, error: msg });
5134
- return emptyInventory2(warnings);
5322
+ return Promise.resolve(emptyInventory2(warnings));
5135
5323
  }
5136
5324
  if (!template || typeof template !== "object" || !Array.isArray(template.resources)) {
5137
5325
  warnings.push(`Invalid ARM template: missing resources array in ${file.path}`);
5138
- return emptyInventory2(warnings);
5326
+ return Promise.resolve(emptyInventory2(warnings));
5139
5327
  }
5140
5328
  const resolvedParams = resolveParameters2(
5141
5329
  template.parameters ?? {},
@@ -5168,7 +5356,7 @@ var ArmParser = class {
5168
5356
  region,
5169
5357
  warningCount: warnings.length
5170
5358
  });
5171
- return inventory;
5359
+ return Promise.resolve(inventory);
5172
5360
  }
5173
5361
  };
5174
5362
  function emptyInventory2(warnings) {
@@ -5318,7 +5506,7 @@ function extractAttributes3(pulumiType, props) {
5318
5506
  return {
5319
5507
  instance_type: asString2(props.instanceClass),
5320
5508
  engine: asString2(props.engine),
5321
- storage_size_gb: asNumber3(props.allocatedStorage),
5509
+ storage_size_gb: num(props.allocatedStorage),
5322
5510
  multi_az: asBoolean2(props.multiAz)
5323
5511
  };
5324
5512
  case "aws:s3/bucket:Bucket":
@@ -5326,8 +5514,8 @@ function extractAttributes3(pulumiType, props) {
5326
5514
  case "aws:ebs/volume:Volume":
5327
5515
  return {
5328
5516
  storage_type: asString2(props.type),
5329
- storage_size_gb: asNumber3(props.size),
5330
- iops: asNumber3(props.iops)
5517
+ storage_size_gb: num(props.size),
5518
+ iops: num(props.iops)
5331
5519
  };
5332
5520
  case "aws:lb/loadBalancer:LoadBalancer":
5333
5521
  return {
@@ -5339,14 +5527,14 @@ function extractAttributes3(pulumiType, props) {
5339
5527
  return {};
5340
5528
  case "aws:lambda/function:Function":
5341
5529
  return {
5342
- memory_size: asNumber3(props.memorySize),
5343
- timeout: asNumber3(props.timeout)
5530
+ memory_size: num(props.memorySize),
5531
+ timeout: num(props.timeout)
5344
5532
  };
5345
5533
  case "aws:dynamodb/table:Table":
5346
5534
  return {
5347
5535
  billing_mode: asString2(props.billingMode) ?? "PROVISIONED",
5348
- read_capacity: asNumber3(props.readCapacity),
5349
- write_capacity: asNumber3(props.writeCapacity)
5536
+ read_capacity: num(props.readCapacity),
5537
+ write_capacity: num(props.writeCapacity)
5350
5538
  };
5351
5539
  case "aws:route53/zone:Zone":
5352
5540
  return {};
@@ -5392,12 +5580,12 @@ function extractAttributes3(pulumiType, props) {
5392
5580
  };
5393
5581
  case "gcp:container:Cluster":
5394
5582
  return {
5395
- node_count: asNumber3(props.initialNodeCount)
5583
+ node_count: num(props.initialNodeCount)
5396
5584
  };
5397
5585
  case "gcp:cloudfunctions:Function":
5398
5586
  return {
5399
- memory_size: asNumber3(props.availableMemoryMb),
5400
- timeout: asNumber3(props.timeout)
5587
+ memory_size: num(props.availableMemoryMb),
5588
+ timeout: num(props.timeout)
5401
5589
  };
5402
5590
  case "gcp:dns:ManagedZone":
5403
5591
  return {};
@@ -5420,14 +5608,6 @@ function extractTags3(props) {
5420
5608
  function asString2(value) {
5421
5609
  return typeof value === "string" ? value : void 0;
5422
5610
  }
5423
- function asNumber3(value) {
5424
- if (typeof value === "number") return value;
5425
- if (typeof value === "string") {
5426
- const n = Number(value);
5427
- return Number.isNaN(n) ? void 0 : n;
5428
- }
5429
- return void 0;
5430
- }
5431
5611
  function asBoolean2(value) {
5432
5612
  if (typeof value === "boolean") return value;
5433
5613
  if (value === "true") return true;
@@ -5450,7 +5630,7 @@ var PulumiParser = class {
5450
5630
  }
5451
5631
  });
5452
5632
  }
5453
- async parse(files, _options) {
5633
+ parse(files, _options) {
5454
5634
  const warnings = [];
5455
5635
  const file = files.find((f) => {
5456
5636
  try {
@@ -5462,7 +5642,7 @@ var PulumiParser = class {
5462
5642
  }
5463
5643
  });
5464
5644
  if (!file) {
5465
- return emptyInventory3(warnings);
5645
+ return Promise.resolve(emptyInventory3(warnings));
5466
5646
  }
5467
5647
  let stackExport;
5468
5648
  try {
@@ -5471,11 +5651,11 @@ var PulumiParser = class {
5471
5651
  const msg = err instanceof Error ? err.message : String(err);
5472
5652
  warnings.push(`Failed to parse Pulumi stack export ${file.path}: ${msg}`);
5473
5653
  logger.warn("Pulumi parse error", { path: file.path, error: msg });
5474
- return emptyInventory3(warnings);
5654
+ return Promise.resolve(emptyInventory3(warnings));
5475
5655
  }
5476
5656
  if (!stackExport?.deployment?.resources || !Array.isArray(stackExport.deployment.resources)) {
5477
5657
  warnings.push(`Invalid Pulumi stack export: missing deployment.resources in ${file.path}`);
5478
- return emptyInventory3(warnings);
5658
+ return Promise.resolve(emptyInventory3(warnings));
5479
5659
  }
5480
5660
  const resources = [];
5481
5661
  for (const pulumiRes of stackExport.deployment.resources) {
@@ -5509,7 +5689,7 @@ var PulumiParser = class {
5509
5689
  region: dominantRegion,
5510
5690
  warningCount: warnings.length
5511
5691
  });
5512
- return inventory;
5692
+ return Promise.resolve(inventory);
5513
5693
  }
5514
5694
  };
5515
5695
  function emptyInventory3(warnings) {
@@ -5606,7 +5786,7 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
5606
5786
  parse_warnings: warnings
5607
5787
  };
5608
5788
  }
5609
- const combined = mergeHclJsons2(parsedJsons.map((p) => p.json));
5789
+ const combined = mergeHclJsons(parsedJsons.map((p) => p.json));
5610
5790
  const variables = resolveVariables(combined, tfvarsContent);
5611
5791
  const providers = ["aws", "azure", "gcp"];
5612
5792
  const defaultRegions = {};
@@ -5625,7 +5805,7 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
5625
5805
  }
5626
5806
  }
5627
5807
  if (resolveModulesEnabled) {
5628
- const moduleBasePath = basePath ?? (parsedJsons.length > 0 ? dirname2(parsedJsons[0].path) : process.cwd());
5808
+ const moduleBasePath = basePath ?? (parsedJsons.length > 0 ? deriveModuleBasePath(parsedJsons[0].path) : process.cwd());
5629
5809
  try {
5630
5810
  const moduleResources = await resolveModules(combined, moduleBasePath, variables, warnings);
5631
5811
  allResources.push(...moduleResources);
@@ -5658,46 +5838,19 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
5658
5838
  });
5659
5839
  return inventory;
5660
5840
  }
5661
- function mergeHclJsons2(jsons) {
5662
- const merged = {};
5663
- for (const json of jsons) {
5664
- for (const [key, value] of Object.entries(json)) {
5665
- if (!(key in merged)) {
5666
- merged[key] = deepClone(value);
5667
- continue;
5668
- }
5669
- const existing = merged[key];
5670
- if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
5671
- merged[key] = mergeObjects2(
5672
- existing,
5673
- value
5674
- );
5675
- } else if (Array.isArray(existing) && Array.isArray(value)) {
5676
- merged[key] = [...existing, ...value];
5677
- } else {
5678
- merged[key] = deepClone(value);
5679
- }
5680
- }
5841
+ function deriveModuleBasePath(filePath) {
5842
+ const normalized = normalize4(filePath);
5843
+ const isWindowsDrive = /^[A-Za-z]:/.test(normalized);
5844
+ const isWindowsUnc = normalized.startsWith("\\\\") || normalized.startsWith("//");
5845
+ if (isAbsolute(normalized) || isWindowsDrive || isWindowsUnc) {
5846
+ return process.cwd();
5681
5847
  }
5682
- return merged;
5683
- }
5684
- function mergeObjects2(a, b) {
5685
- const result = { ...a };
5686
- for (const [key, bVal] of Object.entries(b)) {
5687
- const aVal = result[key];
5688
- if (aVal !== null && typeof aVal === "object" && !Array.isArray(aVal) && bVal !== null && typeof bVal === "object" && !Array.isArray(bVal)) {
5689
- result[key] = mergeObjects2(aVal, bVal);
5690
- } else if (Array.isArray(aVal) && Array.isArray(bVal)) {
5691
- result[key] = [...aVal, ...bVal];
5692
- } else {
5693
- result[key] = deepClone(bVal);
5694
- }
5848
+ const dir = dirname2(normalized);
5849
+ const escapesUpward = dir === ".." || dir.startsWith(`..${sep2}`) || dir.startsWith("../");
5850
+ if (escapesUpward) {
5851
+ return process.cwd();
5695
5852
  }
5696
- return result;
5697
- }
5698
- function deepClone(value) {
5699
- if (value === null || typeof value !== "object") return value;
5700
- return structuredClone(value);
5853
+ return dir;
5701
5854
  }
5702
5855
 
5703
5856
  // src/parsers/dependency-graph.ts
@@ -5828,6 +5981,9 @@ function generateMermaidDiagram(graph) {
5828
5981
  return lines.join("\n");
5829
5982
  }
5830
5983
 
5984
+ // src/schemas/fragments.ts
5985
+ import { z as z2 } from "zod";
5986
+
5831
5987
  // src/schemas/bounded.ts
5832
5988
  import { z } from "zod";
5833
5989
  var MAX_FILE_PATH_LEN = 1024;
@@ -5836,23 +5992,43 @@ var MAX_TFVARS_BYTES = 1 * 1024 * 1024;
5836
5992
  var MAX_PLAN_JSON_BYTES = 20 * 1024 * 1024;
5837
5993
  var MAX_STATE_JSON_BYTES = 20 * 1024 * 1024;
5838
5994
  var MAX_SHORT_STRING_LEN = 256;
5995
+ var MAX_TOTAL_FILE_BYTES = 50 * 1024 * 1024;
5839
5996
  var filePathSchema = z.string().max(MAX_FILE_PATH_LEN, `File path exceeds ${MAX_FILE_PATH_LEN} characters`);
5840
5997
  var fileContentSchema = z.string().max(MAX_FILE_CONTENT_BYTES, `File content exceeds ${MAX_FILE_CONTENT_BYTES} bytes`);
5841
5998
  var tfvarsSchema = z.string().max(MAX_TFVARS_BYTES, `tfvars content exceeds ${MAX_TFVARS_BYTES} bytes`);
5842
5999
  var planJsonSchema = z.string().max(MAX_PLAN_JSON_BYTES, `plan JSON exceeds ${MAX_PLAN_JSON_BYTES} bytes`);
5843
6000
  var stateJsonSchema = z.string().max(MAX_STATE_JSON_BYTES, `state JSON exceeds ${MAX_STATE_JSON_BYTES} bytes`);
5844
6001
  var shortStringSchema = z.string().max(MAX_SHORT_STRING_LEN, `value exceeds ${MAX_SHORT_STRING_LEN} characters`);
6002
+ function assertTotalFileBytesWithin(files, ctx) {
6003
+ let total = 0;
6004
+ for (const f of files) {
6005
+ total += f.content.length;
6006
+ if (total > MAX_TOTAL_FILE_BYTES) {
6007
+ ctx.addIssue({
6008
+ code: z.ZodIssueCode.custom,
6009
+ message: `aggregate file content exceeds ${MAX_TOTAL_FILE_BYTES} bytes`,
6010
+ path: ["files"]
6011
+ });
6012
+ return;
6013
+ }
6014
+ }
6015
+ }
6016
+
6017
+ // src/schemas/fragments.ts
6018
+ var providerEnum = z2.enum(["aws", "azure", "gcp"]);
6019
+ var iacFileEntrySchema = z2.object({
6020
+ path: filePathSchema.describe("File path"),
6021
+ content: fileContentSchema.describe("File content")
6022
+ });
6023
+ var iacFilesSchema = z2.array(iacFileEntrySchema).max(2e3, "files array exceeds 2000 entries").superRefine(assertTotalFileBytesWithin);
6024
+ var tfvarsField = tfvarsSchema.optional().describe("Contents of terraform.tfvars file");
6025
+ var currencyField = z2.enum(SUPPORTED_CURRENCIES).optional().default("USD").describe("Output currency for cost estimates. Defaults to USD.");
5845
6026
 
5846
6027
  // src/tools/analyze-terraform.ts
5847
- var analyzeTerraformSchema = z2.object({
5848
- files: z2.array(
5849
- z2.object({
5850
- path: filePathSchema.describe("File path"),
5851
- content: fileContentSchema.describe("File content (HCL)")
5852
- })
5853
- ).max(2e3, "files array exceeds 2000 entries").describe("Terraform files to analyze"),
5854
- tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
5855
- include_dependencies: z2.boolean().optional().default(false).describe(
6028
+ var analyzeTerraformSchema = z3.object({
6029
+ files: iacFilesSchema.describe("Terraform files to analyze"),
6030
+ tfvars: tfvarsField,
6031
+ include_dependencies: z3.boolean().optional().default(false).describe(
5856
6032
  "When true, parse resource references and depends_on blocks to build a dependency graph and Mermaid diagram"
5857
6033
  )
5858
6034
  });
@@ -5904,7 +6080,7 @@ async function analyzeTerraform(params) {
5904
6080
  }
5905
6081
 
5906
6082
  // src/tools/estimate-cost.ts
5907
- import { z as z3 } from "zod";
6083
+ import { z as z4 } from "zod";
5908
6084
 
5909
6085
  // src/mapping/region-mapper.ts
5910
6086
  var DEFAULT_REGIONS = {
@@ -5933,6 +6109,27 @@ function mapRegion(region, sourceProvider, targetProvider) {
5933
6109
  return mapped;
5934
6110
  }
5935
6111
 
6112
+ // src/calculator/estimate-factory.ts
6113
+ function round2(n) {
6114
+ return Math.round(n * 100) / 100;
6115
+ }
6116
+ function makeCostEstimate(input) {
6117
+ return {
6118
+ resource_id: input.resource.id,
6119
+ resource_type: input.resource.type,
6120
+ resource_name: input.resource.name,
6121
+ provider: input.provider,
6122
+ region: input.region,
6123
+ monthly_cost: round2(input.monthlyCost),
6124
+ yearly_cost: round2(input.monthlyCost * 12),
6125
+ currency: input.currency ?? "USD",
6126
+ breakdown: input.breakdown,
6127
+ confidence: input.confidence,
6128
+ notes: input.notes,
6129
+ pricing_source: input.pricingSource
6130
+ };
6131
+ }
6132
+
5936
6133
  // src/mapping/instance-mapper.ts
5937
6134
  function directionKey(source, target) {
5938
6135
  if (source === target) return null;
@@ -6114,7 +6311,7 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
6114
6311
  `No instance mapping found for ${sourceInstanceType} (${resource.provider} -> ${targetProvider})`
6115
6312
  );
6116
6313
  } else {
6117
- const { getInstanceMap: getInstanceMap2 } = await import("./loader-UWVEXYMR.js");
6314
+ const { getInstanceMap: getInstanceMap2 } = await import("./loader-CMBV7346.js");
6118
6315
  const im = getInstanceMap2();
6119
6316
  const dirKey = `${resource.provider}_to_${targetProvider}`;
6120
6317
  const directMap = im[dirKey];
@@ -6128,11 +6325,7 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
6128
6325
  }
6129
6326
  }
6130
6327
  const effectiveInstance = mappedInstance ?? sourceInstanceType;
6131
- const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(
6132
- effectiveInstance,
6133
- targetRegion,
6134
- resource.attributes.os
6135
- ) : null;
6328
+ const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(effectiveInstance, targetRegion, resource.attributes.os) : null;
6136
6329
  let computeMonthlyCost = 0;
6137
6330
  let pricingSource = "fallback";
6138
6331
  if (computePrice) {
@@ -6253,20 +6446,16 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
6253
6446
  }
6254
6447
  }
6255
6448
  const totalMonthly = computeMonthlyCost + storageMonthlyCost;
6256
- return {
6257
- resource_id: resource.id,
6258
- resource_type: resource.type,
6259
- resource_name: resource.name,
6449
+ return makeCostEstimate({
6450
+ resource,
6260
6451
  provider: targetProvider,
6261
6452
  region: targetRegion,
6262
- monthly_cost: totalMonthly,
6263
- yearly_cost: totalMonthly * 12,
6264
- currency: "USD",
6453
+ monthlyCost: totalMonthly,
6265
6454
  breakdown,
6266
6455
  confidence,
6267
6456
  notes,
6268
- pricing_source: pricingSource
6269
- };
6457
+ pricingSource
6458
+ });
6270
6459
  }
6271
6460
 
6272
6461
  // src/calculator/database.ts
@@ -6369,20 +6558,16 @@ async function calculateDatabaseCost(resource, targetProvider, targetRegion, pri
6369
6558
  }
6370
6559
  }
6371
6560
  const totalMonthly = instanceMonthlyCost + storageMonthlyCost;
6372
- return {
6373
- resource_id: resource.id,
6374
- resource_type: resource.type,
6375
- resource_name: resource.name,
6561
+ return makeCostEstimate({
6562
+ resource,
6376
6563
  provider: targetProvider,
6377
6564
  region: targetRegion,
6378
- monthly_cost: totalMonthly,
6379
- yearly_cost: totalMonthly * 12,
6380
- currency: "USD",
6565
+ monthlyCost: totalMonthly,
6381
6566
  breakdown,
6382
6567
  confidence: mappedInstance ? "high" : "low",
6383
6568
  notes,
6384
- pricing_source: pricingSource
6385
- };
6569
+ pricingSource
6570
+ });
6386
6571
  }
6387
6572
 
6388
6573
  // src/calculator/storage.ts
@@ -6461,20 +6646,16 @@ async function calculateStorageCost(resource, targetProvider, targetRegion, pric
6461
6646
  targetRegion
6462
6647
  });
6463
6648
  }
6464
- return {
6465
- resource_id: resource.id,
6466
- resource_type: resource.type,
6467
- resource_name: resource.name,
6649
+ return makeCostEstimate({
6650
+ resource,
6468
6651
  provider: targetProvider,
6469
6652
  region: targetRegion,
6470
- monthly_cost: totalMonthly,
6471
- yearly_cost: totalMonthly * 12,
6472
- currency: "USD",
6653
+ monthlyCost: totalMonthly,
6473
6654
  breakdown,
6474
6655
  confidence: storagePrice ? "high" : "low",
6475
6656
  notes,
6476
- pricing_source: pricingSource
6477
- };
6657
+ pricingSource
6658
+ });
6478
6659
  }
6479
6660
 
6480
6661
  // src/calculator/network.ts
@@ -6524,20 +6705,16 @@ async function calculateNatGatewayCost(resource, targetProvider, targetRegion, p
6524
6705
  } else {
6525
6706
  notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
6526
6707
  }
6527
- return {
6528
- resource_id: resource.id,
6529
- resource_type: resource.type,
6530
- resource_name: resource.name,
6708
+ return makeCostEstimate({
6709
+ resource,
6531
6710
  provider: targetProvider,
6532
6711
  region: targetRegion,
6533
- monthly_cost: totalMonthly,
6534
- yearly_cost: totalMonthly * 12,
6535
- currency: "USD",
6712
+ monthlyCost: totalMonthly,
6536
6713
  breakdown,
6537
6714
  confidence: natPrice ? "medium" : "low",
6538
6715
  notes,
6539
- pricing_source: natPricingSource
6540
- };
6716
+ pricingSource: natPricingSource
6717
+ });
6541
6718
  }
6542
6719
  async function calculateLoadBalancerCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
6543
6720
  const notes = [];
@@ -6569,20 +6746,16 @@ async function calculateLoadBalancerCost(resource, targetProvider, targetRegion,
6569
6746
  } else {
6570
6747
  notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
6571
6748
  }
6572
- return {
6573
- resource_id: resource.id,
6574
- resource_type: resource.type,
6575
- resource_name: resource.name,
6749
+ return makeCostEstimate({
6750
+ resource,
6576
6751
  provider: targetProvider,
6577
6752
  region: targetRegion,
6578
- monthly_cost: totalMonthly,
6579
- yearly_cost: totalMonthly * 12,
6580
- currency: "USD",
6753
+ monthlyCost: totalMonthly,
6581
6754
  breakdown,
6582
6755
  confidence: lbPrice ? "medium" : "low",
6583
6756
  notes,
6584
- pricing_source: lbPricingSource
6585
- };
6757
+ pricingSource: lbPricingSource
6758
+ });
6586
6759
  }
6587
6760
 
6588
6761
  // src/calculator/kubernetes.ts
@@ -6653,20 +6826,16 @@ async function calculateKubernetesCost(resource, targetProvider, targetRegion, p
6653
6826
  );
6654
6827
  }
6655
6828
  }
6656
- return {
6657
- resource_id: resource.id,
6658
- resource_type: resource.type,
6659
- resource_name: resource.name,
6829
+ return makeCostEstimate({
6830
+ resource,
6660
6831
  provider: targetProvider,
6661
6832
  region: targetRegion,
6662
- monthly_cost: totalMonthly,
6663
- yearly_cost: totalMonthly * 12,
6664
- currency: "USD",
6833
+ monthlyCost: totalMonthly,
6665
6834
  breakdown,
6666
6835
  confidence: k8sPrice ? "medium" : "low",
6667
6836
  notes,
6668
- pricing_source: pricingSource
6669
- };
6837
+ pricingSource
6838
+ });
6670
6839
  }
6671
6840
 
6672
6841
  // src/calculator/serverless.ts
@@ -6721,20 +6890,16 @@ function calculateLambdaCost(resource, targetProvider, targetRegion) {
6721
6890
  }
6722
6891
  const totalMonthly = requestCost + computeCost;
6723
6892
  void timeoutSec;
6724
- return {
6725
- resource_id: resource.id,
6726
- resource_type: resource.type,
6727
- resource_name: resource.name,
6893
+ return makeCostEstimate({
6894
+ resource,
6728
6895
  provider: targetProvider,
6729
6896
  region: targetRegion,
6730
- monthly_cost: totalMonthly,
6731
- yearly_cost: totalMonthly * 12,
6732
- currency: "USD",
6897
+ monthlyCost: totalMonthly,
6733
6898
  breakdown,
6734
6899
  confidence: "medium",
6735
6900
  notes,
6736
- pricing_source: "fallback"
6737
- };
6901
+ pricingSource: "fallback"
6902
+ });
6738
6903
  }
6739
6904
  var DYNAMO_ON_DEMAND_WRITE_PER_MILLION = 1.25;
6740
6905
  var DYNAMO_ON_DEMAND_READ_PER_MILLION = 0.25;
@@ -6804,20 +6969,16 @@ function calculateDynamoDbCost(resource, targetProvider, targetRegion) {
6804
6969
  monthly_cost: storageCost
6805
6970
  });
6806
6971
  totalMonthly += storageCost;
6807
- return {
6808
- resource_id: resource.id,
6809
- resource_type: resource.type,
6810
- resource_name: resource.name,
6972
+ return makeCostEstimate({
6973
+ resource,
6811
6974
  provider: targetProvider,
6812
6975
  region: targetRegion,
6813
- monthly_cost: totalMonthly,
6814
- yearly_cost: totalMonthly * 12,
6815
- currency: "USD",
6976
+ monthlyCost: totalMonthly,
6816
6977
  breakdown,
6817
6978
  confidence: "medium",
6818
6979
  notes,
6819
- pricing_source: "fallback"
6820
- };
6980
+ pricingSource: "fallback"
6981
+ });
6821
6982
  }
6822
6983
  var SQS_STANDARD_PER_MILLION = 0.4;
6823
6984
  var SQS_FIFO_PER_MILLION = 0.5;
@@ -6842,20 +7003,16 @@ function calculateSqsCost(resource, targetProvider, targetRegion) {
6842
7003
  );
6843
7004
  }
6844
7005
  notes.push(`SQS ${queueType} queue pricing at $${pricePerMillion}/million requests`);
6845
- return {
6846
- resource_id: resource.id,
6847
- resource_type: resource.type,
6848
- resource_name: resource.name,
7006
+ return makeCostEstimate({
7007
+ resource,
6849
7008
  provider: targetProvider,
6850
7009
  region: targetRegion,
6851
- monthly_cost: totalMonthly,
6852
- yearly_cost: totalMonthly * 12,
6853
- currency: "USD",
7010
+ monthlyCost: totalMonthly,
6854
7011
  breakdown,
6855
7012
  confidence: "medium",
6856
7013
  notes,
6857
- pricing_source: "fallback"
6858
- };
7014
+ pricingSource: "fallback"
7015
+ });
6859
7016
  }
6860
7017
 
6861
7018
  // src/calculator/cache.ts
@@ -6912,20 +7069,16 @@ function calculateElastiCacheCost(resource, targetProvider, targetRegion) {
6912
7069
  } else {
6913
7070
  notes.push(`No pricing data found for ElastiCache node type ${nodeType}; cost reported as $0`);
6914
7071
  }
6915
- return {
6916
- resource_id: resource.id,
6917
- resource_type: resource.type,
6918
- resource_name: resource.name,
7072
+ return makeCostEstimate({
7073
+ resource,
6919
7074
  provider: targetProvider,
6920
7075
  region: targetRegion,
6921
- monthly_cost: totalMonthly,
6922
- yearly_cost: totalMonthly * 12,
6923
- currency: "USD",
7076
+ monthlyCost: totalMonthly,
6924
7077
  breakdown,
6925
7078
  confidence: basePrice !== void 0 ? "medium" : "low",
6926
7079
  notes,
6927
- pricing_source: pricingSource
6928
- };
7080
+ pricingSource
7081
+ });
6929
7082
  }
6930
7083
  var AZURE_REDIS_PRICES = {
6931
7084
  // Basic (single node, no SLA)
@@ -6978,20 +7131,16 @@ function calculateAzureRedisCacheCost(resource, targetProvider, targetRegion) {
6978
7131
  `No pricing found for Azure Redis Cache ${sku} ${capacityStr.toUpperCase()}; cost reported as $0`
6979
7132
  );
6980
7133
  }
6981
- return {
6982
- resource_id: resource.id,
6983
- resource_type: resource.type,
6984
- resource_name: resource.name,
7134
+ return makeCostEstimate({
7135
+ resource,
6985
7136
  provider: targetProvider,
6986
7137
  region: targetRegion,
6987
- monthly_cost: totalMonthly,
6988
- yearly_cost: totalMonthly * 12,
6989
- currency: "USD",
7138
+ monthlyCost: totalMonthly,
6990
7139
  breakdown,
6991
7140
  confidence: hourlyPrice !== void 0 ? "medium" : "low",
6992
7141
  notes,
6993
- pricing_source: pricingSource
6994
- };
7142
+ pricingSource
7143
+ });
6995
7144
  }
6996
7145
  var GCP_REDIS_PRICES = {
6997
7146
  basic: 0.049,
@@ -7015,20 +7164,16 @@ function calculateGcpRedisCost(resource, targetProvider, targetRegion) {
7015
7164
  notes.push(
7016
7165
  `GCP Memorystore Redis: ${normalizedTier} tier, ${memorySizeGb}GB \u2014 $${pricePerGbHour}/GB/hr`
7017
7166
  );
7018
- return {
7019
- resource_id: resource.id,
7020
- resource_type: resource.type,
7021
- resource_name: resource.name,
7167
+ return makeCostEstimate({
7168
+ resource,
7022
7169
  provider: targetProvider,
7023
7170
  region: targetRegion,
7024
- monthly_cost: totalMonthly,
7025
- yearly_cost: totalMonthly * 12,
7026
- currency: "USD",
7171
+ monthlyCost: totalMonthly,
7027
7172
  breakdown,
7028
7173
  confidence: "medium",
7029
7174
  notes,
7030
- pricing_source: "fallback"
7031
- };
7175
+ pricingSource: "fallback"
7176
+ });
7032
7177
  }
7033
7178
  var CLOUDFRONT_DATA_FIRST_10TB_PER_GB = 0.085;
7034
7179
  var DEFAULT_CLOUDFRONT_TRANSFER_GB = 100;
@@ -7068,20 +7213,16 @@ function calculateCloudFrontCost(resource, targetProvider, targetRegion) {
7068
7213
  notes.push(
7069
7214
  `CloudFront ${priceClass} \u2014 HTTPS request charges excluded (runtime traffic data required)`
7070
7215
  );
7071
- return {
7072
- resource_id: resource.id,
7073
- resource_type: resource.type,
7074
- resource_name: resource.name,
7216
+ return makeCostEstimate({
7217
+ resource,
7075
7218
  provider: targetProvider,
7076
7219
  region: targetRegion,
7077
- monthly_cost: totalMonthly,
7078
- yearly_cost: totalMonthly * 12,
7079
- currency: "USD",
7220
+ monthlyCost: totalMonthly,
7080
7221
  breakdown,
7081
7222
  confidence: "medium",
7082
7223
  notes,
7083
- pricing_source: "fallback"
7084
- };
7224
+ pricingSource: "fallback"
7225
+ });
7085
7226
  }
7086
7227
 
7087
7228
  // src/calculator/paas.ts
@@ -7148,20 +7289,16 @@ function calculateAppServicePlanCost(resource, targetProvider, targetRegion) {
7148
7289
  `No pricing found for App Service Plan SKU "${skuName || skuSize || skuTier}"; cost reported as $0`
7149
7290
  );
7150
7291
  }
7151
- return {
7152
- resource_id: resource.id,
7153
- resource_type: resource.type,
7154
- resource_name: resource.name,
7292
+ return makeCostEstimate({
7293
+ resource,
7155
7294
  provider: targetProvider,
7156
7295
  region: targetRegion,
7157
- monthly_cost: totalMonthly,
7158
- yearly_cost: totalMonthly * 12,
7159
- currency: "USD",
7296
+ monthlyCost: totalMonthly,
7160
7297
  breakdown,
7161
7298
  confidence: hourlyPrice !== void 0 ? "medium" : "low",
7162
7299
  notes,
7163
- pricing_source: pricingSource
7164
- };
7300
+ pricingSource
7301
+ });
7165
7302
  }
7166
7303
  var COSMOS_SERVERLESS_PER_MILLION_RU = 0.25;
7167
7304
  var COSMOS_PROVISIONED_PER_100_RU_PER_HR = 8e-3;
@@ -7219,20 +7356,16 @@ function calculateCosmosDbCost(resource, targetProvider, targetRegion) {
7219
7356
  });
7220
7357
  totalMonthly += storageCost;
7221
7358
  }
7222
- return {
7223
- resource_id: resource.id,
7224
- resource_type: resource.type,
7225
- resource_name: resource.name,
7359
+ return makeCostEstimate({
7360
+ resource,
7226
7361
  provider: targetProvider,
7227
7362
  region: targetRegion,
7228
- monthly_cost: totalMonthly,
7229
- yearly_cost: totalMonthly * 12,
7230
- currency: "USD",
7363
+ monthlyCost: totalMonthly,
7231
7364
  breakdown,
7232
7365
  confidence: "medium",
7233
7366
  notes,
7234
- pricing_source: pricingSource
7235
- };
7367
+ pricingSource
7368
+ });
7236
7369
  }
7237
7370
  var AZURE_FUNC_PER_MILLION_EXECUTIONS = 0.2;
7238
7371
  var AZURE_FUNC_GB_SECOND_PRICE = 16e-6;
@@ -7269,20 +7402,16 @@ function calculateAzureFunctionCost(resource, targetProvider, targetRegion) {
7269
7402
  );
7270
7403
  }
7271
7404
  notes.push("Azure Function App on Consumption plan (dedicated plan not priced separately)");
7272
- return {
7273
- resource_id: resource.id,
7274
- resource_type: resource.type,
7275
- resource_name: resource.name,
7405
+ return makeCostEstimate({
7406
+ resource,
7276
7407
  provider: targetProvider,
7277
7408
  region: targetRegion,
7278
- monthly_cost: totalMonthly,
7279
- yearly_cost: totalMonthly * 12,
7280
- currency: "USD",
7409
+ monthlyCost: totalMonthly,
7281
7410
  breakdown,
7282
7411
  confidence: "medium",
7283
7412
  notes,
7284
- pricing_source: "fallback"
7285
- };
7413
+ pricingSource: "fallback"
7414
+ });
7286
7415
  }
7287
7416
  var CLOUD_RUN_VCPU_PER_SECOND = 24e-6;
7288
7417
  var CLOUD_RUN_MEMORY_GIB_PER_SECOND = 25e-7;
@@ -7355,20 +7484,16 @@ function calculateCloudRunCost(resource, targetProvider, targetRegion) {
7355
7484
  );
7356
7485
  }
7357
7486
  notes.push(`Cloud Run: ${vcpus} vCPU, ${memoryGib.toFixed(2)}GiB memory`);
7358
- return {
7359
- resource_id: resource.id,
7360
- resource_type: resource.type,
7361
- resource_name: resource.name,
7487
+ return makeCostEstimate({
7488
+ resource,
7362
7489
  provider: targetProvider,
7363
7490
  region: targetRegion,
7364
- monthly_cost: totalMonthly,
7365
- yearly_cost: totalMonthly * 12,
7366
- currency: "USD",
7491
+ monthlyCost: totalMonthly,
7367
7492
  breakdown,
7368
7493
  confidence: "medium",
7369
7494
  notes,
7370
- pricing_source: "fallback"
7371
- };
7495
+ pricingSource: "fallback"
7496
+ });
7372
7497
  }
7373
7498
  var BIGQUERY_QUERY_PER_TB = 5;
7374
7499
  var BIGQUERY_STORAGE_PER_GB = 0.02;
@@ -7401,24 +7526,20 @@ function calculateBigQueryCost(resource, targetProvider, targetRegion) {
7401
7526
  `Default assumption: ${DEFAULT_BIGQUERY_QUERY_TB}TB queries/month \u2014 provide monthly_query_tb attribute for accuracy`
7402
7527
  );
7403
7528
  }
7404
- if (resource.attributes.location) {
7529
+ if (typeof resource.attributes.location === "string" && resource.attributes.location) {
7405
7530
  notes.push(`BigQuery location: ${resource.attributes.location}`);
7406
7531
  }
7407
7532
  notes.push("BigQuery on-demand pricing; flat-rate/reservations not included");
7408
- return {
7409
- resource_id: resource.id,
7410
- resource_type: resource.type,
7411
- resource_name: resource.name,
7533
+ return makeCostEstimate({
7534
+ resource,
7412
7535
  provider: targetProvider,
7413
7536
  region: targetRegion,
7414
- monthly_cost: totalMonthly,
7415
- yearly_cost: totalMonthly * 12,
7416
- currency: "USD",
7537
+ monthlyCost: totalMonthly,
7417
7538
  breakdown,
7418
7539
  confidence: "medium",
7419
7540
  notes,
7420
- pricing_source: "fallback"
7421
- };
7541
+ pricingSource: "fallback"
7542
+ });
7422
7543
  }
7423
7544
  var GCF_PER_MILLION_INVOCATIONS = 0.4;
7424
7545
  var GCF_GB_SECOND_PRICE = 25e-7;
@@ -7454,114 +7575,38 @@ function calculateGcpFunctionCost(resource, targetProvider, targetRegion) {
7454
7575
  `Default assumption: ${DEFAULT_GCF_INVOCATIONS.toLocaleString()} invocations/month, ${DEFAULT_GCF_DURATION_MS}ms avg, ${DEFAULT_GCF_MEMORY_MB}MB memory`
7455
7576
  );
7456
7577
  }
7457
- if (resource.attributes.runtime) {
7578
+ if (typeof resource.attributes.runtime === "string" && resource.attributes.runtime) {
7458
7579
  notes.push(`Cloud Function runtime: ${resource.attributes.runtime}`);
7459
7580
  }
7460
- return {
7461
- resource_id: resource.id,
7462
- resource_type: resource.type,
7463
- resource_name: resource.name,
7581
+ return makeCostEstimate({
7582
+ resource,
7464
7583
  provider: targetProvider,
7465
7584
  region: targetRegion,
7466
- monthly_cost: totalMonthly,
7467
- yearly_cost: totalMonthly * 12,
7468
- currency: "USD",
7585
+ monthlyCost: totalMonthly,
7469
7586
  breakdown,
7470
7587
  confidence: "medium",
7471
7588
  notes,
7472
- pricing_source: "fallback"
7473
- };
7589
+ pricingSource: "fallback"
7590
+ });
7474
7591
  }
7475
7592
 
7476
7593
  // src/calculator/data-transfer.ts
7477
7594
  var DEFAULT_EGRESS_GB = 100;
7478
- var AWS_EGRESS_PER_GB = 0.09;
7479
- var AZURE_EGRESS_PER_GB = 0.087;
7480
- var GCP_EGRESS_PER_GB = 0.085;
7481
- var AWS_REGION_MULTIPLIERS = {
7482
- "us-east-1": 1,
7483
- "us-east-2": 1,
7484
- "us-west-1": 1,
7485
- "us-west-2": 1,
7486
- "eu-west-1": 1,
7487
- "eu-west-2": 1,
7488
- "eu-central-1": 1,
7489
- "ap-southeast-1": 1.11,
7490
- // $0.10/GB
7491
- "ap-southeast-2": 1.11,
7492
- "ap-northeast-1": 1.11,
7493
- "ap-northeast-2": 1.11,
7494
- "ap-south-1": 1.11,
7495
- "sa-east-1": 1.31,
7496
- // $0.118/GB
7497
- "ca-central-1": 1,
7498
- "eu-north-1": 1,
7499
- "me-south-1": 1.11,
7500
- "af-south-1": 1.16
7501
- // $0.1046/GB
7502
- };
7503
- var AZURE_REGION_MULTIPLIERS = {
7504
- eastus: 1,
7505
- eastus2: 1,
7506
- westus: 1,
7507
- westus2: 1,
7508
- westus3: 1,
7509
- centralus: 1,
7510
- northeurope: 1,
7511
- westeurope: 1,
7512
- uksouth: 1,
7513
- ukwest: 1,
7514
- germanywestcentral: 1,
7515
- francecentral: 1,
7516
- eastasia: 1.16,
7517
- // Zone 2 rate
7518
- southeastasia: 1.16,
7519
- japaneast: 1.16,
7520
- koreacentral: 1.16,
7521
- australiaeast: 1.24,
7522
- // Zone 3 rate
7523
- brazilsouth: 1.31,
7524
- southafricanorth: 1.31
7525
- };
7526
- var GCP_REGION_MULTIPLIERS = {
7527
- "us-central1": 1,
7528
- "us-east1": 1,
7529
- "us-east4": 1,
7530
- "us-west1": 1,
7531
- "us-west2": 1,
7532
- "us-west3": 1,
7533
- "us-west4": 1,
7534
- "europe-west1": 1,
7535
- "europe-west2": 1,
7536
- "europe-west3": 1,
7537
- "europe-west4": 1,
7538
- "europe-west6": 1,
7539
- "europe-north1": 1,
7540
- "northamerica-northeast1": 1,
7541
- "southamerica-east1": 1.59,
7542
- // $0.135/GB
7543
- "asia-east1": 1.41,
7544
- // $0.12/GB (APAC)
7545
- "asia-east2": 1.41,
7546
- "asia-northeast1": 1.41,
7547
- "asia-northeast2": 1.41,
7548
- "asia-northeast3": 1.41,
7549
- "asia-south1": 1.41,
7550
- "asia-southeast1": 1.41,
7551
- "asia-southeast2": 1.41,
7552
- "australia-southeast1": 1.41,
7553
- "australia-southeast2": 1.41
7595
+ var EGRESS_PRICING = {
7596
+ aws: {
7597
+ ratePerGb: 0.09,
7598
+ note: "Data transfer costs cover internet egress only; intra-region and inter-AZ traffic charges are excluded from this estimate"
7599
+ },
7600
+ azure: {
7601
+ ratePerGb: 0.087,
7602
+ note: "Data transfer costs cover internet egress only; intra-region traffic is free on Azure and excluded from this estimate"
7603
+ },
7604
+ gcp: {
7605
+ ratePerGb: 0.085,
7606
+ note: "Data transfer costs cover internet egress only; intra-region and Google network traffic charges are excluded from this estimate"
7607
+ }
7554
7608
  };
7555
- function awsRegionMultiplier(region) {
7556
- return AWS_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
7557
- }
7558
- function azureRegionMultiplier(region) {
7559
- return AZURE_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
7560
- }
7561
- function gcpRegionMultiplier(region) {
7562
- return GCP_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
7563
- }
7564
- async function calculateAwsDataTransferCost(resource, targetRegion) {
7609
+ function calculateEgressCost(resource, provider, targetRegion) {
7565
7610
  const notes = [];
7566
7611
  const breakdown = [];
7567
7612
  const egressGb = resource.attributes.monthly_egress_gb ?? DEFAULT_EGRESS_GB;
@@ -7570,8 +7615,8 @@ async function calculateAwsDataTransferCost(resource, targetRegion) {
7570
7615
  `Data transfer cost estimated at ${egressGb} GB/month egress; actual costs depend on traffic volume and destination`
7571
7616
  );
7572
7617
  }
7573
- const multiplier = awsRegionMultiplier(targetRegion);
7574
- const ratePerGb = AWS_EGRESS_PER_GB * multiplier;
7618
+ const pricing = EGRESS_PRICING[provider];
7619
+ const ratePerGb = pricing.ratePerGb * regionMultiplier(provider, targetRegion);
7575
7620
  const totalMonthly = ratePerGb * egressGb;
7576
7621
  breakdown.push({
7577
7622
  description: `Internet egress (${egressGb} GB/month, estimated)`,
@@ -7580,97 +7625,26 @@ async function calculateAwsDataTransferCost(resource, targetRegion) {
7580
7625
  unit_price: ratePerGb,
7581
7626
  monthly_cost: totalMonthly
7582
7627
  });
7583
- notes.push(
7584
- "Data transfer costs cover internet egress only; intra-region and inter-AZ traffic charges are excluded from this estimate"
7585
- );
7586
- return {
7587
- resource_id: resource.id,
7588
- resource_type: resource.type,
7589
- resource_name: resource.name,
7590
- provider: "aws",
7628
+ notes.push(pricing.note);
7629
+ return makeCostEstimate({
7630
+ resource,
7631
+ provider,
7591
7632
  region: targetRegion,
7592
- monthly_cost: Math.round(totalMonthly * 100) / 100,
7593
- yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
7594
- currency: "USD",
7633
+ monthlyCost: totalMonthly,
7595
7634
  breakdown,
7596
7635
  confidence: "low",
7597
7636
  notes,
7598
- pricing_source: "fallback"
7599
- };
7600
- }
7601
- async function calculateAzureDataTransferCost(resource, targetRegion) {
7602
- const notes = [];
7603
- const breakdown = [];
7604
- const egressGb = resource.attributes.monthly_egress_gb ?? DEFAULT_EGRESS_GB;
7605
- if (!resource.attributes.monthly_egress_gb || resource.attributes.monthly_egress_gb <= 0) {
7606
- notes.push(
7607
- `Data transfer cost estimated at ${egressGb} GB/month egress; actual costs depend on traffic volume and destination`
7608
- );
7609
- }
7610
- const multiplier = azureRegionMultiplier(targetRegion);
7611
- const ratePerGb = AZURE_EGRESS_PER_GB * multiplier;
7612
- const totalMonthly = ratePerGb * egressGb;
7613
- breakdown.push({
7614
- description: `Internet egress (${egressGb} GB/month, estimated)`,
7615
- unit: "GB",
7616
- quantity: egressGb,
7617
- unit_price: ratePerGb,
7618
- monthly_cost: totalMonthly
7637
+ pricingSource: "fallback"
7619
7638
  });
7620
- notes.push(
7621
- "Data transfer costs cover internet egress only; intra-region traffic is free on Azure and excluded from this estimate"
7622
- );
7623
- return {
7624
- resource_id: resource.id,
7625
- resource_type: resource.type,
7626
- resource_name: resource.name,
7627
- provider: "azure",
7628
- region: targetRegion,
7629
- monthly_cost: Math.round(totalMonthly * 100) / 100,
7630
- yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
7631
- currency: "USD",
7632
- breakdown,
7633
- confidence: "low",
7634
- notes,
7635
- pricing_source: "fallback"
7636
- };
7637
7639
  }
7638
- async function calculateGcpDataTransferCost(resource, targetRegion) {
7639
- const notes = [];
7640
- const breakdown = [];
7641
- const egressGb = resource.attributes.monthly_egress_gb ?? DEFAULT_EGRESS_GB;
7642
- if (!resource.attributes.monthly_egress_gb || resource.attributes.monthly_egress_gb <= 0) {
7643
- notes.push(
7644
- `Data transfer cost estimated at ${egressGb} GB/month egress; actual costs depend on traffic volume and destination`
7645
- );
7646
- }
7647
- const multiplier = gcpRegionMultiplier(targetRegion);
7648
- const ratePerGb = GCP_EGRESS_PER_GB * multiplier;
7649
- const totalMonthly = ratePerGb * egressGb;
7650
- breakdown.push({
7651
- description: `Internet egress (${egressGb} GB/month, estimated)`,
7652
- unit: "GB",
7653
- quantity: egressGb,
7654
- unit_price: ratePerGb,
7655
- monthly_cost: totalMonthly
7656
- });
7657
- notes.push(
7658
- "Data transfer costs cover internet egress only; intra-region and Google network traffic charges are excluded from this estimate"
7659
- );
7660
- return {
7661
- resource_id: resource.id,
7662
- resource_type: resource.type,
7663
- resource_name: resource.name,
7664
- provider: "gcp",
7665
- region: targetRegion,
7666
- monthly_cost: Math.round(totalMonthly * 100) / 100,
7667
- yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
7668
- currency: "USD",
7669
- breakdown,
7670
- confidence: "low",
7671
- notes,
7672
- pricing_source: "fallback"
7673
- };
7640
+ function calculateAwsDataTransferCost(resource, targetRegion) {
7641
+ return calculateEgressCost(resource, "aws", targetRegion);
7642
+ }
7643
+ function calculateAzureDataTransferCost(resource, targetRegion) {
7644
+ return calculateEgressCost(resource, "azure", targetRegion);
7645
+ }
7646
+ function calculateGcpDataTransferCost(resource, targetRegion) {
7647
+ return calculateEgressCost(resource, "gcp", targetRegion);
7674
7648
  }
7675
7649
 
7676
7650
  // src/calculator/container-registry.ts
@@ -7721,20 +7695,16 @@ function calculateContainerRegistryCost(resource, targetProvider, targetRegion)
7721
7695
  monthly_cost: storageCost
7722
7696
  });
7723
7697
  const totalMonthly = storageCost;
7724
- return {
7725
- resource_id: resource.id,
7726
- resource_type: resource.type,
7727
- resource_name: resource.name,
7698
+ return makeCostEstimate({
7699
+ resource,
7728
7700
  provider: targetProvider,
7729
7701
  region: targetRegion,
7730
- monthly_cost: totalMonthly,
7731
- yearly_cost: totalMonthly * 12,
7732
- currency: "USD",
7702
+ monthlyCost: totalMonthly,
7733
7703
  breakdown,
7734
7704
  confidence: hasExplicitStorage ? "high" : "medium",
7735
7705
  notes,
7736
- pricing_source: "bundled"
7737
- };
7706
+ pricingSource: "bundled"
7707
+ });
7738
7708
  }
7739
7709
 
7740
7710
  // src/calculator/secrets.ts
@@ -7829,20 +7799,16 @@ function calculateSecretsCost(resource, targetProvider, targetRegion) {
7829
7799
  });
7830
7800
  totalMonthly += accessOpFee;
7831
7801
  }
7832
- return {
7833
- resource_id: resource.id,
7834
- resource_type: resource.type,
7835
- resource_name: resource.name,
7802
+ return makeCostEstimate({
7803
+ resource,
7836
7804
  provider: targetProvider,
7837
7805
  region: targetRegion,
7838
- monthly_cost: totalMonthly,
7839
- yearly_cost: totalMonthly * 12,
7840
- currency: "USD",
7806
+ monthlyCost: totalMonthly,
7841
7807
  breakdown,
7842
7808
  confidence: hasExplicitSecretCount ? "high" : "medium",
7843
7809
  notes,
7844
- pricing_source: "bundled"
7845
- };
7810
+ pricingSource: "bundled"
7811
+ });
7846
7812
  }
7847
7813
 
7848
7814
  // src/calculator/dns.ts
@@ -7896,20 +7862,16 @@ function calculateDnsCost(resource, targetProvider, targetRegion) {
7896
7862
  monthly_cost: queryCost
7897
7863
  });
7898
7864
  const totalMonthly = zonePrice + queryCost;
7899
- return {
7900
- resource_id: resource.id,
7901
- resource_type: resource.type,
7902
- resource_name: resource.name,
7865
+ return makeCostEstimate({
7866
+ resource,
7903
7867
  provider: targetProvider,
7904
7868
  region: targetRegion,
7905
- monthly_cost: totalMonthly,
7906
- yearly_cost: totalMonthly * 12,
7907
- currency: "USD",
7869
+ monthlyCost: totalMonthly,
7908
7870
  breakdown,
7909
7871
  confidence: hasExplicitQueries ? "high" : "medium",
7910
7872
  notes,
7911
- pricing_source: "bundled"
7912
- };
7873
+ pricingSource: "bundled"
7874
+ });
7913
7875
  }
7914
7876
 
7915
7877
  // src/calculator/api-gateway.ts
@@ -8030,20 +7992,16 @@ function calculateApiGatewayCost(resource, targetProvider, targetRegion) {
8030
7992
  );
8031
7993
  }
8032
7994
  }
8033
- return {
8034
- resource_id: resource.id,
8035
- resource_type: resource.type,
8036
- resource_name: resource.name,
7995
+ return makeCostEstimate({
7996
+ resource,
8037
7997
  provider: targetProvider,
8038
7998
  region: targetRegion,
8039
- monthly_cost: totalMonthly,
8040
- yearly_cost: totalMonthly * 12,
8041
- currency: "USD",
7999
+ monthlyCost: totalMonthly,
8042
8000
  breakdown,
8043
8001
  confidence: hasExplicitRequests ? "high" : "medium",
8044
8002
  notes,
8045
- pricing_source: "fallback"
8046
- };
8003
+ pricingSource: "fallback"
8004
+ });
8047
8005
  }
8048
8006
 
8049
8007
  // src/calculator/waf.ts
@@ -8157,20 +8115,16 @@ function calculateWafCost(resource, targetProvider, targetRegion) {
8157
8115
  totalMonthly += requestCost;
8158
8116
  notes.push("GCP equivalent: Cloud Armor security policy");
8159
8117
  }
8160
- return {
8161
- resource_id: resource.id,
8162
- resource_type: resource.type,
8163
- resource_name: resource.name,
8118
+ return makeCostEstimate({
8119
+ resource,
8164
8120
  provider: targetProvider,
8165
8121
  region: targetRegion,
8166
- monthly_cost: totalMonthly,
8167
- yearly_cost: totalMonthly * 12,
8168
- currency: "USD",
8122
+ monthlyCost: totalMonthly,
8169
8123
  breakdown,
8170
8124
  confidence: hasExplicitRules && hasExplicitRequests ? "high" : "medium",
8171
8125
  notes,
8172
- pricing_source: "fallback"
8173
- };
8126
+ pricingSource: "fallback"
8127
+ });
8174
8128
  }
8175
8129
 
8176
8130
  // src/calculator/search.ts
@@ -8265,20 +8219,16 @@ function calculateSearchCost(resource, targetProvider, targetRegion) {
8265
8219
  "GCP has no direct managed OpenSearch service; estimate based on comparable compute cost"
8266
8220
  );
8267
8221
  }
8268
- return {
8269
- resource_id: resource.id,
8270
- resource_type: resource.type,
8271
- resource_name: resource.name,
8222
+ return makeCostEstimate({
8223
+ resource,
8272
8224
  provider: targetProvider,
8273
8225
  region: targetRegion,
8274
- monthly_cost: totalMonthly,
8275
- yearly_cost: totalMonthly * 12,
8276
- currency: "USD",
8226
+ monthlyCost: totalMonthly,
8277
8227
  breakdown,
8278
8228
  confidence: targetProvider === "aws" ? "medium" : "low",
8279
8229
  notes,
8280
- pricing_source: "fallback"
8281
- };
8230
+ pricingSource: "fallback"
8231
+ });
8282
8232
  }
8283
8233
 
8284
8234
  // src/calculator/messaging.ts
@@ -8411,20 +8361,16 @@ function calculateMessagingCost(resource, targetProvider, targetRegion) {
8411
8361
  );
8412
8362
  }
8413
8363
  }
8414
- return {
8415
- resource_id: resource.id,
8416
- resource_type: resource.type,
8417
- resource_name: resource.name,
8364
+ return makeCostEstimate({
8365
+ resource,
8418
8366
  provider: targetProvider,
8419
8367
  region: targetRegion,
8420
- monthly_cost: totalMonthly,
8421
- yearly_cost: totalMonthly * 12,
8422
- currency: "USD",
8368
+ monthlyCost: totalMonthly,
8423
8369
  breakdown,
8424
8370
  confidence: hasExplicitMessages ? "high" : "medium",
8425
8371
  notes,
8426
- pricing_source: "fallback"
8427
- };
8372
+ pricingSource: "fallback"
8373
+ });
8428
8374
  }
8429
8375
  function calculateMqBrokerCost(resource, targetProvider, targetRegion) {
8430
8376
  const notes = [];
@@ -8461,20 +8407,16 @@ function calculateMqBrokerCost(resource, targetProvider, targetRegion) {
8461
8407
  unit_price: AWS_MQ_STORAGE_PER_GB,
8462
8408
  monthly_cost: storageCost
8463
8409
  });
8464
- return {
8465
- resource_id: resource.id,
8466
- resource_type: resource.type,
8467
- resource_name: resource.name,
8410
+ return makeCostEstimate({
8411
+ resource,
8468
8412
  provider: targetProvider,
8469
8413
  region: targetRegion,
8470
- monthly_cost: totalMonthly,
8471
- yearly_cost: totalMonthly * 12,
8472
- currency: "USD",
8414
+ monthlyCost: totalMonthly,
8473
8415
  breakdown,
8474
8416
  confidence: hourlyPrice !== void 0 ? "medium" : "low",
8475
8417
  notes,
8476
- pricing_source: "fallback"
8477
- };
8418
+ pricingSource: "fallback"
8419
+ });
8478
8420
  }
8479
8421
  function calculateEventHubCost(resource, targetProvider, targetRegion) {
8480
8422
  const notes = [];
@@ -8494,20 +8436,16 @@ function calculateEventHubCost(resource, targetProvider, targetRegion) {
8494
8436
  notes.push(
8495
8437
  `Azure Event Hubs: ${sku} tier, ${capacity} ${unitLabel}(s) \u2014 $${hourlyPrice}/${unitLabel}/hr`
8496
8438
  );
8497
- return {
8498
- resource_id: resource.id,
8499
- resource_type: resource.type,
8500
- resource_name: resource.name,
8439
+ return makeCostEstimate({
8440
+ resource,
8501
8441
  provider: targetProvider,
8502
8442
  region: targetRegion,
8503
- monthly_cost: totalMonthly,
8504
- yearly_cost: totalMonthly * 12,
8505
- currency: "USD",
8443
+ monthlyCost: totalMonthly,
8506
8444
  breakdown,
8507
8445
  confidence: "medium",
8508
8446
  notes,
8509
- pricing_source: "fallback"
8510
- };
8447
+ pricingSource: "fallback"
8448
+ });
8511
8449
  }
8512
8450
  function calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion) {
8513
8451
  const notes = [];
@@ -8530,20 +8468,16 @@ function calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion)
8530
8468
  notes.push(
8531
8469
  `GCP Pub/Sub subscription: $${GCP_PUBSUB_SUBSCRIPTION_PER_MILLION_OPS}/million ack operations`
8532
8470
  );
8533
- return {
8534
- resource_id: resource.id,
8535
- resource_type: resource.type,
8536
- resource_name: resource.name,
8471
+ return makeCostEstimate({
8472
+ resource,
8537
8473
  provider: targetProvider,
8538
8474
  region: targetRegion,
8539
- monthly_cost: opsCost,
8540
- yearly_cost: opsCost * 12,
8541
- currency: "USD",
8475
+ monthlyCost: opsCost,
8542
8476
  breakdown,
8543
8477
  confidence: hasExplicitMessages ? "high" : "medium",
8544
8478
  notes,
8545
- pricing_source: "fallback"
8546
- };
8479
+ pricingSource: "fallback"
8480
+ });
8547
8481
  }
8548
8482
 
8549
8483
  // src/calculator/ml-ai.ts
@@ -8661,20 +8595,16 @@ function calculateMlAiCost(resource, targetProvider, targetRegion) {
8661
8595
  "Azure ML endpoint pricing not yet supported \u2014 consider Azure Machine Learning compute instance costs"
8662
8596
  );
8663
8597
  }
8664
- return {
8665
- resource_id: resource.id,
8666
- resource_type: resource.type,
8667
- resource_name: resource.name,
8598
+ return makeCostEstimate({
8599
+ resource,
8668
8600
  provider: targetProvider,
8669
8601
  region: targetRegion,
8670
- monthly_cost: totalMonthly,
8671
- yearly_cost: totalMonthly * 12,
8672
- currency: "USD",
8602
+ monthlyCost: totalMonthly,
8673
8603
  breakdown,
8674
8604
  confidence: "low",
8675
8605
  notes,
8676
- pricing_source: "fallback"
8677
- };
8606
+ pricingSource: "fallback"
8607
+ });
8678
8608
  }
8679
8609
 
8680
8610
  // src/calculator/cost-engine.ts
@@ -8971,20 +8901,16 @@ var CostEngine = class {
8971
8901
  return calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion);
8972
8902
  }
8973
8903
  logger.warn("CostEngine: unsupported resource type", { type, targetProvider });
8974
- return {
8975
- resource_id: resource.id,
8976
- resource_type: type,
8977
- resource_name: resource.name,
8904
+ return makeCostEstimate({
8905
+ resource,
8978
8906
  provider: targetProvider,
8979
8907
  region: targetRegion,
8980
- monthly_cost: 0,
8981
- yearly_cost: 0,
8982
- currency: "USD",
8908
+ monthlyCost: 0,
8983
8909
  breakdown: [],
8984
8910
  confidence: "low",
8985
8911
  notes: [`Resource type "${type}" is not yet supported by the cost engine`],
8986
- pricing_source: "fallback"
8987
- };
8912
+ pricingSource: "fallback"
8913
+ });
8988
8914
  }
8989
8915
  /**
8990
8916
  * Calculates costs for all resources and aggregates them into a CostBreakdown.
@@ -9050,7 +8976,7 @@ var CostEngine = class {
9050
8976
  name: `data-transfer-${estimate.provider}-${estimate.region}`,
9051
8977
  provider: estimate.provider,
9052
8978
  region: estimate.region,
9053
- attributes: { monthly_egress_gb: 100 },
8979
+ attributes: { monthly_egress_gb: DEFAULT_EGRESS_GB },
9054
8980
  tags: {},
9055
8981
  source_file: "synthetic"
9056
8982
  }
@@ -9078,6 +9004,12 @@ var CostEngine = class {
9078
9004
  }
9079
9005
  }
9080
9006
  const totalMonthly = estimates.reduce((sum, e) => sum + e.monthly_cost, 0);
9007
+ const estimatedEgressMonthly = estimates.filter((e) => DATA_TRANSFER_TYPES.has(e.resource_type)).reduce((sum, e) => sum + e.monthly_cost, 0);
9008
+ if (estimatedEgressMonthly > 0) {
9009
+ warnings.push(
9010
+ `total_monthly includes a synthetic estimate of ${DEFAULT_EGRESS_GB} GB/month internet egress ($${(Math.round(estimatedEgressMonthly * 100) / 100).toFixed(2)}/mo, USD) \u2014 see estimated_egress_monthly. Set CLOUDCOST_INCLUDE_DATA_TRANSFER=false to exclude it.`
9011
+ );
9012
+ }
9081
9013
  const budgetWarnings = this.evaluateBudget(totalMonthly, estimates);
9082
9014
  return {
9083
9015
  provider: targetProvider,
@@ -9091,6 +9023,9 @@ var CostEngine = class {
9091
9023
  by_resource: estimates,
9092
9024
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
9093
9025
  warnings,
9026
+ ...estimatedEgressMonthly > 0 && {
9027
+ estimated_egress_monthly: Math.round(estimatedEgressMonthly * 100) / 100
9028
+ },
9094
9029
  ...budgetWarnings.length > 0 && { budget_warnings: budgetWarnings }
9095
9030
  };
9096
9031
  }
@@ -9135,84 +9070,15 @@ var CostEngine = class {
9135
9070
  }
9136
9071
  };
9137
9072
 
9138
- // src/currency.ts
9139
- var SUPPORTED_CURRENCIES = [
9140
- "USD",
9141
- "EUR",
9142
- "GBP",
9143
- "JPY",
9144
- "CAD",
9145
- "AUD",
9146
- "INR",
9147
- "BRL"
9148
- ];
9149
- var EXCHANGE_RATES = {
9150
- USD: 1,
9151
- EUR: 0.92,
9152
- GBP: 0.79,
9153
- JPY: 149.5,
9154
- CAD: 1.36,
9155
- AUD: 1.53,
9156
- INR: 83.1,
9157
- BRL: 4.97
9158
- };
9159
- function convertCurrency(amountUsd, targetCurrency) {
9160
- const rate = EXCHANGE_RATES[targetCurrency];
9161
- if (rate === void 0 || rate === 1) {
9162
- return amountUsd;
9163
- }
9164
- return amountUsd * rate;
9165
- }
9166
- function convertLineItem(item, currency) {
9167
- return {
9168
- ...item,
9169
- unit_price: convertCurrency(item.unit_price, currency),
9170
- monthly_cost: convertCurrency(item.monthly_cost, currency)
9171
- };
9172
- }
9173
- function convertCostEstimate(estimate, currency) {
9174
- return {
9175
- ...estimate,
9176
- monthly_cost: convertCurrency(estimate.monthly_cost, currency),
9177
- yearly_cost: convertCurrency(estimate.yearly_cost, currency),
9178
- currency,
9179
- breakdown: estimate.breakdown.map((item) => convertLineItem(item, currency))
9180
- };
9181
- }
9182
- function convertBreakdownCurrency(breakdown, currency) {
9183
- const original_usd = {
9184
- total_monthly: breakdown.total_monthly,
9185
- total_yearly: breakdown.total_yearly
9186
- };
9187
- const convertedByService = {};
9188
- for (const [service, amount] of Object.entries(breakdown.by_service)) {
9189
- convertedByService[service] = convertCurrency(amount, currency);
9190
- }
9191
- return {
9192
- ...breakdown,
9193
- total_monthly: convertCurrency(breakdown.total_monthly, currency),
9194
- total_yearly: convertCurrency(breakdown.total_yearly, currency),
9195
- currency,
9196
- by_service: convertedByService,
9197
- by_resource: breakdown.by_resource.map((est) => convertCostEstimate(est, currency)),
9198
- original_usd
9199
- };
9200
- }
9201
-
9202
9073
  // src/tools/estimate-cost.ts
9203
- var estimateCostSchema = z3.object({
9204
- files: z3.array(
9205
- z3.object({
9206
- path: filePathSchema.describe("File path"),
9207
- content: fileContentSchema.describe("File content (HCL)")
9208
- })
9209
- ).max(2e3, "files array exceeds 2000 entries"),
9210
- tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
9211
- provider: z3.enum(["aws", "azure", "gcp"]).describe("Target cloud provider to estimate costs for"),
9212
- region: z3.string().optional().describe(
9074
+ var estimateCostSchema = z4.object({
9075
+ files: iacFilesSchema,
9076
+ tfvars: tfvarsField,
9077
+ provider: providerEnum.describe("Target cloud provider to estimate costs for"),
9078
+ region: shortStringSchema.optional().describe(
9213
9079
  "Target region for pricing lookup. Defaults to the source region mapped to the target provider."
9214
9080
  ),
9215
- currency: z3.enum(SUPPORTED_CURRENCIES).optional().default("USD").describe("Output currency for cost estimates. Defaults to USD.")
9081
+ currency: currencyField
9216
9082
  });
9217
9083
  async function estimateCost(params, pricingEngine, config) {
9218
9084
  const inventory = await parseTerraform(params.files, params.tfvars);
@@ -9249,12 +9115,13 @@ async function estimateCost(params, pricingEngine, config) {
9249
9115
  return {
9250
9116
  ...convertedRest,
9251
9117
  by_resource: byResource,
9252
- warnings: allWarnings
9118
+ warnings: allWarnings,
9119
+ pricing_metadata: getPricingMetadata(targetProvider)
9253
9120
  };
9254
9121
  }
9255
9122
 
9256
9123
  // src/tools/compare-providers.ts
9257
- import { z as z4 } from "zod";
9124
+ import { z as z5 } from "zod";
9258
9125
 
9259
9126
  // src/calculator/reserved.ts
9260
9127
  var DISCOUNT_RATES = {
@@ -9802,15 +9669,23 @@ function generateJsonReport(comparison, resources, monthlyHours = 730, parseWarn
9802
9669
  return JSON.stringify(output);
9803
9670
  }
9804
9671
 
9805
- // src/reporting/csv-report.ts
9806
- var PROVIDERS = ["aws", "azure", "gcp"];
9672
+ // src/reporting/csv-escape.ts
9807
9673
  function csvEscape(value) {
9808
- const str2 = String(value);
9809
- if (str2.includes(",") || str2.includes("\n") || str2.includes('"')) {
9810
- return `"${str2.replace(/"/g, '""')}"`;
9674
+ if (typeof value === "number") {
9675
+ return Number.isFinite(value) ? String(value) : "";
9676
+ }
9677
+ let str2 = String(value);
9678
+ if (/^[=+\-@\t\r]/.test(str2)) {
9679
+ str2 = "'" + str2;
9680
+ }
9681
+ if (/["\n\r,]/.test(str2)) {
9682
+ str2 = `"${str2.replace(/"/g, '""')}"`;
9811
9683
  }
9812
9684
  return str2;
9813
9685
  }
9686
+
9687
+ // src/reporting/csv-report.ts
9688
+ var PROVIDERS = ["aws", "azure", "gcp"];
9814
9689
  function generateCsvReport(comparison, resources, options = {}) {
9815
9690
  const lines = [];
9816
9691
  const tagKey = options.group_by === "tag" ? options.group_by_tag_key : void 0;
@@ -9971,15 +9846,8 @@ function billingPeriodEnd() {
9971
9846
  const now = /* @__PURE__ */ new Date();
9972
9847
  return new Date(now.getFullYear(), now.getMonth() + 1, 1).toISOString().split("T")[0];
9973
9848
  }
9974
- function csvEscape2(value) {
9975
- const str2 = String(value);
9976
- if (str2.includes(",") || str2.includes("\n") || str2.includes('"')) {
9977
- return `"${str2.replace(/"/g, '""')}"`;
9978
- }
9979
- return str2;
9980
- }
9981
9849
  function buildRow(values) {
9982
- return values.map(csvEscape2).join(",");
9850
+ return values.map(csvEscape).join(",");
9983
9851
  }
9984
9852
  function generateFocusReport(comparison, resources) {
9985
9853
  const lines = [];
@@ -10046,17 +9914,12 @@ function generateFocusReport(comparison, resources) {
10046
9914
  }
10047
9915
 
10048
9916
  // src/tools/compare-providers.ts
10049
- var compareProvidersSchema = z4.object({
10050
- files: z4.array(
10051
- z4.object({
10052
- path: filePathSchema.describe("File path"),
10053
- content: fileContentSchema.describe("File content (HCL)")
10054
- })
10055
- ).max(2e3, "files array exceeds 2000 entries"),
10056
- tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
10057
- format: z4.enum(["markdown", "json", "csv", "focus"]).default("markdown").describe("Output report format"),
10058
- providers: z4.array(z4.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Cloud providers to include in the comparison"),
10059
- currency: z4.enum(SUPPORTED_CURRENCIES).optional().default("USD").describe("Output currency for cost estimates. Defaults to USD.")
9917
+ var compareProvidersSchema = z5.object({
9918
+ files: iacFilesSchema,
9919
+ tfvars: tfvarsField,
9920
+ format: z5.enum(["markdown", "json", "csv", "focus"]).default("markdown").describe("Output report format"),
9921
+ providers: z5.array(providerEnum).default(["aws", "azure", "gcp"]).describe("Cloud providers to include in the comparison"),
9922
+ currency: currencyField
10060
9923
  });
10061
9924
  async function compareProviders(params, pricingEngine, config) {
10062
9925
  const inventory = await parseTerraform(params.files, params.tfvars);
@@ -10112,11 +9975,13 @@ async function compareProviders(params, pricingEngine, config) {
10112
9975
  break;
10113
9976
  }
10114
9977
  const dedupedWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...allWarnings])];
9978
+ const pricingMetadata = Object.fromEntries(providerList.map((p) => [p, getPricingMetadata(p)]));
10115
9979
  if (params.format === "json") {
10116
9980
  return {
10117
9981
  report,
10118
9982
  format: params.format,
10119
- warnings: dedupedWarnings
9983
+ warnings: dedupedWarnings,
9984
+ pricing_metadata: pricingMetadata
10120
9985
  };
10121
9986
  }
10122
9987
  const comparisonSummary = {
@@ -10133,21 +9998,17 @@ async function compareProviders(params, pricingEngine, config) {
10133
9998
  report,
10134
9999
  format: params.format,
10135
10000
  comparison: comparisonSummary,
10136
- warnings: dedupedWarnings
10001
+ warnings: dedupedWarnings,
10002
+ pricing_metadata: pricingMetadata
10137
10003
  };
10138
10004
  }
10139
10005
 
10140
10006
  // src/tools/optimize-cost.ts
10141
- import { z as z5 } from "zod";
10142
- var optimizeCostSchema = z5.object({
10143
- files: z5.array(
10144
- z5.object({
10145
- path: filePathSchema.describe("File path"),
10146
- content: fileContentSchema.describe("File content (HCL)")
10147
- })
10148
- ).max(2e3, "files array exceeds 2000 entries"),
10149
- tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
10150
- providers: z5.array(z5.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Providers to calculate costs against for cross-provider recommendations")
10007
+ import { z as z6 } from "zod";
10008
+ var optimizeCostSchema = z6.object({
10009
+ files: iacFilesSchema,
10010
+ tfvars: tfvarsField,
10011
+ providers: z6.array(providerEnum).default(["aws", "azure", "gcp"]).describe("Providers to calculate costs against for cross-provider recommendations")
10151
10012
  });
10152
10013
  async function optimizeCost(params, pricingEngine, config) {
10153
10014
  const inventory = await parseTerraform(params.files, params.tfvars);
@@ -10183,28 +10044,23 @@ async function optimizeCost(params, pricingEngine, config) {
10183
10044
  }
10184
10045
 
10185
10046
  // src/tools/what-if.ts
10186
- import { z as z6 } from "zod";
10187
- var whatIfSchema = z6.object({
10188
- files: z6.array(
10189
- z6.object({
10190
- path: filePathSchema.describe("File path"),
10191
- content: fileContentSchema.describe("File content (HCL)")
10192
- })
10193
- ).max(2e3, "files array exceeds 2000 entries").describe("Terraform HCL files to analyse"),
10194
- changes: z6.array(
10195
- z6.object({
10047
+ import { z as z7 } from "zod";
10048
+ var whatIfSchema = z7.object({
10049
+ files: iacFilesSchema.describe("Terraform HCL files to analyse"),
10050
+ changes: z7.array(
10051
+ z7.object({
10196
10052
  resource_id: shortStringSchema.describe(
10197
10053
  "The resource ID to modify (e.g. aws_instance.web)"
10198
10054
  ),
10199
10055
  attribute: shortStringSchema.describe(
10200
10056
  "The attribute name to change (e.g. instance_type, storage_size_gb)"
10201
10057
  ),
10202
- new_value: z6.union([shortStringSchema, z6.number()]).describe("The new value for the attribute")
10058
+ new_value: z7.union([shortStringSchema, z7.number()]).describe("The new value for the attribute")
10203
10059
  })
10204
10060
  ).max(200, "changes array exceeds 200 entries").describe("List of attribute changes to simulate"),
10205
- provider: z6.enum(["aws", "azure", "gcp"]).optional().describe("Target cloud provider. Defaults to auto-detecting from the files"),
10206
- region: z6.string().optional().describe("Target region for pricing. Defaults to the region detected from provider blocks"),
10207
- tfvars: z6.string().optional().describe("Contents of a terraform.tfvars file for variable resolution")
10061
+ provider: providerEnum.optional().describe("Target cloud provider. Defaults to auto-detecting from the files"),
10062
+ region: shortStringSchema.optional().describe("Target region for pricing. Defaults to the region detected from provider blocks"),
10063
+ tfvars: tfvarsSchema.optional().describe("Contents of a terraform.tfvars file for variable resolution")
10208
10064
  });
10209
10065
  function cloneResources(resources) {
10210
10066
  return JSON.parse(JSON.stringify(resources));
@@ -10300,30 +10156,34 @@ async function whatIf(params, pricingEngine, config) {
10300
10156
  }
10301
10157
 
10302
10158
  export {
10303
- loadConfig,
10304
10159
  setLogLevel,
10305
10160
  logger,
10161
+ loadConfig,
10162
+ SUPPORTED_CURRENCIES,
10163
+ convertCurrency,
10164
+ getExchangeRateInfo,
10165
+ convertBreakdownCurrency,
10306
10166
  PricingCache,
10307
10167
  PricingEngine,
10308
10168
  sanitizeForMessage,
10169
+ coerceToString,
10309
10170
  str,
10310
10171
  num,
10311
10172
  bool,
10312
10173
  safeJsonParse,
10313
10174
  parseTerraform,
10314
- filePathSchema,
10315
- fileContentSchema,
10316
- tfvarsSchema,
10317
10175
  planJsonSchema,
10318
10176
  stateJsonSchema,
10177
+ shortStringSchema,
10178
+ providerEnum,
10179
+ iacFilesSchema,
10180
+ tfvarsField,
10181
+ currencyField,
10319
10182
  analyzeTerraformSchema,
10320
10183
  analyzeTerraform,
10321
10184
  mapRegion,
10322
10185
  mapInstance,
10323
10186
  CostEngine,
10324
- SUPPORTED_CURRENCIES,
10325
- convertCurrency,
10326
- convertBreakdownCurrency,
10327
10187
  estimateCostSchema,
10328
10188
  estimateCost,
10329
10189
  compareProvidersSchema,
@@ -10333,4 +10193,4 @@ export {
10333
10193
  whatIfSchema,
10334
10194
  whatIf
10335
10195
  };
10336
- //# sourceMappingURL=chunk-6O2Y6MKU.js.map
10196
+ //# sourceMappingURL=chunk-CGMU4ETN.js.map