@rawdash/connector-aws-cost 0.16.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.d.ts CHANGED
@@ -64,6 +64,96 @@ interface CostWindow {
64
64
  end: string;
65
65
  }
66
66
  declare function getCostWindow(options: SyncOptions, granularity: 'DAILY' | 'MONTHLY', lookbackDays: number, now?: number): CostWindow;
67
+ declare const awsCostResources: {
68
+ readonly aws_cost_daily: {
69
+ readonly shape: "metric";
70
+ readonly 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.";
71
+ readonly endpoint: "POST GetCostAndUsage";
72
+ readonly unit: "USD";
73
+ readonly granularity: "daily";
74
+ readonly 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.";
75
+ readonly dimensions: [{
76
+ readonly name: "granularity";
77
+ readonly description: "Bucket granularity, DAILY or MONTHLY.";
78
+ }, {
79
+ readonly name: "estimated";
80
+ readonly description: "Whether the bucket is still estimated rather than finalized.";
81
+ }, {
82
+ readonly name: "unit";
83
+ readonly description: "Currency unit reported by AWS, e.g. USD.";
84
+ }, {
85
+ readonly name: "service";
86
+ readonly description: "AWS service name, present when grouping by SERVICE (other group-by dimensions appear as linked_account, tag_<key>, or cost_category_<key>).";
87
+ }];
88
+ readonly responses: {
89
+ readonly daily_cost: z.ZodObject<{
90
+ ResultsByTime: z.ZodArray<z.ZodObject<{
91
+ TimePeriod: z.ZodObject<{
92
+ Start: z.ZodString;
93
+ End: z.ZodString;
94
+ }, z.core.$strip>;
95
+ Total: z.ZodOptional<z.ZodObject<{
96
+ UnblendedCost: z.ZodOptional<z.ZodObject<{
97
+ Amount: z.ZodString;
98
+ Unit: z.ZodString;
99
+ }, z.core.$strip>>;
100
+ }, z.core.$strip>>;
101
+ Groups: z.ZodOptional<z.ZodArray<z.ZodObject<{
102
+ Keys: z.ZodArray<z.ZodString>;
103
+ Metrics: z.ZodObject<{
104
+ UnblendedCost: z.ZodObject<{
105
+ Amount: z.ZodString;
106
+ Unit: z.ZodString;
107
+ }, z.core.$strip>;
108
+ }, z.core.$strip>;
109
+ }, z.core.$strip>>>;
110
+ Estimated: z.ZodOptional<z.ZodBoolean>;
111
+ }, z.core.$strip>>;
112
+ NextPageToken: z.ZodOptional<z.ZodString>;
113
+ }, z.core.$strip>;
114
+ };
115
+ };
116
+ readonly aws_cost_forecast: {
117
+ readonly shape: "metric";
118
+ readonly 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.";
119
+ readonly endpoint: "POST GetCostForecast";
120
+ readonly unit: "USD";
121
+ readonly granularity: "daily";
122
+ readonly notes: "Prefer MONTHLY granularity over long windows since each Cost Explorer query is billed.";
123
+ readonly dimensions: [{
124
+ readonly name: "granularity";
125
+ readonly description: "Bucket granularity, DAILY or MONTHLY.";
126
+ }, {
127
+ readonly name: "unit";
128
+ readonly description: "Currency unit reported by AWS, e.g. USD.";
129
+ }, {
130
+ readonly name: "lowerBound";
131
+ readonly description: "Lower bound of the prediction interval, if provided.";
132
+ }, {
133
+ readonly name: "upperBound";
134
+ readonly description: "Upper bound of the prediction interval, if provided.";
135
+ }];
136
+ readonly responses: {
137
+ readonly forecast: z.ZodObject<{
138
+ Total: z.ZodOptional<z.ZodObject<{
139
+ Amount: z.ZodString;
140
+ Unit: z.ZodString;
141
+ }, z.core.$strip>>;
142
+ ForecastResultsByTime: z.ZodOptional<z.ZodArray<z.ZodObject<{
143
+ TimePeriod: z.ZodObject<{
144
+ Start: z.ZodString;
145
+ End: z.ZodString;
146
+ }, z.core.$strip>;
147
+ MeanValue: z.ZodString;
148
+ PredictionIntervalLowerBound: z.ZodOptional<z.ZodString>;
149
+ PredictionIntervalUpperBound: z.ZodOptional<z.ZodString>;
150
+ }, z.core.$strip>>>;
151
+ }, z.core.$strip>;
152
+ };
153
+ };
154
+ };
155
+ declare const id = "aws-cost";
156
+ declare const cost: ConnectorCost;
67
157
  declare class AwsCostConnector extends BaseAWSConnector<AwsCostSettings> {
68
158
  static readonly id = "aws-cost";
69
159
  static readonly resources: {
@@ -207,4 +297,4 @@ declare class AwsCostConnector extends BaseAWSConnector<AwsCostSettings> {
207
297
  sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
208
298
  }
209
299
 
210
- export { AwsCostConnector, type AwsCostSettings, buildDailyCostSamples, buildForecastSamples, configFields, AwsCostConnector as default, doc, getCostWindow };
300
+ export { AwsCostConnector, type AwsCostSettings, buildDailyCostSamples, buildForecastSamples, configFields, cost, AwsCostConnector as default, doc, getCostWindow, id, awsCostResources as resources };
package/dist/index.js CHANGED
@@ -618,12 +618,12 @@ function buildDailyCostSamples(body, granularity, groupBy) {
618
618
  const groups = result.Groups ?? [];
619
619
  if (groups.length > 0) {
620
620
  for (const group of groups) {
621
- const cost2 = group.Metrics?.["UnblendedCost"];
621
+ const cost3 = group.Metrics?.["UnblendedCost"];
622
622
  const keys = group.Keys ?? [];
623
623
  const attributes = {
624
624
  granularity,
625
625
  estimated,
626
- unit: cost2?.Unit ?? "USD"
626
+ unit: cost3?.Unit ?? "USD"
627
627
  };
628
628
  for (let i = 0; i < keys.length; i++) {
629
629
  attributes[groupAttrName(groupBy, i)] = keys[i] ?? null;
@@ -631,21 +631,21 @@ function buildDailyCostSamples(body, granularity, groupBy) {
631
631
  samples.push({
632
632
  name: DAILY_METRIC_NAME,
633
633
  ts,
634
- value: parseAmount(cost2?.Amount),
634
+ value: parseAmount(cost3?.Amount),
635
635
  attributes
636
636
  });
637
637
  }
638
638
  continue;
639
639
  }
640
- const cost = result.Total?.["UnblendedCost"];
641
- if (!cost) {
640
+ const cost2 = result.Total?.["UnblendedCost"];
641
+ if (!cost2) {
642
642
  continue;
643
643
  }
644
644
  samples.push({
645
645
  name: DAILY_METRIC_NAME,
646
646
  ts,
647
- value: parseAmount(cost.Amount),
648
- attributes: { granularity, estimated, unit: cost.Unit ?? "USD" }
647
+ value: parseAmount(cost2.Amount),
648
+ attributes: { granularity, estimated, unit: cost2.Unit ?? "USD" }
649
649
  });
650
650
  }
651
651
  return samples;
@@ -789,16 +789,18 @@ var awsCostResources = defineResources({
789
789
  responses: { forecast: getCostForecastResponse }
790
790
  }
791
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
+ };
792
799
  var AwsCostConnector = class _AwsCostConnector extends BaseAWSConnector {
793
- static id = "aws-cost";
800
+ static id = id;
794
801
  static resources = awsCostResources;
795
802
  static schemas = schemasFromResources(awsCostResources);
796
- static cost = {
797
- recommendedInterval: "1 day",
798
- minInterval: "1 hour",
799
- perSync: "2 Cost Explorer queries (about $0.02)",
800
- warning: "Each AWS Cost Explorer query is billed $0.01; avoid syncing more often than necessary."
801
- };
803
+ static cost = cost;
802
804
  static create(input, ctx) {
803
805
  const parsed = configFields.parse(input);
804
806
  return new _AwsCostConnector(
@@ -817,7 +819,7 @@ var AwsCostConnector = class _AwsCostConnector extends BaseAWSConnector {
817
819
  ctx
818
820
  );
819
821
  }
820
- id = "aws-cost";
822
+ id = id;
821
823
  async callCostExplorer(action, payload, resource, signal) {
822
824
  const credentials = await this.resolveSigningCredentials(signal);
823
825
  const body = JSON.stringify(payload);
@@ -967,8 +969,11 @@ export {
967
969
  buildDailyCostSamples,
968
970
  buildForecastSamples,
969
971
  configFields,
972
+ cost,
970
973
  index_default as default,
971
974
  doc,
972
- getCostWindow
975
+ getCostWindow,
976
+ id,
977
+ awsCostResources as resources
973
978
  };
974
979
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../aws-shared/src/sigv4.ts","../../aws-shared/src/xml.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../../aws-shared/src/base-aws-connector.ts","../../aws-shared/src/config.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/aws-cost.ts","../src/index.ts"],"sourcesContent":["// AWS Signature Version 4 signing, implemented against the Web Crypto API so\n// the connector carries no AWS SDK dependency. See\n// https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html.\n\nconst encoder = new TextEncoder();\n\nconst ALGORITHM = 'AWS4-HMAC-SHA256';\n\n// Encode to a fresh ArrayBuffer-backed view so the result is a valid\n// `BufferSource` for the Web Crypto APIs under TypeScript's generic typing.\nfunction u8(data: string): Uint8Array<ArrayBuffer> {\n return new Uint8Array(encoder.encode(data));\n}\n\nfunction toHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i]!.toString(16).padStart(2, '0');\n }\n return hex;\n}\n\nexport async function sha256Hex(data: string): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest('SHA-256', u8(data));\n return toHex(digest);\n}\n\nasync function hmac(key: BufferSource, data: string): Promise<ArrayBuffer> {\n const cryptoKey = await globalThis.crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n return globalThis.crypto.subtle.sign('HMAC', cryptoKey, u8(data));\n}\n\nasync function deriveSigningKey(\n secretAccessKey: string,\n dateStamp: string,\n region: string,\n service: string,\n): Promise<ArrayBuffer> {\n const kDate = await hmac(u8(`AWS4${secretAccessKey}`), dateStamp);\n const kRegion = await hmac(kDate, region);\n const kService = await hmac(kRegion, service);\n return hmac(kService, 'aws4_request');\n}\n\nexport interface AmzDate {\n amzDate: string;\n dateStamp: string;\n}\n\n// \"2015-08-30T12:36:00.000Z\" -> { amzDate: \"20150830T123600Z\", dateStamp: \"20150830\" }\nexport function formatAmzDate(date: Date): AmzDate {\n const amzDate = date.toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n return { amzDate, dateStamp: amzDate.slice(0, 8) };\n}\n\nexport interface SignParams {\n method: string;\n host: string;\n path: string;\n query: string;\n headers: Record<string, string>;\n payloadHash: string;\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n service: string;\n amzDate: string;\n dateStamp: string;\n}\n\n// Returns the value for the `Authorization` header. The `headers` map must\n// contain every header that is part of the signature (at minimum `host` and\n// `x-amz-date`); extra unsigned headers sent on the wire are allowed.\nexport async function createAuthorizationHeader(\n params: SignParams,\n): Promise<string> {\n const lowerHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(params.headers)) {\n lowerHeaders[key.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n const sortedNames = Object.keys(lowerHeaders).sort();\n\n const canonicalHeaders = sortedNames\n .map((name) => `${name}:${lowerHeaders[name]}\\n`)\n .join('');\n const signedHeaders = sortedNames.join(';');\n\n const canonicalRequest = [\n params.method,\n params.path,\n params.query,\n canonicalHeaders,\n signedHeaders,\n params.payloadHash,\n ].join('\\n');\n\n const credentialScope = `${params.dateStamp}/${params.region}/${params.service}/aws4_request`;\n const stringToSign = [\n ALGORITHM,\n params.amzDate,\n credentialScope,\n await sha256Hex(canonicalRequest),\n ].join('\\n');\n\n const signingKey = await deriveSigningKey(\n params.secretAccessKey,\n params.dateStamp,\n params.region,\n params.service,\n );\n const signature = toHex(await hmac(signingKey, stringToSign));\n\n return (\n `${ALGORITHM} Credential=${params.accessKeyId}/${credentialScope}, ` +\n `SignedHeaders=${signedHeaders}, Signature=${signature}`\n );\n}\n","// Minimal parser for the handful of AWS Query-protocol (XML) responses this\n// connector consumes: GetMetricData, STS AssumeRole, and error envelopes. It\n// is deliberately narrow — it understands the specific element nesting these\n// responses use rather than being a general-purpose XML parser.\n\nfunction decodeEntities(value: string): string {\n return value\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&amp;/g, '&');\n}\n\n// Inner text of the first `<tag>...</tag>` in `xml`. Returns '' for a\n// self-closing `<tag/>`, and null when the tag is absent. Tags that contain\n// repeated `<member>` children (Timestamps, Values, MetricDataResults) do not\n// nest within themselves, so the first matching close tag is the correct one.\nexport function firstInner(xml: string, tag: string): string | null {\n const escapedTag = tag.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const open = new RegExp(`<${escapedTag}(?:\\\\s[^>]*)?>`).exec(xml);\n if (!open) {\n return new RegExp(`<${escapedTag}\\\\s*/>`).test(xml) ? '' : null;\n }\n const start = open.index + open[0].length;\n const closeIdx = xml.indexOf(`</${tag}>`, start);\n if (closeIdx === -1) {\n return null;\n }\n return xml.slice(start, closeIdx);\n}\n\nexport function firstText(xml: string, tag: string): string | null {\n const inner = firstInner(xml, tag);\n return inner === null ? null : decodeEntities(inner).trim();\n}\n\n// Inner content of each top-level `<member>...</member>`, tracking nesting so\n// that a result member's nested Timestamps/Values members are not mistaken for\n// top-level entries.\nexport function topLevelMembers(xml: string): string[] {\n const results: string[] = [];\n const re = /<member(?:\\s[^>]*)?>|<\\/member>/g;\n let depth = 0;\n let contentStart = -1;\n let match: RegExpExecArray | null;\n while ((match = re.exec(xml)) !== null) {\n if (match[0].startsWith('</')) {\n depth--;\n if (depth === 0 && contentStart !== -1) {\n results.push(xml.slice(contentStart, match.index));\n contentStart = -1;\n }\n } else {\n if (depth === 0) {\n contentStart = match.index + match[0].length;\n }\n depth++;\n }\n }\n return results;\n}\n\nexport interface MetricDataResult {\n id: string;\n label: string;\n statusCode: string;\n timestamps: string[];\n values: number[];\n}\n\nexport interface GetMetricDataParsed {\n results: MetricDataResult[];\n nextToken: string | null;\n}\n\nexport function parseGetMetricData(xml: string): GetMetricDataParsed {\n const resultsBlock = firstInner(xml, 'MetricDataResults') ?? '';\n const results = topLevelMembers(resultsBlock).map((member) => {\n const tsBlock = firstInner(member, 'Timestamps') ?? '';\n const valBlock = firstInner(member, 'Values') ?? '';\n return {\n id: firstText(member, 'Id') ?? '',\n label: firstText(member, 'Label') ?? '',\n statusCode: firstText(member, 'StatusCode') ?? '',\n timestamps: topLevelMembers(tsBlock).map((t) => decodeEntities(t).trim()),\n values: topLevelMembers(valBlock).map((v) =>\n Number(decodeEntities(v).trim()),\n ),\n };\n });\n const nextToken = firstText(xml, 'NextToken');\n return { results, nextToken: nextToken === '' ? null : nextToken };\n}\n\nexport interface StsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken: string;\n expiration: string;\n}\n\nexport function parseAssumeRole(xml: string): StsCredentials | null {\n const credBlock = firstInner(xml, 'Credentials');\n if (credBlock === null) {\n return null;\n }\n const accessKeyId = firstText(credBlock, 'AccessKeyId') ?? '';\n const secretAccessKey = firstText(credBlock, 'SecretAccessKey') ?? '';\n if (accessKeyId === '' || secretAccessKey === '') {\n return null;\n }\n return {\n accessKeyId,\n secretAccessKey,\n sessionToken: firstText(credBlock, 'SessionToken') ?? '',\n expiration: firstText(credBlock, 'Expiration') ?? '',\n };\n}\n\n// AWS Query-protocol error envelopes carry the machine-readable error code in\n// an `<Error><Code>...</Code></Error>` element.\nexport function parseErrorCode(xml: string): string | null {\n return firstText(xml, 'Code');\n}\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n AuthError,\n type HttpClientError,\n type HttpResponse,\n RateLimitError,\n TransientError,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport { BaseConnector, type CredentialsSchema } from '@rawdash/core';\n\nimport { createAuthorizationHeader, formatAmzDate, sha256Hex } from './sigv4';\nimport { type StsCredentials, parseAssumeRole, parseErrorCode } from './xml';\n\nexport interface BaseAWSSettings {\n region: string;\n roleArn?: string;\n externalId?: string;\n}\n\nexport const awsCredentialsSchema = {\n accessKeyId: {\n description: 'AWS access key ID',\n auth: 'optional' as const,\n },\n secretAccessKey: {\n description: 'AWS secret access key',\n auth: 'optional' as const,\n },\n} satisfies CredentialsSchema;\n\nexport type AwsCredentials = typeof awsCredentialsSchema;\n\nexport interface SigningCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n}\n\nconst STS_SERVICE = 'sts';\nconst STS_API_VERSION = '2011-06-15';\nconst ASSUMED_ROLE_TTL_BUFFER_MS = 60_000;\nconst ASSUME_ROLE_DURATION_SECONDS = 3600;\nconst FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8';\n\nfunction readEnv(name: string): string | undefined {\n const env = (\n globalThis as {\n process?: { env?: Record<string, string | undefined> };\n }\n ).process?.env;\n return env?.[name];\n}\n\nexport abstract class BaseAWSConnector<\n TSettings extends BaseAWSSettings,\n> extends BaseConnector<TSettings, AwsCredentials> {\n override readonly credentials = awsCredentialsSchema;\n\n private assumedCreds: {\n value: SigningCredentials;\n expiresAt: number;\n } | null = null;\n\n protected baseCredentials(): SigningCredentials {\n const { accessKeyId, secretAccessKey } = this.creds;\n if (accessKeyId && secretAccessKey) {\n return { accessKeyId, secretAccessKey };\n }\n const envAccessKeyId = readEnv('AWS_ACCESS_KEY_ID');\n const envSecretAccessKey = readEnv('AWS_SECRET_ACCESS_KEY');\n if (envAccessKeyId && envSecretAccessKey) {\n return {\n accessKeyId: envAccessKeyId,\n secretAccessKey: envSecretAccessKey,\n sessionToken: readEnv('AWS_SESSION_TOKEN') || undefined,\n };\n }\n throw new AuthError(\n `${this.id}: no AWS credentials available — provide accessKeyId + secretAccessKey, or set them in the environment for role assumption`,\n );\n }\n\n protected async resolveSigningCredentials(\n signal?: AbortSignal,\n ): Promise<SigningCredentials> {\n if (this.settings.roleArn === undefined) {\n const { accessKeyId, secretAccessKey } = this.creds;\n if (!accessKeyId || !secretAccessKey) {\n throw new AuthError(\n `${this.id}: static-credential auth requires both accessKeyId and secretAccessKey`,\n );\n }\n return { accessKeyId, secretAccessKey };\n }\n\n if (this.assumedCreds && Date.now() < this.assumedCreds.expiresAt) {\n return this.assumedCreds.value;\n }\n return this.assumeRole(this.settings.roleArn, signal);\n }\n\n private async assumeRole(\n roleArn: string,\n signal?: AbortSignal,\n ): Promise<SigningCredentials> {\n const params = new URLSearchParams();\n params.set('Action', 'AssumeRole');\n params.set('Version', STS_API_VERSION);\n params.set('RoleArn', roleArn);\n params.set('RoleSessionName', `rawdash-${this.id}`);\n params.set('DurationSeconds', String(ASSUME_ROLE_DURATION_SECONDS));\n if (this.settings.externalId !== undefined) {\n params.set('ExternalId', this.settings.externalId);\n }\n\n const host = `sts.${this.settings.region}.amazonaws.com`;\n const xml = await this.signedPost({\n host,\n service: STS_SERVICE,\n body: params.toString(),\n signingCredentials: this.baseCredentials(),\n resource: 'assume_role',\n signal,\n });\n\n const parsed = parseAssumeRole(xml);\n if (parsed === null) {\n throw new AuthError(\n `${this.id}: STS AssumeRole returned no usable credentials`,\n );\n }\n this.cacheAssumedCredentials(parsed);\n return {\n accessKeyId: parsed.accessKeyId,\n secretAccessKey: parsed.secretAccessKey,\n sessionToken: parsed.sessionToken || undefined,\n };\n }\n\n private cacheAssumedCredentials(parsed: StsCredentials): void {\n const expirationMs = parseEpoch(parsed.expiration, 'iso');\n const expiresAt =\n expirationMs !== null\n ? expirationMs - ASSUMED_ROLE_TTL_BUFFER_MS\n : Date.now() + (ASSUME_ROLE_DURATION_SECONDS - 60) * 1000;\n this.assumedCreds = {\n value: {\n accessKeyId: parsed.accessKeyId,\n secretAccessKey: parsed.secretAccessKey,\n sessionToken: parsed.sessionToken || undefined,\n },\n expiresAt,\n };\n }\n\n protected async signedPost(args: {\n host: string;\n service: string;\n body: string;\n signingCredentials: SigningCredentials;\n resource: string;\n signal?: AbortSignal;\n }): Promise<string> {\n const { amzDate, dateStamp } = formatAmzDate(new Date());\n const payloadHash = await sha256Hex(args.body);\n\n const signedHeaders: Record<string, string> = {\n host: args.host,\n 'content-type': FORM_CONTENT_TYPE,\n 'x-amz-content-sha256': payloadHash,\n 'x-amz-date': amzDate,\n };\n if (args.signingCredentials.sessionToken !== undefined) {\n signedHeaders['x-amz-security-token'] =\n args.signingCredentials.sessionToken;\n }\n\n const authorization = await createAuthorizationHeader({\n method: 'POST',\n host: args.host,\n path: '/',\n query: '',\n headers: signedHeaders,\n payloadHash,\n accessKeyId: args.signingCredentials.accessKeyId,\n secretAccessKey: args.signingCredentials.secretAccessKey,\n region: this.settings.region,\n service: args.service,\n amzDate,\n dateStamp,\n });\n\n const sendHeaders: Record<string, string> = {\n 'content-type': FORM_CONTENT_TYPE,\n 'x-amz-content-sha256': payloadHash,\n 'x-amz-date': amzDate,\n 'user-agent': connectorUserAgent(this.id),\n Authorization: authorization,\n };\n if (args.signingCredentials.sessionToken !== undefined) {\n sendHeaders['x-amz-security-token'] =\n args.signingCredentials.sessionToken;\n }\n\n try {\n const res: HttpResponse<string> = await this.request<string>(\n {\n url: `https://${args.host}/`,\n method: 'POST',\n headers: sendHeaders,\n body: args.body,\n parseJson: false,\n signal: args.signal,\n },\n { resource: args.resource },\n );\n return res.body;\n } catch (err) {\n throw this.classifyAwsError(err);\n }\n }\n\n protected classifyAwsError(err: unknown): unknown {\n if (!(err instanceof Error) || !('kind' in err)) {\n return err;\n }\n const httpErr = err as HttpClientError;\n const body =\n typeof httpErr.response?.body === 'string' ? httpErr.response.body : '';\n const code = parseErrorCode(body) ?? '';\n const status = httpErr.response?.status ?? 0;\n\n if (\n /throttl|RequestLimitExceeded|TooManyRequests|LimitExceeded/i.test(code)\n ) {\n return new RateLimitError(httpErr.message, httpErr.response);\n }\n if (\n /AccessDenied|UnrecognizedClient|InvalidClientTokenId|SignatureDoesNotMatch|AuthFailure|InvalidAccessKeyId|Forbidden/i.test(\n code,\n )\n ) {\n return new AuthError(httpErr.message, httpErr.response);\n }\n if (status >= 500) {\n return new TransientError(httpErr.message, httpErr.response);\n }\n return err;\n }\n}\n","import { z } from 'zod';\n\nexport const awsAuthConfigShape = {\n region: z\n .string()\n .regex(\n /^[a-z0-9-]+$/,\n 'region must look like an AWS region, e.g. us-east-1',\n )\n .meta({\n label: 'AWS Region',\n description:\n 'The AWS region whose service endpoint you want to call, e.g. us-east-1.',\n placeholder: 'us-east-1',\n }),\n accessKeyId: z.object({ $secret: z.string() }).optional().meta({\n label: 'Access Key ID',\n description:\n '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.',\n secret: true,\n }),\n secretAccessKey: z.object({ $secret: z.string() }).optional().meta({\n label: 'Secret Access Key',\n description: 'AWS secret access key paired with the access key ID above.',\n secret: true,\n }),\n roleArn: z\n .string()\n .regex(\n /^arn:aws:iam::\\d{12}:role\\/.+/,\n 'roleArn must be a full IAM role ARN, e.g. arn:aws:iam::123456789012:role/rawdash',\n )\n .optional()\n .meta({\n label: 'Role ARN',\n description:\n '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.',\n placeholder: 'arn:aws:iam::123456789012:role/rawdash',\n }),\n externalId: z.string().min(1).optional().meta({\n label: 'External ID',\n description:\n 'External ID required by the trust policy of the role being assumed. Only used with Role ARN.',\n }),\n} as const;\n\nexport interface AwsAuthConfig {\n region: string;\n accessKeyId?: { $secret: string };\n secretAccessKey?: { $secret: string };\n roleArn?: string;\n externalId?: string;\n}\n\nexport const awsAuthRefine = {\n predicate: (val: AwsAuthConfig): boolean => {\n const hasRole = val.roleArn !== undefined;\n const hasStatic =\n val.accessKeyId !== undefined && val.secretAccessKey !== undefined;\n if (val.externalId !== undefined && !hasRole) {\n return false;\n }\n return hasRole || hasStatic;\n },\n message:\n 'Provide either accessKeyId + secretAccessKey (static credentials) or roleArn (role assumption). externalId requires roleArn.',\n} as const;\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n BaseAWSConnector,\n type BaseAWSSettings,\n type SigningCredentials,\n awsAuthConfigShape,\n awsAuthRefine,\n createAuthorizationHeader,\n formatAmzDate,\n sha256Hex,\n} from '@rawdash/connector-aws-shared';\nimport {\n AuthError,\n type HttpResponse,\n RateLimitError,\n TransientError,\n connectorUserAgent,\n} from '@rawdash/connector-shared';\nimport {\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorCost,\n type ConnectorDoc,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n schemasFromResources,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n//\n// Cost Explorer is a global service reached through its us-east-1 endpoint, so\n// `region` is hardcoded rather than exposed in the connector config.\n\nconst { region: _region, ...awsAuthWithoutRegion } = awsAuthConfigShape;\n\nexport const configFields = defineConfigFields(\n z\n .object({\n ...awsAuthWithoutRegion,\n granularity: z.enum(['DAILY', 'MONTHLY']).optional().meta({\n label: 'Granularity',\n description:\n '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.',\n }),\n groupBy: z\n .array(\n z\n .string()\n .regex(\n /^(SERVICE|LINKED_ACCOUNT|TAG:.+|COST_CATEGORY:.+)$/,\n 'groupBy entries must be SERVICE, LINKED_ACCOUNT, TAG:<key>, or COST_CATEGORY:<key>',\n ),\n )\n .max(2, 'Cost Explorer accepts at most two group-by dimensions')\n .optional()\n .meta({\n label: 'Group by (optional)',\n description:\n 'Up to two Cost Explorer dimensions to break costs down by, e.g. SERVICE, LINKED_ACCOUNT, or TAG:Environment. Omit for total cost only.',\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Backfill window (days)',\n description:\n 'How many days of history to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n })\n .refine((val) => awsAuthRefine.predicate({ ...val, region: AWS_REGION }), {\n message: awsAuthRefine.message,\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'AWS Cost Explorer',\n category: 'finance',\n brandColor: '#6CAE3E',\n tagline:\n 'Track AWS spend over time and projected month-end costs, optionally broken down by service, account, tag, or cost category.',\n vendor: {\n name: 'Amazon Web Services',\n apiDocs:\n 'https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_Operations_AWS_Cost_Explorer_Service.html',\n website: 'https://aws.amazon.com/aws-cost-management/aws-cost-explorer/',\n },\n auth: {\n summary:\n '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.',\n setup: [\n 'In the AWS console, create an IAM user or role granting `ce:GetCostAndUsage` and `ce:GetCostForecast`.',\n '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\")`.',\n 'For role-assumption auth, set `roleArn` to the role to assume and (if configured) `externalId` to the role’s expected external ID.',\n 'Cost Explorer must be enabled for the account; the first activation can take up to 24 hours before data is queryable.',\n ],\n },\n rateLimit:\n 'Cost Explorer throttling (ThrottlingException) is retried with backoff. Cost Explorer is global and always reached via ce.us-east-1.amazonaws.com.',\n limitations: [\n 'Cost Explorer data can be revised for a couple of days after the fact, so incremental syncs refetch a short trailing window.',\n 'Forecast is unavailable for brand-new accounts (DataUnavailableException is treated as no forecast, not an error).',\n ],\n});\n\nexport interface AwsCostSettings extends BaseAWSSettings {\n granularity?: 'DAILY' | 'MONTHLY';\n groupBy?: readonly string[];\n lookbackDays?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst AWS_REGION = 'us-east-1';\nconst CE_HOST = 'ce.us-east-1.amazonaws.com';\nconst CE_URL = `https://${CE_HOST}/`;\nconst CE_SERVICE = 'ce';\nconst CE_CONTENT_TYPE = 'application/x-amz-json-1.1';\nconst CE_TARGET_PREFIX = 'AWSInsightsIndexService';\n\nconst DAILY_METRIC_NAME = 'aws_cost_daily';\nconst FORECAST_METRIC_NAME = 'aws_cost_forecast';\n\nconst DEFAULT_BACKFILL_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 3;\nconst MS_PER_DAY = 86_400_000;\n\nconst PHASE_ORDER = ['daily_cost', 'forecast'] as const;\ntype AwsCostPhase = (typeof PHASE_ORDER)[number];\n\n// ---------------------------------------------------------------------------\n// Schemas — describe the per-resource API response shape consumed by request()\n// ---------------------------------------------------------------------------\n\nconst amountString = z.string().regex(/^-?\\d+(\\.\\d+)?$/);\nconst ceDateString = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/);\nconst metricAmount = z.object({ Amount: amountString, Unit: z.string() });\n\nconst getCostAndUsageResponse = z.object({\n ResultsByTime: z.array(\n z.object({\n TimePeriod: z.object({ Start: ceDateString, End: ceDateString }),\n Total: z.object({ UnblendedCost: metricAmount.optional() }).optional(),\n Groups: z\n .array(\n z.object({\n Keys: z.array(z.string()),\n Metrics: z.object({ UnblendedCost: metricAmount }),\n }),\n )\n .optional(),\n Estimated: z.boolean().optional(),\n }),\n ),\n NextPageToken: z.string().optional(),\n});\n\nconst getCostForecastResponse = z.object({\n Total: metricAmount.optional(),\n ForecastResultsByTime: z\n .array(\n z.object({\n TimePeriod: z.object({ Start: ceDateString, End: ceDateString }),\n MeanValue: amountString,\n PredictionIntervalLowerBound: amountString.optional(),\n PredictionIntervalUpperBound: amountString.optional(),\n }),\n )\n .optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Runtime response shapes (intentionally permissive — the wire format is\n// `application/x-amz-json-1.1` which the shared client returns as a string,\n// so these are parsed defensively rather than trusted)\n// ---------------------------------------------------------------------------\n\ninterface CostMetricAmount {\n Amount?: string;\n Unit?: string;\n}\ninterface ResultByTime {\n TimePeriod?: { Start?: string; End?: string };\n Total?: Record<string, CostMetricAmount | undefined>;\n Groups?: Array<{\n Keys?: string[];\n Metrics?: Record<string, CostMetricAmount | undefined>;\n }>;\n Estimated?: boolean;\n}\ninterface GetCostAndUsageBody {\n ResultsByTime?: ResultByTime[];\n NextPageToken?: string;\n}\ninterface ForecastResult {\n TimePeriod?: { Start?: string; End?: string };\n MeanValue?: string;\n PredictionIntervalLowerBound?: string;\n PredictionIntervalUpperBound?: string;\n}\ninterface GetCostForecastBody {\n Total?: CostMetricAmount;\n ForecastResultsByTime?: ForecastResult[];\n}\n\n// ---------------------------------------------------------------------------\n// Error mapping — Cost Explorer signals failures via the JSON `__type` field;\n// translate them into the shared error contract the runner understands.\n// Detection is structural (`.kind` + `.response`) rather than `instanceof`,\n// because the shared error classes are bundled per-package — an `instanceof`\n// check against this package's copy would miss errors thrown by core's copy.\n// ---------------------------------------------------------------------------\n\ninterface HttpErrorLike {\n message: string;\n response?: HttpResponse;\n}\n\nfunction asHttpError(err: unknown): HttpErrorLike | null {\n if (\n err instanceof Error &&\n 'kind' in err &&\n typeof (err as { kind?: unknown }).kind === 'string'\n ) {\n return err as unknown as HttpErrorLike;\n }\n return null;\n}\n\nfunction extractAwsErrorType(err: HttpErrorLike): string {\n const body = err.response?.body;\n if (typeof body === 'string') {\n try {\n const parsed = JSON.parse(body) as { __type?: string; Code?: string };\n return parsed.__type ?? parsed.Code ?? body;\n } catch {\n return body;\n }\n }\n if (body && typeof body === 'object') {\n const o = body as { __type?: unknown; Code?: unknown };\n return String(o.__type ?? o.Code ?? '');\n }\n return '';\n}\n\nfunction mapAwsJsonError(err: unknown): unknown {\n const httpError = asHttpError(err);\n if (!httpError) {\n return err;\n }\n const type = extractAwsErrorType(httpError);\n const status = httpError.response?.status ?? 0;\n if (\n /throttl|TooManyRequests|RequestLimitExceeded/i.test(type) ||\n status === 429\n ) {\n return new RateLimitError(httpError.message, httpError.response);\n }\n if (\n /AccessDenied|UnrecognizedClient|InvalidClientTokenId|SignatureDoesNotMatch|AuthFailure|InvalidSignature|ExpiredToken/i.test(\n type,\n ) ||\n status === 403\n ) {\n return new AuthError(httpError.message, httpError.response);\n }\n if (status >= 500) {\n return new TransientError(httpError.message, httpError.response);\n }\n return err;\n}\n\nfunction isDataUnavailable(err: unknown): boolean {\n const httpError = asHttpError(err);\n return (\n httpError !== null &&\n /DataUnavailable/i.test(extractAwsErrorType(httpError))\n );\n}\n\n// ---------------------------------------------------------------------------\n// Pure helpers — exported for unit testing\n// ---------------------------------------------------------------------------\n\nfunction parseAmount(value: string | undefined): number {\n if (value === undefined) {\n return 0;\n }\n const n = Number.parseFloat(value);\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction ceDateToMs(date: string): number {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(date);\n if (!m) {\n return NaN;\n }\n return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nfunction toDateStr(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nfunction addMonthsFirstUtc(ms: number, months: number): number {\n const d = new Date(ms);\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, 1);\n}\n\nfunction startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nfunction groupAttrName(\n groupBy: readonly string[] | undefined,\n index: number,\n): string {\n const dim = groupBy?.[index];\n if (!dim) {\n return `dimension_${index}`;\n }\n if (dim.startsWith('TAG:')) {\n return `tag_${dim.slice(4)}`;\n }\n if (dim.startsWith('COST_CATEGORY:')) {\n return `cost_category_${dim.slice(14)}`;\n }\n return dim.toLowerCase();\n}\n\nfunction toGroupDefinition(dim: string): { Type: string; Key: string } {\n if (dim.startsWith('TAG:')) {\n return { Type: 'TAG', Key: dim.slice(4) };\n }\n if (dim.startsWith('COST_CATEGORY:')) {\n return { Type: 'COST_CATEGORY', Key: dim.slice(14) };\n }\n return { Type: 'DIMENSION', Key: dim };\n}\n\nexport function buildDailyCostSamples(\n body: GetCostAndUsageBody,\n granularity: 'DAILY' | 'MONTHLY',\n groupBy: readonly string[] | undefined,\n): MetricSample[] {\n const samples: MetricSample[] = [];\n for (const result of body.ResultsByTime ?? []) {\n const start = result.TimePeriod?.Start;\n if (start === undefined) {\n continue;\n }\n const ts = ceDateToMs(start);\n if (!Number.isFinite(ts)) {\n continue;\n }\n const estimated = result.Estimated ?? false;\n const groups = result.Groups ?? [];\n if (groups.length > 0) {\n for (const group of groups) {\n const cost = group.Metrics?.['UnblendedCost'];\n const keys = group.Keys ?? [];\n const attributes: Record<string, JSONValue> = {\n granularity,\n estimated,\n unit: cost?.Unit ?? 'USD',\n };\n for (let i = 0; i < keys.length; i++) {\n attributes[groupAttrName(groupBy, i)] = keys[i] ?? null;\n }\n samples.push({\n name: DAILY_METRIC_NAME,\n ts,\n value: parseAmount(cost?.Amount),\n attributes,\n });\n }\n continue;\n }\n const cost = result.Total?.['UnblendedCost'];\n if (!cost) {\n continue;\n }\n samples.push({\n name: DAILY_METRIC_NAME,\n ts,\n value: parseAmount(cost.Amount),\n attributes: { granularity, estimated, unit: cost.Unit ?? 'USD' },\n });\n }\n return samples;\n}\n\nexport function buildForecastSamples(\n body: GetCostForecastBody,\n granularity: 'DAILY' | 'MONTHLY',\n): MetricSample[] {\n const unit = body.Total?.Unit ?? 'USD';\n const samples: MetricSample[] = [];\n for (const result of body.ForecastResultsByTime ?? []) {\n const start = result.TimePeriod?.Start;\n if (start === undefined) {\n continue;\n }\n const ts = ceDateToMs(start);\n if (!Number.isFinite(ts)) {\n continue;\n }\n samples.push({\n name: FORECAST_METRIC_NAME,\n ts,\n value: parseAmount(result.MeanValue),\n attributes: {\n granularity,\n unit,\n lowerBound:\n result.PredictionIntervalLowerBound !== undefined\n ? parseAmount(result.PredictionIntervalLowerBound)\n : null,\n upperBound:\n result.PredictionIntervalUpperBound !== undefined\n ? parseAmount(result.PredictionIntervalUpperBound)\n : null,\n },\n });\n }\n return samples;\n}\n\ninterface CostWindow {\n start: string;\n end: string;\n}\n\nexport function getCostWindow(\n options: SyncOptions,\n granularity: 'DAILY' | 'MONTHLY',\n lookbackDays: number,\n now: number = Date.now(),\n): CostWindow {\n const sinceMs = options.since !== undefined ? Date.parse(options.since) : NaN;\n const hasSince = Number.isFinite(sinceMs);\n\n let days = lookbackDays;\n if (options.mode === 'latest') {\n days = INCREMENTAL_LOOKBACK_DAYS;\n } else if (hasSince) {\n const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);\n days = Math.min(Math.max(elapsed, 1), lookbackDays);\n }\n\n if (granularity === 'MONTHLY') {\n // Derive month delta from calendar months so the bucket containing `since`\n // is included, instead of rounding days/30 which under-fetches at month\n // boundaries (e.g. since=2026-04-30, now=2026-05-27 needs 2 months, not 1).\n let months: number;\n if (options.mode === 'latest') {\n months = 1;\n } else if (hasSince) {\n const since = new Date(sinceMs);\n const nowDate = new Date(now);\n const delta =\n (nowDate.getUTCFullYear() - since.getUTCFullYear()) * 12 +\n (nowDate.getUTCMonth() - since.getUTCMonth()) +\n 1;\n months = Math.max(1, delta);\n } else {\n months = Math.max(1, Math.ceil(lookbackDays / 30));\n }\n return {\n start: toDateStr(addMonthsFirstUtc(now, 1 - months)),\n end: toDateStr(addMonthsFirstUtc(now, 1)),\n };\n }\n\n // End is exclusive; tomorrow 00:00 UTC so the current (estimated) day is\n // included and overwritten on the next sync as it finalizes.\n const end = startOfUtcDay(now) + MS_PER_DAY;\n return { start: toDateStr(end - days * MS_PER_DAY), end: toDateStr(end) };\n}\n\nfunction getForecastWindow(\n granularity: 'DAILY' | 'MONTHLY',\n now: number = Date.now(),\n): CostWindow {\n const start = startOfUtcDay(now);\n if (granularity === 'MONTHLY') {\n return {\n start: toDateStr(start),\n end: toDateStr(addMonthsFirstUtc(now, 3)),\n };\n }\n return { start: toDateStr(start), end: toDateStr(start + 31 * MS_PER_DAY) };\n}\n\ntype AwsCostCursor = ChunkedSyncCursor<AwsCostPhase, CostWindow>;\n\nfunction isAwsCostCursor(value: unknown): value is AwsCostCursor {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n const c = value as { phase?: unknown; page?: unknown };\n if (\n typeof c.phase !== 'string' ||\n !(PHASE_ORDER as readonly string[]).includes(c.phase)\n ) {\n return false;\n }\n const p = c.page as { start?: unknown; end?: unknown } | null | undefined;\n if (p === null) {\n return true;\n }\n if (typeof p !== 'object') {\n return false;\n }\n return typeof p.start === 'string' && typeof p.end === 'string';\n}\n\n// ---------------------------------------------------------------------------\n// Resources\n// ---------------------------------------------------------------------------\n\nconst awsCostResources = defineResources({\n aws_cost_daily: {\n shape: 'metric',\n description:\n '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.',\n endpoint: 'POST GetCostAndUsage',\n unit: 'USD',\n granularity: 'daily',\n notes:\n 'Prefer MONTHLY granularity over long windows since each Cost Explorer query is billed. Cost Explorer accepts at most two group-by dimensions per query.',\n dimensions: [\n {\n name: 'granularity',\n description: 'Bucket granularity, DAILY or MONTHLY.',\n },\n {\n name: 'estimated',\n description:\n 'Whether the bucket is still estimated rather than finalized.',\n },\n {\n name: 'unit',\n description: 'Currency unit reported by AWS, e.g. USD.',\n },\n {\n name: 'service',\n description:\n 'AWS service name, present when grouping by SERVICE (other group-by dimensions appear as linked_account, tag_<key>, or cost_category_<key>).',\n },\n ],\n responses: { daily_cost: getCostAndUsageResponse },\n },\n aws_cost_forecast: {\n shape: 'metric',\n description:\n 'Projected future unblended AWS cost (mean value) with optional lower and upper prediction-interval bounds. Empty when the account has insufficient history to forecast.',\n endpoint: 'POST GetCostForecast',\n unit: 'USD',\n granularity: 'daily',\n notes:\n 'Prefer MONTHLY granularity over long windows since each Cost Explorer query is billed.',\n dimensions: [\n {\n name: 'granularity',\n description: 'Bucket granularity, DAILY or MONTHLY.',\n },\n {\n name: 'unit',\n description: 'Currency unit reported by AWS, e.g. USD.',\n },\n {\n name: 'lowerBound',\n description: 'Lower bound of the prediction interval, if provided.',\n },\n {\n name: 'upperBound',\n description: 'Upper bound of the prediction interval, if provided.',\n },\n ],\n responses: { forecast: getCostForecastResponse },\n },\n});\n\n// ---------------------------------------------------------------------------\n// AwsCostConnector\n// ---------------------------------------------------------------------------\n\nexport class AwsCostConnector extends BaseAWSConnector<AwsCostSettings> {\n static readonly id = 'aws-cost';\n\n static readonly resources = awsCostResources;\n\n static readonly schemas = schemasFromResources(awsCostResources);\n\n static readonly cost: ConnectorCost = {\n recommendedInterval: '1 day',\n minInterval: '1 hour',\n perSync: '2 Cost Explorer queries (about $0.02)',\n warning:\n 'Each AWS Cost Explorer query is billed $0.01; avoid syncing more often than necessary.',\n };\n\n static create(input: unknown, ctx?: ConnectorContext): AwsCostConnector {\n const parsed = configFields.parse(input);\n return new AwsCostConnector(\n {\n region: AWS_REGION,\n roleArn: parsed.roleArn,\n externalId: parsed.externalId,\n granularity: parsed.granularity,\n groupBy: parsed.groupBy,\n lookbackDays: parsed.lookbackDays,\n },\n {\n accessKeyId: parsed.accessKeyId,\n secretAccessKey: parsed.secretAccessKey,\n },\n ctx,\n );\n }\n\n readonly id = 'aws-cost';\n\n private async callCostExplorer<T>(\n action: string,\n payload: Record<string, unknown>,\n resource: string,\n signal?: AbortSignal,\n ): Promise<T> {\n const credentials = await this.resolveSigningCredentials(signal);\n const body = JSON.stringify(payload);\n const headers = await this.buildCeHeaders(action, body, credentials);\n try {\n const res = await this.post<unknown>(CE_URL, {\n resource,\n headers,\n body,\n signal,\n });\n const parsed =\n typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n return parsed as T;\n } catch (err) {\n throw mapAwsJsonError(err);\n }\n }\n\n private async buildCeHeaders(\n action: string,\n body: string,\n credentials: SigningCredentials,\n ): Promise<Record<string, string>> {\n const { amzDate, dateStamp } = formatAmzDate(new Date());\n const payloadHash = await sha256Hex(body);\n const amzTarget = `${CE_TARGET_PREFIX}.${action}`;\n\n const signedHeaders: Record<string, string> = {\n 'content-type': CE_CONTENT_TYPE,\n host: CE_HOST,\n 'x-amz-content-sha256': payloadHash,\n 'x-amz-date': amzDate,\n 'x-amz-target': amzTarget,\n };\n if (credentials.sessionToken !== undefined) {\n signedHeaders['x-amz-security-token'] = credentials.sessionToken;\n }\n\n const authorization = await createAuthorizationHeader({\n method: 'POST',\n host: CE_HOST,\n path: '/',\n query: '',\n headers: signedHeaders,\n payloadHash,\n accessKeyId: credentials.accessKeyId,\n secretAccessKey: credentials.secretAccessKey,\n region: AWS_REGION,\n service: CE_SERVICE,\n amzDate,\n dateStamp,\n });\n\n const sendHeaders: Record<string, string> = {\n 'Content-Type': CE_CONTENT_TYPE,\n 'X-Amz-Content-Sha256': payloadHash,\n 'X-Amz-Date': amzDate,\n 'X-Amz-Target': amzTarget,\n Authorization: authorization,\n 'User-Agent': connectorUserAgent(this.id),\n };\n if (credentials.sessionToken !== undefined) {\n sendHeaders['X-Amz-Security-Token'] = credentials.sessionToken;\n }\n return sendHeaders;\n }\n\n private async syncDailyCost(\n storage: StorageHandle,\n window: CostWindow,\n granularity: 'DAILY' | 'MONTHLY',\n groupBy: readonly string[] | undefined,\n signal?: AbortSignal,\n ): Promise<void> {\n const samples: MetricSample[] = [];\n let nextPageToken: string | undefined;\n do {\n const payload: Record<string, unknown> = {\n TimePeriod: { Start: window.start, End: window.end },\n Granularity: granularity,\n Metrics: ['UnblendedCost'],\n };\n if (groupBy && groupBy.length > 0) {\n payload['GroupBy'] = groupBy.slice(0, 2).map(toGroupDefinition);\n }\n if (nextPageToken) {\n payload['NextPageToken'] = nextPageToken;\n }\n const parsed = await this.callCostExplorer<GetCostAndUsageBody>(\n 'GetCostAndUsage',\n payload,\n 'daily_cost',\n signal,\n );\n samples.push(...buildDailyCostSamples(parsed, granularity, groupBy));\n nextPageToken =\n typeof parsed.NextPageToken === 'string' &&\n parsed.NextPageToken.length > 0\n ? parsed.NextPageToken\n : undefined;\n } while (nextPageToken);\n\n await storage.metrics(samples, { names: [DAILY_METRIC_NAME] });\n }\n\n private async syncForecast(\n storage: StorageHandle,\n granularity: 'DAILY' | 'MONTHLY',\n signal?: AbortSignal,\n ): Promise<void> {\n const window = getForecastWindow(granularity);\n let parsed: GetCostForecastBody;\n try {\n parsed = await this.callCostExplorer<GetCostForecastBody>(\n 'GetCostForecast',\n {\n TimePeriod: { Start: window.start, End: window.end },\n Metric: 'UNBLENDED_COST',\n Granularity: granularity,\n },\n 'forecast',\n signal,\n );\n } catch (err) {\n // A brand-new or low-volume account has no history to forecast from;\n // treat that as \"no forecast\" rather than failing the whole sync.\n if (isDataUnavailable(err)) {\n await storage.metrics([], { names: [FORECAST_METRIC_NAME] });\n return;\n }\n throw err;\n }\n await storage.metrics(buildForecastSamples(parsed, granularity), {\n names: [FORECAST_METRIC_NAME],\n });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const granularity = this.settings.granularity ?? 'DAILY';\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_BACKFILL_DAYS;\n const groupBy = this.settings.groupBy;\n\n const cursor = isAwsCostCursor(options.cursor) ? options.cursor : undefined;\n const page =\n cursor?.page ?? getCostWindow(options, granularity, lookbackDays);\n\n const resumeIdx = cursor ? PHASE_ORDER.indexOf(cursor.phase) : 0;\n const startIdx = resumeIdx >= 0 ? resumeIdx : 0;\n\n for (let i = startIdx; i < PHASE_ORDER.length; i++) {\n const phase = PHASE_ORDER[i]!;\n if (signal?.aborted) {\n return { done: false, cursor: { phase, page } };\n }\n if (\n options.resources &&\n options.resources.size > 0 &&\n !options.resources.has(phase)\n ) {\n continue;\n }\n try {\n if (phase === 'daily_cost') {\n await this.syncDailyCost(storage, page, granularity, groupBy, signal);\n } else {\n await this.syncForecast(storage, granularity, signal);\n }\n } catch (err) {\n if (signal?.aborted) {\n return { done: false, cursor: { phase, page } };\n }\n throw err;\n }\n }\n\n return { done: true };\n }\n}\n","import { AwsCostConnector } from './aws-cost';\n\nexport {\n AwsCostConnector,\n buildDailyCostSamples,\n buildForecastSamples,\n configFields,\n doc,\n getCostWindow,\n} from './aws-cost';\nexport type { AwsCostSettings } from './aws-cost';\nexport default AwsCostConnector;\n"],"mappings":"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","names":["HttpClientError","TransientError","RateLimitError","AuthError","HTTP_CLIENT_VERSION","DEFAULT_USER_AGENT","connectorUserAgent","z","RateLimitError","AuthError","TransientError","cost","connectorUserAgent"]}
1
+ {"version":3,"sources":["../../aws-shared/src/sigv4.ts","../../aws-shared/src/xml.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../../aws-shared/src/base-aws-connector.ts","../../aws-shared/src/config.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/aws-cost.ts","../src/index.ts"],"sourcesContent":["// AWS Signature Version 4 signing, implemented against the Web Crypto API so\n// the connector carries no AWS SDK dependency. See\n// https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html.\n\nconst encoder = new TextEncoder();\n\nconst ALGORITHM = 'AWS4-HMAC-SHA256';\n\n// Encode to a fresh ArrayBuffer-backed view so the result is a valid\n// `BufferSource` for the Web Crypto APIs under TypeScript's generic typing.\nfunction u8(data: string): Uint8Array<ArrayBuffer> {\n return new Uint8Array(encoder.encode(data));\n}\n\nfunction toHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i]!.toString(16).padStart(2, '0');\n }\n return hex;\n}\n\nexport async function sha256Hex(data: string): Promise<string> {\n const digest = await globalThis.crypto.subtle.digest('SHA-256', u8(data));\n return toHex(digest);\n}\n\nasync function hmac(key: BufferSource, data: string): Promise<ArrayBuffer> {\n const cryptoKey = await globalThis.crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n return globalThis.crypto.subtle.sign('HMAC', cryptoKey, u8(data));\n}\n\nasync function deriveSigningKey(\n secretAccessKey: string,\n dateStamp: string,\n region: string,\n service: string,\n): Promise<ArrayBuffer> {\n const kDate = await hmac(u8(`AWS4${secretAccessKey}`), dateStamp);\n const kRegion = await hmac(kDate, region);\n const kService = await hmac(kRegion, service);\n return hmac(kService, 'aws4_request');\n}\n\nexport interface AmzDate {\n amzDate: string;\n dateStamp: string;\n}\n\n// \"2015-08-30T12:36:00.000Z\" -> { amzDate: \"20150830T123600Z\", dateStamp: \"20150830\" }\nexport function formatAmzDate(date: Date): AmzDate {\n const amzDate = date.toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n return { amzDate, dateStamp: amzDate.slice(0, 8) };\n}\n\nexport interface SignParams {\n method: string;\n host: string;\n path: string;\n query: string;\n headers: Record<string, string>;\n payloadHash: string;\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n service: string;\n amzDate: string;\n dateStamp: string;\n}\n\n// Returns the value for the `Authorization` header. The `headers` map must\n// contain every header that is part of the signature (at minimum `host` and\n// `x-amz-date`); extra unsigned headers sent on the wire are allowed.\nexport async function createAuthorizationHeader(\n params: SignParams,\n): Promise<string> {\n const lowerHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(params.headers)) {\n lowerHeaders[key.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n const sortedNames = Object.keys(lowerHeaders).sort();\n\n const canonicalHeaders = sortedNames\n .map((name) => `${name}:${lowerHeaders[name]}\\n`)\n .join('');\n const signedHeaders = sortedNames.join(';');\n\n const canonicalRequest = [\n params.method,\n params.path,\n params.query,\n canonicalHeaders,\n signedHeaders,\n params.payloadHash,\n ].join('\\n');\n\n const credentialScope = `${params.dateStamp}/${params.region}/${params.service}/aws4_request`;\n const stringToSign = [\n ALGORITHM,\n params.amzDate,\n credentialScope,\n await sha256Hex(canonicalRequest),\n ].join('\\n');\n\n const signingKey = await deriveSigningKey(\n params.secretAccessKey,\n params.dateStamp,\n params.region,\n params.service,\n );\n const signature = toHex(await hmac(signingKey, stringToSign));\n\n return (\n `${ALGORITHM} Credential=${params.accessKeyId}/${credentialScope}, ` +\n `SignedHeaders=${signedHeaders}, Signature=${signature}`\n );\n}\n","// Minimal parser for the handful of AWS Query-protocol (XML) responses this\n// connector consumes: GetMetricData, STS AssumeRole, and error envelopes. It\n// is deliberately narrow — it understands the specific element nesting these\n// responses use rather than being a general-purpose XML parser.\n\nfunction decodeEntities(value: string): string {\n return value\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\")\n .replace(/&amp;/g, '&');\n}\n\n// Inner text of the first `<tag>...</tag>` in `xml`. Returns '' for a\n// self-closing `<tag/>`, and null when the tag is absent. Tags that contain\n// repeated `<member>` children (Timestamps, Values, MetricDataResults) do not\n// nest within themselves, so the first matching close tag is the correct one.\nexport function firstInner(xml: string, tag: string): string | null {\n const escapedTag = tag.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const open = new RegExp(`<${escapedTag}(?:\\\\s[^>]*)?>`).exec(xml);\n if (!open) {\n return new RegExp(`<${escapedTag}\\\\s*/>`).test(xml) ? '' : null;\n }\n const start = open.index + open[0].length;\n const closeIdx = xml.indexOf(`</${tag}>`, start);\n if (closeIdx === -1) {\n return null;\n }\n return xml.slice(start, closeIdx);\n}\n\nexport function firstText(xml: string, tag: string): string | null {\n const inner = firstInner(xml, tag);\n return inner === null ? null : decodeEntities(inner).trim();\n}\n\n// Inner content of each top-level `<member>...</member>`, tracking nesting so\n// that a result member's nested Timestamps/Values members are not mistaken for\n// top-level entries.\nexport function topLevelMembers(xml: string): string[] {\n const results: string[] = [];\n const re = /<member(?:\\s[^>]*)?>|<\\/member>/g;\n let depth = 0;\n let contentStart = -1;\n let match: RegExpExecArray | null;\n while ((match = re.exec(xml)) !== null) {\n if (match[0].startsWith('</')) {\n depth--;\n if (depth === 0 && contentStart !== -1) {\n results.push(xml.slice(contentStart, match.index));\n contentStart = -1;\n }\n } else {\n if (depth === 0) {\n contentStart = match.index + match[0].length;\n }\n depth++;\n }\n }\n return results;\n}\n\nexport interface MetricDataResult {\n id: string;\n label: string;\n statusCode: string;\n timestamps: string[];\n values: number[];\n}\n\nexport interface GetMetricDataParsed {\n results: MetricDataResult[];\n nextToken: string | null;\n}\n\nexport function parseGetMetricData(xml: string): GetMetricDataParsed {\n const resultsBlock = firstInner(xml, 'MetricDataResults') ?? '';\n const results = topLevelMembers(resultsBlock).map((member) => {\n const tsBlock = firstInner(member, 'Timestamps') ?? '';\n const valBlock = firstInner(member, 'Values') ?? '';\n return {\n id: firstText(member, 'Id') ?? '',\n label: firstText(member, 'Label') ?? '',\n statusCode: firstText(member, 'StatusCode') ?? '',\n timestamps: topLevelMembers(tsBlock).map((t) => decodeEntities(t).trim()),\n values: topLevelMembers(valBlock).map((v) =>\n Number(decodeEntities(v).trim()),\n ),\n };\n });\n const nextToken = firstText(xml, 'NextToken');\n return { results, nextToken: nextToken === '' ? null : nextToken };\n}\n\nexport interface StsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken: string;\n expiration: string;\n}\n\nexport function parseAssumeRole(xml: string): StsCredentials | null {\n const credBlock = firstInner(xml, 'Credentials');\n if (credBlock === null) {\n return null;\n }\n const accessKeyId = firstText(credBlock, 'AccessKeyId') ?? '';\n const secretAccessKey = firstText(credBlock, 'SecretAccessKey') ?? '';\n if (accessKeyId === '' || secretAccessKey === '') {\n return null;\n }\n return {\n accessKeyId,\n secretAccessKey,\n sessionToken: firstText(credBlock, 'SessionToken') ?? '',\n expiration: firstText(credBlock, 'Expiration') ?? '',\n };\n}\n\n// AWS Query-protocol error envelopes carry the machine-readable error code in\n// an `<Error><Code>...</Code></Error>` element.\nexport function parseErrorCode(xml: string): string | null {\n return firstText(xml, 'Code');\n}\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n AuthError,\n type HttpClientError,\n type HttpResponse,\n RateLimitError,\n TransientError,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport { BaseConnector, type CredentialsSchema } from '@rawdash/core';\n\nimport { createAuthorizationHeader, formatAmzDate, sha256Hex } from './sigv4';\nimport { type StsCredentials, parseAssumeRole, parseErrorCode } from './xml';\n\nexport interface BaseAWSSettings {\n region: string;\n roleArn?: string;\n externalId?: string;\n}\n\nexport const awsCredentialsSchema = {\n accessKeyId: {\n description: 'AWS access key ID',\n auth: 'optional' as const,\n },\n secretAccessKey: {\n description: 'AWS secret access key',\n auth: 'optional' as const,\n },\n} satisfies CredentialsSchema;\n\nexport type AwsCredentials = typeof awsCredentialsSchema;\n\nexport interface SigningCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n}\n\nconst STS_SERVICE = 'sts';\nconst STS_API_VERSION = '2011-06-15';\nconst ASSUMED_ROLE_TTL_BUFFER_MS = 60_000;\nconst ASSUME_ROLE_DURATION_SECONDS = 3600;\nconst FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8';\n\nfunction readEnv(name: string): string | undefined {\n const env = (\n globalThis as {\n process?: { env?: Record<string, string | undefined> };\n }\n ).process?.env;\n return env?.[name];\n}\n\nexport abstract class BaseAWSConnector<\n TSettings extends BaseAWSSettings,\n> extends BaseConnector<TSettings, AwsCredentials> {\n override readonly credentials = awsCredentialsSchema;\n\n private assumedCreds: {\n value: SigningCredentials;\n expiresAt: number;\n } | null = null;\n\n protected baseCredentials(): SigningCredentials {\n const { accessKeyId, secretAccessKey } = this.creds;\n if (accessKeyId && secretAccessKey) {\n return { accessKeyId, secretAccessKey };\n }\n const envAccessKeyId = readEnv('AWS_ACCESS_KEY_ID');\n const envSecretAccessKey = readEnv('AWS_SECRET_ACCESS_KEY');\n if (envAccessKeyId && envSecretAccessKey) {\n return {\n accessKeyId: envAccessKeyId,\n secretAccessKey: envSecretAccessKey,\n sessionToken: readEnv('AWS_SESSION_TOKEN') || undefined,\n };\n }\n throw new AuthError(\n `${this.id}: no AWS credentials available — provide accessKeyId + secretAccessKey, or set them in the environment for role assumption`,\n );\n }\n\n protected async resolveSigningCredentials(\n signal?: AbortSignal,\n ): Promise<SigningCredentials> {\n if (this.settings.roleArn === undefined) {\n const { accessKeyId, secretAccessKey } = this.creds;\n if (!accessKeyId || !secretAccessKey) {\n throw new AuthError(\n `${this.id}: static-credential auth requires both accessKeyId and secretAccessKey`,\n );\n }\n return { accessKeyId, secretAccessKey };\n }\n\n if (this.assumedCreds && Date.now() < this.assumedCreds.expiresAt) {\n return this.assumedCreds.value;\n }\n return this.assumeRole(this.settings.roleArn, signal);\n }\n\n private async assumeRole(\n roleArn: string,\n signal?: AbortSignal,\n ): Promise<SigningCredentials> {\n const params = new URLSearchParams();\n params.set('Action', 'AssumeRole');\n params.set('Version', STS_API_VERSION);\n params.set('RoleArn', roleArn);\n params.set('RoleSessionName', `rawdash-${this.id}`);\n params.set('DurationSeconds', String(ASSUME_ROLE_DURATION_SECONDS));\n if (this.settings.externalId !== undefined) {\n params.set('ExternalId', this.settings.externalId);\n }\n\n const host = `sts.${this.settings.region}.amazonaws.com`;\n const xml = await this.signedPost({\n host,\n service: STS_SERVICE,\n body: params.toString(),\n signingCredentials: this.baseCredentials(),\n resource: 'assume_role',\n signal,\n });\n\n const parsed = parseAssumeRole(xml);\n if (parsed === null) {\n throw new AuthError(\n `${this.id}: STS AssumeRole returned no usable credentials`,\n );\n }\n this.cacheAssumedCredentials(parsed);\n return {\n accessKeyId: parsed.accessKeyId,\n secretAccessKey: parsed.secretAccessKey,\n sessionToken: parsed.sessionToken || undefined,\n };\n }\n\n private cacheAssumedCredentials(parsed: StsCredentials): void {\n const expirationMs = parseEpoch(parsed.expiration, 'iso');\n const expiresAt =\n expirationMs !== null\n ? expirationMs - ASSUMED_ROLE_TTL_BUFFER_MS\n : Date.now() + (ASSUME_ROLE_DURATION_SECONDS - 60) * 1000;\n this.assumedCreds = {\n value: {\n accessKeyId: parsed.accessKeyId,\n secretAccessKey: parsed.secretAccessKey,\n sessionToken: parsed.sessionToken || undefined,\n },\n expiresAt,\n };\n }\n\n protected async signedPost(args: {\n host: string;\n service: string;\n body: string;\n signingCredentials: SigningCredentials;\n resource: string;\n signal?: AbortSignal;\n }): Promise<string> {\n const { amzDate, dateStamp } = formatAmzDate(new Date());\n const payloadHash = await sha256Hex(args.body);\n\n const signedHeaders: Record<string, string> = {\n host: args.host,\n 'content-type': FORM_CONTENT_TYPE,\n 'x-amz-content-sha256': payloadHash,\n 'x-amz-date': amzDate,\n };\n if (args.signingCredentials.sessionToken !== undefined) {\n signedHeaders['x-amz-security-token'] =\n args.signingCredentials.sessionToken;\n }\n\n const authorization = await createAuthorizationHeader({\n method: 'POST',\n host: args.host,\n path: '/',\n query: '',\n headers: signedHeaders,\n payloadHash,\n accessKeyId: args.signingCredentials.accessKeyId,\n secretAccessKey: args.signingCredentials.secretAccessKey,\n region: this.settings.region,\n service: args.service,\n amzDate,\n dateStamp,\n });\n\n const sendHeaders: Record<string, string> = {\n 'content-type': FORM_CONTENT_TYPE,\n 'x-amz-content-sha256': payloadHash,\n 'x-amz-date': amzDate,\n 'user-agent': connectorUserAgent(this.id),\n Authorization: authorization,\n };\n if (args.signingCredentials.sessionToken !== undefined) {\n sendHeaders['x-amz-security-token'] =\n args.signingCredentials.sessionToken;\n }\n\n try {\n const res: HttpResponse<string> = await this.request<string>(\n {\n url: `https://${args.host}/`,\n method: 'POST',\n headers: sendHeaders,\n body: args.body,\n parseJson: false,\n signal: args.signal,\n },\n { resource: args.resource },\n );\n return res.body;\n } catch (err) {\n throw this.classifyAwsError(err);\n }\n }\n\n protected classifyAwsError(err: unknown): unknown {\n if (!(err instanceof Error) || !('kind' in err)) {\n return err;\n }\n const httpErr = err as HttpClientError;\n const body =\n typeof httpErr.response?.body === 'string' ? httpErr.response.body : '';\n const code = parseErrorCode(body) ?? '';\n const status = httpErr.response?.status ?? 0;\n\n if (\n /throttl|RequestLimitExceeded|TooManyRequests|LimitExceeded/i.test(code)\n ) {\n return new RateLimitError(httpErr.message, httpErr.response);\n }\n if (\n /AccessDenied|UnrecognizedClient|InvalidClientTokenId|SignatureDoesNotMatch|AuthFailure|InvalidAccessKeyId|Forbidden/i.test(\n code,\n )\n ) {\n return new AuthError(httpErr.message, httpErr.response);\n }\n if (status >= 500) {\n return new TransientError(httpErr.message, httpErr.response);\n }\n return err;\n }\n}\n","import { z } from 'zod';\n\nexport const awsAuthConfigShape = {\n region: z\n .string()\n .regex(\n /^[a-z0-9-]+$/,\n 'region must look like an AWS region, e.g. us-east-1',\n )\n .meta({\n label: 'AWS Region',\n description:\n 'The AWS region whose service endpoint you want to call, e.g. us-east-1.',\n placeholder: 'us-east-1',\n }),\n accessKeyId: z.object({ $secret: z.string() }).optional().meta({\n label: 'Access Key ID',\n description:\n '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.',\n secret: true,\n }),\n secretAccessKey: z.object({ $secret: z.string() }).optional().meta({\n label: 'Secret Access Key',\n description: 'AWS secret access key paired with the access key ID above.',\n secret: true,\n }),\n roleArn: z\n .string()\n .regex(\n /^arn:aws:iam::\\d{12}:role\\/.+/,\n 'roleArn must be a full IAM role ARN, e.g. arn:aws:iam::123456789012:role/rawdash',\n )\n .optional()\n .meta({\n label: 'Role ARN',\n description:\n '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.',\n placeholder: 'arn:aws:iam::123456789012:role/rawdash',\n }),\n externalId: z.string().min(1).optional().meta({\n label: 'External ID',\n description:\n 'External ID required by the trust policy of the role being assumed. Only used with Role ARN.',\n }),\n} as const;\n\nexport interface AwsAuthConfig {\n region: string;\n accessKeyId?: { $secret: string };\n secretAccessKey?: { $secret: string };\n roleArn?: string;\n externalId?: string;\n}\n\nexport const awsAuthRefine = {\n predicate: (val: AwsAuthConfig): boolean => {\n const hasRole = val.roleArn !== undefined;\n const hasStatic =\n val.accessKeyId !== undefined && val.secretAccessKey !== undefined;\n if (val.externalId !== undefined && !hasRole) {\n return false;\n }\n return hasRole || hasStatic;\n },\n message:\n 'Provide either accessKeyId + secretAccessKey (static credentials) or roleArn (role assumption). externalId requires roleArn.',\n} as const;\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n BaseAWSConnector,\n type BaseAWSSettings,\n type SigningCredentials,\n awsAuthConfigShape,\n awsAuthRefine,\n createAuthorizationHeader,\n formatAmzDate,\n sha256Hex,\n} from '@rawdash/connector-aws-shared';\nimport {\n AuthError,\n type HttpResponse,\n RateLimitError,\n TransientError,\n connectorUserAgent,\n} from '@rawdash/connector-shared';\nimport {\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorCost,\n type ConnectorDoc,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n schemasFromResources,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n//\n// Cost Explorer is a global service reached through its us-east-1 endpoint, so\n// `region` is hardcoded rather than exposed in the connector config.\n\nconst { region: _region, ...awsAuthWithoutRegion } = awsAuthConfigShape;\n\nexport const configFields = defineConfigFields(\n z\n .object({\n ...awsAuthWithoutRegion,\n granularity: z.enum(['DAILY', 'MONTHLY']).optional().meta({\n label: 'Granularity',\n description:\n '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.',\n }),\n groupBy: z\n .array(\n z\n .string()\n .regex(\n /^(SERVICE|LINKED_ACCOUNT|TAG:.+|COST_CATEGORY:.+)$/,\n 'groupBy entries must be SERVICE, LINKED_ACCOUNT, TAG:<key>, or COST_CATEGORY:<key>',\n ),\n )\n .max(2, 'Cost Explorer accepts at most two group-by dimensions')\n .optional()\n .meta({\n label: 'Group by (optional)',\n description:\n 'Up to two Cost Explorer dimensions to break costs down by, e.g. SERVICE, LINKED_ACCOUNT, or TAG:Environment. Omit for total cost only.',\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Backfill window (days)',\n description:\n 'How many days of history to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n })\n .refine((val) => awsAuthRefine.predicate({ ...val, region: AWS_REGION }), {\n message: awsAuthRefine.message,\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'AWS Cost Explorer',\n category: 'finance',\n brandColor: '#6CAE3E',\n tagline:\n 'Track AWS spend over time and projected month-end costs, optionally broken down by service, account, tag, or cost category.',\n vendor: {\n name: 'Amazon Web Services',\n apiDocs:\n 'https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_Operations_AWS_Cost_Explorer_Service.html',\n website: 'https://aws.amazon.com/aws-cost-management/aws-cost-explorer/',\n },\n auth: {\n summary:\n '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.',\n setup: [\n 'In the AWS console, create an IAM user or role granting `ce:GetCostAndUsage` and `ce:GetCostForecast`.',\n '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\")`.',\n 'For role-assumption auth, set `roleArn` to the role to assume and (if configured) `externalId` to the role’s expected external ID.',\n 'Cost Explorer must be enabled for the account; the first activation can take up to 24 hours before data is queryable.',\n ],\n },\n rateLimit:\n 'Cost Explorer throttling (ThrottlingException) is retried with backoff. Cost Explorer is global and always reached via ce.us-east-1.amazonaws.com.',\n limitations: [\n 'Cost Explorer data can be revised for a couple of days after the fact, so incremental syncs refetch a short trailing window.',\n 'Forecast is unavailable for brand-new accounts (DataUnavailableException is treated as no forecast, not an error).',\n ],\n});\n\nexport interface AwsCostSettings extends BaseAWSSettings {\n granularity?: 'DAILY' | 'MONTHLY';\n groupBy?: readonly string[];\n lookbackDays?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst AWS_REGION = 'us-east-1';\nconst CE_HOST = 'ce.us-east-1.amazonaws.com';\nconst CE_URL = `https://${CE_HOST}/`;\nconst CE_SERVICE = 'ce';\nconst CE_CONTENT_TYPE = 'application/x-amz-json-1.1';\nconst CE_TARGET_PREFIX = 'AWSInsightsIndexService';\n\nconst DAILY_METRIC_NAME = 'aws_cost_daily';\nconst FORECAST_METRIC_NAME = 'aws_cost_forecast';\n\nconst DEFAULT_BACKFILL_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 3;\nconst MS_PER_DAY = 86_400_000;\n\nconst PHASE_ORDER = ['daily_cost', 'forecast'] as const;\ntype AwsCostPhase = (typeof PHASE_ORDER)[number];\n\n// ---------------------------------------------------------------------------\n// Schemas — describe the per-resource API response shape consumed by request()\n// ---------------------------------------------------------------------------\n\nconst amountString = z.string().regex(/^-?\\d+(\\.\\d+)?$/);\nconst ceDateString = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/);\nconst metricAmount = z.object({ Amount: amountString, Unit: z.string() });\n\nconst getCostAndUsageResponse = z.object({\n ResultsByTime: z.array(\n z.object({\n TimePeriod: z.object({ Start: ceDateString, End: ceDateString }),\n Total: z.object({ UnblendedCost: metricAmount.optional() }).optional(),\n Groups: z\n .array(\n z.object({\n Keys: z.array(z.string()),\n Metrics: z.object({ UnblendedCost: metricAmount }),\n }),\n )\n .optional(),\n Estimated: z.boolean().optional(),\n }),\n ),\n NextPageToken: z.string().optional(),\n});\n\nconst getCostForecastResponse = z.object({\n Total: metricAmount.optional(),\n ForecastResultsByTime: z\n .array(\n z.object({\n TimePeriod: z.object({ Start: ceDateString, End: ceDateString }),\n MeanValue: amountString,\n PredictionIntervalLowerBound: amountString.optional(),\n PredictionIntervalUpperBound: amountString.optional(),\n }),\n )\n .optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Runtime response shapes (intentionally permissive — the wire format is\n// `application/x-amz-json-1.1` which the shared client returns as a string,\n// so these are parsed defensively rather than trusted)\n// ---------------------------------------------------------------------------\n\ninterface CostMetricAmount {\n Amount?: string;\n Unit?: string;\n}\ninterface ResultByTime {\n TimePeriod?: { Start?: string; End?: string };\n Total?: Record<string, CostMetricAmount | undefined>;\n Groups?: Array<{\n Keys?: string[];\n Metrics?: Record<string, CostMetricAmount | undefined>;\n }>;\n Estimated?: boolean;\n}\ninterface GetCostAndUsageBody {\n ResultsByTime?: ResultByTime[];\n NextPageToken?: string;\n}\ninterface ForecastResult {\n TimePeriod?: { Start?: string; End?: string };\n MeanValue?: string;\n PredictionIntervalLowerBound?: string;\n PredictionIntervalUpperBound?: string;\n}\ninterface GetCostForecastBody {\n Total?: CostMetricAmount;\n ForecastResultsByTime?: ForecastResult[];\n}\n\n// ---------------------------------------------------------------------------\n// Error mapping — Cost Explorer signals failures via the JSON `__type` field;\n// translate them into the shared error contract the runner understands.\n// Detection is structural (`.kind` + `.response`) rather than `instanceof`,\n// because the shared error classes are bundled per-package — an `instanceof`\n// check against this package's copy would miss errors thrown by core's copy.\n// ---------------------------------------------------------------------------\n\ninterface HttpErrorLike {\n message: string;\n response?: HttpResponse;\n}\n\nfunction asHttpError(err: unknown): HttpErrorLike | null {\n if (\n err instanceof Error &&\n 'kind' in err &&\n typeof (err as { kind?: unknown }).kind === 'string'\n ) {\n return err as unknown as HttpErrorLike;\n }\n return null;\n}\n\nfunction extractAwsErrorType(err: HttpErrorLike): string {\n const body = err.response?.body;\n if (typeof body === 'string') {\n try {\n const parsed = JSON.parse(body) as { __type?: string; Code?: string };\n return parsed.__type ?? parsed.Code ?? body;\n } catch {\n return body;\n }\n }\n if (body && typeof body === 'object') {\n const o = body as { __type?: unknown; Code?: unknown };\n return String(o.__type ?? o.Code ?? '');\n }\n return '';\n}\n\nfunction mapAwsJsonError(err: unknown): unknown {\n const httpError = asHttpError(err);\n if (!httpError) {\n return err;\n }\n const type = extractAwsErrorType(httpError);\n const status = httpError.response?.status ?? 0;\n if (\n /throttl|TooManyRequests|RequestLimitExceeded/i.test(type) ||\n status === 429\n ) {\n return new RateLimitError(httpError.message, httpError.response);\n }\n if (\n /AccessDenied|UnrecognizedClient|InvalidClientTokenId|SignatureDoesNotMatch|AuthFailure|InvalidSignature|ExpiredToken/i.test(\n type,\n ) ||\n status === 403\n ) {\n return new AuthError(httpError.message, httpError.response);\n }\n if (status >= 500) {\n return new TransientError(httpError.message, httpError.response);\n }\n return err;\n}\n\nfunction isDataUnavailable(err: unknown): boolean {\n const httpError = asHttpError(err);\n return (\n httpError !== null &&\n /DataUnavailable/i.test(extractAwsErrorType(httpError))\n );\n}\n\n// ---------------------------------------------------------------------------\n// Pure helpers — exported for unit testing\n// ---------------------------------------------------------------------------\n\nfunction parseAmount(value: string | undefined): number {\n if (value === undefined) {\n return 0;\n }\n const n = Number.parseFloat(value);\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction ceDateToMs(date: string): number {\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(date);\n if (!m) {\n return NaN;\n }\n return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nfunction toDateStr(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nfunction addMonthsFirstUtc(ms: number, months: number): number {\n const d = new Date(ms);\n return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, 1);\n}\n\nfunction startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nfunction groupAttrName(\n groupBy: readonly string[] | undefined,\n index: number,\n): string {\n const dim = groupBy?.[index];\n if (!dim) {\n return `dimension_${index}`;\n }\n if (dim.startsWith('TAG:')) {\n return `tag_${dim.slice(4)}`;\n }\n if (dim.startsWith('COST_CATEGORY:')) {\n return `cost_category_${dim.slice(14)}`;\n }\n return dim.toLowerCase();\n}\n\nfunction toGroupDefinition(dim: string): { Type: string; Key: string } {\n if (dim.startsWith('TAG:')) {\n return { Type: 'TAG', Key: dim.slice(4) };\n }\n if (dim.startsWith('COST_CATEGORY:')) {\n return { Type: 'COST_CATEGORY', Key: dim.slice(14) };\n }\n return { Type: 'DIMENSION', Key: dim };\n}\n\nexport function buildDailyCostSamples(\n body: GetCostAndUsageBody,\n granularity: 'DAILY' | 'MONTHLY',\n groupBy: readonly string[] | undefined,\n): MetricSample[] {\n const samples: MetricSample[] = [];\n for (const result of body.ResultsByTime ?? []) {\n const start = result.TimePeriod?.Start;\n if (start === undefined) {\n continue;\n }\n const ts = ceDateToMs(start);\n if (!Number.isFinite(ts)) {\n continue;\n }\n const estimated = result.Estimated ?? false;\n const groups = result.Groups ?? [];\n if (groups.length > 0) {\n for (const group of groups) {\n const cost = group.Metrics?.['UnblendedCost'];\n const keys = group.Keys ?? [];\n const attributes: Record<string, JSONValue> = {\n granularity,\n estimated,\n unit: cost?.Unit ?? 'USD',\n };\n for (let i = 0; i < keys.length; i++) {\n attributes[groupAttrName(groupBy, i)] = keys[i] ?? null;\n }\n samples.push({\n name: DAILY_METRIC_NAME,\n ts,\n value: parseAmount(cost?.Amount),\n attributes,\n });\n }\n continue;\n }\n const cost = result.Total?.['UnblendedCost'];\n if (!cost) {\n continue;\n }\n samples.push({\n name: DAILY_METRIC_NAME,\n ts,\n value: parseAmount(cost.Amount),\n attributes: { granularity, estimated, unit: cost.Unit ?? 'USD' },\n });\n }\n return samples;\n}\n\nexport function buildForecastSamples(\n body: GetCostForecastBody,\n granularity: 'DAILY' | 'MONTHLY',\n): MetricSample[] {\n const unit = body.Total?.Unit ?? 'USD';\n const samples: MetricSample[] = [];\n for (const result of body.ForecastResultsByTime ?? []) {\n const start = result.TimePeriod?.Start;\n if (start === undefined) {\n continue;\n }\n const ts = ceDateToMs(start);\n if (!Number.isFinite(ts)) {\n continue;\n }\n samples.push({\n name: FORECAST_METRIC_NAME,\n ts,\n value: parseAmount(result.MeanValue),\n attributes: {\n granularity,\n unit,\n lowerBound:\n result.PredictionIntervalLowerBound !== undefined\n ? parseAmount(result.PredictionIntervalLowerBound)\n : null,\n upperBound:\n result.PredictionIntervalUpperBound !== undefined\n ? parseAmount(result.PredictionIntervalUpperBound)\n : null,\n },\n });\n }\n return samples;\n}\n\ninterface CostWindow {\n start: string;\n end: string;\n}\n\nexport function getCostWindow(\n options: SyncOptions,\n granularity: 'DAILY' | 'MONTHLY',\n lookbackDays: number,\n now: number = Date.now(),\n): CostWindow {\n const sinceMs = options.since !== undefined ? Date.parse(options.since) : NaN;\n const hasSince = Number.isFinite(sinceMs);\n\n let days = lookbackDays;\n if (options.mode === 'latest') {\n days = INCREMENTAL_LOOKBACK_DAYS;\n } else if (hasSince) {\n const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);\n days = Math.min(Math.max(elapsed, 1), lookbackDays);\n }\n\n if (granularity === 'MONTHLY') {\n // Derive month delta from calendar months so the bucket containing `since`\n // is included, instead of rounding days/30 which under-fetches at month\n // boundaries (e.g. since=2026-04-30, now=2026-05-27 needs 2 months, not 1).\n let months: number;\n if (options.mode === 'latest') {\n months = 1;\n } else if (hasSince) {\n const since = new Date(sinceMs);\n const nowDate = new Date(now);\n const delta =\n (nowDate.getUTCFullYear() - since.getUTCFullYear()) * 12 +\n (nowDate.getUTCMonth() - since.getUTCMonth()) +\n 1;\n months = Math.max(1, delta);\n } else {\n months = Math.max(1, Math.ceil(lookbackDays / 30));\n }\n return {\n start: toDateStr(addMonthsFirstUtc(now, 1 - months)),\n end: toDateStr(addMonthsFirstUtc(now, 1)),\n };\n }\n\n // End is exclusive; tomorrow 00:00 UTC so the current (estimated) day is\n // included and overwritten on the next sync as it finalizes.\n const end = startOfUtcDay(now) + MS_PER_DAY;\n return { start: toDateStr(end - days * MS_PER_DAY), end: toDateStr(end) };\n}\n\nfunction getForecastWindow(\n granularity: 'DAILY' | 'MONTHLY',\n now: number = Date.now(),\n): CostWindow {\n const start = startOfUtcDay(now);\n if (granularity === 'MONTHLY') {\n return {\n start: toDateStr(start),\n end: toDateStr(addMonthsFirstUtc(now, 3)),\n };\n }\n return { start: toDateStr(start), end: toDateStr(start + 31 * MS_PER_DAY) };\n}\n\ntype AwsCostCursor = ChunkedSyncCursor<AwsCostPhase, CostWindow>;\n\nfunction isAwsCostCursor(value: unknown): value is AwsCostCursor {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n const c = value as { phase?: unknown; page?: unknown };\n if (\n typeof c.phase !== 'string' ||\n !(PHASE_ORDER as readonly string[]).includes(c.phase)\n ) {\n return false;\n }\n const p = c.page as { start?: unknown; end?: unknown } | null | undefined;\n if (p === null) {\n return true;\n }\n if (typeof p !== 'object') {\n return false;\n }\n return typeof p.start === 'string' && typeof p.end === 'string';\n}\n\n// ---------------------------------------------------------------------------\n// Resources\n// ---------------------------------------------------------------------------\n\nexport const awsCostResources = defineResources({\n aws_cost_daily: {\n shape: 'metric',\n description:\n '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.',\n endpoint: 'POST GetCostAndUsage',\n unit: 'USD',\n granularity: 'daily',\n notes:\n 'Prefer MONTHLY granularity over long windows since each Cost Explorer query is billed. Cost Explorer accepts at most two group-by dimensions per query.',\n dimensions: [\n {\n name: 'granularity',\n description: 'Bucket granularity, DAILY or MONTHLY.',\n },\n {\n name: 'estimated',\n description:\n 'Whether the bucket is still estimated rather than finalized.',\n },\n {\n name: 'unit',\n description: 'Currency unit reported by AWS, e.g. USD.',\n },\n {\n name: 'service',\n description:\n 'AWS service name, present when grouping by SERVICE (other group-by dimensions appear as linked_account, tag_<key>, or cost_category_<key>).',\n },\n ],\n responses: { daily_cost: getCostAndUsageResponse },\n },\n aws_cost_forecast: {\n shape: 'metric',\n description:\n 'Projected future unblended AWS cost (mean value) with optional lower and upper prediction-interval bounds. Empty when the account has insufficient history to forecast.',\n endpoint: 'POST GetCostForecast',\n unit: 'USD',\n granularity: 'daily',\n notes:\n 'Prefer MONTHLY granularity over long windows since each Cost Explorer query is billed.',\n dimensions: [\n {\n name: 'granularity',\n description: 'Bucket granularity, DAILY or MONTHLY.',\n },\n {\n name: 'unit',\n description: 'Currency unit reported by AWS, e.g. USD.',\n },\n {\n name: 'lowerBound',\n description: 'Lower bound of the prediction interval, if provided.',\n },\n {\n name: 'upperBound',\n description: 'Upper bound of the prediction interval, if provided.',\n },\n ],\n responses: { forecast: getCostForecastResponse },\n },\n});\n\n// ---------------------------------------------------------------------------\n// AwsCostConnector\n// ---------------------------------------------------------------------------\n\nexport const id = 'aws-cost';\n\nexport const cost: ConnectorCost = {\n recommendedInterval: '1 day',\n minInterval: '1 hour',\n perSync: '2 Cost Explorer queries (about $0.02)',\n warning:\n 'Each AWS Cost Explorer query is billed $0.01; avoid syncing more often than necessary.',\n};\n\nexport class AwsCostConnector extends BaseAWSConnector<AwsCostSettings> {\n static readonly id = id;\n\n static readonly resources = awsCostResources;\n\n static readonly schemas = schemasFromResources(awsCostResources);\n\n static readonly cost = cost;\n\n static create(input: unknown, ctx?: ConnectorContext): AwsCostConnector {\n const parsed = configFields.parse(input);\n return new AwsCostConnector(\n {\n region: AWS_REGION,\n roleArn: parsed.roleArn,\n externalId: parsed.externalId,\n granularity: parsed.granularity,\n groupBy: parsed.groupBy,\n lookbackDays: parsed.lookbackDays,\n },\n {\n accessKeyId: parsed.accessKeyId,\n secretAccessKey: parsed.secretAccessKey,\n },\n ctx,\n );\n }\n\n readonly id = id;\n\n private async callCostExplorer<T>(\n action: string,\n payload: Record<string, unknown>,\n resource: string,\n signal?: AbortSignal,\n ): Promise<T> {\n const credentials = await this.resolveSigningCredentials(signal);\n const body = JSON.stringify(payload);\n const headers = await this.buildCeHeaders(action, body, credentials);\n try {\n const res = await this.post<unknown>(CE_URL, {\n resource,\n headers,\n body,\n signal,\n });\n const parsed =\n typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n return parsed as T;\n } catch (err) {\n throw mapAwsJsonError(err);\n }\n }\n\n private async buildCeHeaders(\n action: string,\n body: string,\n credentials: SigningCredentials,\n ): Promise<Record<string, string>> {\n const { amzDate, dateStamp } = formatAmzDate(new Date());\n const payloadHash = await sha256Hex(body);\n const amzTarget = `${CE_TARGET_PREFIX}.${action}`;\n\n const signedHeaders: Record<string, string> = {\n 'content-type': CE_CONTENT_TYPE,\n host: CE_HOST,\n 'x-amz-content-sha256': payloadHash,\n 'x-amz-date': amzDate,\n 'x-amz-target': amzTarget,\n };\n if (credentials.sessionToken !== undefined) {\n signedHeaders['x-amz-security-token'] = credentials.sessionToken;\n }\n\n const authorization = await createAuthorizationHeader({\n method: 'POST',\n host: CE_HOST,\n path: '/',\n query: '',\n headers: signedHeaders,\n payloadHash,\n accessKeyId: credentials.accessKeyId,\n secretAccessKey: credentials.secretAccessKey,\n region: AWS_REGION,\n service: CE_SERVICE,\n amzDate,\n dateStamp,\n });\n\n const sendHeaders: Record<string, string> = {\n 'Content-Type': CE_CONTENT_TYPE,\n 'X-Amz-Content-Sha256': payloadHash,\n 'X-Amz-Date': amzDate,\n 'X-Amz-Target': amzTarget,\n Authorization: authorization,\n 'User-Agent': connectorUserAgent(this.id),\n };\n if (credentials.sessionToken !== undefined) {\n sendHeaders['X-Amz-Security-Token'] = credentials.sessionToken;\n }\n return sendHeaders;\n }\n\n private async syncDailyCost(\n storage: StorageHandle,\n window: CostWindow,\n granularity: 'DAILY' | 'MONTHLY',\n groupBy: readonly string[] | undefined,\n signal?: AbortSignal,\n ): Promise<void> {\n const samples: MetricSample[] = [];\n let nextPageToken: string | undefined;\n do {\n const payload: Record<string, unknown> = {\n TimePeriod: { Start: window.start, End: window.end },\n Granularity: granularity,\n Metrics: ['UnblendedCost'],\n };\n if (groupBy && groupBy.length > 0) {\n payload['GroupBy'] = groupBy.slice(0, 2).map(toGroupDefinition);\n }\n if (nextPageToken) {\n payload['NextPageToken'] = nextPageToken;\n }\n const parsed = await this.callCostExplorer<GetCostAndUsageBody>(\n 'GetCostAndUsage',\n payload,\n 'daily_cost',\n signal,\n );\n samples.push(...buildDailyCostSamples(parsed, granularity, groupBy));\n nextPageToken =\n typeof parsed.NextPageToken === 'string' &&\n parsed.NextPageToken.length > 0\n ? parsed.NextPageToken\n : undefined;\n } while (nextPageToken);\n\n await storage.metrics(samples, { names: [DAILY_METRIC_NAME] });\n }\n\n private async syncForecast(\n storage: StorageHandle,\n granularity: 'DAILY' | 'MONTHLY',\n signal?: AbortSignal,\n ): Promise<void> {\n const window = getForecastWindow(granularity);\n let parsed: GetCostForecastBody;\n try {\n parsed = await this.callCostExplorer<GetCostForecastBody>(\n 'GetCostForecast',\n {\n TimePeriod: { Start: window.start, End: window.end },\n Metric: 'UNBLENDED_COST',\n Granularity: granularity,\n },\n 'forecast',\n signal,\n );\n } catch (err) {\n // A brand-new or low-volume account has no history to forecast from;\n // treat that as \"no forecast\" rather than failing the whole sync.\n if (isDataUnavailable(err)) {\n await storage.metrics([], { names: [FORECAST_METRIC_NAME] });\n return;\n }\n throw err;\n }\n await storage.metrics(buildForecastSamples(parsed, granularity), {\n names: [FORECAST_METRIC_NAME],\n });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const granularity = this.settings.granularity ?? 'DAILY';\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_BACKFILL_DAYS;\n const groupBy = this.settings.groupBy;\n\n const cursor = isAwsCostCursor(options.cursor) ? options.cursor : undefined;\n const page =\n cursor?.page ?? getCostWindow(options, granularity, lookbackDays);\n\n const resumeIdx = cursor ? PHASE_ORDER.indexOf(cursor.phase) : 0;\n const startIdx = resumeIdx >= 0 ? resumeIdx : 0;\n\n for (let i = startIdx; i < PHASE_ORDER.length; i++) {\n const phase = PHASE_ORDER[i]!;\n if (signal?.aborted) {\n return { done: false, cursor: { phase, page } };\n }\n if (\n options.resources &&\n options.resources.size > 0 &&\n !options.resources.has(phase)\n ) {\n continue;\n }\n try {\n if (phase === 'daily_cost') {\n await this.syncDailyCost(storage, page, granularity, groupBy, signal);\n } else {\n await this.syncForecast(storage, granularity, signal);\n }\n } catch (err) {\n if (signal?.aborted) {\n return { done: false, cursor: { phase, page } };\n }\n throw err;\n }\n }\n\n return { done: true };\n }\n}\n","import { AwsCostConnector } from './aws-cost';\n\nexport {\n AwsCostConnector,\n buildDailyCostSamples,\n buildForecastSamples,\n configFields,\n cost,\n doc,\n getCostWindow,\n id,\n awsCostResources as resources,\n} from './aws-cost';\nexport type { AwsCostSettings } from './aws-cost';\nexport default AwsCostConnector;\n"],"mappings":"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","names":["HttpClientError","TransientError","RateLimitError","AuthError","HTTP_CLIENT_VERSION","DEFAULT_USER_AGENT","connectorUserAgent","z","RateLimitError","AuthError","TransientError","cost","connectorUserAgent"]}
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@rawdash/connector-aws-cost",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Rawdash connector for AWS Cost Explorer — daily/monthly cost and forecast metrics, grouped by service, account, or tag",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
+ "sideEffects": false,
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/rawdash/rawdash.git",
@@ -23,7 +24,7 @@
23
24
  },
24
25
  "dependencies": {
25
26
  "zod": "^4.4.3",
26
- "@rawdash/core": "0.16.0"
27
+ "@rawdash/core": "0.17.0"
27
28
  },
28
29
  "devDependencies": {
29
30
  "fast-check": "^4.8.0",
@@ -32,7 +33,7 @@
32
33
  "vitest": "^4.1.4",
33
34
  "@rawdash/connector-aws-shared": "0.1.0",
34
35
  "@rawdash/connector-shared": "0.3.0",
35
- "@rawdash/connector-test-utils": "0.0.3"
36
+ "@rawdash/connector-test-utils": "0.0.4"
36
37
  },
37
38
  "scripts": {
38
39
  "build": "tsup",