@rawdash/connector-gcp-billing 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { bqQueryResponseSchema } from '@rawdash/connector-gcp-shared';
1
2
  import { BaseConnector, ConnectorCost, ConnectorContext, SyncOptions, StorageHandle, SyncResult, MetricSample, ConnectorDoc } from '@rawdash/core';
2
3
  import { z } from 'zod';
3
4
 
@@ -8,10 +9,10 @@ declare const configFields: z.ZodObject<{
8
9
  bqDataset: z.ZodString;
9
10
  bqLocation: z.ZodOptional<z.ZodString>;
10
11
  groupBy: z.ZodOptional<z.ZodArray<z.ZodEnum<{
12
+ location: "location";
11
13
  service: "service";
12
14
  project: "project";
13
15
  sku: "sku";
14
- location: "location";
15
16
  }>>>;
16
17
  lookbackDays: z.ZodOptional<z.ZodNumber>;
17
18
  serviceAccountJson: z.ZodObject<{
@@ -33,26 +34,6 @@ declare const gcpBillingCredentials: {
33
34
  };
34
35
  };
35
36
  type GcpBillingCredentials = typeof gcpBillingCredentials;
36
- declare const bqQueryResponseSchema: z.ZodObject<{
37
- jobComplete: z.ZodOptional<z.ZodBoolean>;
38
- schema: z.ZodOptional<z.ZodObject<{
39
- fields: z.ZodArray<z.ZodObject<{
40
- name: z.ZodString;
41
- type: z.ZodString;
42
- }, z.core.$strip>>;
43
- }, z.core.$strip>>;
44
- rows: z.ZodOptional<z.ZodArray<z.ZodObject<{
45
- f: z.ZodArray<z.ZodObject<{
46
- v: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
- }, z.core.$strip>>;
48
- }, z.core.$strip>>>;
49
- pageToken: z.ZodOptional<z.ZodString>;
50
- jobReference: z.ZodOptional<z.ZodObject<{
51
- projectId: z.ZodString;
52
- jobId: z.ZodString;
53
- location: z.ZodOptional<z.ZodString>;
54
- }, z.core.$strip>>;
55
- }, z.core.$strip>;
56
37
  declare const gcpBillingResources: {
57
38
  readonly gcp_cost_daily: {
58
39
  readonly shape: "metric";
@@ -196,9 +177,9 @@ declare class GcpBillingConnector extends BaseConnector<GcpBillingSettings, GcpB
196
177
  auth: "required";
197
178
  };
198
179
  };
199
- private cachedToken;
180
+ private tokenProvider?;
200
181
  private getAccessToken;
201
- private runQuery;
182
+ private fetchBigQueryPage;
202
183
  sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
203
184
  }
204
185
  interface CostWindow {
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // ../gcp-shared/dist/index.js
2
2
  import { z } from "zod";
3
3
  import { z as z2 } from "zod";
4
+ import { z as z3 } from "zod";
4
5
  var serviceAccountKeySchema = z.object({
5
6
  client_email: z.string().min(1),
6
7
  private_key: z.string().min(1),
@@ -77,6 +78,15 @@ async function buildServiceAccountJwt(serviceAccountJson, scope) {
77
78
  body
78
79
  };
79
80
  }
81
+ function buildRefreshTokenGrant(credentials) {
82
+ const body = new URLSearchParams({
83
+ grant_type: "refresh_token",
84
+ refresh_token: credentials.refreshToken,
85
+ client_id: credentials.clientId,
86
+ client_secret: credentials.clientSecret
87
+ }).toString();
88
+ return { url: "https://oauth2.googleapis.com/token", body };
89
+ }
80
90
  var gcpAuthConfigShape = {
81
91
  serviceAccountJson: z2.object({ $secret: z2.string().trim().min(1) }).meta({
82
92
  label: "Service Account JSON",
@@ -84,8 +94,6 @@ var gcpAuthConfigShape = {
84
94
  secret: true
85
95
  })
86
96
  };
87
-
88
- // ../../connector-shared/dist/index.js
89
97
  var HttpClientError = class extends Error {
90
98
  response;
91
99
  constructor(message, response) {
@@ -99,10 +107,227 @@ var AuthError = class extends HttpClientError {
99
107
  };
100
108
  var HTTP_CLIENT_VERSION = "0.0.0";
101
109
  var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
110
+ function parseEpoch(value, unit) {
111
+ if (value === null || value === void 0) {
112
+ return null;
113
+ }
114
+ if (unit === "iso") {
115
+ if (typeof value !== "string") {
116
+ return null;
117
+ }
118
+ const ms = new Date(value).getTime();
119
+ return Number.isFinite(ms) ? ms : null;
120
+ }
121
+ if (typeof value === "string" && value.trim() === "") {
122
+ return null;
123
+ }
124
+ const n = typeof value === "number" ? value : Number(value);
125
+ if (!Number.isFinite(n)) {
126
+ return null;
127
+ }
128
+ const result = unit === "s" ? n * 1e3 : n;
129
+ return Number.isFinite(result) ? result : null;
130
+ }
131
+ var BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
132
+ var BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
133
+ var BQ_API_BASE = "https://bigquery.googleapis.com/bigquery/v2";
134
+ var BQ_READONLY_SCOPE = "https://www.googleapis.com/auth/bigquery.readonly";
135
+ var BQ_PAGE_SIZE = 1e4;
136
+ var BQ_QUERY_TIMEOUT_MS = 3e4;
137
+ var bqQueryResponseSchema = z3.object({
138
+ jobComplete: z3.boolean().optional(),
139
+ schema: z3.object({
140
+ fields: z3.array(z3.object({ name: z3.string(), type: z3.string() }))
141
+ }).optional(),
142
+ rows: z3.array(
143
+ z3.object({
144
+ f: z3.array(z3.object({ v: z3.string().nullable().optional() }))
145
+ })
146
+ ).optional(),
147
+ pageToken: z3.string().optional(),
148
+ jobReference: z3.object({
149
+ projectId: z3.string(),
150
+ jobId: z3.string(),
151
+ location: z3.string().optional()
152
+ }).optional()
153
+ });
154
+ function buildBigQueryPageRequest(opts) {
155
+ const pageSize = opts.pageSize ?? BQ_PAGE_SIZE;
156
+ const timeoutMs = opts.timeoutMs ?? BQ_QUERY_TIMEOUT_MS;
157
+ if (opts.pageToken === void 0) {
158
+ const url2 = `${BQ_API_BASE}/projects/${encodeURIComponent(
159
+ opts.projectId
160
+ )}/queries`;
161
+ const body = {
162
+ query: opts.sql,
163
+ useLegacySql: false,
164
+ maxResults: pageSize,
165
+ timeoutMs
166
+ };
167
+ if (opts.location !== void 0) {
168
+ body["location"] = opts.location;
169
+ }
170
+ return { method: "POST", url: url2, body: JSON.stringify(body) };
171
+ }
172
+ if (opts.jobReference === void 0) {
173
+ throw new Error(
174
+ "cannot fetch the next page of BigQuery results without a jobReference"
175
+ );
176
+ }
177
+ const params = new URLSearchParams({
178
+ pageToken: opts.pageToken,
179
+ maxResults: String(pageSize),
180
+ timeoutMs: String(timeoutMs)
181
+ });
182
+ const location = opts.jobReference.location ?? opts.location;
183
+ if (location !== void 0) {
184
+ params.set("location", location);
185
+ }
186
+ const url = `${BQ_API_BASE}/projects/${encodeURIComponent(
187
+ opts.jobReference.projectId
188
+ )}/queries/${encodeURIComponent(opts.jobReference.jobId)}?${params.toString()}`;
189
+ return { method: "GET", url };
190
+ }
191
+ async function collectBigQueryPages(opts) {
192
+ const rows = [];
193
+ let pageToken;
194
+ let jobReference;
195
+ let page = 0;
196
+ const phaseStart = Date.now();
197
+ do {
198
+ if (opts.signal?.aborted) {
199
+ return { rows, aborted: true };
200
+ }
201
+ const request = buildBigQueryPageRequest({
202
+ projectId: opts.projectId,
203
+ sql: opts.sql,
204
+ pageToken,
205
+ jobReference,
206
+ location: opts.location,
207
+ pageSize: opts.pageSize
208
+ });
209
+ let response;
210
+ try {
211
+ response = await opts.fetchPage(request, opts.signal);
212
+ } catch (err) {
213
+ opts.logger?.warn("fetch page failed", {
214
+ resource: opts.resource,
215
+ page: page + 1,
216
+ error: err instanceof Error ? err.message : String(err)
217
+ });
218
+ throw err;
219
+ }
220
+ if (response.jobComplete === false) {
221
+ throw new Error(opts.jobIncompleteMessage);
222
+ }
223
+ if (response.jobReference !== void 0) {
224
+ jobReference = response.jobReference;
225
+ }
226
+ const pageRows = opts.mapRows(response);
227
+ rows.push(...pageRows);
228
+ pageToken = typeof response.pageToken === "string" && response.pageToken.length > 0 ? response.pageToken : void 0;
229
+ page += 1;
230
+ opts.logger?.info("fetched page", {
231
+ resource: opts.resource,
232
+ page,
233
+ items: pageRows.length,
234
+ next: pageToken ?? null
235
+ });
236
+ } while (pageToken !== void 0);
237
+ opts.logger?.info("resource done", {
238
+ resource: opts.resource,
239
+ pages: page,
240
+ items: rows.length,
241
+ duration_ms: Date.now() - phaseStart
242
+ });
243
+ return { rows, aborted: false };
244
+ }
245
+ function indexBqFields(response) {
246
+ const fieldIndex = {};
247
+ (response.schema?.fields ?? []).forEach((field, idx) => {
248
+ fieldIndex[field.name] = idx;
249
+ });
250
+ return fieldIndex;
251
+ }
252
+ function readBqCell(cells, fieldIndex, name) {
253
+ const idx = fieldIndex[name];
254
+ if (idx === void 0) {
255
+ return null;
256
+ }
257
+ const raw = cells[idx]?.v;
258
+ if (raw === void 0 || raw === null) {
259
+ return null;
260
+ }
261
+ return raw;
262
+ }
263
+ function parseBqDateOrEpoch(value) {
264
+ const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
265
+ if (dateMatch) {
266
+ return Date.UTC(
267
+ Number(dateMatch[1]),
268
+ Number(dateMatch[2]) - 1,
269
+ Number(dateMatch[3])
270
+ );
271
+ }
272
+ return parseEpoch(value, "iso");
273
+ }
274
+ var GcpAccessTokenProvider = class {
275
+ constructor(opts) {
276
+ this.opts = opts;
277
+ }
278
+ opts;
279
+ cached = null;
280
+ async resolveGrant() {
281
+ const serviceAccountJson = this.opts.getServiceAccountJson();
282
+ if (serviceAccountJson) {
283
+ return buildServiceAccountJwt(serviceAccountJson, this.opts.scope);
284
+ }
285
+ const refreshTokenCredentials = this.opts.getRefreshTokenCredentials?.();
286
+ if (refreshTokenCredentials) {
287
+ return buildRefreshTokenGrant(refreshTokenCredentials);
288
+ }
289
+ throw new AuthError(
290
+ `${this.opts.connectorId}: missing serviceAccountJson or refresh-token credentials`
291
+ );
292
+ }
293
+ async getToken(signal) {
294
+ if (this.cached && Date.now() < this.cached.expiresAt) {
295
+ return this.cached.token;
296
+ }
297
+ const { url, body } = await this.resolveGrant();
298
+ const res = await this.opts.post(url, {
299
+ resource: "oauth_token",
300
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
301
+ body,
302
+ signal
303
+ });
304
+ const expiresIn = res.body.expires_in ?? 3600;
305
+ this.cached = {
306
+ token: res.body.access_token,
307
+ expiresAt: Date.now() + (expiresIn - 60) * 1e3
308
+ };
309
+ return this.cached.token;
310
+ }
311
+ };
312
+ var MS_PER_DAY = 864e5;
313
+ function pad2(n) {
314
+ return String(n).padStart(2, "0");
315
+ }
316
+ function toDateStr(ms) {
317
+ const d = new Date(ms);
318
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
319
+ }
320
+ function startOfUtcDay(ms) {
321
+ return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;
322
+ }
323
+
324
+ // ../../connector-shared/dist/index.js
325
+ var HTTP_CLIENT_VERSION2 = "0.0.0";
326
+ var DEFAULT_USER_AGENT2 = `rawdash-connector/${HTTP_CLIENT_VERSION2} (+https://rawdash.dev)`;
102
327
  function connectorUserAgent(connectorId) {
103
- return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
328
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION2} (+https://rawdash.dev)`;
104
329
  }
105
- function parseEpoch(value, unit) {
330
+ function parseEpoch2(value, unit) {
106
331
  if (value === null || value === void 0) {
107
332
  return null;
108
333
  }
@@ -132,19 +357,17 @@ import {
132
357
  defineResources,
133
358
  schemasFromResources
134
359
  } from "@rawdash/core";
135
- import { z as z3 } from "zod";
136
- var BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
137
- var BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
360
+ import { z as z4 } from "zod";
138
361
  var DIMENSION_VALUES = ["service", "project", "sku", "location"];
139
362
  var configFields = defineConfigFields(
140
- z3.object({
363
+ z4.object({
141
364
  ...gcpAuthConfigShape,
142
- bqProject: z3.string().regex(BQ_IDENT_RE, "bqProject must be a valid GCP project id").meta({
365
+ bqProject: z4.string().regex(BQ_IDENT_RE, "bqProject must be a valid GCP project id").meta({
143
366
  label: "BigQuery project ID",
144
367
  description: "Project that hosts the BigQuery billing-export dataset (also the project used to bill the BigQuery queries this connector runs).",
145
368
  placeholder: "my-billing-project"
146
369
  }),
147
- bqDataset: z3.string().regex(
370
+ bqDataset: z4.string().regex(
148
371
  BQ_DATASET_RE,
149
372
  "bqDataset must be a valid BigQuery dataset id (letters, digits, and underscores; must start with a letter or underscore)"
150
373
  ).meta({
@@ -152,12 +375,12 @@ var configFields = defineConfigFields(
152
375
  description: "BigQuery dataset containing the Cloud Billing export tables (gcp_billing_export_v1_*).",
153
376
  placeholder: "billing_export"
154
377
  }),
155
- bqLocation: z3.string().min(1).optional().meta({
378
+ bqLocation: z4.string().min(1).optional().meta({
156
379
  label: "BigQuery location",
157
380
  description: "Region or multi-region of the billing dataset (e.g. US, EU, us-central1). Defaults to US.",
158
381
  placeholder: "US"
159
382
  }),
160
- groupBy: z3.array(z3.enum(DIMENSION_VALUES)).nonempty().max(
383
+ groupBy: z4.array(z4.enum(DIMENSION_VALUES)).nonempty().max(
161
384
  3,
162
385
  "groupBy accepts at most three dimensions to keep query cardinality bounded"
163
386
  ).refine(
@@ -167,7 +390,7 @@ var configFields = defineConfigFields(
167
390
  label: "Group by (optional)",
168
391
  description: 'Dimensions to break daily costs down by. Pick from service, project, sku, location. Defaults to ["service"].'
169
392
  }),
170
- lookbackDays: z3.number().int().positive().max(720).optional().meta({
393
+ lookbackDays: z4.number().int().positive().max(720).optional().meta({
171
394
  label: "Backfill window (days)",
172
395
  description: "How many days of history to query on a full sync. Defaults to 90.",
173
396
  placeholder: "90"
@@ -181,6 +404,7 @@ var doc = defineConnectorDoc({
181
404
  tagline: "Track Google Cloud spend over time from the Cloud Billing -> BigQuery export, optionally broken down by service, project, SKU, or location.",
182
405
  vendor: {
183
406
  name: "Google Cloud",
407
+ domain: "cloud.google.com",
184
408
  apiDocs: "https://cloud.google.com/billing/docs/how-to/export-data-bigquery",
185
409
  website: "https://cloud.google.com/billing"
186
410
  },
@@ -202,13 +426,9 @@ var doc = defineConnectorDoc({
202
426
  "Cost data is back-revised by GCP for several days; an incremental sync refetches the trailing 5 days to pick up corrections."
203
427
  ]
204
428
  });
205
- var BQ_API_BASE = "https://bigquery.googleapis.com/bigquery/v2";
206
- var BQ_SCOPE = "https://www.googleapis.com/auth/bigquery.readonly";
207
429
  var COST_METRIC_NAME = "gcp_cost_daily";
208
430
  var DEFAULT_LOOKBACK_DAYS = 90;
209
431
  var INCREMENTAL_LOOKBACK_DAYS = 5;
210
- var MS_PER_DAY = 864e5;
211
- var PAGE_SIZE = 1e4;
212
432
  var DEFAULT_GROUP_BY = ["service"];
213
433
  var gcpBillingCredentials = {
214
434
  serviceAccountJson: {
@@ -216,23 +436,6 @@ var gcpBillingCredentials = {
216
436
  auth: "required"
217
437
  }
218
438
  };
219
- var bqQueryResponseSchema = z3.object({
220
- jobComplete: z3.boolean().optional(),
221
- schema: z3.object({
222
- fields: z3.array(z3.object({ name: z3.string(), type: z3.string() }))
223
- }).optional(),
224
- rows: z3.array(
225
- z3.object({
226
- f: z3.array(z3.object({ v: z3.string().nullable().optional() }))
227
- })
228
- ).optional(),
229
- pageToken: z3.string().optional(),
230
- jobReference: z3.object({
231
- projectId: z3.string(),
232
- jobId: z3.string(),
233
- location: z3.string().optional()
234
- }).optional()
235
- });
236
439
  var gcpBillingResources = defineResources({
237
440
  [COST_METRIC_NAME]: {
238
441
  shape: "metric",
@@ -294,56 +497,35 @@ var GcpBillingConnector = class _GcpBillingConnector extends BaseConnector {
294
497
  }
295
498
  id = id;
296
499
  credentials = gcpBillingCredentials;
297
- cachedToken = null;
298
- async getAccessToken(signal) {
299
- if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {
300
- return this.cachedToken.token;
301
- }
302
- const { serviceAccountJson } = this.creds;
303
- if (!serviceAccountJson) {
304
- throw new AuthError(`${this.id}: missing serviceAccountJson credential`);
305
- }
306
- const { url, body } = await buildServiceAccountJwt(
307
- serviceAccountJson,
308
- BQ_SCOPE
309
- );
310
- const res = await this.post(url, {
311
- resource: "oauth_token",
312
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
313
- body,
314
- signal
500
+ tokenProvider;
501
+ getAccessToken(signal) {
502
+ this.tokenProvider ??= new GcpAccessTokenProvider({
503
+ connectorId: this.id,
504
+ scope: BQ_READONLY_SCOPE,
505
+ getServiceAccountJson: () => this.creds.serviceAccountJson,
506
+ post: (url, opts) => this.post(url, opts)
315
507
  });
316
- const expiresIn = res.body.expires_in ?? 3600;
317
- this.cachedToken = {
318
- token: res.body.access_token,
319
- expiresAt: Date.now() + (expiresIn - 60) * 1e3
320
- };
321
- return this.cachedToken.token;
508
+ return this.tokenProvider.getToken(signal);
322
509
  }
323
- async runQuery(accessToken, sql, pageToken, signal) {
324
- const url = `${BQ_API_BASE}/projects/${encodeURIComponent(
325
- this.settings.bqProject
326
- )}/queries`;
327
- const body = {
328
- query: sql,
329
- useLegacySql: false,
330
- maxResults: PAGE_SIZE,
331
- timeoutMs: 3e4
510
+ async fetchBigQueryPage(request, signal) {
511
+ const accessToken = await this.getAccessToken(signal);
512
+ const headers = {
513
+ Authorization: `Bearer ${accessToken}`,
514
+ "Content-Type": "application/json",
515
+ "User-Agent": connectorUserAgent(this.id)
332
516
  };
333
- if (this.settings.bqLocation !== void 0) {
334
- body["location"] = this.settings.bqLocation;
335
- }
336
- if (pageToken !== void 0) {
337
- body["pageToken"] = pageToken;
517
+ if (request.method === "POST") {
518
+ const res2 = await this.post(request.url, {
519
+ resource: "daily_cost",
520
+ headers,
521
+ body: request.body,
522
+ signal
523
+ });
524
+ return res2.body;
338
525
  }
339
- const res = await this.post(url, {
526
+ const res = await this.get(request.url, {
340
527
  resource: "daily_cost",
341
- headers: {
342
- Authorization: `Bearer ${accessToken}`,
343
- "Content-Type": "application/json",
344
- "User-Agent": connectorUserAgent(this.id)
345
- },
346
- body: JSON.stringify(body),
528
+ headers,
347
529
  signal
348
530
  });
349
531
  return res.body;
@@ -361,49 +543,23 @@ var GcpBillingConnector = class _GcpBillingConnector extends BaseConnector {
361
543
  startDate: window.startDate,
362
544
  endDate: window.endDate
363
545
  });
364
- const samples = [];
365
- let pageToken;
366
- let page = 0;
367
- const phaseStart = Date.now();
368
- do {
369
- if (signal?.aborted) {
370
- return { done: false };
371
- }
372
- const accessToken = await this.getAccessToken(signal);
373
- let response;
374
- try {
375
- response = await this.runQuery(accessToken, sql, pageToken, signal);
376
- } catch (err) {
377
- this.logger.warn("fetch page failed", {
378
- resource: "daily_cost",
379
- page: page + 1,
380
- error: err instanceof Error ? err.message : String(err)
381
- });
382
- throw err;
383
- }
384
- if (response.jobComplete === false) {
385
- throw new Error(
386
- `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the groupBy or lookbackDays so the query finishes faster.`
387
- );
388
- }
389
- const pageSamples = buildSamplesFromBqResponse(response, groupBy);
390
- samples.push(...pageSamples);
391
- pageToken = typeof response.pageToken === "string" && response.pageToken.length > 0 ? response.pageToken : void 0;
392
- page += 1;
393
- this.logger.info("fetched page", {
546
+ const { rows: samples, aborted } = await collectBigQueryPages(
547
+ {
548
+ projectId: this.settings.bqProject,
549
+ sql,
394
550
  resource: "daily_cost",
395
- page,
396
- items: pageSamples.length,
397
- next: pageToken ?? null
398
- });
399
- } while (pageToken !== void 0);
551
+ location: this.settings.bqLocation,
552
+ signal,
553
+ logger: this.logger,
554
+ mapRows: (response) => buildSamplesFromBqResponse(response, groupBy),
555
+ jobIncompleteMessage: `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the groupBy or lookbackDays so the query finishes faster.`,
556
+ fetchPage: (request, sig) => this.fetchBigQueryPage(request, sig)
557
+ }
558
+ );
559
+ if (aborted) {
560
+ return { done: false };
561
+ }
400
562
  await storage.metrics(samples, { names: [COST_METRIC_NAME] });
401
- this.logger.info("resource done", {
402
- resource: "daily_cost",
403
- pages: page,
404
- items: samples.length,
405
- duration_ms: Date.now() - phaseStart
406
- });
407
563
  return { done: true };
408
564
  }
409
565
  };
@@ -427,23 +583,13 @@ function buildBillingSql(args) {
427
583
  `ORDER BY date`
428
584
  ].join("\n");
429
585
  }
430
- function pad2(n) {
431
- return String(n).padStart(2, "0");
432
- }
433
- function toDateStr(ms) {
434
- const d = new Date(ms);
435
- return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
436
- }
437
- function startOfUtcDay(ms) {
438
- return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;
439
- }
440
586
  function getCostWindow(options, lookbackDays, now = Date.now()) {
441
587
  const endMs = startOfUtcDay(now) + MS_PER_DAY;
442
588
  let days = lookbackDays;
443
589
  if (options.mode === "latest") {
444
590
  days = INCREMENTAL_LOOKBACK_DAYS;
445
591
  } else if (options.since !== void 0) {
446
- const sinceMs = parseEpoch(options.since, "iso");
592
+ const sinceMs = parseEpoch2(options.since, "iso");
447
593
  if (sinceMs !== null) {
448
594
  const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);
449
595
  days = Math.min(
@@ -458,14 +604,10 @@ function getCostWindow(options, lookbackDays, now = Date.now()) {
458
604
  };
459
605
  }
460
606
  function buildSamplesFromBqResponse(response, groupBy) {
461
- const schema = response.schema?.fields ?? [];
462
- const fieldIndex = {};
463
- schema.forEach((field, idx) => {
464
- fieldIndex[field.name] = idx;
465
- });
607
+ const fieldIndex = indexBqFields(response);
466
608
  const samples = [];
467
609
  for (const row of response.rows ?? []) {
468
- const dateValue = readCell(row.f, fieldIndex, "date");
610
+ const dateValue = readBqCell(row.f, fieldIndex, "date");
469
611
  if (dateValue === null) {
470
612
  continue;
471
613
  }
@@ -473,7 +615,7 @@ function buildSamplesFromBqResponse(response, groupBy) {
473
615
  if (ts === null) {
474
616
  continue;
475
617
  }
476
- const costValue = readCell(row.f, fieldIndex, "cost");
618
+ const costValue = readBqCell(row.f, fieldIndex, "cost");
477
619
  if (costValue === null) {
478
620
  continue;
479
621
  }
@@ -483,10 +625,10 @@ function buildSamplesFromBqResponse(response, groupBy) {
483
625
  }
484
626
  const attributes = {};
485
627
  for (const dim of groupBy) {
486
- const v = readCell(row.f, fieldIndex, dim);
628
+ const v = readBqCell(row.f, fieldIndex, dim);
487
629
  attributes[dim] = v ?? null;
488
630
  }
489
- const currency = readCell(row.f, fieldIndex, "currency");
631
+ const currency = readBqCell(row.f, fieldIndex, "currency");
490
632
  if (currency !== null) {
491
633
  attributes["currency"] = currency;
492
634
  }
@@ -494,28 +636,6 @@ function buildSamplesFromBqResponse(response, groupBy) {
494
636
  }
495
637
  return samples;
496
638
  }
497
- function readCell(cells, fieldIndex, name) {
498
- const idx = fieldIndex[name];
499
- if (idx === void 0) {
500
- return null;
501
- }
502
- const raw = cells[idx]?.v;
503
- if (raw === void 0 || raw === null) {
504
- return null;
505
- }
506
- return raw;
507
- }
508
- function parseBqDateOrEpoch(value) {
509
- const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
510
- if (dateMatch) {
511
- return Date.UTC(
512
- Number(dateMatch[1]),
513
- Number(dateMatch[2]) - 1,
514
- Number(dateMatch[3])
515
- );
516
- }
517
- return parseEpoch(value, "iso");
518
- }
519
639
 
520
640
  // src/index.ts
521
641
  var index_default = GcpBillingConnector;
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/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"]}
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","../../gcp-shared/src/bigquery.ts","../../gcp-shared/src/access-token.ts","../../gcp-shared/src/dates.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\nexport interface RefreshTokenCredentials {\n refreshToken: string;\n clientId: string;\n clientSecret: string;\n}\n\nexport function buildRefreshTokenGrant(credentials: RefreshTokenCredentials): {\n url: string;\n body: string;\n} {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: credentials.refreshToken,\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n }).toString();\n\n return { url: 'https://oauth2.googleapis.com/token', body };\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 { parseEpoch } from '@rawdash/connector-shared';\nimport { z } from 'zod';\n\nexport const BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nexport const BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport const BQ_API_BASE = 'https://bigquery.googleapis.com/bigquery/v2';\nexport const BQ_READONLY_SCOPE =\n 'https://www.googleapis.com/auth/bigquery.readonly';\nexport const BQ_PAGE_SIZE = 10_000;\nexport const BQ_QUERY_TIMEOUT_MS = 30_000;\n\nexport const 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 type BqQueryResponse = z.infer<typeof bqQueryResponseSchema>;\nexport type BqJobReference = NonNullable<BqQueryResponse['jobReference']>;\n\nexport type BqPageRequest =\n | { method: 'POST'; url: string; body: string }\n | { method: 'GET'; url: string };\n\nexport interface BqPageLogger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n}\n\nexport function buildBigQueryPageRequest(opts: {\n projectId: string;\n sql: string;\n pageToken: string | undefined;\n jobReference: BqJobReference | undefined;\n location?: string;\n pageSize?: number;\n timeoutMs?: number;\n}): BqPageRequest {\n const pageSize = opts.pageSize ?? BQ_PAGE_SIZE;\n const timeoutMs = opts.timeoutMs ?? BQ_QUERY_TIMEOUT_MS;\n\n if (opts.pageToken === undefined) {\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.projectId,\n )}/queries`;\n const body: Record<string, unknown> = {\n query: opts.sql,\n useLegacySql: false,\n maxResults: pageSize,\n timeoutMs,\n };\n if (opts.location !== undefined) {\n body['location'] = opts.location;\n }\n return { method: 'POST', url, body: JSON.stringify(body) };\n }\n\n if (opts.jobReference === undefined) {\n throw new Error(\n 'cannot fetch the next page of BigQuery results without a jobReference',\n );\n }\n\n const params = new URLSearchParams({\n pageToken: opts.pageToken,\n maxResults: String(pageSize),\n timeoutMs: String(timeoutMs),\n });\n const location = opts.jobReference.location ?? opts.location;\n if (location !== undefined) {\n params.set('location', location);\n }\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.jobReference.projectId,\n )}/queries/${encodeURIComponent(opts.jobReference.jobId)}?${params.toString()}`;\n return { method: 'GET', url };\n}\n\nexport async function collectBigQueryPages<T>(opts: {\n projectId: string;\n sql: string;\n resource: string;\n fetchPage: (\n request: BqPageRequest,\n signal: AbortSignal | undefined,\n ) => Promise<BqQueryResponse>;\n mapRows: (response: BqQueryResponse) => T[];\n jobIncompleteMessage: string;\n location?: string;\n pageSize?: number;\n signal?: AbortSignal;\n logger?: BqPageLogger;\n}): Promise<{ rows: T[]; aborted: boolean }> {\n const rows: T[] = [];\n let pageToken: string | undefined;\n let jobReference: BqJobReference | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (opts.signal?.aborted) {\n return { rows, aborted: true };\n }\n const request = buildBigQueryPageRequest({\n projectId: opts.projectId,\n sql: opts.sql,\n pageToken,\n jobReference,\n location: opts.location,\n pageSize: opts.pageSize,\n });\n let response: BqQueryResponse;\n try {\n response = await opts.fetchPage(request, opts.signal);\n } catch (err) {\n opts.logger?.warn('fetch page failed', {\n resource: opts.resource,\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(opts.jobIncompleteMessage);\n }\n if (response.jobReference !== undefined) {\n jobReference = response.jobReference;\n }\n const pageRows = opts.mapRows(response);\n rows.push(...pageRows);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n opts.logger?.info('fetched page', {\n resource: opts.resource,\n page,\n items: pageRows.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n opts.logger?.info('resource done', {\n resource: opts.resource,\n pages: page,\n items: rows.length,\n duration_ms: Date.now() - phaseStart,\n });\n return { rows, aborted: false };\n}\n\nexport function indexBqFields(\n response: BqQueryResponse,\n): Record<string, number> {\n const fieldIndex: Record<string, number> = {};\n (response.schema?.fields ?? []).forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n return fieldIndex;\n}\n\nexport function readBqCell(\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\nexport function 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 { AuthError } from '@rawdash/connector-shared';\n\nimport {\n type RefreshTokenCredentials,\n buildRefreshTokenGrant,\n buildServiceAccountJwt,\n} from './auth';\n\ninterface GcpTokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport type GcpTokenPoster = (\n url: string,\n opts: {\n resource: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n },\n) => Promise<{ body: GcpTokenResponse }>;\n\nexport class GcpAccessTokenProvider {\n private cached: { token: string; expiresAt: number } | null = null;\n\n constructor(\n private readonly opts: {\n connectorId: string;\n scope: string;\n getServiceAccountJson: () => string | undefined;\n getRefreshTokenCredentials?: () => RefreshTokenCredentials | undefined;\n post: GcpTokenPoster;\n },\n ) {}\n\n private async resolveGrant(): Promise<{ url: string; body: string }> {\n const serviceAccountJson = this.opts.getServiceAccountJson();\n if (serviceAccountJson) {\n return buildServiceAccountJwt(serviceAccountJson, this.opts.scope);\n }\n const refreshTokenCredentials = this.opts.getRefreshTokenCredentials?.();\n if (refreshTokenCredentials) {\n return buildRefreshTokenGrant(refreshTokenCredentials);\n }\n throw new AuthError(\n `${this.opts.connectorId}: missing serviceAccountJson or refresh-token credentials`,\n );\n }\n\n async getToken(signal?: AbortSignal): Promise<string> {\n if (this.cached && Date.now() < this.cached.expiresAt) {\n return this.cached.token;\n }\n const { url, body } = await this.resolveGrant();\n const res = await this.opts.post(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.cached = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cached.token;\n }\n}\n","export const MS_PER_DAY = 86_400_000;\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nexport function toDateStr(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nexport function startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\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 BQ_DATASET_RE,\n BQ_IDENT_RE,\n BQ_READONLY_SCOPE,\n type BqPageRequest,\n type BqQueryResponse,\n GcpAccessTokenProvider,\n MS_PER_DAY,\n bqQueryResponseSchema,\n collectBigQueryPages,\n gcpAuthConfigShape,\n indexBqFields,\n parseBqDateOrEpoch,\n readBqCell as readCell,\n startOfUtcDay,\n toDateStr,\n tokenResponseSchema,\n} from '@rawdash/connector-gcp-shared';\nimport { connectorUserAgent, parseEpoch } 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 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 domain: 'cloud.google.com',\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 COST_METRIC_NAME = 'gcp_cost_daily';\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 5;\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\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 tokenProvider?: GcpAccessTokenProvider;\n\n private getAccessToken(signal?: AbortSignal): Promise<string> {\n this.tokenProvider ??= new GcpAccessTokenProvider({\n connectorId: this.id,\n scope: BQ_READONLY_SCOPE,\n getServiceAccountJson: () => this.creds.serviceAccountJson,\n post: (url, opts) =>\n this.post<{ access_token: string; expires_in?: number }>(url, opts),\n });\n return this.tokenProvider.getToken(signal);\n }\n\n private async fetchBigQueryPage(\n request: BqPageRequest,\n signal: AbortSignal | undefined,\n ): Promise<BqQueryResponse> {\n const accessToken = await this.getAccessToken(signal);\n const headers = {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent(this.id),\n };\n if (request.method === 'POST') {\n const res = await this.post<BqQueryResponse>(request.url, {\n resource: 'daily_cost',\n headers,\n body: request.body,\n signal,\n });\n return res.body;\n }\n const res = await this.get<BqQueryResponse>(request.url, {\n resource: 'daily_cost',\n headers,\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 { rows: samples, aborted } = await collectBigQueryPages<MetricSample>(\n {\n projectId: this.settings.bqProject,\n sql,\n resource: 'daily_cost',\n location: this.settings.bqLocation,\n signal,\n logger: this.logger,\n mapRows: (response) => buildSamplesFromBqResponse(response, groupBy),\n jobIncompleteMessage: `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the groupBy or lookbackDays so the query finishes faster.`,\n fetchPage: (request, sig) => this.fetchBigQueryPage(request, sig),\n },\n );\n if (aborted) {\n return { done: false };\n }\n await storage.metrics(samples, { names: [COST_METRIC_NAME] });\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\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 fieldIndex = indexBqFields(response);\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","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","url","HTTP_CLIENT_VERSION","DEFAULT_USER_AGENT","parseEpoch","z","res","parseEpoch"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawdash/connector-gcp-billing",
3
- "version": "0.21.1",
3
+ "version": "0.23.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.21.1"
27
+ "@rawdash/core": "0.23.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "fast-check": "^4.8.0",
@@ -33,7 +33,7 @@
33
33
  "vitest": "^4.1.4",
34
34
  "@rawdash/connector-gcp-shared": "0.1.0",
35
35
  "@rawdash/connector-shared": "0.3.1",
36
- "@rawdash/connector-test-utils": "0.0.9"
36
+ "@rawdash/connector-test-utils": "0.0.10"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsup",