@rawdash/connector-aws-cost 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1530 @@
1
+ // ../aws-shared/dist/index.js
2
+ import { z } from "zod";
3
+ import { z as z2 } from "zod";
4
+ import { z as z3 } from "zod";
5
+ import { z as z4 } from "zod";
6
+ import { z as z5 } from "zod";
7
+ var encoder = new TextEncoder();
8
+ var ALGORITHM = "AWS4-HMAC-SHA256";
9
+ function u8(data) {
10
+ return new Uint8Array(encoder.encode(data));
11
+ }
12
+ function toHex(buffer) {
13
+ const bytes = new Uint8Array(buffer);
14
+ let hex = "";
15
+ for (let i = 0; i < bytes.length; i++) {
16
+ hex += bytes[i].toString(16).padStart(2, "0");
17
+ }
18
+ return hex;
19
+ }
20
+ async function sha256Hex(data) {
21
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", u8(data));
22
+ return toHex(digest);
23
+ }
24
+ async function hmac(key, data) {
25
+ const cryptoKey = await globalThis.crypto.subtle.importKey(
26
+ "raw",
27
+ key,
28
+ { name: "HMAC", hash: "SHA-256" },
29
+ false,
30
+ ["sign"]
31
+ );
32
+ return globalThis.crypto.subtle.sign("HMAC", cryptoKey, u8(data));
33
+ }
34
+ async function deriveSigningKey(secretAccessKey, dateStamp, region, service) {
35
+ const kDate = await hmac(u8(`AWS4${secretAccessKey}`), dateStamp);
36
+ const kRegion = await hmac(kDate, region);
37
+ const kService = await hmac(kRegion, service);
38
+ return hmac(kService, "aws4_request");
39
+ }
40
+ function formatAmzDate(date) {
41
+ const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
42
+ return { amzDate, dateStamp: amzDate.slice(0, 8) };
43
+ }
44
+ async function createAuthorizationHeader(params) {
45
+ const lowerHeaders = {};
46
+ for (const [key, value] of Object.entries(params.headers)) {
47
+ lowerHeaders[key.toLowerCase()] = value.trim().replace(/\s+/g, " ");
48
+ }
49
+ const sortedNames = Object.keys(lowerHeaders).sort();
50
+ const canonicalHeaders = sortedNames.map((name) => `${name}:${lowerHeaders[name]}
51
+ `).join("");
52
+ const signedHeaders = sortedNames.join(";");
53
+ const canonicalRequest = [
54
+ params.method,
55
+ params.path,
56
+ params.query,
57
+ canonicalHeaders,
58
+ signedHeaders,
59
+ params.payloadHash
60
+ ].join("\n");
61
+ const credentialScope = `${params.dateStamp}/${params.region}/${params.service}/aws4_request`;
62
+ const stringToSign = [
63
+ ALGORITHM,
64
+ params.amzDate,
65
+ credentialScope,
66
+ await sha256Hex(canonicalRequest)
67
+ ].join("\n");
68
+ const signingKey = await deriveSigningKey(
69
+ params.secretAccessKey,
70
+ params.dateStamp,
71
+ params.region,
72
+ params.service
73
+ );
74
+ const signature = toHex(await hmac(signingKey, stringToSign));
75
+ return `${ALGORITHM} Credential=${params.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
76
+ }
77
+ function decodeEntities(value) {
78
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
79
+ }
80
+ function firstInner(xml, tag) {
81
+ const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82
+ const open = new RegExp(`<${escapedTag}(?:\\s[^>]*)?>`).exec(xml);
83
+ if (!open) {
84
+ return new RegExp(`<${escapedTag}\\s*/>`).test(xml) ? "" : null;
85
+ }
86
+ const start = open.index + open[0].length;
87
+ const closeIdx = xml.indexOf(`</${tag}>`, start);
88
+ if (closeIdx === -1) {
89
+ return null;
90
+ }
91
+ return xml.slice(start, closeIdx);
92
+ }
93
+ function firstText(xml, tag) {
94
+ const inner = firstInner(xml, tag);
95
+ return inner === null ? null : decodeEntities(inner).trim();
96
+ }
97
+ function parseAssumeRole(xml) {
98
+ const credBlock = firstInner(xml, "Credentials");
99
+ if (credBlock === null) {
100
+ return null;
101
+ }
102
+ const accessKeyId = firstText(credBlock, "AccessKeyId") ?? "";
103
+ const secretAccessKey = firstText(credBlock, "SecretAccessKey") ?? "";
104
+ if (accessKeyId === "" || secretAccessKey === "") {
105
+ return null;
106
+ }
107
+ return {
108
+ accessKeyId,
109
+ secretAccessKey,
110
+ sessionToken: firstText(credBlock, "SessionToken") ?? "",
111
+ expiration: firstText(credBlock, "Expiration") ?? ""
112
+ };
113
+ }
114
+ function parseErrorCode(xml) {
115
+ return firstText(xml, "Code");
116
+ }
117
+ var HttpClientError = class extends Error {
118
+ response;
119
+ constructor(message, response) {
120
+ super(message);
121
+ this.name = new.target.name;
122
+ this.response = response;
123
+ }
124
+ };
125
+ var TransientError = class extends HttpClientError {
126
+ kind = "transient";
127
+ };
128
+ var RateLimitError = class extends HttpClientError {
129
+ kind = "rate_limit";
130
+ retryAfter;
131
+ constructor(message, response, retryAfter) {
132
+ super(message, response);
133
+ this.retryAfter = retryAfter;
134
+ }
135
+ };
136
+ var AuthError = class extends HttpClientError {
137
+ kind = "auth";
138
+ };
139
+ var HTTP_CLIENT_VERSION = "0.0.0";
140
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
141
+ function connectorUserAgent(connectorId) {
142
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
143
+ }
144
+ function parseEpoch(value, unit) {
145
+ if (value === null || value === void 0) {
146
+ return null;
147
+ }
148
+ if (unit === "iso") {
149
+ if (typeof value !== "string") {
150
+ return null;
151
+ }
152
+ const ms = new Date(value).getTime();
153
+ return Number.isFinite(ms) ? ms : null;
154
+ }
155
+ if (typeof value === "string" && value.trim() === "") {
156
+ return null;
157
+ }
158
+ const n = typeof value === "number" ? value : Number(value);
159
+ if (!Number.isFinite(n)) {
160
+ return null;
161
+ }
162
+ const result = unit === "s" ? n * 1e3 : n;
163
+ return Number.isFinite(result) ? result : null;
164
+ }
165
+ var HttpClientError2 = class extends Error {
166
+ response;
167
+ constructor(message, response) {
168
+ super(message);
169
+ this.name = new.target.name;
170
+ this.response = response;
171
+ }
172
+ };
173
+ var TransientError2 = class extends HttpClientError2 {
174
+ kind = "transient";
175
+ };
176
+ var RateLimitError2 = class extends HttpClientError2 {
177
+ kind = "rate_limit";
178
+ retryAfter;
179
+ constructor(message, response, retryAfter) {
180
+ super(message, response);
181
+ this.retryAfter = retryAfter;
182
+ }
183
+ };
184
+ var AuthError2 = class extends HttpClientError2 {
185
+ kind = "auth";
186
+ };
187
+ var UpstreamBugError = class extends HttpClientError2 {
188
+ kind = "upstream_bug";
189
+ };
190
+ var ClientBugError = class extends HttpClientError2 {
191
+ kind = "client_bug";
192
+ };
193
+ function classifyStatus(status) {
194
+ if (status === 429) {
195
+ return "rate_limit";
196
+ }
197
+ if (status === 401 || status === 403) {
198
+ return "auth";
199
+ }
200
+ if (status === 408) {
201
+ return "transient";
202
+ }
203
+ if (status >= 500) {
204
+ return "upstream_bug";
205
+ }
206
+ if (status >= 400) {
207
+ return "client_bug";
208
+ }
209
+ return "client_bug";
210
+ }
211
+ function errorForStatus(message, response, retryAfter) {
212
+ const kind = classifyStatus(response.status);
213
+ switch (kind) {
214
+ case "rate_limit":
215
+ return new RateLimitError2(message, response, retryAfter);
216
+ case "auth":
217
+ return new AuthError2(message, response);
218
+ case "transient":
219
+ return new TransientError2(message, response);
220
+ case "upstream_bug":
221
+ return new UpstreamBugError(message, response);
222
+ case "client_bug":
223
+ return new ClientBugError(message, response);
224
+ }
225
+ }
226
+ var defaultRetryOn = (status, err) => {
227
+ if (err instanceof RateLimitError2) {
228
+ return true;
229
+ }
230
+ if (err instanceof TransientError2) {
231
+ return true;
232
+ }
233
+ if (status === null) {
234
+ return err instanceof Error && !(err instanceof HttpClientError2);
235
+ }
236
+ if (status === 408 || status === 429) {
237
+ return true;
238
+ }
239
+ if (status >= 500) {
240
+ return true;
241
+ }
242
+ return false;
243
+ };
244
+ function parseRetryAfter(headerValue, now = /* @__PURE__ */ new Date()) {
245
+ if (!headerValue) {
246
+ return void 0;
247
+ }
248
+ const trimmed = headerValue.trim();
249
+ if (/^\d+$/.test(trimmed)) {
250
+ return new Date(now.getTime() + Number(trimmed) * 1e3);
251
+ }
252
+ const parsed = Date.parse(trimmed);
253
+ if (Number.isNaN(parsed)) {
254
+ return void 0;
255
+ }
256
+ return new Date(parsed);
257
+ }
258
+ function sleep(ms, signal) {
259
+ if (signal?.aborted) {
260
+ return Promise.reject(signal.reason ?? new Error("Aborted"));
261
+ }
262
+ return new Promise((resolve, reject) => {
263
+ const onAbort = () => {
264
+ clearTimeout(timer);
265
+ reject(signal.reason ?? new Error("Aborted"));
266
+ };
267
+ const timer = setTimeout(() => {
268
+ signal?.removeEventListener("abort", onAbort);
269
+ resolve();
270
+ }, ms);
271
+ signal?.addEventListener("abort", onAbort, { once: true });
272
+ });
273
+ }
274
+ var HTTP_CLIENT_VERSION2 = "0.0.0";
275
+ var DEFAULT_USER_AGENT2 = `rawdash-connector/${HTTP_CLIENT_VERSION2} (+https://rawdash.dev)`;
276
+ var DEFAULT_TIMEOUT_MS = 1e4;
277
+ var DEFAULT_MAX_ATTEMPTS = 3;
278
+ var DEFAULT_INITIAL_DELAY_MS = 1e3;
279
+ var DEFAULT_MAX_DELAY_MS = 6e4;
280
+ var OBSERVER_TIMEOUT_MS = 250;
281
+ async function notifyObserver(observer, event) {
282
+ let result;
283
+ try {
284
+ result = observer(event);
285
+ } catch (err) {
286
+ console.warn("[connector-shared] request observer threw:", err);
287
+ return;
288
+ }
289
+ if (!(result instanceof Promise)) {
290
+ return;
291
+ }
292
+ const guarded = result.catch((err) => {
293
+ console.warn("[connector-shared] request observer rejected:", err);
294
+ });
295
+ let timer;
296
+ const timeout = new Promise((resolve) => {
297
+ timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);
298
+ });
299
+ try {
300
+ await Promise.race([guarded, timeout]);
301
+ } finally {
302
+ if (timer) {
303
+ clearTimeout(timer);
304
+ }
305
+ }
306
+ }
307
+ function newRequestId() {
308
+ const c = globalThis.crypto;
309
+ if (c?.randomUUID) {
310
+ return c.randomUUID();
311
+ }
312
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
313
+ }
314
+ function mergeHeaders(defaults, overrides) {
315
+ const merged = {};
316
+ for (const [k, v] of Object.entries(defaults)) {
317
+ merged[k.toLowerCase()] = v;
318
+ }
319
+ if (overrides) {
320
+ for (const [k, v] of Object.entries(overrides)) {
321
+ merged[k.toLowerCase()] = v;
322
+ }
323
+ }
324
+ return merged;
325
+ }
326
+ function linkTimeoutSignal(parent, timeoutMs) {
327
+ const controller = new AbortController();
328
+ const onParentAbort = () => {
329
+ controller.abort(parent?.reason);
330
+ };
331
+ if (parent) {
332
+ if (parent.aborted) {
333
+ controller.abort(parent.reason);
334
+ } else {
335
+ parent.addEventListener("abort", onParentAbort, { once: true });
336
+ }
337
+ }
338
+ const timer = setTimeout(() => {
339
+ controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));
340
+ }, timeoutMs);
341
+ return {
342
+ signal: controller.signal,
343
+ cancel: () => {
344
+ clearTimeout(timer);
345
+ if (parent) {
346
+ parent.removeEventListener("abort", onParentAbort);
347
+ }
348
+ }
349
+ };
350
+ }
351
+ async function readBody(res, parseJson) {
352
+ if (res.status === 204 || res.status === 205) {
353
+ return null;
354
+ }
355
+ const contentType = res.headers.get("content-type") ?? "";
356
+ if (parseJson && contentType.includes("application/json")) {
357
+ const text = await res.text();
358
+ if (text.length === 0) {
359
+ return null;
360
+ }
361
+ return JSON.parse(text);
362
+ }
363
+ return res.text();
364
+ }
365
+ async function request(req, options) {
366
+ const fetchImpl = options.fetch ?? globalThis.fetch;
367
+ const retry = req.retry ?? {};
368
+ const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
369
+ const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
370
+ const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
371
+ const retryOn = retry.retryOn ?? defaultRetryOn;
372
+ const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;
373
+ const parseJson = req.parseJson ?? true;
374
+ const headers = mergeHeaders(
375
+ {
376
+ "User-Agent": DEFAULT_USER_AGENT2,
377
+ Accept: "application/json"
378
+ },
379
+ req.headers
380
+ );
381
+ let lastErr;
382
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
383
+ req.signal?.throwIfAborted();
384
+ const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);
385
+ let res;
386
+ try {
387
+ res = await fetchImpl(req.url, {
388
+ method: req.method ?? "GET",
389
+ headers,
390
+ body: req.body,
391
+ signal
392
+ });
393
+ } catch (err2) {
394
+ cancel();
395
+ if (req.signal?.aborted) {
396
+ throw req.signal.reason ?? err2;
397
+ }
398
+ const error = err2 instanceof Error ? err2 : new Error(String(err2));
399
+ lastErr = error;
400
+ if (attempt < maxAttempts - 1 && retryOn(null, error)) {
401
+ const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);
402
+ await sleep(delay, req.signal);
403
+ continue;
404
+ }
405
+ throw new TransientError2(error.message);
406
+ }
407
+ cancel();
408
+ const body = await readBody(res, parseJson);
409
+ const httpResponse = {
410
+ status: res.status,
411
+ headers: res.headers,
412
+ body
413
+ };
414
+ if (req.rateLimit) {
415
+ const state = req.rateLimit.parse(res.headers);
416
+ if (state) {
417
+ httpResponse.rateLimitState = state;
418
+ }
419
+ }
420
+ if (options.observer) {
421
+ await notifyObserver(options.observer, {
422
+ url: req.url,
423
+ method: req.method ?? "GET",
424
+ status: res.status,
425
+ resource: options.resource,
426
+ requestId: options.requestId ?? newRequestId(),
427
+ body
428
+ });
429
+ }
430
+ if (res.ok) {
431
+ return httpResponse;
432
+ }
433
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
434
+ const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? "GET"} ${req.url}`;
435
+ const err = errorForStatus(message, httpResponse, retryAfter);
436
+ if (attempt < maxAttempts - 1 && retryOn(res.status, err) && !(err instanceof AuthError2) && !(err instanceof ClientBugError)) {
437
+ lastErr = err;
438
+ let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);
439
+ if (err instanceof RateLimitError2 && retryAfter) {
440
+ const wait = retryAfter.getTime() - Date.now();
441
+ if (wait > 0) {
442
+ delay = Math.min(wait, maxDelayMs);
443
+ }
444
+ }
445
+ await sleep(delay, req.signal);
446
+ continue;
447
+ }
448
+ throw err;
449
+ }
450
+ throw lastErr ?? new UpstreamBugError("Exhausted retry attempts");
451
+ }
452
+ function computeDelay(attempt, initialDelayMs, maxDelayMs) {
453
+ const base = initialDelayMs * 2 ** attempt;
454
+ const jitter = base * 0.25 * Math.random();
455
+ return Math.min(base + jitter, maxDelayMs);
456
+ }
457
+ var MAX_VALUE_LEN = 120;
458
+ function truncate(s, max = MAX_VALUE_LEN) {
459
+ if (s.length <= max) {
460
+ return s;
461
+ }
462
+ return `${s.slice(0, max - 1)}\u2026`;
463
+ }
464
+ function formatValue(value) {
465
+ if (value === null) {
466
+ return "null";
467
+ }
468
+ if (value === void 0) {
469
+ return "";
470
+ }
471
+ if (typeof value === "number" || typeof value === "boolean") {
472
+ return String(value);
473
+ }
474
+ if (typeof value === "string") {
475
+ const t = truncate(value);
476
+ if (/[\s"=]/.test(t)) {
477
+ return JSON.stringify(t);
478
+ }
479
+ return t;
480
+ }
481
+ if (typeof value === "bigint") {
482
+ return value.toString();
483
+ }
484
+ let json;
485
+ try {
486
+ json = JSON.stringify(value);
487
+ } catch {
488
+ json = void 0;
489
+ }
490
+ return truncate(json ?? String(value));
491
+ }
492
+ function formatLogFields(fields) {
493
+ if (!fields) {
494
+ return "";
495
+ }
496
+ const parts = [];
497
+ for (const [k, v] of Object.entries(fields)) {
498
+ if (v === void 0) {
499
+ continue;
500
+ }
501
+ parts.push(`${k}=${formatValue(v)}`);
502
+ }
503
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
504
+ }
505
+ function formatLogLine(scope, event, fields) {
506
+ return `[${scope}] ${event}${formatLogFields(fields)}`;
507
+ }
508
+ function createDefaultConnectorLogger(opts) {
509
+ return {
510
+ info(event, fields) {
511
+ console.info(formatLogLine(opts.scope, event, fields));
512
+ },
513
+ warn(event, fields) {
514
+ console.warn(formatLogLine(opts.scope, event, fields));
515
+ }
516
+ };
517
+ }
518
+ function isSecret(value) {
519
+ return typeof value === "object" && value !== null && "$secret" in value && typeof value.$secret === "string";
520
+ }
521
+ var secretRefSchema = z.strictObject({
522
+ $secret: z.string()
523
+ });
524
+ var EnvSecretsResolver = class {
525
+ resolve(name) {
526
+ const env = globalThis.process?.env;
527
+ const raw = env?.[name];
528
+ if (raw === void 0) {
529
+ return void 0;
530
+ }
531
+ if (raw.length === 0) {
532
+ return raw;
533
+ }
534
+ const first = raw.charCodeAt(0);
535
+ if (first !== 123 && first !== 91) {
536
+ return raw;
537
+ }
538
+ try {
539
+ return JSON.parse(raw);
540
+ } catch {
541
+ return raw;
542
+ }
543
+ }
544
+ };
545
+ function resolveSecrets(obj, resolver) {
546
+ if (isSecret(obj)) {
547
+ const name = obj.$secret;
548
+ const value = resolver.resolve(name);
549
+ if (value === void 0) {
550
+ throw new Error(
551
+ `Missing secret "${name}". Set it via process.env.${name} or the CLI: rawdash secrets set ${name} ...`
552
+ );
553
+ }
554
+ return value;
555
+ }
556
+ if (Array.isArray(obj)) {
557
+ return obj.map((item) => resolveSecrets(item, resolver));
558
+ }
559
+ if (typeof obj === "object" && obj !== null) {
560
+ const result = {};
561
+ for (const [key, val] of Object.entries(obj)) {
562
+ Object.defineProperty(result, key, {
563
+ value: resolveSecrets(val, resolver),
564
+ enumerable: true,
565
+ configurable: true,
566
+ writable: true
567
+ });
568
+ }
569
+ return result;
570
+ }
571
+ return obj;
572
+ }
573
+ var BaseConnector = class {
574
+ credentials;
575
+ settings;
576
+ creds;
577
+ rawCredInput;
578
+ ctx;
579
+ cachedLogger;
580
+ constructor(settings, creds, ctx) {
581
+ this.settings = settings;
582
+ this.rawCredInput = creds;
583
+ this.ctx = ctx ?? {};
584
+ this.creds = creds ? resolveSecrets(
585
+ creds,
586
+ this.ctx.secretsResolver ?? new EnvSecretsResolver()
587
+ ) : {};
588
+ }
589
+ get logger() {
590
+ if (!this.cachedLogger) {
591
+ this.cachedLogger = this.ctx.logger ?? createDefaultConnectorLogger({ scope: this.id });
592
+ }
593
+ return this.cachedLogger;
594
+ }
595
+ request(req, opts) {
596
+ return request(req, {
597
+ resource: opts.resource,
598
+ requestId: opts.requestId,
599
+ observer: this.ctx.observer
600
+ });
601
+ }
602
+ get(url, opts) {
603
+ return this.request(
604
+ {
605
+ url,
606
+ method: "GET",
607
+ headers: opts.headers,
608
+ signal: opts.signal,
609
+ rateLimit: opts.rateLimit
610
+ },
611
+ { resource: opts.resource, requestId: opts.requestId }
612
+ );
613
+ }
614
+ post(url, opts) {
615
+ return this.request(
616
+ {
617
+ url,
618
+ method: "POST",
619
+ headers: opts.headers,
620
+ body: opts.body,
621
+ signal: opts.signal,
622
+ rateLimit: opts.rateLimit
623
+ },
624
+ { resource: opts.resource, requestId: opts.requestId }
625
+ );
626
+ }
627
+ isResourceEnabled(resource) {
628
+ const enabled = this.settings?.resources;
629
+ if (!enabled || enabled.length === 0) {
630
+ return true;
631
+ }
632
+ return enabled.includes(resource);
633
+ }
634
+ serializeConfig() {
635
+ const config = {
636
+ ...this.settings
637
+ };
638
+ if (this.rawCredInput) {
639
+ for (const [key, value] of Object.entries(
640
+ this.rawCredInput
641
+ )) {
642
+ if (value !== void 0) {
643
+ config[key] = value;
644
+ }
645
+ }
646
+ }
647
+ return config;
648
+ }
649
+ sleep(ms, signal) {
650
+ if (signal?.aborted) {
651
+ return Promise.reject(signal.reason ?? new Error("Aborted"));
652
+ }
653
+ return new Promise((resolve, reject) => {
654
+ const onAbort = () => {
655
+ clearTimeout(timer);
656
+ reject(signal.reason ?? new Error("Aborted"));
657
+ };
658
+ const timer = setTimeout(() => {
659
+ signal?.removeEventListener("abort", onAbort);
660
+ resolve();
661
+ }, ms);
662
+ signal?.addEventListener("abort", onAbort, { once: true });
663
+ });
664
+ }
665
+ async withRetry(fn, options) {
666
+ const {
667
+ maxAttempts = 10,
668
+ initialDelayMs = 1e3,
669
+ maxDelayMs = 1e4,
670
+ signal
671
+ } = options ?? {};
672
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
673
+ signal?.throwIfAborted();
674
+ const result = await fn(signal);
675
+ if (result.status === "done") {
676
+ return result.value;
677
+ }
678
+ if (attempt < maxAttempts - 1) {
679
+ const delay = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
680
+ await this.sleep(delay, signal);
681
+ }
682
+ }
683
+ return null;
684
+ }
685
+ };
686
+ var shapeSchema = z2.enum([
687
+ "event",
688
+ "entity",
689
+ "metric",
690
+ "edge",
691
+ "distribution"
692
+ ]);
693
+ var aggFnSchema = z2.enum([
694
+ "count",
695
+ "sum",
696
+ "avg",
697
+ "min",
698
+ "max",
699
+ "latest",
700
+ "first"
701
+ ]);
702
+ var filterOperatorSchema = z2.enum([
703
+ "eq",
704
+ "neq",
705
+ "gt",
706
+ "gte",
707
+ "lt",
708
+ "lte",
709
+ "contains"
710
+ ]);
711
+ var filterConditionSchema = z2.object({
712
+ field: z2.string(),
713
+ op: filterOperatorSchema,
714
+ value: z2.union([z2.string(), z2.number(), z2.boolean()])
715
+ });
716
+ var filterClauseSchema = z2.union([
717
+ filterConditionSchema,
718
+ z2.object({ or: z2.array(filterConditionSchema) })
719
+ ]);
720
+ var groupBySchema = z2.object({
721
+ field: z2.string(),
722
+ granularity: z2.enum(["hour", "day", "week", "month"])
723
+ });
724
+ var computedMetricSchema = z2.object({
725
+ connectorId: z2.string(),
726
+ shape: shapeSchema,
727
+ name: z2.string().optional(),
728
+ entityType: z2.string().optional(),
729
+ field: z2.string().optional(),
730
+ fn: aggFnSchema,
731
+ window: z2.string().optional(),
732
+ filter: z2.array(filterClauseSchema).optional(),
733
+ groupBy: groupBySchema.optional()
734
+ }).refine((m) => m.fn === "count" || m.field !== void 0, {
735
+ message: 'field is required unless fn is "count"',
736
+ path: ["field"]
737
+ });
738
+ var titleField = z2.string().meta({ label: "Title", description: "Widget title." });
739
+ var statWidgetSchema = z2.object({
740
+ kind: z2.literal("stat"),
741
+ title: titleField,
742
+ metric: computedMetricSchema.meta({
743
+ label: "Metric",
744
+ description: "Computed metric definition."
745
+ }),
746
+ window: z2.string().optional().meta({ label: "Window", description: "Time window, e.g. '7d'." }),
747
+ compare: z2.enum(["none", "previous-period"]).default("none").meta({ label: "Compare", description: "Comparison mode." })
748
+ });
749
+ var statusWidgetSchema = z2.object({
750
+ kind: z2.literal("status"),
751
+ title: titleField,
752
+ source: z2.string().meta({
753
+ label: "Source",
754
+ description: "Connector or data source reference."
755
+ })
756
+ });
757
+ var timeseriesWidgetSchema = z2.object({
758
+ kind: z2.literal("timeseries"),
759
+ title: titleField,
760
+ metric: computedMetricSchema.meta({
761
+ label: "Metric",
762
+ description: "Computed metric definition."
763
+ }),
764
+ window: z2.string().meta({ label: "Window", description: "Time window, e.g. '30d'." }),
765
+ granularity: z2.enum(["hour", "day", "week"]).default("day").meta({ label: "Granularity", description: "Time bucket size." })
766
+ });
767
+ var distributionWidgetSchema = z2.object({
768
+ kind: z2.literal("distribution"),
769
+ title: titleField,
770
+ metric: computedMetricSchema.meta({
771
+ label: "Metric",
772
+ description: "Computed metric definition."
773
+ }),
774
+ window: z2.string().meta({ label: "Window", description: "Time window, e.g. '7d'." })
775
+ });
776
+ var widgetSchemas = {
777
+ stat: statWidgetSchema,
778
+ status: statusWidgetSchema,
779
+ timeseries: timeseriesWidgetSchema,
780
+ distribution: distributionWidgetSchema
781
+ };
782
+ var widgetSchema = z2.discriminatedUnion("kind", [
783
+ statWidgetSchema,
784
+ statusWidgetSchema,
785
+ timeseriesWidgetSchema,
786
+ distributionWidgetSchema
787
+ ]);
788
+ var VALID_WIDGET_KINDS = new Set(Object.keys(widgetSchemas));
789
+ var wireConnectorSchema = z4.object({
790
+ name: z4.string(),
791
+ connectorId: z4.string(),
792
+ displayName: z4.string().optional(),
793
+ config: z4.record(z4.string(), z4.unknown()),
794
+ syncIntervalSeconds: z4.number().optional(),
795
+ enabled: z4.boolean().optional()
796
+ });
797
+ var wireDashboardSchema = z4.object({
798
+ id: z4.string().optional(),
799
+ name: z4.string(),
800
+ slug: z4.string(),
801
+ config: z4.record(z4.string(), z4.unknown())
802
+ });
803
+ var wireConfigSchema = z4.object({
804
+ connectors: z4.array(wireConnectorSchema).optional(),
805
+ dashboards: z4.array(wireDashboardSchema).optional()
806
+ });
807
+ var awsCredentialsSchema = {
808
+ accessKeyId: {
809
+ description: "AWS access key ID",
810
+ auth: "optional"
811
+ },
812
+ secretAccessKey: {
813
+ description: "AWS secret access key",
814
+ auth: "optional"
815
+ }
816
+ };
817
+ var STS_SERVICE = "sts";
818
+ var STS_API_VERSION = "2011-06-15";
819
+ var ASSUMED_ROLE_TTL_BUFFER_MS = 6e4;
820
+ var ASSUME_ROLE_DURATION_SECONDS = 3600;
821
+ var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8";
822
+ function readEnv(name) {
823
+ const env = globalThis.process?.env;
824
+ return env?.[name];
825
+ }
826
+ var BaseAWSConnector = class extends BaseConnector {
827
+ credentials = awsCredentialsSchema;
828
+ assumedCreds = null;
829
+ baseCredentials() {
830
+ const { accessKeyId, secretAccessKey } = this.creds;
831
+ if (accessKeyId && secretAccessKey) {
832
+ return { accessKeyId, secretAccessKey };
833
+ }
834
+ const envAccessKeyId = readEnv("AWS_ACCESS_KEY_ID");
835
+ const envSecretAccessKey = readEnv("AWS_SECRET_ACCESS_KEY");
836
+ if (envAccessKeyId && envSecretAccessKey) {
837
+ return {
838
+ accessKeyId: envAccessKeyId,
839
+ secretAccessKey: envSecretAccessKey,
840
+ sessionToken: readEnv("AWS_SESSION_TOKEN") || void 0
841
+ };
842
+ }
843
+ throw new AuthError(
844
+ `${this.id}: no AWS credentials available \u2014 provide accessKeyId + secretAccessKey, or set them in the environment for role assumption`
845
+ );
846
+ }
847
+ async resolveSigningCredentials(signal) {
848
+ if (this.settings.roleArn === void 0) {
849
+ const { accessKeyId, secretAccessKey } = this.creds;
850
+ if (!accessKeyId || !secretAccessKey) {
851
+ throw new AuthError(
852
+ `${this.id}: static-credential auth requires both accessKeyId and secretAccessKey`
853
+ );
854
+ }
855
+ return { accessKeyId, secretAccessKey };
856
+ }
857
+ if (this.assumedCreds && Date.now() < this.assumedCreds.expiresAt) {
858
+ return this.assumedCreds.value;
859
+ }
860
+ return this.assumeRole(this.settings.roleArn, signal);
861
+ }
862
+ async assumeRole(roleArn, signal) {
863
+ const params = new URLSearchParams();
864
+ params.set("Action", "AssumeRole");
865
+ params.set("Version", STS_API_VERSION);
866
+ params.set("RoleArn", roleArn);
867
+ params.set("RoleSessionName", `rawdash-${this.id}`);
868
+ params.set("DurationSeconds", String(ASSUME_ROLE_DURATION_SECONDS));
869
+ if (this.settings.externalId !== void 0) {
870
+ params.set("ExternalId", this.settings.externalId);
871
+ }
872
+ const host = `sts.${this.settings.region}.amazonaws.com`;
873
+ const xml = await this.signedPost({
874
+ host,
875
+ service: STS_SERVICE,
876
+ body: params.toString(),
877
+ signingCredentials: this.baseCredentials(),
878
+ resource: "assume_role",
879
+ signal
880
+ });
881
+ const parsed = parseAssumeRole(xml);
882
+ if (parsed === null) {
883
+ throw new AuthError(
884
+ `${this.id}: STS AssumeRole returned no usable credentials`
885
+ );
886
+ }
887
+ this.cacheAssumedCredentials(parsed);
888
+ return {
889
+ accessKeyId: parsed.accessKeyId,
890
+ secretAccessKey: parsed.secretAccessKey,
891
+ sessionToken: parsed.sessionToken || void 0
892
+ };
893
+ }
894
+ cacheAssumedCredentials(parsed) {
895
+ const expirationMs = parseEpoch(parsed.expiration, "iso");
896
+ const expiresAt = expirationMs !== null ? expirationMs - ASSUMED_ROLE_TTL_BUFFER_MS : Date.now() + (ASSUME_ROLE_DURATION_SECONDS - 60) * 1e3;
897
+ this.assumedCreds = {
898
+ value: {
899
+ accessKeyId: parsed.accessKeyId,
900
+ secretAccessKey: parsed.secretAccessKey,
901
+ sessionToken: parsed.sessionToken || void 0
902
+ },
903
+ expiresAt
904
+ };
905
+ }
906
+ async signedPost(args) {
907
+ const { amzDate, dateStamp } = formatAmzDate(/* @__PURE__ */ new Date());
908
+ const payloadHash = await sha256Hex(args.body);
909
+ const signedHeaders = {
910
+ host: args.host,
911
+ "content-type": FORM_CONTENT_TYPE,
912
+ "x-amz-content-sha256": payloadHash,
913
+ "x-amz-date": amzDate
914
+ };
915
+ if (args.signingCredentials.sessionToken !== void 0) {
916
+ signedHeaders["x-amz-security-token"] = args.signingCredentials.sessionToken;
917
+ }
918
+ const authorization = await createAuthorizationHeader({
919
+ method: "POST",
920
+ host: args.host,
921
+ path: "/",
922
+ query: "",
923
+ headers: signedHeaders,
924
+ payloadHash,
925
+ accessKeyId: args.signingCredentials.accessKeyId,
926
+ secretAccessKey: args.signingCredentials.secretAccessKey,
927
+ region: this.settings.region,
928
+ service: args.service,
929
+ amzDate,
930
+ dateStamp
931
+ });
932
+ const sendHeaders = {
933
+ "content-type": FORM_CONTENT_TYPE,
934
+ "x-amz-content-sha256": payloadHash,
935
+ "x-amz-date": amzDate,
936
+ "user-agent": connectorUserAgent(this.id),
937
+ Authorization: authorization
938
+ };
939
+ if (args.signingCredentials.sessionToken !== void 0) {
940
+ sendHeaders["x-amz-security-token"] = args.signingCredentials.sessionToken;
941
+ }
942
+ try {
943
+ const res = await this.request(
944
+ {
945
+ url: `https://${args.host}/`,
946
+ method: "POST",
947
+ headers: sendHeaders,
948
+ body: args.body,
949
+ parseJson: false,
950
+ signal: args.signal
951
+ },
952
+ { resource: args.resource }
953
+ );
954
+ return res.body;
955
+ } catch (err) {
956
+ throw this.classifyAwsError(err);
957
+ }
958
+ }
959
+ classifyAwsError(err) {
960
+ if (!(err instanceof Error) || !("kind" in err)) {
961
+ return err;
962
+ }
963
+ const httpErr = err;
964
+ const body = typeof httpErr.response?.body === "string" ? httpErr.response.body : "";
965
+ const code = parseErrorCode(body) ?? "";
966
+ const status = httpErr.response?.status ?? 0;
967
+ if (/throttl|RequestLimitExceeded|TooManyRequests|LimitExceeded/i.test(code)) {
968
+ return new RateLimitError(httpErr.message, httpErr.response);
969
+ }
970
+ if (/AccessDenied|UnrecognizedClient|InvalidClientTokenId|SignatureDoesNotMatch|AuthFailure|InvalidAccessKeyId|Forbidden/i.test(
971
+ code
972
+ )) {
973
+ return new AuthError(httpErr.message, httpErr.response);
974
+ }
975
+ if (status >= 500) {
976
+ return new TransientError(httpErr.message, httpErr.response);
977
+ }
978
+ return err;
979
+ }
980
+ };
981
+ var awsAuthConfigShape = {
982
+ region: z5.string().regex(
983
+ /^[a-z0-9-]+$/,
984
+ "region must look like an AWS region, e.g. us-east-1"
985
+ ).meta({
986
+ label: "AWS Region",
987
+ description: "The AWS region whose service endpoint you want to call, e.g. us-east-1.",
988
+ placeholder: "us-east-1"
989
+ }),
990
+ accessKeyId: z5.object({ $secret: z5.string() }).optional().meta({
991
+ label: "Access Key ID",
992
+ description: "AWS access key ID for an IAM principal with permission to call the relevant service. Use together with the secret access key for static-credential auth.",
993
+ secret: true
994
+ }),
995
+ secretAccessKey: z5.object({ $secret: z5.string() }).optional().meta({
996
+ label: "Secret Access Key",
997
+ description: "AWS secret access key paired with the access key ID above.",
998
+ secret: true
999
+ }),
1000
+ roleArn: z5.string().regex(
1001
+ /^arn:aws:iam::\d{12}:role\/.+/,
1002
+ "roleArn must be a full IAM role ARN, e.g. arn:aws:iam::123456789012:role/rawdash"
1003
+ ).optional().meta({
1004
+ label: "Role ARN",
1005
+ description: "IAM role to assume via STS instead of using static keys. The base credentials (the access key above, or the ambient AWS environment) must be allowed to sts:AssumeRole this role.",
1006
+ placeholder: "arn:aws:iam::123456789012:role/rawdash"
1007
+ }),
1008
+ externalId: z5.string().min(1).optional().meta({
1009
+ label: "External ID",
1010
+ description: "External ID required by the trust policy of the role being assumed. Only used with Role ARN."
1011
+ })
1012
+ };
1013
+ var awsAuthRefine = {
1014
+ predicate: (val) => {
1015
+ const hasRole = val.roleArn !== void 0;
1016
+ const hasStatic = val.accessKeyId !== void 0 && val.secretAccessKey !== void 0;
1017
+ if (val.externalId !== void 0 && !hasRole) {
1018
+ return false;
1019
+ }
1020
+ return hasRole || hasStatic;
1021
+ },
1022
+ message: "Provide either accessKeyId + secretAccessKey (static credentials) or roleArn (role assumption). externalId requires roleArn."
1023
+ };
1024
+
1025
+ // ../../connector-shared/dist/index.js
1026
+ var HttpClientError3 = class extends Error {
1027
+ response;
1028
+ constructor(message, response) {
1029
+ super(message);
1030
+ this.name = new.target.name;
1031
+ this.response = response;
1032
+ }
1033
+ };
1034
+ var TransientError3 = class extends HttpClientError3 {
1035
+ kind = "transient";
1036
+ };
1037
+ var RateLimitError3 = class extends HttpClientError3 {
1038
+ kind = "rate_limit";
1039
+ retryAfter;
1040
+ constructor(message, response, retryAfter) {
1041
+ super(message, response);
1042
+ this.retryAfter = retryAfter;
1043
+ }
1044
+ };
1045
+ var AuthError3 = class extends HttpClientError3 {
1046
+ kind = "auth";
1047
+ };
1048
+ var HTTP_CLIENT_VERSION3 = "0.0.0";
1049
+ var DEFAULT_USER_AGENT3 = `rawdash-connector/${HTTP_CLIENT_VERSION3} (+https://rawdash.dev)`;
1050
+ function connectorUserAgent2(connectorId) {
1051
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION3} (+https://rawdash.dev)`;
1052
+ }
1053
+
1054
+ // src/aws-cost.ts
1055
+ import {
1056
+ defineConfigFields
1057
+ } from "@rawdash/core";
1058
+ import { z as z6 } from "zod";
1059
+ var { region: _region, ...awsAuthWithoutRegion } = awsAuthConfigShape;
1060
+ var configFields = defineConfigFields(
1061
+ z6.object({
1062
+ ...awsAuthWithoutRegion,
1063
+ granularity: z6.enum(["DAILY", "MONTHLY"]).optional().meta({
1064
+ label: "Granularity",
1065
+ description: "Time granularity of cost buckets. DAILY (default) or MONTHLY. Each Cost Explorer query is billed at $0.01, so MONTHLY is cheaper over long windows."
1066
+ }),
1067
+ groupBy: z6.array(
1068
+ z6.string().regex(
1069
+ /^(SERVICE|LINKED_ACCOUNT|TAG:.+|COST_CATEGORY:.+)$/,
1070
+ "groupBy entries must be SERVICE, LINKED_ACCOUNT, TAG:<key>, or COST_CATEGORY:<key>"
1071
+ )
1072
+ ).max(2, "Cost Explorer accepts at most two group-by dimensions").optional().meta({
1073
+ label: "Group by (optional)",
1074
+ description: "Up to two Cost Explorer dimensions to break costs down by, e.g. SERVICE, LINKED_ACCOUNT, or TAG:Environment. Omit for total cost only."
1075
+ }),
1076
+ lookbackDays: z6.number().int().positive().optional().meta({
1077
+ label: "Backfill window (days)",
1078
+ description: "How many days of history to fetch on a full sync. Defaults to 90.",
1079
+ placeholder: "90"
1080
+ })
1081
+ }).refine((val) => awsAuthRefine.predicate({ ...val, region: AWS_REGION }), {
1082
+ message: awsAuthRefine.message
1083
+ })
1084
+ );
1085
+ var AWS_REGION = "us-east-1";
1086
+ var CE_HOST = "ce.us-east-1.amazonaws.com";
1087
+ var CE_URL = `https://${CE_HOST}/`;
1088
+ var CE_SERVICE = "ce";
1089
+ var CE_CONTENT_TYPE = "application/x-amz-json-1.1";
1090
+ var CE_TARGET_PREFIX = "AWSInsightsIndexService";
1091
+ var DAILY_METRIC_NAME = "aws_cost_daily";
1092
+ var FORECAST_METRIC_NAME = "aws_cost_forecast";
1093
+ var DEFAULT_BACKFILL_DAYS = 90;
1094
+ var INCREMENTAL_LOOKBACK_DAYS = 3;
1095
+ var MS_PER_DAY = 864e5;
1096
+ var PHASE_ORDER = ["daily_cost", "forecast"];
1097
+ var amountString = z6.string().regex(/^-?\d+(\.\d+)?$/);
1098
+ var ceDateString = z6.string().regex(/^\d{4}-\d{2}-\d{2}$/);
1099
+ var metricAmount = z6.object({ Amount: amountString, Unit: z6.string() });
1100
+ var getCostAndUsageResponse = z6.object({
1101
+ ResultsByTime: z6.array(
1102
+ z6.object({
1103
+ TimePeriod: z6.object({ Start: ceDateString, End: ceDateString }),
1104
+ Total: z6.object({ UnblendedCost: metricAmount.optional() }).optional(),
1105
+ Groups: z6.array(
1106
+ z6.object({
1107
+ Keys: z6.array(z6.string()),
1108
+ Metrics: z6.object({ UnblendedCost: metricAmount })
1109
+ })
1110
+ ).optional(),
1111
+ Estimated: z6.boolean().optional()
1112
+ })
1113
+ ),
1114
+ NextPageToken: z6.string().optional()
1115
+ });
1116
+ var getCostForecastResponse = z6.object({
1117
+ Total: metricAmount.optional(),
1118
+ ForecastResultsByTime: z6.array(
1119
+ z6.object({
1120
+ TimePeriod: z6.object({ Start: ceDateString, End: ceDateString }),
1121
+ MeanValue: amountString,
1122
+ PredictionIntervalLowerBound: amountString.optional(),
1123
+ PredictionIntervalUpperBound: amountString.optional()
1124
+ })
1125
+ ).optional()
1126
+ });
1127
+ function asHttpError(err) {
1128
+ if (err instanceof Error && "kind" in err && typeof err.kind === "string") {
1129
+ return err;
1130
+ }
1131
+ return null;
1132
+ }
1133
+ function extractAwsErrorType(err) {
1134
+ const body = err.response?.body;
1135
+ if (typeof body === "string") {
1136
+ try {
1137
+ const parsed = JSON.parse(body);
1138
+ return parsed.__type ?? parsed.Code ?? body;
1139
+ } catch {
1140
+ return body;
1141
+ }
1142
+ }
1143
+ if (body && typeof body === "object") {
1144
+ const o = body;
1145
+ return String(o.__type ?? o.Code ?? "");
1146
+ }
1147
+ return "";
1148
+ }
1149
+ function mapAwsJsonError(err) {
1150
+ const httpError = asHttpError(err);
1151
+ if (!httpError) {
1152
+ return err;
1153
+ }
1154
+ const type = extractAwsErrorType(httpError);
1155
+ const status = httpError.response?.status ?? 0;
1156
+ if (/throttl|TooManyRequests|RequestLimitExceeded/i.test(type) || status === 429) {
1157
+ return new RateLimitError3(httpError.message, httpError.response);
1158
+ }
1159
+ if (/AccessDenied|UnrecognizedClient|InvalidClientTokenId|SignatureDoesNotMatch|AuthFailure|InvalidSignature|ExpiredToken/i.test(
1160
+ type
1161
+ ) || status === 403) {
1162
+ return new AuthError3(httpError.message, httpError.response);
1163
+ }
1164
+ if (status >= 500) {
1165
+ return new TransientError3(httpError.message, httpError.response);
1166
+ }
1167
+ return err;
1168
+ }
1169
+ function isDataUnavailable(err) {
1170
+ const httpError = asHttpError(err);
1171
+ return httpError !== null && /DataUnavailable/i.test(extractAwsErrorType(httpError));
1172
+ }
1173
+ function parseAmount(value) {
1174
+ if (value === void 0) {
1175
+ return 0;
1176
+ }
1177
+ const n = Number.parseFloat(value);
1178
+ return Number.isFinite(n) ? n : 0;
1179
+ }
1180
+ function ceDateToMs(date) {
1181
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
1182
+ if (!m) {
1183
+ return NaN;
1184
+ }
1185
+ return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
1186
+ }
1187
+ function pad2(n) {
1188
+ return String(n).padStart(2, "0");
1189
+ }
1190
+ function toDateStr(ms) {
1191
+ const d = new Date(ms);
1192
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
1193
+ }
1194
+ function addMonthsFirstUtc(ms, months) {
1195
+ const d = new Date(ms);
1196
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, 1);
1197
+ }
1198
+ function startOfUtcDay(ms) {
1199
+ return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;
1200
+ }
1201
+ function groupAttrName(groupBy, index) {
1202
+ const dim = groupBy?.[index];
1203
+ if (!dim) {
1204
+ return `dimension_${index}`;
1205
+ }
1206
+ if (dim.startsWith("TAG:")) {
1207
+ return `tag_${dim.slice(4)}`;
1208
+ }
1209
+ if (dim.startsWith("COST_CATEGORY:")) {
1210
+ return `cost_category_${dim.slice(14)}`;
1211
+ }
1212
+ return dim.toLowerCase();
1213
+ }
1214
+ function toGroupDefinition(dim) {
1215
+ if (dim.startsWith("TAG:")) {
1216
+ return { Type: "TAG", Key: dim.slice(4) };
1217
+ }
1218
+ if (dim.startsWith("COST_CATEGORY:")) {
1219
+ return { Type: "COST_CATEGORY", Key: dim.slice(14) };
1220
+ }
1221
+ return { Type: "DIMENSION", Key: dim };
1222
+ }
1223
+ function buildDailyCostSamples(body, granularity, groupBy) {
1224
+ const samples = [];
1225
+ for (const result of body.ResultsByTime ?? []) {
1226
+ const start = result.TimePeriod?.Start;
1227
+ if (start === void 0) {
1228
+ continue;
1229
+ }
1230
+ const ts = ceDateToMs(start);
1231
+ if (!Number.isFinite(ts)) {
1232
+ continue;
1233
+ }
1234
+ const estimated = result.Estimated ?? false;
1235
+ const groups = result.Groups ?? [];
1236
+ if (groups.length > 0) {
1237
+ for (const group of groups) {
1238
+ const cost2 = group.Metrics?.["UnblendedCost"];
1239
+ const keys = group.Keys ?? [];
1240
+ const attributes = {
1241
+ granularity,
1242
+ estimated,
1243
+ unit: cost2?.Unit ?? "USD"
1244
+ };
1245
+ for (let i = 0; i < keys.length; i++) {
1246
+ attributes[groupAttrName(groupBy, i)] = keys[i] ?? null;
1247
+ }
1248
+ samples.push({
1249
+ name: DAILY_METRIC_NAME,
1250
+ ts,
1251
+ value: parseAmount(cost2?.Amount),
1252
+ attributes
1253
+ });
1254
+ }
1255
+ continue;
1256
+ }
1257
+ const cost = result.Total?.["UnblendedCost"];
1258
+ if (!cost) {
1259
+ continue;
1260
+ }
1261
+ samples.push({
1262
+ name: DAILY_METRIC_NAME,
1263
+ ts,
1264
+ value: parseAmount(cost.Amount),
1265
+ attributes: { granularity, estimated, unit: cost.Unit ?? "USD" }
1266
+ });
1267
+ }
1268
+ return samples;
1269
+ }
1270
+ function buildForecastSamples(body, granularity) {
1271
+ const unit = body.Total?.Unit ?? "USD";
1272
+ const samples = [];
1273
+ for (const result of body.ForecastResultsByTime ?? []) {
1274
+ const start = result.TimePeriod?.Start;
1275
+ if (start === void 0) {
1276
+ continue;
1277
+ }
1278
+ const ts = ceDateToMs(start);
1279
+ if (!Number.isFinite(ts)) {
1280
+ continue;
1281
+ }
1282
+ samples.push({
1283
+ name: FORECAST_METRIC_NAME,
1284
+ ts,
1285
+ value: parseAmount(result.MeanValue),
1286
+ attributes: {
1287
+ granularity,
1288
+ unit,
1289
+ lowerBound: result.PredictionIntervalLowerBound !== void 0 ? parseAmount(result.PredictionIntervalLowerBound) : null,
1290
+ upperBound: result.PredictionIntervalUpperBound !== void 0 ? parseAmount(result.PredictionIntervalUpperBound) : null
1291
+ }
1292
+ });
1293
+ }
1294
+ return samples;
1295
+ }
1296
+ function getCostWindow(options, granularity, lookbackDays, now = Date.now()) {
1297
+ const sinceMs = options.since !== void 0 ? Date.parse(options.since) : NaN;
1298
+ const hasSince = Number.isFinite(sinceMs);
1299
+ let days = lookbackDays;
1300
+ if (options.mode === "latest") {
1301
+ days = INCREMENTAL_LOOKBACK_DAYS;
1302
+ } else if (hasSince) {
1303
+ const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);
1304
+ days = Math.min(Math.max(elapsed, 1), lookbackDays);
1305
+ }
1306
+ if (granularity === "MONTHLY") {
1307
+ let months;
1308
+ if (options.mode === "latest") {
1309
+ months = 1;
1310
+ } else if (hasSince) {
1311
+ const since = new Date(sinceMs);
1312
+ const nowDate = new Date(now);
1313
+ const delta = (nowDate.getUTCFullYear() - since.getUTCFullYear()) * 12 + (nowDate.getUTCMonth() - since.getUTCMonth()) + 1;
1314
+ months = Math.max(1, delta);
1315
+ } else {
1316
+ months = Math.max(1, Math.ceil(lookbackDays / 30));
1317
+ }
1318
+ return {
1319
+ start: toDateStr(addMonthsFirstUtc(now, 1 - months)),
1320
+ end: toDateStr(addMonthsFirstUtc(now, 1))
1321
+ };
1322
+ }
1323
+ const end = startOfUtcDay(now) + MS_PER_DAY;
1324
+ return { start: toDateStr(end - days * MS_PER_DAY), end: toDateStr(end) };
1325
+ }
1326
+ function getForecastWindow(granularity, now = Date.now()) {
1327
+ const start = startOfUtcDay(now);
1328
+ if (granularity === "MONTHLY") {
1329
+ return {
1330
+ start: toDateStr(start),
1331
+ end: toDateStr(addMonthsFirstUtc(now, 3))
1332
+ };
1333
+ }
1334
+ return { start: toDateStr(start), end: toDateStr(start + 31 * MS_PER_DAY) };
1335
+ }
1336
+ function isAwsCostCursor(value) {
1337
+ if (typeof value !== "object" || value === null) {
1338
+ return false;
1339
+ }
1340
+ const c = value;
1341
+ if (typeof c.phase !== "string" || !PHASE_ORDER.includes(c.phase)) {
1342
+ return false;
1343
+ }
1344
+ const p = c.page;
1345
+ if (p === null) {
1346
+ return true;
1347
+ }
1348
+ if (typeof p !== "object") {
1349
+ return false;
1350
+ }
1351
+ return typeof p.start === "string" && typeof p.end === "string";
1352
+ }
1353
+ var AwsCostConnector = class _AwsCostConnector extends BaseAWSConnector {
1354
+ static id = "aws-cost";
1355
+ static schemas = {
1356
+ daily_cost: getCostAndUsageResponse,
1357
+ forecast: getCostForecastResponse
1358
+ };
1359
+ static create(input, ctx) {
1360
+ const parsed = configFields.parse(input);
1361
+ return new _AwsCostConnector(
1362
+ {
1363
+ region: AWS_REGION,
1364
+ roleArn: parsed.roleArn,
1365
+ externalId: parsed.externalId,
1366
+ granularity: parsed.granularity,
1367
+ groupBy: parsed.groupBy,
1368
+ lookbackDays: parsed.lookbackDays
1369
+ },
1370
+ {
1371
+ accessKeyId: parsed.accessKeyId,
1372
+ secretAccessKey: parsed.secretAccessKey
1373
+ },
1374
+ ctx
1375
+ );
1376
+ }
1377
+ id = "aws-cost";
1378
+ async callCostExplorer(action, payload, resource, signal) {
1379
+ const credentials = await this.resolveSigningCredentials(signal);
1380
+ const body = JSON.stringify(payload);
1381
+ const headers = await this.buildCeHeaders(action, body, credentials);
1382
+ try {
1383
+ const res = await this.post(CE_URL, {
1384
+ resource,
1385
+ headers,
1386
+ body,
1387
+ signal
1388
+ });
1389
+ const parsed = typeof res.body === "string" ? JSON.parse(res.body) : res.body;
1390
+ return parsed;
1391
+ } catch (err) {
1392
+ throw mapAwsJsonError(err);
1393
+ }
1394
+ }
1395
+ async buildCeHeaders(action, body, credentials) {
1396
+ const { amzDate, dateStamp } = formatAmzDate(/* @__PURE__ */ new Date());
1397
+ const payloadHash = await sha256Hex(body);
1398
+ const amzTarget = `${CE_TARGET_PREFIX}.${action}`;
1399
+ const signedHeaders = {
1400
+ "content-type": CE_CONTENT_TYPE,
1401
+ host: CE_HOST,
1402
+ "x-amz-content-sha256": payloadHash,
1403
+ "x-amz-date": amzDate,
1404
+ "x-amz-target": amzTarget
1405
+ };
1406
+ if (credentials.sessionToken !== void 0) {
1407
+ signedHeaders["x-amz-security-token"] = credentials.sessionToken;
1408
+ }
1409
+ const authorization = await createAuthorizationHeader({
1410
+ method: "POST",
1411
+ host: CE_HOST,
1412
+ path: "/",
1413
+ query: "",
1414
+ headers: signedHeaders,
1415
+ payloadHash,
1416
+ accessKeyId: credentials.accessKeyId,
1417
+ secretAccessKey: credentials.secretAccessKey,
1418
+ region: AWS_REGION,
1419
+ service: CE_SERVICE,
1420
+ amzDate,
1421
+ dateStamp
1422
+ });
1423
+ const sendHeaders = {
1424
+ "Content-Type": CE_CONTENT_TYPE,
1425
+ "X-Amz-Content-Sha256": payloadHash,
1426
+ "X-Amz-Date": amzDate,
1427
+ "X-Amz-Target": amzTarget,
1428
+ Authorization: authorization,
1429
+ "User-Agent": connectorUserAgent2(this.id)
1430
+ };
1431
+ if (credentials.sessionToken !== void 0) {
1432
+ sendHeaders["X-Amz-Security-Token"] = credentials.sessionToken;
1433
+ }
1434
+ return sendHeaders;
1435
+ }
1436
+ async syncDailyCost(storage, window, granularity, groupBy, signal) {
1437
+ const samples = [];
1438
+ let nextPageToken;
1439
+ do {
1440
+ const payload = {
1441
+ TimePeriod: { Start: window.start, End: window.end },
1442
+ Granularity: granularity,
1443
+ Metrics: ["UnblendedCost"]
1444
+ };
1445
+ if (groupBy && groupBy.length > 0) {
1446
+ payload["GroupBy"] = groupBy.slice(0, 2).map(toGroupDefinition);
1447
+ }
1448
+ if (nextPageToken) {
1449
+ payload["NextPageToken"] = nextPageToken;
1450
+ }
1451
+ const parsed = await this.callCostExplorer(
1452
+ "GetCostAndUsage",
1453
+ payload,
1454
+ "daily_cost",
1455
+ signal
1456
+ );
1457
+ samples.push(...buildDailyCostSamples(parsed, granularity, groupBy));
1458
+ nextPageToken = typeof parsed.NextPageToken === "string" && parsed.NextPageToken.length > 0 ? parsed.NextPageToken : void 0;
1459
+ } while (nextPageToken);
1460
+ await storage.metrics(samples, { names: [DAILY_METRIC_NAME] });
1461
+ }
1462
+ async syncForecast(storage, granularity, signal) {
1463
+ const window = getForecastWindow(granularity);
1464
+ let parsed;
1465
+ try {
1466
+ parsed = await this.callCostExplorer(
1467
+ "GetCostForecast",
1468
+ {
1469
+ TimePeriod: { Start: window.start, End: window.end },
1470
+ Metric: "UNBLENDED_COST",
1471
+ Granularity: granularity
1472
+ },
1473
+ "forecast",
1474
+ signal
1475
+ );
1476
+ } catch (err) {
1477
+ if (isDataUnavailable(err)) {
1478
+ await storage.metrics([], { names: [FORECAST_METRIC_NAME] });
1479
+ return;
1480
+ }
1481
+ throw err;
1482
+ }
1483
+ await storage.metrics(buildForecastSamples(parsed, granularity), {
1484
+ names: [FORECAST_METRIC_NAME]
1485
+ });
1486
+ }
1487
+ async sync(options, storage, signal) {
1488
+ const granularity = this.settings.granularity ?? "DAILY";
1489
+ const lookbackDays = this.settings.lookbackDays ?? DEFAULT_BACKFILL_DAYS;
1490
+ const groupBy = this.settings.groupBy;
1491
+ const cursor = isAwsCostCursor(options.cursor) ? options.cursor : void 0;
1492
+ const page = cursor?.page ?? getCostWindow(options, granularity, lookbackDays);
1493
+ const resumeIdx = cursor ? PHASE_ORDER.indexOf(cursor.phase) : 0;
1494
+ const startIdx = resumeIdx >= 0 ? resumeIdx : 0;
1495
+ for (let i = startIdx; i < PHASE_ORDER.length; i++) {
1496
+ const phase = PHASE_ORDER[i];
1497
+ if (signal?.aborted) {
1498
+ return { done: false, cursor: { phase, page } };
1499
+ }
1500
+ if (options.resources && options.resources.size > 0 && !options.resources.has(phase)) {
1501
+ continue;
1502
+ }
1503
+ try {
1504
+ if (phase === "daily_cost") {
1505
+ await this.syncDailyCost(storage, page, granularity, groupBy, signal);
1506
+ } else {
1507
+ await this.syncForecast(storage, granularity, signal);
1508
+ }
1509
+ } catch (err) {
1510
+ if (signal?.aborted) {
1511
+ return { done: false, cursor: { phase, page } };
1512
+ }
1513
+ throw err;
1514
+ }
1515
+ }
1516
+ return { done: true };
1517
+ }
1518
+ };
1519
+
1520
+ // src/index.ts
1521
+ var index_default = AwsCostConnector;
1522
+ export {
1523
+ AwsCostConnector,
1524
+ buildDailyCostSamples,
1525
+ buildForecastSamples,
1526
+ configFields,
1527
+ index_default as default,
1528
+ getCostWindow
1529
+ };
1530
+ //# sourceMappingURL=index.js.map