@rawdash/connector-aws-cost 0.15.0 → 0.17.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 CHANGED
@@ -1,9 +1,6 @@
1
1
  // ../aws-shared/dist/index.js
2
+ import { BaseConnector } from "@rawdash/core";
2
3
  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
4
  var encoder = new TextEncoder();
8
5
  var ALGORITHM = "AWS4-HMAC-SHA256";
9
6
  function u8(data) {
@@ -162,648 +159,6 @@ function parseEpoch(value, unit) {
162
159
  const result = unit === "s" ? n * 1e3 : n;
163
160
  return Number.isFinite(result) ? result : null;
164
161
  }
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
162
  var awsCredentialsSchema = {
808
163
  accessKeyId: {
809
164
  description: "AWS access key ID",
@@ -979,7 +334,7 @@ var BaseAWSConnector = class extends BaseConnector {
979
334
  }
980
335
  };
981
336
  var awsAuthConfigShape = {
982
- region: z5.string().regex(
337
+ region: z.string().regex(
983
338
  /^[a-z0-9-]+$/,
984
339
  "region must look like an AWS region, e.g. us-east-1"
985
340
  ).meta({
@@ -987,17 +342,17 @@ var awsAuthConfigShape = {
987
342
  description: "The AWS region whose service endpoint you want to call, e.g. us-east-1.",
988
343
  placeholder: "us-east-1"
989
344
  }),
990
- accessKeyId: z5.object({ $secret: z5.string() }).optional().meta({
345
+ accessKeyId: z.object({ $secret: z.string() }).optional().meta({
991
346
  label: "Access Key ID",
992
347
  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
348
  secret: true
994
349
  }),
995
- secretAccessKey: z5.object({ $secret: z5.string() }).optional().meta({
350
+ secretAccessKey: z.object({ $secret: z.string() }).optional().meta({
996
351
  label: "Secret Access Key",
997
352
  description: "AWS secret access key paired with the access key ID above.",
998
353
  secret: true
999
354
  }),
1000
- roleArn: z5.string().regex(
355
+ roleArn: z.string().regex(
1001
356
  /^arn:aws:iam::\d{12}:role\/.+/,
1002
357
  "roleArn must be a full IAM role ARN, e.g. arn:aws:iam::123456789012:role/rawdash"
1003
358
  ).optional().meta({
@@ -1005,7 +360,7 @@ var awsAuthConfigShape = {
1005
360
  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
361
  placeholder: "arn:aws:iam::123456789012:role/rawdash"
1007
362
  }),
1008
- externalId: z5.string().min(1).optional().meta({
363
+ externalId: z.string().min(1).optional().meta({
1009
364
  label: "External ID",
1010
365
  description: "External ID required by the trust policy of the role being assumed. Only used with Role ARN."
1011
366
  })
@@ -1023,7 +378,7 @@ var awsAuthRefine = {
1023
378
  };
1024
379
 
1025
380
  // ../../connector-shared/dist/index.js
1026
- var HttpClientError3 = class extends Error {
381
+ var HttpClientError2 = class extends Error {
1027
382
  response;
1028
383
  constructor(message, response) {
1029
384
  super(message);
@@ -1031,10 +386,10 @@ var HttpClientError3 = class extends Error {
1031
386
  this.response = response;
1032
387
  }
1033
388
  };
1034
- var TransientError3 = class extends HttpClientError3 {
389
+ var TransientError2 = class extends HttpClientError2 {
1035
390
  kind = "transient";
1036
391
  };
1037
- var RateLimitError3 = class extends HttpClientError3 {
392
+ var RateLimitError2 = class extends HttpClientError2 {
1038
393
  kind = "rate_limit";
1039
394
  retryAfter;
1040
395
  constructor(message, response, retryAfter) {
@@ -1042,30 +397,33 @@ var RateLimitError3 = class extends HttpClientError3 {
1042
397
  this.retryAfter = retryAfter;
1043
398
  }
1044
399
  };
1045
- var AuthError3 = class extends HttpClientError3 {
400
+ var AuthError2 = class extends HttpClientError2 {
1046
401
  kind = "auth";
1047
402
  };
1048
- var HTTP_CLIENT_VERSION3 = "0.0.0";
1049
- var DEFAULT_USER_AGENT3 = `rawdash-connector/${HTTP_CLIENT_VERSION3} (+https://rawdash.dev)`;
403
+ var HTTP_CLIENT_VERSION2 = "0.0.0";
404
+ var DEFAULT_USER_AGENT2 = `rawdash-connector/${HTTP_CLIENT_VERSION2} (+https://rawdash.dev)`;
1050
405
  function connectorUserAgent2(connectorId) {
1051
- return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION3} (+https://rawdash.dev)`;
406
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION2} (+https://rawdash.dev)`;
1052
407
  }
1053
408
 
1054
409
  // src/aws-cost.ts
1055
410
  import {
1056
- defineConfigFields
411
+ defineConfigFields,
412
+ defineConnectorDoc,
413
+ defineResources,
414
+ schemasFromResources
1057
415
  } from "@rawdash/core";
1058
- import { z as z6 } from "zod";
416
+ import { z as z2 } from "zod";
1059
417
  var { region: _region, ...awsAuthWithoutRegion } = awsAuthConfigShape;
1060
418
  var configFields = defineConfigFields(
1061
- z6.object({
419
+ z2.object({
1062
420
  ...awsAuthWithoutRegion,
1063
- granularity: z6.enum(["DAILY", "MONTHLY"]).optional().meta({
421
+ granularity: z2.enum(["DAILY", "MONTHLY"]).optional().meta({
1064
422
  label: "Granularity",
1065
423
  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
424
  }),
1067
- groupBy: z6.array(
1068
- z6.string().regex(
425
+ groupBy: z2.array(
426
+ z2.string().regex(
1069
427
  /^(SERVICE|LINKED_ACCOUNT|TAG:.+|COST_CATEGORY:.+)$/,
1070
428
  "groupBy entries must be SERVICE, LINKED_ACCOUNT, TAG:<key>, or COST_CATEGORY:<key>"
1071
429
  )
@@ -1073,7 +431,7 @@ var configFields = defineConfigFields(
1073
431
  label: "Group by (optional)",
1074
432
  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
433
  }),
1076
- lookbackDays: z6.number().int().positive().optional().meta({
434
+ lookbackDays: z2.number().int().positive().optional().meta({
1077
435
  label: "Backfill window (days)",
1078
436
  description: "How many days of history to fetch on a full sync. Defaults to 90.",
1079
437
  placeholder: "90"
@@ -1082,6 +440,31 @@ var configFields = defineConfigFields(
1082
440
  message: awsAuthRefine.message
1083
441
  })
1084
442
  );
443
+ var doc = defineConnectorDoc({
444
+ displayName: "AWS Cost Explorer",
445
+ category: "finance",
446
+ brandColor: "#6CAE3E",
447
+ tagline: "Track AWS spend over time and projected month-end costs, optionally broken down by service, account, tag, or cost category.",
448
+ vendor: {
449
+ name: "Amazon Web Services",
450
+ apiDocs: "https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_Operations_AWS_Cost_Explorer_Service.html",
451
+ website: "https://aws.amazon.com/aws-cost-management/aws-cost-explorer/"
452
+ },
453
+ auth: {
454
+ summary: "Authenticate either with a long-lived IAM access key pair or by assuming an IAM role (Role ARN with an optional External ID). The principal needs the `ce:GetCostAndUsage` and `ce:GetCostForecast` permissions. Cost Explorer is a global service reached through its us-east-1 endpoint.",
455
+ setup: [
456
+ "In the AWS console, create an IAM user or role granting `ce:GetCostAndUsage` and `ce:GetCostForecast`.",
457
+ 'For access-key auth, generate an access key pair and store both halves as secrets, then reference them as `accessKeyId: secret("AWS_ACCESS_KEY_ID")` and `secretAccessKey: secret("AWS_SECRET_ACCESS_KEY")`.',
458
+ "For role-assumption auth, set `roleArn` to the role to assume and (if configured) `externalId` to the role\u2019s expected external ID.",
459
+ "Cost Explorer must be enabled for the account; the first activation can take up to 24 hours before data is queryable."
460
+ ]
461
+ },
462
+ rateLimit: "Cost Explorer throttling (ThrottlingException) is retried with backoff. Cost Explorer is global and always reached via ce.us-east-1.amazonaws.com.",
463
+ limitations: [
464
+ "Cost Explorer data can be revised for a couple of days after the fact, so incremental syncs refetch a short trailing window.",
465
+ "Forecast is unavailable for brand-new accounts (DataUnavailableException is treated as no forecast, not an error)."
466
+ ]
467
+ });
1085
468
  var AWS_REGION = "us-east-1";
1086
469
  var CE_HOST = "ce.us-east-1.amazonaws.com";
1087
470
  var CE_URL = `https://${CE_HOST}/`;
@@ -1094,30 +477,30 @@ var DEFAULT_BACKFILL_DAYS = 90;
1094
477
  var INCREMENTAL_LOOKBACK_DAYS = 3;
1095
478
  var MS_PER_DAY = 864e5;
1096
479
  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 })
480
+ var amountString = z2.string().regex(/^-?\d+(\.\d+)?$/);
481
+ var ceDateString = z2.string().regex(/^\d{4}-\d{2}-\d{2}$/);
482
+ var metricAmount = z2.object({ Amount: amountString, Unit: z2.string() });
483
+ var getCostAndUsageResponse = z2.object({
484
+ ResultsByTime: z2.array(
485
+ z2.object({
486
+ TimePeriod: z2.object({ Start: ceDateString, End: ceDateString }),
487
+ Total: z2.object({ UnblendedCost: metricAmount.optional() }).optional(),
488
+ Groups: z2.array(
489
+ z2.object({
490
+ Keys: z2.array(z2.string()),
491
+ Metrics: z2.object({ UnblendedCost: metricAmount })
1109
492
  })
1110
493
  ).optional(),
1111
- Estimated: z6.boolean().optional()
494
+ Estimated: z2.boolean().optional()
1112
495
  })
1113
496
  ),
1114
- NextPageToken: z6.string().optional()
497
+ NextPageToken: z2.string().optional()
1115
498
  });
1116
- var getCostForecastResponse = z6.object({
499
+ var getCostForecastResponse = z2.object({
1117
500
  Total: metricAmount.optional(),
1118
- ForecastResultsByTime: z6.array(
1119
- z6.object({
1120
- TimePeriod: z6.object({ Start: ceDateString, End: ceDateString }),
501
+ ForecastResultsByTime: z2.array(
502
+ z2.object({
503
+ TimePeriod: z2.object({ Start: ceDateString, End: ceDateString }),
1121
504
  MeanValue: amountString,
1122
505
  PredictionIntervalLowerBound: amountString.optional(),
1123
506
  PredictionIntervalUpperBound: amountString.optional()
@@ -1154,15 +537,15 @@ function mapAwsJsonError(err) {
1154
537
  const type = extractAwsErrorType(httpError);
1155
538
  const status = httpError.response?.status ?? 0;
1156
539
  if (/throttl|TooManyRequests|RequestLimitExceeded/i.test(type) || status === 429) {
1157
- return new RateLimitError3(httpError.message, httpError.response);
540
+ return new RateLimitError2(httpError.message, httpError.response);
1158
541
  }
1159
542
  if (/AccessDenied|UnrecognizedClient|InvalidClientTokenId|SignatureDoesNotMatch|AuthFailure|InvalidSignature|ExpiredToken/i.test(
1160
543
  type
1161
544
  ) || status === 403) {
1162
- return new AuthError3(httpError.message, httpError.response);
545
+ return new AuthError2(httpError.message, httpError.response);
1163
546
  }
1164
547
  if (status >= 500) {
1165
- return new TransientError3(httpError.message, httpError.response);
548
+ return new TransientError2(httpError.message, httpError.response);
1166
549
  }
1167
550
  return err;
1168
551
  }
@@ -1235,12 +618,12 @@ function buildDailyCostSamples(body, granularity, groupBy) {
1235
618
  const groups = result.Groups ?? [];
1236
619
  if (groups.length > 0) {
1237
620
  for (const group of groups) {
1238
- const cost2 = group.Metrics?.["UnblendedCost"];
621
+ const cost3 = group.Metrics?.["UnblendedCost"];
1239
622
  const keys = group.Keys ?? [];
1240
623
  const attributes = {
1241
624
  granularity,
1242
625
  estimated,
1243
- unit: cost2?.Unit ?? "USD"
626
+ unit: cost3?.Unit ?? "USD"
1244
627
  };
1245
628
  for (let i = 0; i < keys.length; i++) {
1246
629
  attributes[groupAttrName(groupBy, i)] = keys[i] ?? null;
@@ -1248,21 +631,21 @@ function buildDailyCostSamples(body, granularity, groupBy) {
1248
631
  samples.push({
1249
632
  name: DAILY_METRIC_NAME,
1250
633
  ts,
1251
- value: parseAmount(cost2?.Amount),
634
+ value: parseAmount(cost3?.Amount),
1252
635
  attributes
1253
636
  });
1254
637
  }
1255
638
  continue;
1256
639
  }
1257
- const cost = result.Total?.["UnblendedCost"];
1258
- if (!cost) {
640
+ const cost2 = result.Total?.["UnblendedCost"];
641
+ if (!cost2) {
1259
642
  continue;
1260
643
  }
1261
644
  samples.push({
1262
645
  name: DAILY_METRIC_NAME,
1263
646
  ts,
1264
- value: parseAmount(cost.Amount),
1265
- attributes: { granularity, estimated, unit: cost.Unit ?? "USD" }
647
+ value: parseAmount(cost2.Amount),
648
+ attributes: { granularity, estimated, unit: cost2.Unit ?? "USD" }
1266
649
  });
1267
650
  }
1268
651
  return samples;
@@ -1350,12 +733,74 @@ function isAwsCostCursor(value) {
1350
733
  }
1351
734
  return typeof p.start === "string" && typeof p.end === "string";
1352
735
  }
736
+ var awsCostResources = defineResources({
737
+ aws_cost_daily: {
738
+ shape: "metric",
739
+ description: "Historical unblended AWS cost per time bucket, optionally split across the configured group-by dimensions. The current bucket is estimated and overwritten on later syncs as it finalizes.",
740
+ endpoint: "POST GetCostAndUsage",
741
+ unit: "USD",
742
+ granularity: "daily",
743
+ notes: "Prefer MONTHLY granularity over long windows since each Cost Explorer query is billed. Cost Explorer accepts at most two group-by dimensions per query.",
744
+ dimensions: [
745
+ {
746
+ name: "granularity",
747
+ description: "Bucket granularity, DAILY or MONTHLY."
748
+ },
749
+ {
750
+ name: "estimated",
751
+ description: "Whether the bucket is still estimated rather than finalized."
752
+ },
753
+ {
754
+ name: "unit",
755
+ description: "Currency unit reported by AWS, e.g. USD."
756
+ },
757
+ {
758
+ name: "service",
759
+ description: "AWS service name, present when grouping by SERVICE (other group-by dimensions appear as linked_account, tag_<key>, or cost_category_<key>)."
760
+ }
761
+ ],
762
+ responses: { daily_cost: getCostAndUsageResponse }
763
+ },
764
+ aws_cost_forecast: {
765
+ shape: "metric",
766
+ description: "Projected future unblended AWS cost (mean value) with optional lower and upper prediction-interval bounds. Empty when the account has insufficient history to forecast.",
767
+ endpoint: "POST GetCostForecast",
768
+ unit: "USD",
769
+ granularity: "daily",
770
+ notes: "Prefer MONTHLY granularity over long windows since each Cost Explorer query is billed.",
771
+ dimensions: [
772
+ {
773
+ name: "granularity",
774
+ description: "Bucket granularity, DAILY or MONTHLY."
775
+ },
776
+ {
777
+ name: "unit",
778
+ description: "Currency unit reported by AWS, e.g. USD."
779
+ },
780
+ {
781
+ name: "lowerBound",
782
+ description: "Lower bound of the prediction interval, if provided."
783
+ },
784
+ {
785
+ name: "upperBound",
786
+ description: "Upper bound of the prediction interval, if provided."
787
+ }
788
+ ],
789
+ responses: { forecast: getCostForecastResponse }
790
+ }
791
+ });
792
+ var id = "aws-cost";
793
+ var cost = {
794
+ recommendedInterval: "1 day",
795
+ minInterval: "1 hour",
796
+ perSync: "2 Cost Explorer queries (about $0.02)",
797
+ warning: "Each AWS Cost Explorer query is billed $0.01; avoid syncing more often than necessary."
798
+ };
1353
799
  var AwsCostConnector = class _AwsCostConnector extends BaseAWSConnector {
1354
- static id = "aws-cost";
1355
- static schemas = {
1356
- daily_cost: getCostAndUsageResponse,
1357
- forecast: getCostForecastResponse
1358
- };
800
+ static id = id;
801
+ static resources = awsCostResources;
802
+ static schemas = schemasFromResources(awsCostResources);
803
+ static cost = cost;
1359
804
  static create(input, ctx) {
1360
805
  const parsed = configFields.parse(input);
1361
806
  return new _AwsCostConnector(
@@ -1374,7 +819,7 @@ var AwsCostConnector = class _AwsCostConnector extends BaseAWSConnector {
1374
819
  ctx
1375
820
  );
1376
821
  }
1377
- id = "aws-cost";
822
+ id = id;
1378
823
  async callCostExplorer(action, payload, resource, signal) {
1379
824
  const credentials = await this.resolveSigningCredentials(signal);
1380
825
  const body = JSON.stringify(payload);
@@ -1524,7 +969,11 @@ export {
1524
969
  buildDailyCostSamples,
1525
970
  buildForecastSamples,
1526
971
  configFields,
972
+ cost,
1527
973
  index_default as default,
1528
- getCostWindow
974
+ doc,
975
+ getCostWindow,
976
+ id,
977
+ awsCostResources as resources
1529
978
  };
1530
979
  //# sourceMappingURL=index.js.map