@rawdash/connector-gcp-billing 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -328,9 +328,6 @@ var GcpBillingConnector = class _GcpBillingConnector extends BaseConnector {
328
328
  query: sql,
329
329
  useLegacySql: false,
330
330
  maxResults: PAGE_SIZE,
331
- // 30s synchronous wait. If BigQuery still isn't done it returns
332
- // jobComplete:false; sync() treats that as an error rather than
333
- // silently persisting partial results.
334
331
  timeoutMs: 3e4
335
332
  };
336
333
  if (this.settings.bqLocation !== void 0) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../gcp-shared/src/auth.ts","../../gcp-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/gcp-billing.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport interface ServiceAccountKey {\n client_email: string;\n private_key: string;\n token_uri?: string;\n project_id?: string;\n}\n\nconst serviceAccountKeySchema = z.object({\n client_email: z.string().min(1),\n private_key: z.string().min(1),\n token_uri: z.string().url().optional(),\n project_id: z.string().optional(),\n});\n\nexport interface TokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport const tokenResponseSchema = z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n});\n\nfunction base64urlFromBytes(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64urlFromString(str: string): string {\n return base64urlFromBytes(new TextEncoder().encode(str));\n}\n\nasync function signRS256JWT(\n payload: Record<string, unknown>,\n privateKeyPem: string,\n): Promise<string> {\n const header = { alg: 'RS256', typ: 'JWT' };\n const headerB64 = base64urlFromString(JSON.stringify(header));\n const payloadB64 = base64urlFromString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const pemContent = privateKeyPem\n .replace(/-----BEGIN PRIVATE KEY-----/g, '')\n .replace(/-----END PRIVATE KEY-----/g, '')\n .replace(/\\s/g, '');\n const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));\n\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n der,\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await globalThis.crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(signingInput),\n );\n\n return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;\n}\n\nexport function parseServiceAccountJson(value: string): ServiceAccountKey {\n const trimmed = value.trim();\n if (trimmed.startsWith('{')) {\n return serviceAccountKeySchema.parse(JSON.parse(trimmed));\n }\n const binary = atob(trimmed);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const decoded = new TextDecoder().decode(bytes);\n return serviceAccountKeySchema.parse(JSON.parse(decoded));\n}\n\n/**\n * Build the OAuth 2.0 token-exchange request body for a Google service account.\n * The caller posts the returned body to the returned url and parses the\n * resulting `access_token` (see {@link tokenResponseSchema}).\n *\n * Scope is a space-delimited list of OAuth scope URLs (e.g.\n * `https://www.googleapis.com/auth/monitoring.read`).\n */\nexport async function buildServiceAccountJwt(\n serviceAccountJson: string,\n scope: string,\n): Promise<{ url: string; body: string }> {\n const sa = parseServiceAccountJson(serviceAccountJson);\n const now = Math.floor(Date.now() / 1000);\n const jwt = await signRS256JWT(\n {\n iss: sa.client_email,\n scope,\n aud: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now,\n },\n sa.private_key,\n );\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }).toString();\n\n return {\n url: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n body,\n };\n}\n","import { z } from 'zod';\n\n/**\n * Shared GCP config fragment - reused by both `@rawdash/connector-gcp-monitoring`\n * and `@rawdash/connector-gcp-billing`. Both authenticate with a service-account\n * JSON key; the per-connector schema spreads this in alongside its own fields.\n */\nexport const gcpAuthConfigShape = {\n serviceAccountJson: z.object({ $secret: z.string().trim().min(1) }).meta({\n label: 'Service Account JSON',\n description:\n 'Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.',\n secret: true,\n }),\n} as const;\n\nexport interface GcpAuthConfig {\n serviceAccountJson: { $secret: string };\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 buildServiceAccountJwt,\n gcpAuthConfigShape,\n tokenResponseSchema,\n} from '@rawdash/connector-gcp-shared';\nimport {\n AuthError,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorCost,\n type ConnectorDoc,\n type CredentialsSchema,\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\nconst BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nconst BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst DIMENSION_VALUES = ['service', 'project', 'sku', 'location'] as const;\ntype Dimension = (typeof DIMENSION_VALUES)[number];\n\nexport const configFields = defineConfigFields(\n z.object({\n ...gcpAuthConfigShape,\n bqProject: z\n .string()\n .regex(BQ_IDENT_RE, 'bqProject must be a valid GCP project id')\n .meta({\n label: 'BigQuery project ID',\n description:\n 'Project that hosts the BigQuery billing-export dataset (also the project used to bill the BigQuery queries this connector runs).',\n placeholder: 'my-billing-project',\n }),\n bqDataset: z\n .string()\n .regex(\n BQ_DATASET_RE,\n 'bqDataset must be a valid BigQuery dataset id (letters, digits, and underscores; must start with a letter or underscore)',\n )\n .meta({\n label: 'BigQuery dataset',\n description:\n 'BigQuery dataset containing the Cloud Billing export tables (gcp_billing_export_v1_*).',\n placeholder: 'billing_export',\n }),\n bqLocation: z.string().min(1).optional().meta({\n label: 'BigQuery location',\n description:\n 'Region or multi-region of the billing dataset (e.g. US, EU, us-central1). Defaults to US.',\n placeholder: 'US',\n }),\n groupBy: z\n .array(z.enum(DIMENSION_VALUES))\n .nonempty()\n .max(\n 3,\n 'groupBy accepts at most three dimensions to keep query cardinality bounded',\n )\n .refine(\n (dims) => new Set(dims).size === dims.length,\n 'groupBy values must be unique',\n )\n .optional()\n .meta({\n label: 'Group by (optional)',\n description:\n 'Dimensions to break daily costs down by. Pick from service, project, sku, location. Defaults to [\"service\"].',\n }),\n lookbackDays: z.number().int().positive().max(720).optional().meta({\n label: 'Backfill window (days)',\n description:\n 'How many days of history to query on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Google Cloud Billing',\n category: 'finance',\n brandColor: '#669DF6',\n tagline:\n 'Track Google Cloud spend over time from the Cloud Billing -> BigQuery export, optionally broken down by service, project, SKU, or location.',\n vendor: {\n name: 'Google Cloud',\n apiDocs:\n 'https://cloud.google.com/billing/docs/how-to/export-data-bigquery',\n website: 'https://cloud.google.com/billing',\n },\n auth: {\n summary:\n 'Authenticate against the BigQuery API with a Google service account JSON key. The service account needs the BigQuery Data Viewer role on the billing-export dataset and the BigQuery Job User role on the project that runs the queries.',\n setup: [\n 'Enable the Cloud Billing -> BigQuery export in the GCP console (Billing -> Billing export -> BigQuery export). This is a manual one-time setup; data starts flowing into the configured dataset within a day.',\n 'Create a service account at Google Cloud -> IAM & Admin -> Service Accounts (or grant an existing one access).',\n 'Grant the service account roles/bigquery.dataViewer on the billing dataset (so it can read the export tables) and roles/bigquery.jobUser on the bqProject (so it can run query jobs).',\n 'Generate a JSON key for the service account and store its contents as a secret (e.g. GCP_BILLING_SA_JSON).',\n 'Reference the key from config as serviceAccountJson: secret(\"GCP_BILLING_SA_JSON\") and set bqProject + bqDataset to the export location.',\n ],\n },\n rateLimit:\n 'BigQuery jobs.query is rate-limited per project; standard 429 / RESOURCE_EXHAUSTED responses are retried with backoff. Each connector sync runs one query (or a small number when paginated).',\n limitations: [\n 'Requires the Cloud Billing -> BigQuery export to be configured in the GCP console; that step is manual and one-time, and only days after the configuration date are present in the export.',\n 'Queries the gcp_billing_export_v1_* table family (standard usage cost export). The detailed resource-level export (gcp_billing_export_resource_v1_*) is not used.',\n 'Each BigQuery query is billed against the bqProject; over long windows or wide groupBy axes the cost adds up. Prefer narrow groupBy and reasonable lookbackDays.',\n 'Cost data is back-revised by GCP for several days; an incremental sync refetches the trailing 5 days to pick up corrections.',\n ],\n});\n\n// ---------------------------------------------------------------------------\n// Constants / settings\n// ---------------------------------------------------------------------------\n\nconst BQ_API_BASE = 'https://bigquery.googleapis.com/bigquery/v2';\nconst BQ_SCOPE = 'https://www.googleapis.com/auth/bigquery.readonly';\nconst COST_METRIC_NAME = 'gcp_cost_daily';\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 5;\nconst MS_PER_DAY = 86_400_000;\nconst PAGE_SIZE = 10_000;\nconst DEFAULT_GROUP_BY: readonly Dimension[] = ['service'];\n\nexport interface GcpBillingSettings {\n bqProject: string;\n bqDataset: string;\n bqLocation?: string;\n groupBy?: readonly Dimension[];\n lookbackDays?: number;\n}\n\nconst gcpBillingCredentials = {\n serviceAccountJson: {\n description: 'Google service account JSON key (raw JSON or base64)',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype GcpBillingCredentials = typeof gcpBillingCredentials;\n\n// ---------------------------------------------------------------------------\n// BigQuery API response schemas\n// ---------------------------------------------------------------------------\n\nconst bqQueryResponseSchema = z.object({\n jobComplete: z.boolean().optional(),\n schema: z\n .object({\n fields: z.array(z.object({ name: z.string(), type: z.string() })),\n })\n .optional(),\n rows: z\n .array(\n z.object({\n f: z.array(z.object({ v: z.string().nullable().optional() })),\n }),\n )\n .optional(),\n pageToken: z.string().optional(),\n jobReference: z\n .object({\n projectId: z.string(),\n jobId: z.string(),\n location: z.string().optional(),\n })\n .optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Resources\n// ---------------------------------------------------------------------------\n\nexport const gcpBillingResources = defineResources({\n [COST_METRIC_NAME]: {\n shape: 'metric',\n description:\n 'Historical GCP cost per day, summed over the dimensions in `groupBy`. One sample per (date, dimension tuple). Pulls from the gcp_billing_export_v1_* tables in BigQuery.',\n endpoint: 'POST /bigquery/v2/projects/{bqProject}/queries',\n unit: 'USD',\n granularity: 'daily',\n notes:\n 'BigQuery charges per query; prefer narrow groupBy and reasonable lookbackDays. The trailing 5 days are always refetched on incremental syncs to pick up back-revisions.',\n dimensions: [\n {\n name: 'service',\n description:\n 'GCP service description (e.g. Compute Engine, BigQuery). Present when groupBy includes service.',\n },\n {\n name: 'project',\n description:\n 'GCP project id the spend is attributed to. Present when groupBy includes project.',\n },\n {\n name: 'sku',\n description:\n 'GCP SKU description (e.g. N1 Predefined Instance Core running in Americas). Present when groupBy includes sku.',\n },\n {\n name: 'location',\n description:\n 'GCP location/region the spend is attributed to. Present when groupBy includes location.',\n },\n { name: 'currency', description: 'Billing currency reported by GCP.' },\n ],\n responses: {\n oauth_token: tokenResponseSchema,\n daily_cost: bqQueryResponseSchema,\n },\n },\n});\n\nexport const id = 'gcp-billing';\n\nexport const cost: ConnectorCost = {\n recommendedInterval: '1 day',\n minInterval: '1 hour',\n perSync: '1 BigQuery query over the gcp_billing_export_v1_* table family',\n warning:\n 'Each BigQuery query is billed against the bqProject. Prefer once-a-day syncs and a focused groupBy.',\n};\n\n// ---------------------------------------------------------------------------\n// GcpBillingConnector\n// ---------------------------------------------------------------------------\n\nexport class GcpBillingConnector extends BaseConnector<\n GcpBillingSettings,\n GcpBillingCredentials\n> {\n static readonly id = id;\n\n static readonly resources = gcpBillingResources;\n\n static readonly schemas = schemasFromResources(gcpBillingResources);\n\n static readonly cost = cost;\n\n static create(input: unknown, ctx?: ConnectorContext): GcpBillingConnector {\n const parsed = configFields.parse(input);\n return new GcpBillingConnector(\n {\n bqProject: parsed.bqProject,\n bqDataset: parsed.bqDataset,\n bqLocation: parsed.bqLocation,\n groupBy: parsed.groupBy,\n lookbackDays: parsed.lookbackDays,\n },\n { serviceAccountJson: parsed.serviceAccountJson },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = gcpBillingCredentials;\n\n private cachedToken: { token: string; expiresAt: number } | null = null;\n\n private async getAccessToken(signal?: AbortSignal): Promise<string> {\n if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {\n return this.cachedToken.token;\n }\n const { serviceAccountJson } = this.creds;\n if (!serviceAccountJson) {\n throw new AuthError(`${this.id}: missing serviceAccountJson credential`);\n }\n const { url, body } = await buildServiceAccountJwt(\n serviceAccountJson,\n BQ_SCOPE,\n );\n const res = await this.post<{\n access_token: string;\n expires_in?: number;\n }>(url, {\n resource: 'oauth_token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n signal,\n });\n const expiresIn = res.body.expires_in ?? 3600;\n this.cachedToken = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cachedToken.token;\n }\n\n private async runQuery(\n accessToken: string,\n sql: string,\n pageToken: string | undefined,\n signal?: AbortSignal,\n ): Promise<z.infer<typeof bqQueryResponseSchema>> {\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n this.settings.bqProject,\n )}/queries`;\n\n const body: Record<string, unknown> = {\n query: sql,\n useLegacySql: false,\n maxResults: PAGE_SIZE,\n // 30s synchronous wait. If BigQuery still isn't done it returns\n // jobComplete:false; sync() treats that as an error rather than\n // silently persisting partial results.\n timeoutMs: 30_000,\n };\n if (this.settings.bqLocation !== undefined) {\n body['location'] = this.settings.bqLocation;\n }\n if (pageToken !== undefined) {\n body['pageToken'] = pageToken;\n }\n\n const res = await this.post<z.infer<typeof bqQueryResponseSchema>>(url, {\n resource: 'daily_cost',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent(this.id),\n },\n body: JSON.stringify(body),\n signal,\n });\n return res.body;\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const groupBy = this.settings.groupBy ?? DEFAULT_GROUP_BY;\n const window = getCostWindow(\n options,\n this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS,\n );\n const sql = buildBillingSql({\n bqProject: this.settings.bqProject,\n bqDataset: this.settings.bqDataset,\n groupBy,\n startDate: window.startDate,\n endDate: window.endDate,\n });\n\n const samples: MetricSample[] = [];\n let pageToken: string | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (signal?.aborted) {\n return { done: false };\n }\n const accessToken = await this.getAccessToken(signal);\n let response: z.infer<typeof bqQueryResponseSchema>;\n try {\n response = await this.runQuery(accessToken, sql, pageToken, signal);\n } catch (err) {\n this.logger.warn('fetch page failed', {\n resource: 'daily_cost',\n page: page + 1,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n if (response.jobComplete === false) {\n throw new Error(\n `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the groupBy or lookbackDays so the query finishes faster.`,\n );\n }\n const pageSamples = buildSamplesFromBqResponse(response, groupBy);\n samples.push(...pageSamples);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n this.logger.info('fetched page', {\n resource: 'daily_cost',\n page,\n items: pageSamples.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n await storage.metrics(samples, { names: [COST_METRIC_NAME] });\n this.logger.info('resource done', {\n resource: 'daily_cost',\n pages: page,\n items: samples.length,\n duration_ms: Date.now() - phaseStart,\n });\n return { done: true };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Pure helpers - exported for unit testing\n// ---------------------------------------------------------------------------\n\ninterface CostWindow {\n startDate: string;\n endDate: string;\n}\n\nconst DIM_TO_SELECT: Record<Dimension, { select: string; alias: string }> = {\n service: { select: 'service.description', alias: 'service' },\n project: { select: 'project.id', alias: 'project' },\n sku: { select: 'sku.description', alias: 'sku' },\n location: { select: 'location.location', alias: 'location' },\n};\n\nexport function buildBillingSql(args: {\n bqProject: string;\n bqDataset: string;\n groupBy: readonly Dimension[];\n startDate: string;\n endDate: string;\n}): string {\n // bqProject/bqDataset are validated by configFields against a strict\n // identifier regex; the date literals come from getCostWindow which formats\n // them as YYYY-MM-DD. Both are safe to interpolate.\n const dims = args.groupBy.map((d) => DIM_TO_SELECT[d]);\n const selectCols = ['DATE(usage_start_time) AS date']\n .concat(dims.map((d) => `${d.select} AS ${d.alias}`))\n .concat(['SUM(cost) AS cost', 'ANY_VALUE(currency) AS currency']);\n const groupCols = ['date'].concat(dims.map((d) => d.alias));\n const table = `\\`${args.bqProject}.${args.bqDataset}.gcp_billing_export_v1_*\\``;\n return [\n `SELECT ${selectCols.join(', ')}`,\n `FROM ${table}`,\n `WHERE DATE(usage_start_time) >= DATE('${args.startDate}')`,\n ` AND DATE(usage_start_time) < DATE('${args.endDate}')`,\n `GROUP BY ${groupCols.join(', ')}`,\n `ORDER BY date`,\n ].join('\\n');\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 startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nexport function getCostWindow(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): CostWindow {\n // End is exclusive; tomorrow 00:00 UTC so today's partial spend is included\n // and overwritten on the next sync as more usage rolls in.\n const endMs = startOfUtcDay(now) + MS_PER_DAY;\n let days = lookbackDays;\n if (options.mode === 'latest') {\n days = INCREMENTAL_LOOKBACK_DAYS;\n } else if (options.since !== undefined) {\n const sinceMs = parseEpoch(options.since, 'iso');\n if (sinceMs !== null) {\n const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);\n days = Math.min(\n Math.max(elapsed + INCREMENTAL_LOOKBACK_DAYS, 1),\n lookbackDays,\n );\n }\n }\n return {\n startDate: toDateStr(endMs - days * MS_PER_DAY),\n endDate: toDateStr(endMs),\n };\n}\n\nexport function buildSamplesFromBqResponse(\n response: z.infer<typeof bqQueryResponseSchema>,\n groupBy: readonly Dimension[],\n): MetricSample[] {\n const schema = response.schema?.fields ?? [];\n const fieldIndex: Record<string, number> = {};\n schema.forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n\n const samples: MetricSample[] = [];\n for (const row of response.rows ?? []) {\n const dateValue = readCell(row.f, fieldIndex, 'date');\n if (dateValue === null) {\n continue;\n }\n const ts = parseBqDateOrEpoch(dateValue);\n if (ts === null) {\n continue;\n }\n const costValue = readCell(row.f, fieldIndex, 'cost');\n if (costValue === null) {\n continue;\n }\n const value = Number.parseFloat(costValue);\n if (!Number.isFinite(value)) {\n continue;\n }\n const attributes: Record<string, JSONValue> = {};\n for (const dim of groupBy) {\n const v = readCell(row.f, fieldIndex, dim);\n attributes[dim] = v ?? null;\n }\n const currency = readCell(row.f, fieldIndex, 'currency');\n if (currency !== null) {\n attributes['currency'] = currency;\n }\n samples.push({ name: COST_METRIC_NAME, ts, value, attributes });\n }\n return samples;\n}\n\nfunction readCell(\n cells: ReadonlyArray<{ v?: string | null }>,\n fieldIndex: Record<string, number>,\n name: string,\n): string | null {\n const idx = fieldIndex[name];\n if (idx === undefined) {\n return null;\n }\n const raw = cells[idx]?.v;\n if (raw === undefined || raw === null) {\n return null;\n }\n return raw;\n}\n\nfunction parseBqDateOrEpoch(value: string): number | null {\n // BigQuery's DATE type renders as 'YYYY-MM-DD'; widening to a full ISO\n // timestamp also works in case a future query selects a TIMESTAMP column.\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (dateMatch) {\n return Date.UTC(\n Number(dateMatch[1]),\n Number(dateMatch[2]) - 1,\n Number(dateMatch[3]),\n );\n }\n return parseEpoch(value, 'iso');\n}\n","import { GcpBillingConnector } from './gcp-billing';\n\nexport {\n GcpBillingConnector,\n buildBillingSql,\n buildSamplesFromBqResponse,\n configFields,\n cost,\n doc,\n getCostWindow,\n id,\n gcpBillingResources as resources,\n} from './gcp-billing';\nexport type { GcpBillingSettings } from './gcp-billing';\nexport default GcpBillingConnector;\n"],"mappings":"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","names":["z","z"]}
1
+ {"version":3,"sources":["../../gcp-shared/src/auth.ts","../../gcp-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/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/gcp-billing.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport interface ServiceAccountKey {\n client_email: string;\n private_key: string;\n token_uri?: string;\n project_id?: string;\n}\n\nconst serviceAccountKeySchema = z.object({\n client_email: z.string().min(1),\n private_key: z.string().min(1),\n token_uri: z.string().url().optional(),\n project_id: z.string().optional(),\n});\n\nexport interface TokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport const tokenResponseSchema = z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n});\n\nfunction base64urlFromBytes(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64urlFromString(str: string): string {\n return base64urlFromBytes(new TextEncoder().encode(str));\n}\n\nasync function signRS256JWT(\n payload: Record<string, unknown>,\n privateKeyPem: string,\n): Promise<string> {\n const header = { alg: 'RS256', typ: 'JWT' };\n const headerB64 = base64urlFromString(JSON.stringify(header));\n const payloadB64 = base64urlFromString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const pemContent = privateKeyPem\n .replace(/-----BEGIN PRIVATE KEY-----/g, '')\n .replace(/-----END PRIVATE KEY-----/g, '')\n .replace(/\\s/g, '');\n const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));\n\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n der,\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await globalThis.crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(signingInput),\n );\n\n return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;\n}\n\nexport function parseServiceAccountJson(value: string): ServiceAccountKey {\n const trimmed = value.trim();\n if (trimmed.startsWith('{')) {\n return serviceAccountKeySchema.parse(JSON.parse(trimmed));\n }\n const binary = atob(trimmed);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const decoded = new TextDecoder().decode(bytes);\n return serviceAccountKeySchema.parse(JSON.parse(decoded));\n}\n\nexport async function buildServiceAccountJwt(\n serviceAccountJson: string,\n scope: string,\n): Promise<{ url: string; body: string }> {\n const sa = parseServiceAccountJson(serviceAccountJson);\n const now = Math.floor(Date.now() / 1000);\n const jwt = await signRS256JWT(\n {\n iss: sa.client_email,\n scope,\n aud: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now,\n },\n sa.private_key,\n );\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }).toString();\n\n return {\n url: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n body,\n };\n}\n","import { z } from 'zod';\n\nexport const gcpAuthConfigShape = {\n serviceAccountJson: z.object({ $secret: z.string().trim().min(1) }).meta({\n label: 'Service Account JSON',\n description:\n 'Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.',\n secret: true,\n }),\n} as const;\n\nexport interface GcpAuthConfig {\n serviceAccountJson: { $secret: string };\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 async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\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 buildServiceAccountJwt,\n gcpAuthConfigShape,\n tokenResponseSchema,\n} from '@rawdash/connector-gcp-shared';\nimport {\n AuthError,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorCost,\n type ConnectorDoc,\n type CredentialsSchema,\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\nconst BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nconst BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\nconst DIMENSION_VALUES = ['service', 'project', 'sku', 'location'] as const;\ntype Dimension = (typeof DIMENSION_VALUES)[number];\n\nexport const configFields = defineConfigFields(\n z.object({\n ...gcpAuthConfigShape,\n bqProject: z\n .string()\n .regex(BQ_IDENT_RE, 'bqProject must be a valid GCP project id')\n .meta({\n label: 'BigQuery project ID',\n description:\n 'Project that hosts the BigQuery billing-export dataset (also the project used to bill the BigQuery queries this connector runs).',\n placeholder: 'my-billing-project',\n }),\n bqDataset: z\n .string()\n .regex(\n BQ_DATASET_RE,\n 'bqDataset must be a valid BigQuery dataset id (letters, digits, and underscores; must start with a letter or underscore)',\n )\n .meta({\n label: 'BigQuery dataset',\n description:\n 'BigQuery dataset containing the Cloud Billing export tables (gcp_billing_export_v1_*).',\n placeholder: 'billing_export',\n }),\n bqLocation: z.string().min(1).optional().meta({\n label: 'BigQuery location',\n description:\n 'Region or multi-region of the billing dataset (e.g. US, EU, us-central1). Defaults to US.',\n placeholder: 'US',\n }),\n groupBy: z\n .array(z.enum(DIMENSION_VALUES))\n .nonempty()\n .max(\n 3,\n 'groupBy accepts at most three dimensions to keep query cardinality bounded',\n )\n .refine(\n (dims) => new Set(dims).size === dims.length,\n 'groupBy values must be unique',\n )\n .optional()\n .meta({\n label: 'Group by (optional)',\n description:\n 'Dimensions to break daily costs down by. Pick from service, project, sku, location. Defaults to [\"service\"].',\n }),\n lookbackDays: z.number().int().positive().max(720).optional().meta({\n label: 'Backfill window (days)',\n description:\n 'How many days of history to query on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Google Cloud Billing',\n category: 'finance',\n brandColor: '#669DF6',\n tagline:\n 'Track Google Cloud spend over time from the Cloud Billing -> BigQuery export, optionally broken down by service, project, SKU, or location.',\n vendor: {\n name: 'Google Cloud',\n apiDocs:\n 'https://cloud.google.com/billing/docs/how-to/export-data-bigquery',\n website: 'https://cloud.google.com/billing',\n },\n auth: {\n summary:\n 'Authenticate against the BigQuery API with a Google service account JSON key. The service account needs the BigQuery Data Viewer role on the billing-export dataset and the BigQuery Job User role on the project that runs the queries.',\n setup: [\n 'Enable the Cloud Billing -> BigQuery export in the GCP console (Billing -> Billing export -> BigQuery export). This is a manual one-time setup; data starts flowing into the configured dataset within a day.',\n 'Create a service account at Google Cloud -> IAM & Admin -> Service Accounts (or grant an existing one access).',\n 'Grant the service account roles/bigquery.dataViewer on the billing dataset (so it can read the export tables) and roles/bigquery.jobUser on the bqProject (so it can run query jobs).',\n 'Generate a JSON key for the service account and store its contents as a secret (e.g. GCP_BILLING_SA_JSON).',\n 'Reference the key from config as serviceAccountJson: secret(\"GCP_BILLING_SA_JSON\") and set bqProject + bqDataset to the export location.',\n ],\n },\n rateLimit:\n 'BigQuery jobs.query is rate-limited per project; standard 429 / RESOURCE_EXHAUSTED responses are retried with backoff. Each connector sync runs one query (or a small number when paginated).',\n limitations: [\n 'Requires the Cloud Billing -> BigQuery export to be configured in the GCP console; that step is manual and one-time, and only days after the configuration date are present in the export.',\n 'Queries the gcp_billing_export_v1_* table family (standard usage cost export). The detailed resource-level export (gcp_billing_export_resource_v1_*) is not used.',\n 'Each BigQuery query is billed against the bqProject; over long windows or wide groupBy axes the cost adds up. Prefer narrow groupBy and reasonable lookbackDays.',\n 'Cost data is back-revised by GCP for several days; an incremental sync refetches the trailing 5 days to pick up corrections.',\n ],\n});\n\nconst BQ_API_BASE = 'https://bigquery.googleapis.com/bigquery/v2';\nconst BQ_SCOPE = 'https://www.googleapis.com/auth/bigquery.readonly';\nconst COST_METRIC_NAME = 'gcp_cost_daily';\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 5;\nconst MS_PER_DAY = 86_400_000;\nconst PAGE_SIZE = 10_000;\nconst DEFAULT_GROUP_BY: readonly Dimension[] = ['service'];\n\nexport interface GcpBillingSettings {\n bqProject: string;\n bqDataset: string;\n bqLocation?: string;\n groupBy?: readonly Dimension[];\n lookbackDays?: number;\n}\n\nconst gcpBillingCredentials = {\n serviceAccountJson: {\n description: 'Google service account JSON key (raw JSON or base64)',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype GcpBillingCredentials = typeof gcpBillingCredentials;\n\nconst bqQueryResponseSchema = z.object({\n jobComplete: z.boolean().optional(),\n schema: z\n .object({\n fields: z.array(z.object({ name: z.string(), type: z.string() })),\n })\n .optional(),\n rows: z\n .array(\n z.object({\n f: z.array(z.object({ v: z.string().nullable().optional() })),\n }),\n )\n .optional(),\n pageToken: z.string().optional(),\n jobReference: z\n .object({\n projectId: z.string(),\n jobId: z.string(),\n location: z.string().optional(),\n })\n .optional(),\n});\n\nexport const gcpBillingResources = defineResources({\n [COST_METRIC_NAME]: {\n shape: 'metric',\n description:\n 'Historical GCP cost per day, summed over the dimensions in `groupBy`. One sample per (date, dimension tuple). Pulls from the gcp_billing_export_v1_* tables in BigQuery.',\n endpoint: 'POST /bigquery/v2/projects/{bqProject}/queries',\n unit: 'USD',\n granularity: 'daily',\n notes:\n 'BigQuery charges per query; prefer narrow groupBy and reasonable lookbackDays. The trailing 5 days are always refetched on incremental syncs to pick up back-revisions.',\n dimensions: [\n {\n name: 'service',\n description:\n 'GCP service description (e.g. Compute Engine, BigQuery). Present when groupBy includes service.',\n },\n {\n name: 'project',\n description:\n 'GCP project id the spend is attributed to. Present when groupBy includes project.',\n },\n {\n name: 'sku',\n description:\n 'GCP SKU description (e.g. N1 Predefined Instance Core running in Americas). Present when groupBy includes sku.',\n },\n {\n name: 'location',\n description:\n 'GCP location/region the spend is attributed to. Present when groupBy includes location.',\n },\n { name: 'currency', description: 'Billing currency reported by GCP.' },\n ],\n responses: {\n oauth_token: tokenResponseSchema,\n daily_cost: bqQueryResponseSchema,\n },\n },\n});\n\nexport const id = 'gcp-billing';\n\nexport const cost: ConnectorCost = {\n recommendedInterval: '1 day',\n minInterval: '1 hour',\n perSync: '1 BigQuery query over the gcp_billing_export_v1_* table family',\n warning:\n 'Each BigQuery query is billed against the bqProject. Prefer once-a-day syncs and a focused groupBy.',\n};\n\nexport class GcpBillingConnector extends BaseConnector<\n GcpBillingSettings,\n GcpBillingCredentials\n> {\n static readonly id = id;\n\n static readonly resources = gcpBillingResources;\n\n static readonly schemas = schemasFromResources(gcpBillingResources);\n\n static readonly cost = cost;\n\n static create(input: unknown, ctx?: ConnectorContext): GcpBillingConnector {\n const parsed = configFields.parse(input);\n return new GcpBillingConnector(\n {\n bqProject: parsed.bqProject,\n bqDataset: parsed.bqDataset,\n bqLocation: parsed.bqLocation,\n groupBy: parsed.groupBy,\n lookbackDays: parsed.lookbackDays,\n },\n { serviceAccountJson: parsed.serviceAccountJson },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = gcpBillingCredentials;\n\n private cachedToken: { token: string; expiresAt: number } | null = null;\n\n private async getAccessToken(signal?: AbortSignal): Promise<string> {\n if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {\n return this.cachedToken.token;\n }\n const { serviceAccountJson } = this.creds;\n if (!serviceAccountJson) {\n throw new AuthError(`${this.id}: missing serviceAccountJson credential`);\n }\n const { url, body } = await buildServiceAccountJwt(\n serviceAccountJson,\n BQ_SCOPE,\n );\n const res = await this.post<{\n access_token: string;\n expires_in?: number;\n }>(url, {\n resource: 'oauth_token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n signal,\n });\n const expiresIn = res.body.expires_in ?? 3600;\n this.cachedToken = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cachedToken.token;\n }\n\n private async runQuery(\n accessToken: string,\n sql: string,\n pageToken: string | undefined,\n signal?: AbortSignal,\n ): Promise<z.infer<typeof bqQueryResponseSchema>> {\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n this.settings.bqProject,\n )}/queries`;\n\n const body: Record<string, unknown> = {\n query: sql,\n useLegacySql: false,\n maxResults: PAGE_SIZE,\n timeoutMs: 30_000,\n };\n if (this.settings.bqLocation !== undefined) {\n body['location'] = this.settings.bqLocation;\n }\n if (pageToken !== undefined) {\n body['pageToken'] = pageToken;\n }\n\n const res = await this.post<z.infer<typeof bqQueryResponseSchema>>(url, {\n resource: 'daily_cost',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent(this.id),\n },\n body: JSON.stringify(body),\n signal,\n });\n return res.body;\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const groupBy = this.settings.groupBy ?? DEFAULT_GROUP_BY;\n const window = getCostWindow(\n options,\n this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS,\n );\n const sql = buildBillingSql({\n bqProject: this.settings.bqProject,\n bqDataset: this.settings.bqDataset,\n groupBy,\n startDate: window.startDate,\n endDate: window.endDate,\n });\n\n const samples: MetricSample[] = [];\n let pageToken: string | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (signal?.aborted) {\n return { done: false };\n }\n const accessToken = await this.getAccessToken(signal);\n let response: z.infer<typeof bqQueryResponseSchema>;\n try {\n response = await this.runQuery(accessToken, sql, pageToken, signal);\n } catch (err) {\n this.logger.warn('fetch page failed', {\n resource: 'daily_cost',\n page: page + 1,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n if (response.jobComplete === false) {\n throw new Error(\n `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the groupBy or lookbackDays so the query finishes faster.`,\n );\n }\n const pageSamples = buildSamplesFromBqResponse(response, groupBy);\n samples.push(...pageSamples);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n this.logger.info('fetched page', {\n resource: 'daily_cost',\n page,\n items: pageSamples.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n await storage.metrics(samples, { names: [COST_METRIC_NAME] });\n this.logger.info('resource done', {\n resource: 'daily_cost',\n pages: page,\n items: samples.length,\n duration_ms: Date.now() - phaseStart,\n });\n return { done: true };\n }\n}\n\ninterface CostWindow {\n startDate: string;\n endDate: string;\n}\n\nconst DIM_TO_SELECT: Record<Dimension, { select: string; alias: string }> = {\n service: { select: 'service.description', alias: 'service' },\n project: { select: 'project.id', alias: 'project' },\n sku: { select: 'sku.description', alias: 'sku' },\n location: { select: 'location.location', alias: 'location' },\n};\n\nexport function buildBillingSql(args: {\n bqProject: string;\n bqDataset: string;\n groupBy: readonly Dimension[];\n startDate: string;\n endDate: string;\n}): string {\n const dims = args.groupBy.map((d) => DIM_TO_SELECT[d]);\n const selectCols = ['DATE(usage_start_time) AS date']\n .concat(dims.map((d) => `${d.select} AS ${d.alias}`))\n .concat(['SUM(cost) AS cost', 'ANY_VALUE(currency) AS currency']);\n const groupCols = ['date'].concat(dims.map((d) => d.alias));\n const table = `\\`${args.bqProject}.${args.bqDataset}.gcp_billing_export_v1_*\\``;\n return [\n `SELECT ${selectCols.join(', ')}`,\n `FROM ${table}`,\n `WHERE DATE(usage_start_time) >= DATE('${args.startDate}')`,\n ` AND DATE(usage_start_time) < DATE('${args.endDate}')`,\n `GROUP BY ${groupCols.join(', ')}`,\n `ORDER BY date`,\n ].join('\\n');\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 startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nexport function getCostWindow(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): CostWindow {\n const endMs = startOfUtcDay(now) + MS_PER_DAY;\n let days = lookbackDays;\n if (options.mode === 'latest') {\n days = INCREMENTAL_LOOKBACK_DAYS;\n } else if (options.since !== undefined) {\n const sinceMs = parseEpoch(options.since, 'iso');\n if (sinceMs !== null) {\n const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);\n days = Math.min(\n Math.max(elapsed + INCREMENTAL_LOOKBACK_DAYS, 1),\n lookbackDays,\n );\n }\n }\n return {\n startDate: toDateStr(endMs - days * MS_PER_DAY),\n endDate: toDateStr(endMs),\n };\n}\n\nexport function buildSamplesFromBqResponse(\n response: z.infer<typeof bqQueryResponseSchema>,\n groupBy: readonly Dimension[],\n): MetricSample[] {\n const schema = response.schema?.fields ?? [];\n const fieldIndex: Record<string, number> = {};\n schema.forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n\n const samples: MetricSample[] = [];\n for (const row of response.rows ?? []) {\n const dateValue = readCell(row.f, fieldIndex, 'date');\n if (dateValue === null) {\n continue;\n }\n const ts = parseBqDateOrEpoch(dateValue);\n if (ts === null) {\n continue;\n }\n const costValue = readCell(row.f, fieldIndex, 'cost');\n if (costValue === null) {\n continue;\n }\n const value = Number.parseFloat(costValue);\n if (!Number.isFinite(value)) {\n continue;\n }\n const attributes: Record<string, JSONValue> = {};\n for (const dim of groupBy) {\n const v = readCell(row.f, fieldIndex, dim);\n attributes[dim] = v ?? null;\n }\n const currency = readCell(row.f, fieldIndex, 'currency');\n if (currency !== null) {\n attributes['currency'] = currency;\n }\n samples.push({ name: COST_METRIC_NAME, ts, value, attributes });\n }\n return samples;\n}\n\nfunction readCell(\n cells: ReadonlyArray<{ v?: string | null }>,\n fieldIndex: Record<string, number>,\n name: string,\n): string | null {\n const idx = fieldIndex[name];\n if (idx === undefined) {\n return null;\n }\n const raw = cells[idx]?.v;\n if (raw === undefined || raw === null) {\n return null;\n }\n return raw;\n}\n\nfunction parseBqDateOrEpoch(value: string): number | null {\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (dateMatch) {\n return Date.UTC(\n Number(dateMatch[1]),\n Number(dateMatch[2]) - 1,\n Number(dateMatch[3]),\n );\n }\n return parseEpoch(value, 'iso');\n}\n","import { GcpBillingConnector } from './gcp-billing';\n\nexport {\n GcpBillingConnector,\n buildBillingSql,\n buildSamplesFromBqResponse,\n configFields,\n cost,\n doc,\n getCostWindow,\n id,\n gcpBillingResources as resources,\n} from './gcp-billing';\nexport type { GcpBillingSettings } from './gcp-billing';\nexport default GcpBillingConnector;\n"],"mappings":"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","names":["z","z"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawdash/connector-gcp-billing",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Rawdash connector for Google Cloud Billing — syncs daily spend from a Cloud Billing -> BigQuery export, broken down by service and project",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "zod": "^4.4.3",
27
- "@rawdash/core": "0.19.0"
27
+ "@rawdash/core": "0.20.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "fast-check": "^4.8.0",
@@ -32,8 +32,8 @@
32
32
  "typescript": "^5.7.2",
33
33
  "vitest": "^4.1.4",
34
34
  "@rawdash/connector-gcp-shared": "0.1.0",
35
- "@rawdash/connector-shared": "0.3.0",
36
- "@rawdash/connector-test-utils": "0.0.6"
35
+ "@rawdash/connector-shared": "0.3.1",
36
+ "@rawdash/connector-test-utils": "0.0.7"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsup",