@rawdash/connector-aws-cost 0.15.0 → 0.16.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/LICENSE +202 -0
- package/README.md +66 -104
- package/dist/index.d.ts +94 -3
- package/dist/index.js +133 -689
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
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","../../../core/src/aggregate.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","../../../core/src/secrets.ts","../../../core/src/connector.ts","../../../core/src/paginate-chunked.ts","../../../core/src/widget-schemas.ts","../../../core/src/config.ts","../../../core/src/engine.ts","../../../core/src/retention.ts","../../../core/src/config-fields.ts","../../../core/src/compute.ts","../../../core/src/backfill-window.ts","../../../core/src/resolve-widget.ts","../../../core/src/widget-etag.ts","../../../core/src/registry.ts","../../../core/src/storage-handle-guard.ts","../../../core/src/in-memory-storage.ts","../../../core/src/wire-config.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(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/&/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 type { Widget } from './config';\nimport type {\n AggregateRequest,\n AggregateValue,\n Entity,\n JSONValue,\n StorageHandle,\n} from './connector';\n\nexport const AGGREGATE_ENTITY_TYPE = '__widget_aggregate';\n\nexport interface AggregateClassification {\n via: 'aggregate' | 'entity-sync';\n request?: AggregateRequest;\n}\n\nexport function classifyWidget(widget: Widget): AggregateClassification {\n if (widget.kind !== 'stat') {\n return { via: 'entity-sync' };\n }\n const metric = widget.metric;\n if (metric.fn !== 'count' && metric.fn !== 'latest') {\n return { via: 'entity-sync' };\n }\n if (metric.groupBy !== undefined) {\n return { via: 'entity-sync' };\n }\n if (metric.window !== undefined || widget.window !== undefined) {\n return { via: 'entity-sync' };\n }\n const resource = metric.name ?? metric.entityType;\n if (!resource) {\n return { via: 'entity-sync' };\n }\n if (metric.fn === 'latest' && metric.field === undefined) {\n return { via: 'entity-sync' };\n }\n return {\n via: 'aggregate',\n request: {\n fn: metric.fn,\n resource,\n field: metric.field,\n filter: metric.filter,\n },\n };\n}\n\nexport function aggregateKey(dashboardId: string, widgetId: string): string {\n return `${dashboardId}:${widgetId}`;\n}\n\nexport async function writeAggregate(\n storage: StorageHandle,\n dashboardId: string,\n widgetId: string,\n value: AggregateValue,\n): Promise<void> {\n const entity: Entity = {\n type: AGGREGATE_ENTITY_TYPE,\n id: aggregateKey(dashboardId, widgetId),\n attributes: { value: value as JSONValue },\n updated_at: Date.now(),\n };\n await storage.entity(entity);\n}\n\nexport async function readAggregate(\n storage: StorageHandle,\n dashboardId: string,\n widgetId: string,\n): Promise<{ value: AggregateValue; updatedAt: number } | null> {\n const entity = await storage.getEntity(\n AGGREGATE_ENTITY_TYPE,\n aggregateKey(dashboardId, widgetId),\n );\n if (!entity) {\n return null;\n }\n return {\n value: (entity.attributes['value'] ?? null) as AggregateValue,\n updatedAt: entity.updated_at,\n };\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 { z } from 'zod';\n\nexport type Secret = { $secret: string };\nexport type SecretRef = Secret;\n\nexport function secret(name: string): Secret {\n if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {\n throw new Error(\n `Invalid secret name \"${name}\". Must match /^[A-Z][A-Z0-9_]*$/ ` +\n `(uppercase letters, digits, underscores; must start with a letter).`,\n );\n }\n return { $secret: name };\n}\n\nexport function isSecret(value: unknown): value is Secret {\n return (\n typeof value === 'object' &&\n value !== null &&\n '$secret' in value &&\n typeof (value as Secret).$secret === 'string'\n );\n}\n\nexport const secretRefSchema: z.ZodType<SecretRef> = z.strictObject({\n $secret: z.string(),\n});\n\nexport function withSecretRef<T extends z.ZodTypeAny>(\n schema: T,\n): z.ZodUnion<[T, z.ZodType<SecretRef>]> {\n return z.union([schema, secretRefSchema]);\n}\n\nexport interface SecretsResolver {\n resolve(name: string): unknown;\n}\n\nexport class EnvSecretsResolver implements SecretsResolver {\n resolve(name: string): unknown {\n const env = (\n globalThis as { process?: { env?: Record<string, string | undefined> } }\n ).process?.env;\n const raw = env?.[name];\n if (raw === undefined) {\n return undefined;\n }\n if (raw.length === 0) {\n return raw;\n }\n const first = raw.charCodeAt(0);\n if (first !== 0x7b /* { */ && first !== 0x5b /* [ */) {\n return raw;\n }\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n }\n}\n\nexport function extractSecretNames(value: unknown): string[] {\n const names: string[] = [];\n const visit = (v: unknown): void => {\n if (isSecret(v)) {\n names.push(v.$secret);\n return;\n }\n if (v && typeof v === 'object') {\n if (Array.isArray(v)) {\n v.forEach(visit);\n } else {\n Object.values(v as Record<string, unknown>).forEach(visit);\n }\n }\n };\n visit(value);\n return [...new Set(names)];\n}\n\nexport function resolveSecrets<T>(obj: T, resolver: SecretsResolver): T {\n if (isSecret(obj)) {\n const name = obj.$secret;\n const value = resolver.resolve(name);\n if (value === undefined) {\n throw new Error(\n `Missing secret \"${name}\". Set it via process.env.${name} or the CLI: rawdash secrets set ${name} ...`,\n );\n }\n return value as unknown as T;\n }\n if (Array.isArray(obj)) {\n return obj.map((item) => resolveSecrets(item, resolver)) as unknown as T;\n }\n if (typeof obj === 'object' && obj !== null) {\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(obj as object)) {\n Object.defineProperty(result, key, {\n value: resolveSecrets(val, resolver),\n enumerable: true,\n configurable: true,\n writable: true,\n });\n }\n return result as T;\n }\n return obj;\n}\n","import {\n type ConnectorLogger,\n type HttpRequest,\n type HttpResponse,\n type RequestObserver,\n createDefaultConnectorLogger,\n request as sharedRequest,\n} from '@rawdash/connector-shared';\n\nimport type { FilterClause } from './filters';\nimport {\n EnvSecretsResolver,\n type Secret,\n type SecretsResolver,\n resolveSecrets,\n} from './secrets';\n\nexport type JSONValue =\n | string\n | number\n | boolean\n | null\n | JSONValue[]\n | { [key: string]: JSONValue };\n\n// ---------------------------------------------------------------------------\n// Five storage shapes\n// ---------------------------------------------------------------------------\n\nexport interface Event {\n name: string;\n start_ts: number;\n end_ts: number | null;\n attributes: Record<string, JSONValue>;\n}\n\nexport interface Entity {\n type: string;\n id: string;\n attributes: Record<string, JSONValue>;\n updated_at: number;\n}\n\nexport interface MetricSample {\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, JSONValue>;\n}\n\nexport interface Edge {\n from_type: string;\n from_id: string;\n kind: string;\n to_type: string;\n to_id: string;\n attributes: Record<string, JSONValue>;\n updated_at: number;\n}\n\nexport type Distribution =\n | {\n name: string;\n ts: number;\n kind: 'histogram';\n data: {\n buckets: Array<{ le: number; count: number }>;\n count: number;\n sum: number;\n };\n attributes: Record<string, JSONValue>;\n }\n | {\n name: string;\n ts: number;\n kind: 'summary';\n data: {\n quantiles: Array<{ q: number; value: number }>;\n count: number;\n sum: number;\n };\n attributes: Record<string, JSONValue>;\n };\n\n// ---------------------------------------------------------------------------\n// Storage query types\n// ---------------------------------------------------------------------------\n\nexport interface EventQuery {\n name?: string;\n start?: number;\n end?: number;\n}\n\nexport interface EntityQuery {\n type: string;\n}\n\nexport interface MetricQuery {\n name?: string;\n start?: number;\n end?: number;\n}\n\nexport interface EdgeQuery {\n fromType?: string;\n fromId?: string;\n kind?: string;\n toType?: string;\n toId?: string;\n}\n\nexport interface DistributionQuery {\n name?: string;\n start?: number;\n end?: number;\n}\n\n// ---------------------------------------------------------------------------\n// StorageHandle — write and read surface\n// ---------------------------------------------------------------------------\n\nexport interface StorageHandle {\n event(e: Event): Promise<void>;\n entity(e: Entity): Promise<void>;\n metric(m: MetricSample): Promise<void>;\n edge(e: Edge): Promise<void>;\n distribution(d: Distribution): Promise<void>;\n\n events(es: Event[], scope?: { names?: string[] }): Promise<void>;\n entities(es: Entity[], scope?: { types?: string[] }): Promise<void>;\n metrics(ms: MetricSample[], scope?: { names?: string[] }): Promise<void>;\n edges(es: Edge[], scope?: { kinds?: string[] }): Promise<void>;\n distributions(\n ds: Distribution[],\n scope?: { names?: string[] },\n ): Promise<void>;\n\n queryEvents(q: EventQuery): Promise<Event[]>;\n getEntity(type: string, id: string): Promise<Entity | null>;\n queryEntities(q: EntityQuery): Promise<Entity[]>;\n queryMetrics(q: MetricQuery): Promise<MetricSample[]>;\n traverse(q: EdgeQuery): Promise<Edge[]>;\n queryDistributions(q: DistributionQuery): Promise<Distribution[]>;\n\n // Deletes all rows in the given time-series shape whose timestamp column is\n // strictly less than `tsUnixMs`. Only covers append-only shapes (events,\n // metrics, distributions). Entities and edges are excluded because they hold\n // the latest known state per primary key — deleting by age would lose live\n // data. The right model for those shapes is \"expire when source disappears.\"\n deleteOlderThan(\n shape: 'events' | 'metrics' | 'distributions',\n tsUnixMs: number,\n ): Promise<{ rowsDeleted: number }>;\n\n getHealth?(): Promise<ConnectorHealth | null>;\n}\n\nexport interface ConnectorHealth {\n status: 'idle' | 'syncing' | 'error' | 'auth_failed' | 'paused';\n lastSyncAt: string | null;\n lastError: string | null;\n syncIntervalSeconds: number;\n}\n\n// ---------------------------------------------------------------------------\n// Credentials\n// ---------------------------------------------------------------------------\n\nexport interface CredentialField {\n description: string;\n auth?: 'none' | 'optional' | 'required';\n}\n\nexport type CredentialsSchema = Record<string, CredentialField>;\n\nexport type InferCredentials<TCreds extends CredentialsSchema> = {\n [K in keyof TCreds]: TCreds[K] extends { auth: 'required' }\n ? string\n : string | undefined;\n};\n\nexport type InferCredentialInput<TCreds extends CredentialsSchema> = {\n [K in keyof TCreds]: TCreds[K] extends { auth: 'required' }\n ? string | Secret\n : string | Secret | undefined;\n};\n\n// ---------------------------------------------------------------------------\n// Sync + Connector\n// ---------------------------------------------------------------------------\n\nexport interface SyncOptions {\n mode: 'full' | 'latest';\n since?: string;\n cursor?: unknown;\n resources?: ReadonlySet<string>;\n}\n\nexport interface SyncResult {\n done: boolean;\n cursor?: unknown;\n transientError?: unknown;\n}\n\nexport interface AggregateRequest {\n fn: 'count' | 'latest';\n resource: string;\n field?: string;\n filter?: FilterClause[];\n}\n\n// A resolved value is treated by the runner as a successful aggregate write\n// (including `null` and `0`, which are legitimate \"no data\" answers). If the\n// connector cannot serve this combination of resource/field/filter at all,\n// it MUST throw — that signals \"unsupported\", causes the runner to skip the\n// scalar write, and keeps the resource in the entity-sync allowlist so\n// computeMetric can take over.\nexport type AggregateValue = JSONValue;\n\nexport interface Connector {\n readonly id: string;\n readonly credentials?: CredentialsSchema;\n serializeConfig(): Record<string, unknown>;\n sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult>;\n aggregate?(\n req: AggregateRequest,\n signal?: AbortSignal,\n ): Promise<AggregateValue>;\n // Optional config-time validation: throw a descriptive Error if the\n // connector cannot serve count(resource, filter). Core can call this\n // when validating user-authored configs so unsupported filters fail\n // fast instead of at sync time. Connectors that don't implement this\n // are assumed to accept any filter the runtime hands them.\n validateCountFilter?(resource: string, filter: FilterClause[]): void;\n}\n\nexport interface ConnectorContext {\n observer?: RequestObserver;\n secretsResolver?: SecretsResolver;\n logger?: ConnectorLogger;\n}\n\nexport interface ConnectorRequestOptions {\n resource: string;\n requestId?: string;\n}\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n signal?: AbortSignal;\n}\n\nexport abstract class BaseConnector<\n TSettings = unknown,\n TCreds extends CredentialsSchema = CredentialsSchema,\n> implements Connector {\n abstract readonly id: string;\n readonly credentials?: TCreds;\n\n protected settings: TSettings;\n protected creds: InferCredentials<TCreds>;\n private rawCredInput: InferCredentialInput<TCreds> | undefined;\n private ctx: ConnectorContext;\n private cachedLogger: ConnectorLogger | undefined;\n\n constructor(\n settings: TSettings,\n creds?: InferCredentialInput<TCreds>,\n ctx?: ConnectorContext,\n ) {\n this.settings = settings;\n this.rawCredInput = creds;\n this.ctx = ctx ?? {};\n this.creds = creds\n ? (resolveSecrets(\n creds,\n this.ctx.secretsResolver ?? new EnvSecretsResolver(),\n ) as InferCredentials<TCreds>)\n : ({} as InferCredentials<TCreds>);\n }\n\n protected get logger(): ConnectorLogger {\n if (!this.cachedLogger) {\n this.cachedLogger =\n this.ctx.logger ?? createDefaultConnectorLogger({ scope: this.id });\n }\n return this.cachedLogger;\n }\n\n protected request<T = unknown>(\n req: HttpRequest,\n opts: ConnectorRequestOptions,\n ): Promise<HttpResponse<T>> {\n return sharedRequest<T>(req, {\n resource: opts.resource,\n requestId: opts.requestId,\n observer: this.ctx.observer,\n });\n }\n\n protected get<T = unknown>(\n url: string,\n opts: ConnectorRequestOptions & {\n headers?: Record<string, string>;\n signal?: AbortSignal;\n rateLimit?: HttpRequest['rateLimit'];\n },\n ): Promise<HttpResponse<T>> {\n return this.request<T>(\n {\n url,\n method: 'GET',\n headers: opts.headers,\n signal: opts.signal,\n rateLimit: opts.rateLimit,\n },\n { resource: opts.resource, requestId: opts.requestId },\n );\n }\n\n protected post<T = unknown>(\n url: string,\n opts: ConnectorRequestOptions & {\n body?: HttpRequest['body'];\n headers?: Record<string, string>;\n signal?: AbortSignal;\n rateLimit?: HttpRequest['rateLimit'];\n },\n ): Promise<HttpResponse<T>> {\n return this.request<T>(\n {\n url,\n method: 'POST',\n headers: opts.headers,\n body: opts.body,\n signal: opts.signal,\n rateLimit: opts.rateLimit,\n },\n { resource: opts.resource, requestId: opts.requestId },\n );\n }\n\n protected isResourceEnabled<R extends string>(resource: R): boolean {\n const enabled = (this.settings as { resources?: readonly R[] } | null)\n ?.resources;\n if (!enabled || enabled.length === 0) {\n return true;\n }\n return enabled.includes(resource);\n }\n\n serializeConfig(): Record<string, unknown> {\n const config: Record<string, unknown> = {\n ...(this.settings as Record<string, unknown>),\n };\n if (this.rawCredInput) {\n for (const [key, value] of Object.entries(\n this.rawCredInput as Record<string, unknown>,\n )) {\n if (value !== undefined) {\n config[key] = value;\n }\n }\n }\n return config;\n }\n\n protected 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\n protected async withRetry<T>(\n fn: (\n signal?: AbortSignal,\n ) => Promise<{ status: 'done'; value: T } | { status: 'retry' }>,\n options?: RetryPolicy,\n ): Promise<T | null> {\n const {\n maxAttempts = 10,\n initialDelayMs = 1000,\n maxDelayMs = 10000,\n signal,\n } = options ?? {};\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n signal?.throwIfAborted();\n const result = await fn(signal);\n if (result.status === 'done') {\n return result.value;\n }\n if (attempt < maxAttempts - 1) {\n const delay = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);\n await this.sleep(delay, signal);\n }\n }\n\n return null;\n }\n\n abstract sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult>;\n\n aggregate?(\n req: AggregateRequest,\n signal?: AbortSignal,\n ): Promise<AggregateValue>;\n}\n\nexport function defineConnector<TSettings>() {\n return function <\n TCreds extends CredentialsSchema = Record<string, never>,\n >(def: {\n id: string;\n credentials?: TCreds;\n sync: (\n this: { settings: TSettings; creds: InferCredentials<TCreds> },\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ) => Promise<SyncResult>;\n aggregate?: (\n this: { settings: TSettings; creds: InferCredentials<TCreds> },\n req: AggregateRequest,\n signal?: AbortSignal,\n ) => Promise<AggregateValue>;\n validateCountFilter?: (\n this: { settings: TSettings; creds: InferCredentials<TCreds> },\n resource: string,\n filter: FilterClause[],\n ) => void;\n }): {\n new (\n settings: TSettings,\n creds?: InferCredentialInput<TCreds>,\n ctx?: ConnectorContext,\n ): Connector;\n readonly id: string;\n readonly credentials: TCreds | undefined;\n } {\n class DynamicConnector extends BaseConnector<TSettings, TCreds> {\n static readonly id = def.id;\n static readonly credentials = def.credentials;\n\n readonly id = def.id;\n override readonly credentials = def.credentials;\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n return def.sync.call(\n { settings: this.settings, creds: this.creds },\n options,\n storage,\n signal,\n );\n }\n\n override aggregate = def.aggregate\n ? async (\n req: AggregateRequest,\n signal?: AbortSignal,\n ): Promise<AggregateValue> => {\n return def.aggregate!.call(\n { settings: this.settings, creds: this.creds },\n req,\n signal,\n );\n }\n : undefined;\n\n validateCountFilter = def.validateCountFilter\n ? (resource: string, filter: FilterClause[]): void => {\n def.validateCountFilter!.call(\n { settings: this.settings, creds: this.creds },\n resource,\n filter,\n );\n }\n : undefined;\n }\n\n return DynamicConnector as unknown as {\n new (\n settings: TSettings,\n creds?: InferCredentialInput<TCreds>,\n ctx?: ConnectorContext,\n ): Connector;\n readonly id: string;\n readonly credentials: TCreds | undefined;\n };\n };\n}\n","import type { ConnectorLogger } from '@rawdash/connector-shared';\n\nimport type { SyncResult } from './connector';\n\nexport interface ChunkedSyncCursor<TPhase extends string, TPage> {\n phase: TPhase;\n page: TPage | null;\n}\n\nexport function selectActivePhases<R extends string, P extends string>(\n resourceToPhase: (resource: R) => P,\n order: readonly P[],\n enabled: readonly R[] | undefined,\n): P[] {\n if (!enabled || enabled.length === 0) {\n return [...order];\n }\n const want = new Set<P>();\n for (const r of enabled) {\n want.add(resourceToPhase(r));\n }\n return order.filter((p) => want.has(p));\n}\n\nexport function makeChunkedCursorGuard<TPhase extends string>(\n phases: readonly TPhase[],\n): (value: unknown) => value is ChunkedSyncCursor<TPhase, string> {\n const phaseSet = new Set<string>(phases);\n return (value: unknown): value is ChunkedSyncCursor<TPhase, string> => {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n const v = value as { phase?: unknown; page?: unknown };\n if (typeof v.phase !== 'string' || !phaseSet.has(v.phase)) {\n return false;\n }\n if (v.page !== null && typeof v.page !== 'string') {\n return false;\n }\n return true;\n };\n}\n\nexport interface FetchPageResult<TPage> {\n items: unknown[];\n next: TPage | null;\n}\n\nexport interface ChunkedSyncOptions<TPhase extends string, TPage> {\n phases: readonly TPhase[];\n cursor: ChunkedSyncCursor<TPhase, TPage> | undefined;\n signal: AbortSignal | undefined;\n fetchPage: (\n phase: TPhase,\n page: TPage | null,\n signal: AbortSignal | undefined,\n ) => Promise<FetchPageResult<TPage>>;\n writeBatch: (\n phase: TPhase,\n items: unknown[],\n page: TPage | null,\n ) => Promise<void>;\n logger?: ConnectorLogger;\n}\n\nfunction truncateCursor(page: unknown): string | undefined {\n if (page === null || page === undefined) {\n return undefined;\n }\n const s = typeof page === 'string' ? page : JSON.stringify(page);\n if (s.length <= 80) {\n return s;\n }\n return `${s.slice(0, 79)}…`;\n}\n\nexport async function paginateChunked<TPhase extends string, TPage>(\n opts: ChunkedSyncOptions<TPhase, TPage>,\n): Promise<SyncResult> {\n const { phases, cursor, signal, fetchPage, writeBatch, logger } = opts;\n\n if (phases.length === 0) {\n return { done: true };\n }\n\n const resumeIdx = cursor ? phases.indexOf(cursor.phase) : -1;\n const hasKnownResumePhase = resumeIdx >= 0;\n const startIdx = hasKnownResumePhase ? resumeIdx : 0;\n\n for (let i = startIdx; i < phases.length; i++) {\n const phase = phases[i]!;\n let page: TPage | null =\n i === startIdx && hasKnownResumePhase ? cursor!.page : null;\n let pageCount = 0;\n let itemCount = 0;\n const phaseStart = Date.now();\n\n while (true) {\n if (signal?.aborted) {\n return {\n done: false,\n cursor: { phase, page } satisfies ChunkedSyncCursor<TPhase, TPage>,\n };\n }\n pageCount += 1;\n let items: unknown[];\n let next: TPage | null;\n try {\n ({ items, next } = await fetchPage(phase, page, signal));\n } catch (err) {\n if (\n signal?.aborted ||\n (err instanceof Error && err.name === 'AbortError')\n ) {\n return {\n done: false,\n cursor: { phase, page } satisfies ChunkedSyncCursor<TPhase, TPage>,\n };\n }\n logger?.warn('fetch page failed', {\n resource: phase,\n page: pageCount,\n cursor: truncateCursor(page),\n error: err instanceof Error ? err.message : String(err),\n });\n return {\n done: false,\n cursor: { phase, page } satisfies ChunkedSyncCursor<TPhase, TPage>,\n transientError: err,\n };\n }\n itemCount += items.length;\n logger?.info('fetched page', {\n resource: phase,\n page: pageCount,\n items: items.length,\n cursor: truncateCursor(page),\n next: truncateCursor(next),\n });\n try {\n await writeBatch(phase, items, page);\n } catch (err) {\n if (\n signal?.aborted ||\n (err instanceof Error && err.name === 'AbortError')\n ) {\n return {\n done: false,\n cursor: { phase, page } satisfies ChunkedSyncCursor<TPhase, TPage>,\n };\n }\n logger?.warn('write batch failed', {\n resource: phase,\n page: pageCount,\n cursor: truncateCursor(page),\n error: err instanceof Error ? err.message : String(err),\n });\n return {\n done: false,\n cursor: { phase, page } satisfies ChunkedSyncCursor<TPhase, TPage>,\n transientError: err,\n };\n }\n if (next === null) {\n break;\n }\n page = next;\n }\n logger?.info('resource done', {\n resource: phase,\n pages: pageCount,\n items: itemCount,\n duration_ms: Date.now() - phaseStart,\n });\n }\n\n return { done: true };\n}\n","import { z } from 'zod';\n\nexport const shapeSchema = z.enum([\n 'event',\n 'entity',\n 'metric',\n 'edge',\n 'distribution',\n]);\n\nexport const aggFnSchema = z.enum([\n 'count',\n 'sum',\n 'avg',\n 'min',\n 'max',\n 'latest',\n 'first',\n]);\n\nexport const filterOperatorSchema = z.enum([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'contains',\n]);\n\nexport const filterConditionSchema = z.object({\n field: z.string(),\n op: filterOperatorSchema,\n value: z.union([z.string(), z.number(), z.boolean()]),\n});\n\nexport const filterClauseSchema = z.union([\n filterConditionSchema,\n z.object({ or: z.array(filterConditionSchema) }),\n]);\n\nexport const groupBySchema = z.object({\n field: z.string(),\n granularity: z.enum(['hour', 'day', 'week', 'month']),\n});\n\nexport const computedMetricSchema = z\n .object({\n connectorId: z.string(),\n shape: shapeSchema,\n name: z.string().optional(),\n entityType: z.string().optional(),\n field: z.string().optional(),\n fn: aggFnSchema,\n window: z.string().optional(),\n filter: z.array(filterClauseSchema).optional(),\n groupBy: groupBySchema.optional(),\n })\n .refine((m) => m.fn === 'count' || m.field !== undefined, {\n message: 'field is required unless fn is \"count\"',\n path: ['field'],\n });\n\nconst titleField = z\n .string()\n .meta({ label: 'Title', description: 'Widget title.' });\n\nexport const statWidgetSchema = z.object({\n kind: z.literal('stat'),\n title: titleField,\n metric: computedMetricSchema.meta({\n label: 'Metric',\n description: 'Computed metric definition.',\n }),\n window: z\n .string()\n .optional()\n .meta({ label: 'Window', description: \"Time window, e.g. '7d'.\" }),\n compare: z\n .enum(['none', 'previous-period'])\n .default('none')\n .meta({ label: 'Compare', description: 'Comparison mode.' }),\n});\n\nexport const statusWidgetSchema = z.object({\n kind: z.literal('status'),\n title: titleField,\n source: z.string().meta({\n label: 'Source',\n description: 'Connector or data source reference.',\n }),\n});\n\nexport const timeseriesWidgetSchema = z.object({\n kind: z.literal('timeseries'),\n title: titleField,\n metric: computedMetricSchema.meta({\n label: 'Metric',\n description: 'Computed metric definition.',\n }),\n window: z\n .string()\n .meta({ label: 'Window', description: \"Time window, e.g. '30d'.\" }),\n granularity: z\n .enum(['hour', 'day', 'week'])\n .default('day')\n .meta({ label: 'Granularity', description: 'Time bucket size.' }),\n});\n\nexport const distributionWidgetSchema = z.object({\n kind: z.literal('distribution'),\n title: titleField,\n metric: computedMetricSchema.meta({\n label: 'Metric',\n description: 'Computed metric definition.',\n }),\n window: z\n .string()\n .meta({ label: 'Window', description: \"Time window, e.g. '7d'.\" }),\n});\n\nexport const widgetSchemas = {\n stat: statWidgetSchema,\n status: statusWidgetSchema,\n timeseries: timeseriesWidgetSchema,\n distribution: distributionWidgetSchema,\n} as const;\n\nexport const widgetSchema = z.discriminatedUnion('kind', [\n statWidgetSchema,\n statusWidgetSchema,\n timeseriesWidgetSchema,\n distributionWidgetSchema,\n]);\n\nexport type WidgetKind = keyof typeof widgetSchemas;\n\nexport function getWidgetSchema(kind: WidgetKind) {\n return widgetSchemas[kind];\n}\n","import type { FilterClause, FilterCondition, FilterOperator } from './filters';\nimport type { RetentionConfig } from './retention';\nimport { getWidgetSchema, widgetSchemas } from './widget-schemas';\nimport type { WidgetKind } from './widget-schemas';\n\nexport type { FilterClause, FilterCondition, FilterOperator };\n\n// ---------------------------------------------------------------------------\n// Aggregation functions\n// ---------------------------------------------------------------------------\n\nexport type AggFn =\n | 'count'\n | 'sum'\n | 'avg'\n | 'min'\n | 'max'\n | 'latest'\n | 'first';\n\n// ---------------------------------------------------------------------------\n// Shape\n// ---------------------------------------------------------------------------\n\nexport type Shape = 'event' | 'entity' | 'metric' | 'edge' | 'distribution';\n\n// ---------------------------------------------------------------------------\n// GroupBy\n// ---------------------------------------------------------------------------\n\nexport interface GroupBy {\n field: string;\n granularity: 'hour' | 'day' | 'week' | 'month';\n}\n\n// ---------------------------------------------------------------------------\n// Metric\n// ---------------------------------------------------------------------------\n\nexport interface Metric {\n connector: { name: string };\n shape: Shape;\n name?: string;\n entityType?: string;\n field?: string;\n fn: AggFn;\n window?: string;\n filter?: FilterClause[];\n groupBy?: GroupBy;\n}\n\nexport interface ComputedMetric {\n readonly connectorId: string;\n readonly shape: Shape;\n readonly name?: string;\n readonly entityType?: string;\n readonly field?: string;\n readonly fn: AggFn;\n readonly window?: string;\n readonly filter?: FilterClause[];\n readonly groupBy?: GroupBy;\n}\n\n// ---------------------------------------------------------------------------\n// Widget definition\n// ---------------------------------------------------------------------------\n\nexport interface StatWidget {\n kind: 'stat';\n title: string;\n metric: ComputedMetric;\n window?: string;\n compare?: 'none' | 'previous-period';\n}\n\nexport interface StatusWidget {\n kind: 'status';\n title: string;\n source: string;\n}\n\nexport interface TimeseriesWidget {\n kind: 'timeseries';\n title: string;\n metric: ComputedMetric;\n window: string;\n granularity?: 'hour' | 'day' | 'week';\n}\n\nexport interface DistributionWidget {\n kind: 'distribution';\n title: string;\n metric: ComputedMetric;\n window: string;\n}\n\nexport type Widget =\n | StatWidget\n | StatusWidget\n | TimeseriesWidget\n | DistributionWidget;\n\nexport type { WidgetKind };\n\n// ---------------------------------------------------------------------------\n// Dashboard config\n// ---------------------------------------------------------------------------\n\nexport interface ConfiguredConnector {\n name: string;\n connectorId: string;\n config: Record<string, unknown>;\n syncIntervalSeconds?: number;\n enabled?: boolean;\n displayName?: string;\n}\n\nexport interface Dashboard {\n widgets: Record<string, Widget>;\n}\n\nexport interface DashboardConfig {\n connectors: ConfiguredConnector[];\n dashboards: Record<string, Dashboard>;\n retention?: RetentionConfig;\n}\n\n// ---------------------------------------------------------------------------\n// defineDashboard\n// ---------------------------------------------------------------------------\n\nconst VALID_WIDGET_KINDS = new Set<string>(Object.keys(widgetSchemas));\n\nexport function defineDashboard(options: {\n widgets: Record<string, Widget>;\n}): Dashboard {\n for (const [key, widget] of Object.entries(options.widgets)) {\n if (!VALID_WIDGET_KINDS.has(widget.kind)) {\n throw new Error(\n `Widget \"${key}\": unknown kind \"${widget.kind}\". Must be one of: ${[...VALID_WIDGET_KINDS].join(', ')}`,\n );\n }\n const schema = getWidgetSchema(widget.kind as WidgetKind);\n const result = schema.safeParse(widget);\n if (!result.success) {\n throw new Error(\n `Widget \"${key}\" (kind \"${widget.kind}\"): ${result.error.issues.map((i) => i.message).join('; ')}`,\n );\n }\n }\n return { widgets: options.widgets };\n}\n\n// ---------------------------------------------------------------------------\n// defineMetric\n// ---------------------------------------------------------------------------\n\nexport function defineMetric(options: Metric): ComputedMetric {\n return {\n connectorId: options.connector.name,\n shape: options.shape,\n name: options.name,\n entityType: options.entityType,\n field: options.field,\n fn: options.fn,\n window: options.window,\n filter: options.filter,\n groupBy: options.groupBy,\n };\n}\n\n// ---------------------------------------------------------------------------\n// defineConfig\n// ---------------------------------------------------------------------------\n\nconst VALID_SHAPES = new Set<string>([\n 'event',\n 'entity',\n 'metric',\n 'edge',\n 'distribution',\n]);\nconst VALID_FNS = new Set<string>([\n 'count',\n 'sum',\n 'avg',\n 'min',\n 'max',\n 'latest',\n 'first',\n]);\n\nconst SAFE_KEY_RE = /^[a-zA-Z0-9_-]+$/;\n\nfunction validateConfig(config: DashboardConfig): void {\n if (config.retention) {\n const { maxAge, maxSize, floor, intervalMs } = config.retention;\n if (maxAge !== undefined && (!Number.isFinite(maxAge) || maxAge < 0)) {\n throw new Error('retention.maxAge must be a finite number >= 0');\n }\n if (maxSize !== undefined && (!Number.isInteger(maxSize) || maxSize < 0)) {\n throw new Error('retention.maxSize must be an integer >= 0');\n }\n if (floor !== undefined && (!Number.isInteger(floor) || floor < 0)) {\n throw new Error('retention.floor must be an integer >= 0');\n }\n if (\n intervalMs !== undefined &&\n (!Number.isFinite(intervalMs) || intervalMs <= 0)\n ) {\n throw new Error('retention.intervalMs must be a finite number > 0');\n }\n }\n\n if (\n !config.dashboards ||\n typeof config.dashboards !== 'object' ||\n Array.isArray(config.dashboards)\n ) {\n throw new Error(\n 'defineConfig: config must include a \"dashboards\" record — did you mean to migrate from the old flat \"widgets\" shape?',\n );\n }\n\n if (!Array.isArray(config.connectors)) {\n throw new Error('defineConfig: \"connectors\" must be an array');\n }\n\n const connectorNames = new Set<string>();\n for (const entry of config.connectors) {\n if (!entry || typeof entry !== 'object') {\n throw new Error(\n 'defineConfig: every connector entry must be an object with \"name\", \"connectorId\", and \"config\"',\n );\n }\n if (!entry.name) {\n throw new Error('defineConfig: every connector entry must have a \"name\"');\n }\n if (!entry.connectorId) {\n throw new Error(\n `defineConfig: connector \"${entry.name}\" must have a \"connectorId\" (the connector type id)`,\n );\n }\n if (\n entry.config === null ||\n typeof entry.config !== 'object' ||\n Array.isArray(entry.config)\n ) {\n throw new Error(\n `defineConfig: connector \"${entry.name}\" must have a \"config\" object`,\n );\n }\n if (connectorNames.has(entry.name)) {\n throw new Error(\n `defineConfig: duplicate connector name \"${entry.name}\". Each instance must have a unique name.`,\n );\n }\n connectorNames.add(entry.name);\n }\n\n for (const [dashboardKey, dashboard] of Object.entries(config.dashboards)) {\n if (\n !dashboard.widgets ||\n typeof dashboard.widgets !== 'object' ||\n Array.isArray(dashboard.widgets)\n ) {\n throw new Error(\n `Dashboard \"${dashboardKey}\" must define a \"widgets\" record`,\n );\n }\n\n if (!SAFE_KEY_RE.test(dashboardKey)) {\n throw new Error(\n `Dashboard key \"${dashboardKey}\" contains URL-unsafe characters; use only letters, digits, hyphens, and underscores`,\n );\n }\n\n for (const [widgetKey, widget] of Object.entries(dashboard.widgets)) {\n const ref = `Dashboard \"${dashboardKey}\", widget \"${widgetKey}\"`;\n\n if (!SAFE_KEY_RE.test(widgetKey)) {\n throw new Error(\n `${ref}: widget key contains URL-unsafe characters; use only letters, digits, hyphens, and underscores`,\n );\n }\n\n if (widget.kind === 'status') {\n continue;\n }\n\n const { connectorId, shape, fn } = widget.metric;\n\n if (!connectorNames.has(connectorId)) {\n throw new Error(\n `${ref}: connector \"${connectorId}\" is not listed in connectors`,\n );\n }\n\n if (!VALID_SHAPES.has(shape)) {\n throw new Error(`${ref}: invalid shape \"${shape}\"`);\n }\n\n if (!VALID_FNS.has(fn)) {\n throw new Error(`${ref}: invalid fn \"${fn}\"`);\n }\n }\n }\n}\n\nexport function defineConfig(config: DashboardConfig): DashboardConfig {\n validateConfig(config);\n return config;\n}\n","export type SyncStatus = 'idle' | 'queued' | 'running' | 'succeeded' | 'failed';\n\nexport interface SyncState {\n status: SyncStatus;\n queuedAt: string | null;\n startedAt: string | null;\n lastSyncAt: string | null;\n lastError: string | null;\n}\n\nexport const ACTIVE_SYNC_STATUSES: ReadonlySet<SyncStatus> = new Set([\n 'queued',\n 'running',\n]);\n\nexport function isSyncActive(status: SyncStatus): boolean {\n return ACTIVE_SYNC_STATUSES.has(status);\n}\n","import type {\n Distribution,\n Event,\n MetricSample,\n StorageHandle,\n} from './connector';\n\n// ---------------------------------------------------------------------------\n// RetentionConfig\n// ---------------------------------------------------------------------------\n\nexport interface RetentionConfig {\n maxAge?: number;\n maxSize?: number;\n floor?: number;\n intervalMs?: number;\n}\n\n// ---------------------------------------------------------------------------\n// RetentionDeletionPlan — rows eligible for deletion across time-series shapes\n// ---------------------------------------------------------------------------\n\nexport interface RetentionDeletionPlan {\n events: Event[];\n metrics: MetricSample[];\n distributions: Distribution[];\n}\n\n// ---------------------------------------------------------------------------\n// selectForDeletion — pure computation\n//\n// Receives rows pre-sorted newest-first (descending by timestamp).\n// Returns the subset that should be deleted given the policy.\n//\n// Rules applied in order:\n// 1. Rows beyond maxSize are candidates.\n// 2. Rows older than maxAge milliseconds are candidates.\n// 3. Rows within the newest `floor` positions are always kept (overrides 1 & 2).\n// ---------------------------------------------------------------------------\n\nexport function selectForDeletion<T>(\n rows: T[],\n getTs: (row: T) => number,\n config: RetentionConfig,\n nowMs: number = Date.now(),\n): T[] {\n const { maxAge, maxSize, floor = 0 } = config;\n\n if (maxAge === undefined && maxSize === undefined) {\n return [];\n }\n\n const toDelete: T[] = [];\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i]!;\n if (i < floor) {\n continue;\n }\n\n const overSize = maxSize !== undefined && i >= maxSize;\n const tooOld = maxAge !== undefined && getTs(row) < nowMs - maxAge;\n\n if (overSize || tooOld) {\n toDelete.push(row);\n }\n }\n\n return toDelete;\n}\n\n// ---------------------------------------------------------------------------\n// computeRetention — async, queries the handle and returns deletion candidates\n//\n// Only covers time-series shapes (events, metrics, distributions) since those\n// grow unboundedly via append. Entities and edges are upsert-keyed and do not\n// accumulate the same way.\n// ---------------------------------------------------------------------------\n\nexport async function computeRetention(\n handle: StorageHandle,\n config: RetentionConfig,\n nowMs: number = Date.now(),\n): Promise<RetentionDeletionPlan> {\n const [events, metrics, distributions] = await Promise.all([\n handle.queryEvents({}),\n handle.queryMetrics({}),\n handle.queryDistributions({}),\n ]);\n\n const sortedEvents = [...events].sort((a, b) => b.start_ts - a.start_ts);\n const sortedMetrics = [...metrics].sort((a, b) => b.ts - a.ts);\n const sortedDistributions = [...distributions].sort((a, b) => b.ts - a.ts);\n\n return {\n events: selectForDeletion(sortedEvents, (e) => e.start_ts, config, nowMs),\n metrics: selectForDeletion(sortedMetrics, (m) => m.ts, config, nowMs),\n distributions: selectForDeletion(\n sortedDistributions,\n (d) => d.ts,\n config,\n nowMs,\n ),\n };\n}\n","import { z } from 'zod';\n\nexport type ConfigFieldsSchema = z.ZodObject<z.ZodRawShape>;\n\nexport function defineConfigFields<T extends z.ZodRawShape>(\n schema: z.ZodObject<T>,\n): z.ZodObject<T> {\n if (!(schema instanceof z.ZodObject)) {\n throw new Error(\n `configFields must be a Zod object schema (z.object({...})). Received: ${Object.prototype.toString.call(schema)}`,\n );\n }\n return schema;\n}\n","import type { ComputedMetric } from './config';\nimport type { StorageHandle } from './connector';\n\ntype FilterClause = NonNullable<ComputedMetric['filter']>[number];\ntype FilterCondition = Exclude<FilterClause, { or: unknown[] }>;\n\nfunction matchesCondition(\n record: Record<string, unknown>,\n cond: FilterCondition,\n): boolean {\n const val = record[cond.field];\n switch (cond.op) {\n case 'eq':\n return val === cond.value;\n case 'neq':\n return val !== cond.value;\n case 'gt':\n if (typeof val !== 'number' || typeof cond.value !== 'number') {\n return false;\n }\n return val > cond.value;\n case 'gte':\n if (typeof val !== 'number' || typeof cond.value !== 'number') {\n return false;\n }\n return val >= cond.value;\n case 'lt':\n if (typeof val !== 'number' || typeof cond.value !== 'number') {\n return false;\n }\n return val < cond.value;\n case 'lte':\n if (typeof val !== 'number' || typeof cond.value !== 'number') {\n return false;\n }\n return val <= cond.value;\n case 'contains':\n return String(val).includes(String(cond.value));\n default:\n return false;\n }\n}\n\nfunction applyFilter(\n record: Record<string, unknown>,\n filter: ComputedMetric['filter'],\n): boolean {\n if (!filter) {\n return true;\n }\n for (const clause of filter) {\n if ('or' in clause) {\n if (!clause.or.some((cond) => matchesCondition(record, cond))) {\n return false;\n }\n } else {\n if (!matchesCondition(record, clause)) {\n return false;\n }\n }\n }\n return true;\n}\n\nconst WINDOW_MS: Record<string, number> = {\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n m: 2_592_000_000,\n};\n\nfunction parseWindowMs(window: string): number | null {\n const match = /^(\\d+)(h|d|w|m)$/.exec(window);\n if (!match) {\n return null;\n }\n const unitMs = WINDOW_MS[match[2]!];\n if (unitMs === undefined) {\n return null;\n }\n return parseInt(match[1]!) * unitMs;\n}\n\nfunction truncateToGranularity(ts: number, granularity: string): string {\n const d = new Date(ts);\n switch (granularity) {\n case 'hour':\n d.setUTCMinutes(0, 0, 0);\n return d.toISOString();\n case 'day':\n d.setUTCHours(0, 0, 0, 0);\n return d.toISOString().slice(0, 10);\n case 'week': {\n d.setUTCDate(d.getUTCDate() - d.getUTCDay());\n d.setUTCHours(0, 0, 0, 0);\n return d.toISOString().slice(0, 10);\n }\n case 'month':\n d.setUTCDate(1);\n d.setUTCHours(0, 0, 0, 0);\n return d.toISOString().slice(0, 7);\n default:\n return d.toISOString().slice(0, 10);\n }\n}\n\nfunction computeAgg(\n records: Record<string, unknown>[],\n field: string | undefined,\n fn: string,\n): unknown {\n if (fn === 'count') {\n return records.length;\n }\n if (field === undefined) {\n throw new Error(`computeAgg: fn \"${fn}\" requires a field`);\n }\n if (fn === 'latest') {\n return records.at(-1)?.[field] ?? null;\n }\n if (fn === 'first') {\n return records[0]?.[field] ?? null;\n }\n const values = records\n .map((r) => r[field])\n .filter((v) => v !== undefined && v !== null);\n const nonNumeric = values.find((v) => typeof v !== 'number');\n if (nonNumeric !== undefined) {\n throw new Error(\n `computeAgg: fn \"${fn}\" requires numeric values for field \"${field}\", got ${typeof nonNumeric} (${String(nonNumeric)})`,\n );\n }\n const numbers = values as number[];\n if (fn === 'sum') {\n return numbers.reduce((a, b) => a + b, 0);\n }\n if (fn === 'avg') {\n return numbers.length > 0\n ? numbers.reduce((a, b) => a + b, 0) / numbers.length\n : null;\n }\n if (fn === 'min') {\n return numbers.length > 0\n ? numbers.reduce((a, b) => (a < b ? a : b))\n : null;\n }\n if (fn === 'max') {\n return numbers.length > 0\n ? numbers.reduce((a, b) => (a > b ? a : b))\n : null;\n }\n return null;\n}\n\nfunction sortByTs(\n records: Record<string, unknown>[],\n tsField: string,\n): Record<string, unknown>[] {\n return [...records].sort((a, b) => {\n return (a[tsField] as number) - (b[tsField] as number);\n });\n}\n\nfunction computeGroupBy(\n records: Record<string, unknown>[],\n metric: ComputedMetric,\n tsField: string,\n): unknown {\n const { field, granularity } = metric.groupBy!;\n const groups = new Map<string, Record<string, unknown>[]>();\n\n for (const record of records) {\n const ts = record[field] as number | undefined;\n if (ts === undefined || typeof ts !== 'number') {\n continue;\n }\n const key = truncateToGranularity(ts, granularity);\n if (!groups.has(key)) {\n groups.set(key, []);\n }\n groups.get(key)!.push(record);\n }\n\n return [...groups.entries()]\n .map(([key, groupRecords]) => ({\n date: key,\n value: computeAgg(\n sortByTs(groupRecords, tsField),\n metric.field,\n metric.fn,\n ),\n }))\n .sort((a, b) => (a.date < b.date ? -1 : 1));\n}\n\nfunction getTimestampField(shape: string): string {\n switch (shape) {\n case 'event':\n return 'start_ts';\n case 'metric':\n case 'distribution':\n return 'ts';\n case 'entity':\n case 'edge':\n return 'updated_at';\n default:\n return 'start_ts';\n }\n}\n\nexport async function computeMetric(\n storage: StorageHandle,\n metric: ComputedMetric,\n): Promise<unknown> {\n const tsField = getTimestampField(metric.shape);\n\n const windowMs = metric.window ? parseWindowMs(metric.window) : null;\n const windowStart = windowMs !== null ? Date.now() - windowMs : undefined;\n\n let records: Record<string, unknown>[];\n\n switch (metric.shape) {\n case 'event': {\n const events = await storage.queryEvents({\n name: metric.name,\n start: windowStart,\n });\n records = events.map((e) => ({\n ...e.attributes,\n name: e.name,\n start_ts: e.start_ts,\n end_ts: e.end_ts,\n }));\n break;\n }\n\n case 'entity': {\n const type = metric.entityType ?? metric.name ?? '';\n const entities = await storage.queryEntities({ type });\n records = entities.map((e) => ({\n ...e.attributes,\n type: e.type,\n id: e.id,\n updated_at: e.updated_at,\n }));\n if (windowStart !== undefined) {\n records = records.filter((r) => (r[tsField] as number) >= windowStart);\n }\n break;\n }\n\n case 'metric': {\n const metrics = await storage.queryMetrics({\n name: metric.name,\n start: windowStart,\n });\n records = metrics.map((m) => ({\n ...m.attributes,\n name: m.name,\n ts: m.ts,\n value: m.value,\n }));\n break;\n }\n\n case 'edge': {\n const edges = await storage.traverse({ kind: metric.name });\n records = edges.map((e) => ({\n ...e.attributes,\n from_type: e.from_type,\n from_id: e.from_id,\n kind: e.kind,\n to_type: e.to_type,\n to_id: e.to_id,\n updated_at: e.updated_at,\n }));\n if (windowStart !== undefined) {\n records = records.filter((r) => (r[tsField] as number) >= windowStart);\n }\n break;\n }\n\n case 'distribution': {\n const distributions = await storage.queryDistributions({\n name: metric.name,\n start: windowStart,\n });\n records = distributions.map((d) => ({\n ...d.attributes,\n name: d.name,\n ts: d.ts,\n kind: d.kind,\n data: d.data,\n }));\n break;\n }\n\n default:\n return null;\n }\n\n const filtered = records.filter((r) => applyFilter(r, metric.filter));\n const sorted = sortByTs(filtered, tsField);\n\n if (metric.groupBy) {\n return computeGroupBy(sorted, metric, tsField);\n }\n\n return computeAgg(sorted, metric.field, metric.fn);\n}\n","import type { DashboardConfig, Widget } from './config';\n\nexport interface ResourceBackfill {\n requiredWindowMs: number | undefined;\n}\n\nexport type ConnectorBackfill = Map<string, ResourceBackfill>;\n\nconst WINDOW_UNIT_MS: Record<string, number> = {\n h: 3_600_000,\n d: 86_400_000,\n w: 604_800_000,\n m: 2_592_000_000,\n};\n\nfunction parseWindowMs(window: string): number | undefined {\n const match = /^(\\d+)(h|d|w|m)$/.exec(window);\n if (!match) {\n return undefined;\n }\n const unitMs = WINDOW_UNIT_MS[match[2]!];\n if (unitMs === undefined) {\n return undefined;\n }\n return parseInt(match[1]!) * unitMs;\n}\n\nfunction widgetWindow(widget: Widget): string | undefined {\n switch (widget.kind) {\n case 'stat':\n return widget.window ?? widget.metric.window;\n case 'timeseries':\n case 'distribution':\n return widget.window;\n case 'status':\n return undefined;\n }\n}\n\ninterface WidgetReference {\n connectorName: string;\n resourceName: string | undefined;\n}\n\nfunction widgetReference(widget: Widget): WidgetReference {\n if (widget.kind === 'status') {\n return { connectorName: widget.source, resourceName: undefined };\n }\n return {\n connectorName: widget.metric.connectorId,\n resourceName: widget.metric.name ?? widget.metric.entityType,\n };\n}\n\nfunction mergeWindow(\n existing: number | undefined,\n next: number | undefined,\n): number | undefined {\n if (next === undefined) {\n return existing;\n }\n if (existing === undefined) {\n return next;\n }\n return Math.max(existing, next);\n}\n\nexport function computeConnectorBackfill(\n config: DashboardConfig,\n): Map<string, ConnectorBackfill> {\n const result = new Map<string, ConnectorBackfill>();\n for (const dashboard of Object.values(config.dashboards)) {\n for (const widget of Object.values(dashboard.widgets)) {\n const { connectorName, resourceName } = widgetReference(widget);\n const windowStr = widgetWindow(widget);\n const windowMs = windowStr ? parseWindowMs(windowStr) : undefined;\n let resources = result.get(connectorName);\n if (!resources) {\n resources = new Map<string, ResourceBackfill>();\n result.set(connectorName, resources);\n }\n if (resourceName === undefined) {\n continue;\n }\n const existing = resources.get(resourceName);\n resources.set(resourceName, {\n requiredWindowMs: mergeWindow(existing?.requiredWindowMs, windowMs),\n });\n }\n }\n return result;\n}\n","import { classifyWidget, readAggregate } from './aggregate';\nimport { computeMetric } from './compute';\nimport type { Widget } from './config';\nimport type { ConnectorHealth } from './connector';\nimport type { ServerStorage } from './server-storage';\nimport type { CachedWidget, WidgetSyncState } from './wire';\n\nconst FAILING_CONNECTOR_STATUSES: ReadonlySet<ConnectorHealth['status']> =\n new Set(['error', 'auth_failed', 'paused']);\n\nfunction deriveSyncStateFromHealth(health: ConnectorHealth): WidgetSyncState {\n if (health.status === 'syncing') {\n return 'syncing';\n }\n if (FAILING_CONNECTOR_STATUSES.has(health.status)) {\n return 'failing';\n }\n if (!health.lastSyncAt) {\n return 'unsynced';\n }\n const ageMs = Date.now() - new Date(health.lastSyncAt).getTime();\n const windowMs = 2 * health.syncIntervalSeconds * 1000;\n return ageMs <= windowMs ? 'fresh' : 'stale';\n}\n\nfunction buildMetaFromHealth(health: ConnectorHealth): Record<string, unknown> {\n const meta: Record<string, unknown> = { connectorStatus: health.status };\n if (health.lastError) {\n meta['lastError'] = health.lastError;\n }\n return meta;\n}\n\nexport async function resolveWidget(\n dashboardId: string,\n widgetId: string,\n widget: Widget,\n connectors: readonly string[] | undefined,\n storage: ServerStorage,\n): Promise<CachedWidget | undefined> {\n const connectorId =\n widget.kind === 'status' ? widget.source : widget.metric.connectorId;\n if (connectors !== undefined && !connectors.includes(connectorId)) {\n return undefined;\n }\n const handle = storage.getStorageHandle(connectorId);\n const health = (await handle.getHealth?.()) ?? null;\n let data: unknown = null;\n if (widget.kind !== 'status') {\n const classification = classifyWidget(widget);\n if (classification.via === 'aggregate') {\n const cached = await readAggregate(handle, dashboardId, widgetId);\n data = cached ? cached.value : await computeMetric(handle, widget.metric);\n } else {\n data = await computeMetric(handle, widget.metric);\n }\n }\n\n let syncState: WidgetSyncState | undefined;\n let meta: Record<string, unknown> | undefined;\n if (health) {\n syncState = deriveSyncStateFromHealth(health);\n meta = buildMetaFromHealth(health);\n } else if (data === null || data === undefined) {\n syncState = 'unsynced';\n } else {\n syncState = 'fresh';\n }\n\n return {\n widgetId,\n connectorId,\n data,\n cachedAt: health?.lastSyncAt ?? null,\n syncState,\n syncIntervalSeconds: health?.syncIntervalSeconds,\n meta,\n };\n}\n","import type { Widget } from './config';\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value) ?? 'null';\n }\n if (Array.isArray(value)) {\n return '[' + value.map(stableStringify).join(',') + ']';\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const parts = keys.map(\n (k) =>\n JSON.stringify(k) +\n ':' +\n stableStringify((value as Record<string, unknown>)[k]),\n );\n return '{' + parts.join(',') + '}';\n}\n\n/**\n * Stable 32-bit hex hash of a widget's declarative config. Used as the\n * config-dependent component of the widget ETag so that a config edit\n * invalidates the cached ETag even when `lastSyncAt` is unchanged.\n *\n * Not cryptographic — collision-resistant enough for cache busting.\n */\nexport function hashWidgetConfig(widget: Widget): string {\n const s = stableStringify(widget);\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\n/**\n * Build the per-widget ETag value (unquoted). Combines the widget's\n * `lastSyncAt` (the connector's last successful sync timestamp, which is\n * what `CachedWidget.cachedAt` reflects) and a hash of the widget config.\n */\nexport function computeWidgetEtag(\n lastSyncAt: string | null,\n widget: Widget,\n): string {\n return `\"${lastSyncAt ?? 'null'}-${hashWidgetConfig(widget)}\"`;\n}\n","import type { ConnectorLogger } from '@rawdash/connector-shared';\nimport type { z } from 'zod';\n\nimport type { ConfiguredConnector } from './config';\nimport type {\n Connector,\n ConnectorContext,\n CredentialsSchema,\n} from './connector';\nimport type { SecretsResolver } from './secrets';\n\n/**\n * Map of resource name → Zod schema describing the raw API response shape\n * for that resource. Resource names must match the `resource` tag passed to\n * `request()` (see {@link BaseConnector.request}) so the shape-drift pipeline\n * can correlate observations with their declared shape.\n *\n * Consumed by:\n * - the cloud baseline generator, which walks this map at deploy time to\n * populate `connector_baselines`\n * - property tests in `@rawdash/connector-test-utils`, which fuzz against\n * each schema\n *\n * See `docs/authoring-a-connector.md` for the authoring guide.\n */\nexport type ConnectorSchemas = Readonly<Record<string, z.ZodType>>;\n\n/**\n * Compile-time contract every connector class must satisfy. Declaring\n * `static schemas` is mandatory — without it, the connector cannot be added\n * to a {@link ConnectorRegistry} and TypeScript will fail the build.\n */\nexport type ConnectorClass = {\n new (settings: never, creds?: never, ctx?: ConnectorContext): Connector;\n readonly credentials?: CredentialsSchema;\n readonly schemas: ConnectorSchemas;\n};\n\nexport type ConnectorRegistry = Record<string, ConnectorClass>;\n\nexport function instantiateConnector(\n entry: ConfiguredConnector,\n registry: ConnectorRegistry,\n secretsResolver?: SecretsResolver,\n logger?: ConnectorLogger,\n): Connector {\n const Cls = registry[entry.connectorId];\n if (!Cls) {\n throw new Error(\n `Unknown connector type \"${entry.connectorId}\" for instance \"${entry.name}\". ` +\n `Add it to the connectorRegistry.`,\n );\n }\n const credSchema = Cls.credentials;\n const settings: Record<string, unknown> = {};\n const creds: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(entry.config)) {\n if (credSchema && Object.prototype.hasOwnProperty.call(credSchema, key)) {\n creds[key] = value;\n } else {\n settings[key] = value;\n }\n }\n return new Cls(settings as never, (credSchema ? creds : undefined) as never, {\n secretsResolver,\n logger,\n });\n}\n","import type { StorageHandle } from './connector';\n\nexport function withAbortSignal(\n handle: StorageHandle,\n signal: AbortSignal,\n): StorageHandle {\n let warned = false;\n const warnOnce = (method: string): void => {\n if (warned) {\n return;\n }\n warned = true;\n console.warn(\n `[rawdash storage] dropping post-abort write '${method}' — connector continued writing after AbortSignal fired`,\n );\n };\n\n return {\n event: async (e) => {\n if (signal.aborted) {\n warnOnce('event');\n return;\n }\n await handle.event(e);\n },\n entity: async (e) => {\n if (signal.aborted) {\n warnOnce('entity');\n return;\n }\n await handle.entity(e);\n },\n metric: async (m) => {\n if (signal.aborted) {\n warnOnce('metric');\n return;\n }\n await handle.metric(m);\n },\n edge: async (e) => {\n if (signal.aborted) {\n warnOnce('edge');\n return;\n }\n await handle.edge(e);\n },\n distribution: async (d) => {\n if (signal.aborted) {\n warnOnce('distribution');\n return;\n }\n await handle.distribution(d);\n },\n events: async (es, scope) => {\n if (signal.aborted) {\n warnOnce('events');\n return;\n }\n await handle.events(es, scope);\n },\n entities: async (es, scope) => {\n if (signal.aborted) {\n warnOnce('entities');\n return;\n }\n await handle.entities(es, scope);\n },\n metrics: async (ms, scope) => {\n if (signal.aborted) {\n warnOnce('metrics');\n return;\n }\n await handle.metrics(ms, scope);\n },\n edges: async (es, scope) => {\n if (signal.aborted) {\n warnOnce('edges');\n return;\n }\n await handle.edges(es, scope);\n },\n distributions: async (ds, scope) => {\n if (signal.aborted) {\n warnOnce('distributions');\n return;\n }\n await handle.distributions(ds, scope);\n },\n deleteOlderThan: async (shape, tsUnixMs) => {\n if (signal.aborted) {\n warnOnce('deleteOlderThan');\n return { rowsDeleted: 0 };\n }\n return handle.deleteOlderThan(shape, tsUnixMs);\n },\n queryEvents: (q) => handle.queryEvents(q),\n getEntity: (type, id) => handle.getEntity(type, id),\n queryEntities: (q) => handle.queryEntities(q),\n queryMetrics: (q) => handle.queryMetrics(q),\n traverse: (q) => handle.traverse(q),\n queryDistributions: (q) => handle.queryDistributions(q),\n ...(handle.getHealth ? { getHealth: handle.getHealth.bind(handle) } : {}),\n };\n}\n","import type {\n ConnectorHealth,\n Distribution,\n DistributionQuery,\n Edge,\n EdgeQuery,\n Entity,\n EntityQuery,\n Event,\n EventQuery,\n MetricQuery,\n MetricSample,\n StorageHandle,\n} from './connector';\nimport type { SyncState } from './engine';\nimport type { GetStorageHandleOptions, ServerStorage } from './server-storage';\nimport { withAbortSignal } from './storage-handle-guard';\n\nexport class InMemoryStorage implements ServerStorage {\n private eventStore = new Map<string, Event[]>();\n private entityStore = new Map<string, Map<string, Map<string, Entity>>>();\n private metricStore = new Map<string, MetricSample[]>();\n private edgeStore = new Map<string, Edge[]>();\n private distributionStore = new Map<string, Distribution[]>();\n private lastWriteAt = new Map<string, string>();\n private syncState: SyncState = {\n status: 'idle',\n queuedAt: null,\n startedAt: null,\n lastSyncAt: null,\n lastError: null,\n };\n\n getStorageHandle(\n connectorId: string,\n options?: GetStorageHandleOptions,\n ): StorageHandle {\n const handle = this.buildHandle(connectorId);\n return options?.signal ? withAbortSignal(handle, options.signal) : handle;\n }\n\n private buildHandle(connectorId: string): StorageHandle {\n const touch = (): void => {\n this.lastWriteAt.set(connectorId, new Date().toISOString());\n };\n const getEntityMap = (): Map<string, Map<string, Entity>> => {\n if (!this.entityStore.has(connectorId)) {\n this.entityStore.set(connectorId, new Map());\n }\n return this.entityStore.get(connectorId)!;\n };\n\n const upsertEntities = (es: Entity[]): void => {\n const byType = getEntityMap();\n for (const e of es) {\n if (!byType.has(e.type)) {\n byType.set(e.type, new Map());\n }\n byType.get(e.type)!.set(e.id, e);\n }\n };\n\n const upsertEdges = (es: Edge[]): void => {\n const existing = this.edgeStore.get(connectorId) ?? [];\n const index = new Map<string, number>();\n for (let i = 0; i < existing.length; i++) {\n const e = existing[i]!;\n index.set(\n `${e.from_type}:${e.from_id}:${e.kind}:${e.to_type}:${e.to_id}`,\n i,\n );\n }\n for (const e of es) {\n const key = `${e.from_type}:${e.from_id}:${e.kind}:${e.to_type}:${e.to_id}`;\n const idx = index.get(key);\n if (idx !== undefined) {\n existing[idx] = e;\n } else {\n index.set(key, existing.length);\n existing.push(e);\n }\n }\n this.edgeStore.set(connectorId, existing);\n };\n\n return {\n event: async (e) => {\n if (!this.eventStore.has(connectorId)) {\n this.eventStore.set(connectorId, []);\n }\n this.eventStore.get(connectorId)!.push(e);\n touch();\n },\n\n entity: async (e) => {\n upsertEntities([e]);\n touch();\n },\n\n metric: async (m) => {\n if (!this.metricStore.has(connectorId)) {\n this.metricStore.set(connectorId, []);\n }\n this.metricStore.get(connectorId)!.push(m);\n touch();\n },\n\n edge: async (e) => {\n upsertEdges([e]);\n touch();\n },\n\n distribution: async (d) => {\n if (!this.distributionStore.has(connectorId)) {\n this.distributionStore.set(connectorId, []);\n }\n this.distributionStore.get(connectorId)!.push(d);\n touch();\n },\n\n events: async (es, scope) => {\n const names = new Set(scope?.names ?? es.map((e) => e.name));\n const kept = (this.eventStore.get(connectorId) ?? []).filter(\n (e) => !names.has(e.name),\n );\n this.eventStore.set(connectorId, [...kept, ...es]);\n touch();\n },\n\n entities: async (es, scope) => {\n const byType = getEntityMap();\n const types = new Set(scope?.types ?? es.map((e) => e.type));\n for (const type of types) {\n byType.set(type, new Map());\n }\n upsertEntities(es);\n touch();\n },\n\n metrics: async (ms, scope) => {\n const names = new Set(scope?.names ?? ms.map((m) => m.name));\n const kept = (this.metricStore.get(connectorId) ?? []).filter(\n (m) => !names.has(m.name),\n );\n this.metricStore.set(connectorId, [...kept, ...ms]);\n touch();\n },\n\n edges: async (es, scope) => {\n const kinds = new Set(scope?.kinds ?? es.map((e) => e.kind));\n const kept = (this.edgeStore.get(connectorId) ?? []).filter(\n (e) => !kinds.has(e.kind),\n );\n this.edgeStore.set(connectorId, kept);\n upsertEdges(es);\n touch();\n },\n\n distributions: async (ds, scope) => {\n const names = new Set(scope?.names ?? ds.map((d) => d.name));\n const kept = (this.distributionStore.get(connectorId) ?? []).filter(\n (d) => !names.has(d.name),\n );\n this.distributionStore.set(connectorId, [...kept, ...ds]);\n touch();\n },\n\n queryEvents: async (q: EventQuery) => {\n let results = this.eventStore.get(connectorId) ?? [];\n if (q.name !== undefined) {\n results = results.filter((e) => e.name === q.name);\n }\n if (q.start !== undefined) {\n results = results.filter((e) => e.start_ts >= q.start!);\n }\n if (q.end !== undefined) {\n results = results.filter((e) => e.start_ts <= q.end!);\n }\n return results;\n },\n\n getEntity: async (type: string, id: string) => {\n return getEntityMap().get(type)?.get(id) ?? null;\n },\n\n queryEntities: async (q: EntityQuery) => {\n const byType = getEntityMap().get(q.type);\n if (!byType) {\n return [];\n }\n return Array.from(byType.values());\n },\n\n queryMetrics: async (q: MetricQuery) => {\n let results = this.metricStore.get(connectorId) ?? [];\n if (q.name !== undefined) {\n results = results.filter((m) => m.name === q.name);\n }\n if (q.start !== undefined) {\n results = results.filter((m) => m.ts >= q.start!);\n }\n if (q.end !== undefined) {\n results = results.filter((m) => m.ts <= q.end!);\n }\n return results;\n },\n\n traverse: async (q: EdgeQuery) => {\n let results = this.edgeStore.get(connectorId) ?? [];\n if (q.fromType !== undefined) {\n results = results.filter((e) => e.from_type === q.fromType);\n }\n if (q.fromId !== undefined) {\n results = results.filter((e) => e.from_id === q.fromId);\n }\n if (q.kind !== undefined) {\n results = results.filter((e) => e.kind === q.kind);\n }\n if (q.toType !== undefined) {\n results = results.filter((e) => e.to_type === q.toType);\n }\n if (q.toId !== undefined) {\n results = results.filter((e) => e.to_id === q.toId);\n }\n return results;\n },\n\n queryDistributions: async (q: DistributionQuery) => {\n let results = this.distributionStore.get(connectorId) ?? [];\n if (q.name !== undefined) {\n results = results.filter((d) => d.name === q.name);\n }\n if (q.start !== undefined) {\n results = results.filter((d) => d.ts >= q.start!);\n }\n if (q.end !== undefined) {\n results = results.filter((d) => d.ts <= q.end!);\n }\n return results;\n },\n\n deleteOlderThan: async (shape, tsUnixMs) => {\n if (shape === 'events') {\n const before = this.eventStore.get(connectorId) ?? [];\n const after = before.filter((e) => e.start_ts >= tsUnixMs);\n this.eventStore.set(connectorId, after);\n return { rowsDeleted: before.length - after.length };\n } else if (shape === 'metrics') {\n const before = this.metricStore.get(connectorId) ?? [];\n const after = before.filter((m) => m.ts >= tsUnixMs);\n this.metricStore.set(connectorId, after);\n return { rowsDeleted: before.length - after.length };\n } else if (shape === 'distributions') {\n const before = this.distributionStore.get(connectorId) ?? [];\n const after = before.filter((d) => d.ts >= tsUnixMs);\n this.distributionStore.set(connectorId, after);\n return { rowsDeleted: before.length - after.length };\n } else {\n throw new Error(\n `Unsupported shape for deleteOlderThan: ${String(shape)}`,\n );\n }\n },\n\n getHealth: async (): Promise<ConnectorHealth> => {\n return {\n status: 'idle',\n lastSyncAt: this.lastWriteAt.get(connectorId) ?? null,\n lastError: null,\n syncIntervalSeconds: 0,\n };\n },\n };\n }\n\n async getSyncState(): Promise<SyncState> {\n return { ...this.syncState };\n }\n\n async markSyncQueued(): Promise<boolean> {\n if (\n this.syncState.status === 'queued' ||\n this.syncState.status === 'running'\n ) {\n return false;\n }\n this.syncState = {\n ...this.syncState,\n status: 'queued',\n queuedAt: new Date().toISOString(),\n startedAt: null,\n };\n return true;\n }\n\n async markSyncRunning(): Promise<boolean> {\n if (this.syncState.status !== 'queued') {\n return false;\n }\n this.syncState = {\n ...this.syncState,\n status: 'running',\n startedAt: new Date().toISOString(),\n };\n return true;\n }\n\n async markSyncSucceeded(): Promise<void> {\n const now = new Date().toISOString();\n this.syncState = {\n status: 'succeeded',\n queuedAt: null,\n startedAt: null,\n lastSyncAt: now,\n lastError: null,\n };\n }\n\n async markSyncFailed(error: string): Promise<void> {\n this.syncState = {\n ...this.syncState,\n status: 'failed',\n queuedAt: null,\n startedAt: null,\n lastError: error,\n };\n }\n}\n","import { z } from 'zod';\n\nimport type { DashboardConfig } from './config';\n\nexport const wireConnectorSchema = z.object({\n name: z.string(),\n connectorId: z.string(),\n displayName: z.string().optional(),\n config: z.record(z.string(), z.unknown()),\n syncIntervalSeconds: z.number().optional(),\n enabled: z.boolean().optional(),\n});\n\nexport const wireDashboardSchema = z.object({\n id: z.string().optional(),\n name: z.string(),\n slug: z.string(),\n config: z.record(z.string(), z.unknown()),\n});\n\nexport const wireConfigSchema = z.object({\n connectors: z.array(wireConnectorSchema).optional(),\n dashboards: z.array(wireDashboardSchema).optional(),\n});\n\nexport type WireConnector = z.infer<typeof wireConnectorSchema>;\nexport type WireDashboard = z.infer<typeof wireDashboardSchema>;\nexport type WireConfig = z.infer<typeof wireConfigSchema>;\n\nexport function toWireConfig(config: DashboardConfig): WireConfig {\n return {\n connectors: config.connectors.map((entry) => ({\n name: entry.name,\n connectorId: entry.connectorId,\n displayName: entry.displayName ?? entry.name,\n config: entry.config,\n syncIntervalSeconds: entry.syncIntervalSeconds ?? 300,\n enabled: entry.enabled ?? true,\n })),\n dashboards: Object.entries(config.dashboards).map(([id, dash]) => ({\n id,\n name: id,\n slug: id,\n config: { widgets: dash.widgets },\n })),\n };\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 JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\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 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// AwsCostConnector\n// ---------------------------------------------------------------------------\n\nexport class AwsCostConnector extends BaseAWSConnector<AwsCostSettings> {\n static readonly id = 'aws-cost';\n\n static readonly schemas = {\n daily_cost: getCostAndUsageResponse,\n forecast: getCostForecastResponse,\n } as const;\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 getCostWindow,\n} from './aws-cost';\nexport type { AwsCostSettings } from './aws-cost';\nexport default AwsCostConnector;\n"],"mappings":"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","names":["z","HttpClientError","TransientError","RateLimitError","AuthError","HTTP_CLIENT_VERSION","DEFAULT_USER_AGENT","err","z","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(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/&/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"]}
|