runline 0.5.2 → 0.6.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.
@@ -0,0 +1,113 @@
1
+ import { createSign } from "node:crypto";
2
+ const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
3
+ const REFRESH_SKEW_MS = 60_000;
4
+ export async function googleAccessToken(ctx, pluginName, scopes) {
5
+ const cfg = ctx.connection.config;
6
+ if (cfg.accessToken &&
7
+ typeof cfg.accessTokenExpiresAt === "number" &&
8
+ Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
9
+ return cfg.accessToken;
10
+ }
11
+ if (hasServiceAccountConfig(cfg)) {
12
+ return refreshServiceAccountAccessToken(ctx, pluginName, cfg, scopes);
13
+ }
14
+ return refreshOAuthAccessToken(ctx, pluginName, cfg);
15
+ }
16
+ function hasServiceAccountConfig(cfg) {
17
+ return !!cfg.serviceAccountJson || !!(cfg.serviceAccountEmail && cfg.serviceAccountPrivateKey);
18
+ }
19
+ async function refreshOAuthAccessToken(ctx, pluginName, cfg) {
20
+ const { clientId, clientSecret, refreshToken } = cfg;
21
+ if (!clientId || !clientSecret || !refreshToken) {
22
+ throw new Error(`${pluginName}: missing OAuth clientId/clientSecret/refreshToken or service account credentials. Run the OAuth helper or set serviceAccountJson.`);
23
+ }
24
+ const body = new URLSearchParams({
25
+ client_id: clientId,
26
+ client_secret: clientSecret,
27
+ refresh_token: refreshToken,
28
+ grant_type: "refresh_token",
29
+ });
30
+ const res = await fetch(TOKEN_ENDPOINT, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
33
+ body: body.toString(),
34
+ });
35
+ if (!res.ok) {
36
+ throw new Error(`${pluginName}: token refresh failed (${res.status}): ${await res.text()}`);
37
+ }
38
+ const data = (await res.json());
39
+ const expiresAt = Date.now() + data.expires_in * 1000;
40
+ await ctx.updateConnection({
41
+ accessToken: data.access_token,
42
+ accessTokenExpiresAt: expiresAt,
43
+ });
44
+ return data.access_token;
45
+ }
46
+ async function refreshServiceAccountAccessToken(ctx, pluginName, cfg, scopes) {
47
+ const serviceAccount = parseServiceAccount(pluginName, cfg);
48
+ const now = Math.floor(Date.now() / 1000);
49
+ const assertion = signJwt({
50
+ alg: "RS256",
51
+ typ: "JWT",
52
+ }, {
53
+ iss: serviceAccount.client_email,
54
+ scope: scopes.join(" "),
55
+ aud: TOKEN_ENDPOINT,
56
+ iat: now,
57
+ exp: now + 3600,
58
+ ...(cfg.serviceAccountSubject ? { sub: cfg.serviceAccountSubject } : {}),
59
+ }, serviceAccount.private_key);
60
+ const body = new URLSearchParams({
61
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
62
+ assertion,
63
+ });
64
+ const res = await fetch(TOKEN_ENDPOINT, {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
67
+ body: body.toString(),
68
+ });
69
+ if (!res.ok) {
70
+ throw new Error(`${pluginName}: service account token failed (${res.status}): ${await res.text()}`);
71
+ }
72
+ const data = (await res.json());
73
+ const expiresAt = Date.now() + data.expires_in * 1000;
74
+ await ctx.updateConnection({
75
+ accessToken: data.access_token,
76
+ accessTokenExpiresAt: expiresAt,
77
+ });
78
+ return data.access_token;
79
+ }
80
+ function parseServiceAccount(pluginName, cfg) {
81
+ if (cfg.serviceAccountJson) {
82
+ try {
83
+ const parsed = JSON.parse(cfg.serviceAccountJson);
84
+ if (parsed.client_email && parsed.private_key) {
85
+ return { client_email: parsed.client_email, private_key: parsed.private_key };
86
+ }
87
+ }
88
+ catch (err) {
89
+ throw new Error(`${pluginName}: invalid serviceAccountJson: ${err.message}`);
90
+ }
91
+ }
92
+ if (cfg.serviceAccountEmail && cfg.serviceAccountPrivateKey) {
93
+ return {
94
+ client_email: cfg.serviceAccountEmail,
95
+ private_key: cfg.serviceAccountPrivateKey.replace(/\\n/g, "\n"),
96
+ };
97
+ }
98
+ throw new Error(`${pluginName}: service account requires serviceAccountJson or serviceAccountEmail/serviceAccountPrivateKey`);
99
+ }
100
+ function signJwt(header, payload, privateKey) {
101
+ const encodedHeader = base64url(JSON.stringify(header));
102
+ const encodedPayload = base64url(JSON.stringify(payload));
103
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
104
+ const signer = createSign("RSA-SHA256");
105
+ signer.update(signingInput);
106
+ signer.end();
107
+ const signature = signer.sign(privateKey.replace(/\\n/g, "\n"));
108
+ return `${signingInput}.${base64url(signature)}`;
109
+ }
110
+ function base64url(input) {
111
+ const buf = typeof input === "string" ? Buffer.from(input, "utf-8") : input;
112
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
113
+ }
@@ -20,46 +20,10 @@
20
20
  * raw Gmail API response (including base64url `raw` for format=raw,
21
21
  * or parsed `payload` tree for format=full).
22
22
  */
23
- // ─── OAuth ───────────────────────────────────────────────────────
24
- const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
25
- const REFRESH_SKEW_MS = 60_000;
26
- async function refreshAccessToken(ctx) {
27
- const cfg = ctx.connection.config;
28
- const { clientId, clientSecret, refreshToken } = cfg;
29
- if (!clientId || !clientSecret || !refreshToken) {
30
- throw new Error("gmail: missing clientId/clientSecret/refreshToken. Run the Gmail OAuth helper to seed these.");
31
- }
32
- const body = new URLSearchParams({
33
- client_id: clientId,
34
- client_secret: clientSecret,
35
- refresh_token: refreshToken,
36
- grant_type: "refresh_token",
37
- });
38
- const res = await fetch(TOKEN_ENDPOINT, {
39
- method: "POST",
40
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
41
- body: body.toString(),
42
- });
43
- if (!res.ok) {
44
- const text = await res.text();
45
- throw new Error(`gmail: token refresh failed (${res.status}): ${text}`);
46
- }
47
- const data = (await res.json());
48
- const expiresAt = Date.now() + data.expires_in * 1000;
49
- await ctx.updateConnection({
50
- accessToken: data.access_token,
51
- accessTokenExpiresAt: expiresAt,
52
- });
53
- return data.access_token;
54
- }
23
+ import { googleAccessToken } from "../../_shared/googleAuth.js";
24
+ // ─── Auth ────────────────────────────────────────────────────────
55
25
  async function accessToken(ctx) {
56
- const cfg = ctx.connection.config;
57
- if (cfg.accessToken &&
58
- typeof cfg.accessTokenExpiresAt === "number" &&
59
- Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
60
- return cfg.accessToken;
61
- }
62
- return refreshAccessToken(ctx);
26
+ return googleAccessToken(ctx, "gmail", SCOPES);
63
27
  }
64
28
  // ─── Request ─────────────────────────────────────────────────────
65
29
  const API_BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
@@ -527,22 +491,46 @@ export default function gmail(rl) {
527
491
  rl.setConnectionSchema({
528
492
  clientId: {
529
493
  type: "string",
530
- required: true,
494
+ required: false,
531
495
  description: "Google OAuth2 client ID",
532
496
  env: "GMAIL_CLIENT_ID",
533
497
  },
534
498
  clientSecret: {
535
499
  type: "string",
536
- required: true,
500
+ required: false,
537
501
  description: "Google OAuth2 client secret",
538
502
  env: "GMAIL_CLIENT_SECRET",
539
503
  },
540
504
  refreshToken: {
541
505
  type: "string",
542
- required: true,
506
+ required: false,
543
507
  description: "OAuth2 refresh token (obtained via login flow)",
544
508
  env: "GMAIL_REFRESH_TOKEN",
545
509
  },
510
+ serviceAccountJson: {
511
+ type: "string",
512
+ required: false,
513
+ description: "Google service account JSON credential",
514
+ env: "GMAIL_SERVICE_ACCOUNT_JSON",
515
+ },
516
+ serviceAccountEmail: {
517
+ type: "string",
518
+ required: false,
519
+ description: "Google service account email",
520
+ env: "GMAIL_SERVICE_ACCOUNT_EMAIL",
521
+ },
522
+ serviceAccountPrivateKey: {
523
+ type: "string",
524
+ required: false,
525
+ description: "Google service account private key",
526
+ env: "GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY",
527
+ },
528
+ serviceAccountSubject: {
529
+ type: "string",
530
+ required: false,
531
+ description: "User email to impersonate with domain-wide delegation",
532
+ env: "GMAIL_SERVICE_ACCOUNT_SUBJECT",
533
+ },
546
534
  accessToken: {
547
535
  type: "string",
548
536
  required: false,
@@ -28,48 +28,12 @@
28
28
  * call `event.listInstances`.
29
29
  */
30
30
  import rrulePkg from "rrule";
31
+ import { googleAccessToken } from "../../_shared/googleAuth.js";
31
32
  // `rrule` ships as CJS; named imports fail under Node ESM.
32
33
  const { RRule } = rrulePkg;
33
- // ─── OAuth ───────────────────────────────────────────────────────
34
- const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
35
- const REFRESH_SKEW_MS = 60_000;
36
- async function refreshAccessToken(ctx) {
37
- const cfg = ctx.connection.config;
38
- const { clientId, clientSecret, refreshToken } = cfg;
39
- if (!clientId || !clientSecret || !refreshToken) {
40
- throw new Error("googleCalendar: missing clientId/clientSecret/refreshToken. Run the Google Calendar OAuth helper to seed these.");
41
- }
42
- const body = new URLSearchParams({
43
- client_id: clientId,
44
- client_secret: clientSecret,
45
- refresh_token: refreshToken,
46
- grant_type: "refresh_token",
47
- });
48
- const res = await fetch(TOKEN_ENDPOINT, {
49
- method: "POST",
50
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
51
- body: body.toString(),
52
- });
53
- if (!res.ok) {
54
- const text = await res.text();
55
- throw new Error(`googleCalendar: token refresh failed (${res.status}): ${text}`);
56
- }
57
- const data = (await res.json());
58
- const expiresAt = Date.now() + data.expires_in * 1000;
59
- await ctx.updateConnection({
60
- accessToken: data.access_token,
61
- accessTokenExpiresAt: expiresAt,
62
- });
63
- return data.access_token;
64
- }
34
+ // ─── Auth ────────────────────────────────────────────────────────
65
35
  async function accessToken(ctx) {
66
- const cfg = ctx.connection.config;
67
- if (cfg.accessToken &&
68
- typeof cfg.accessTokenExpiresAt === "number" &&
69
- Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
70
- return cfg.accessToken;
71
- }
72
- return refreshAccessToken(ctx);
36
+ return googleAccessToken(ctx, "googleCalendar", SCOPES);
73
37
  }
74
38
  // ─── Request ─────────────────────────────────────────────────────
75
39
  const API_BASE = "https://www.googleapis.com/calendar/v3";
@@ -328,22 +292,46 @@ export default function googleCalendar(rl) {
328
292
  rl.setConnectionSchema({
329
293
  clientId: {
330
294
  type: "string",
331
- required: true,
295
+ required: false,
332
296
  description: "Google OAuth2 client ID",
333
297
  env: "GOOGLE_CALENDAR_CLIENT_ID",
334
298
  },
335
299
  clientSecret: {
336
300
  type: "string",
337
- required: true,
301
+ required: false,
338
302
  description: "Google OAuth2 client secret",
339
303
  env: "GOOGLE_CALENDAR_CLIENT_SECRET",
340
304
  },
341
305
  refreshToken: {
342
306
  type: "string",
343
- required: true,
307
+ required: false,
344
308
  description: "OAuth2 refresh token (obtained via login flow)",
345
309
  env: "GOOGLE_CALENDAR_REFRESH_TOKEN",
346
310
  },
311
+ serviceAccountJson: {
312
+ type: "string",
313
+ required: false,
314
+ description: "Google service account JSON credential",
315
+ env: "GOOGLE_CALENDAR_SERVICE_ACCOUNT_JSON",
316
+ },
317
+ serviceAccountEmail: {
318
+ type: "string",
319
+ required: false,
320
+ description: "Google service account email",
321
+ env: "GOOGLE_CALENDAR_SERVICE_ACCOUNT_EMAIL",
322
+ },
323
+ serviceAccountPrivateKey: {
324
+ type: "string",
325
+ required: false,
326
+ description: "Google service account private key",
327
+ env: "GOOGLE_CALENDAR_SERVICE_ACCOUNT_PRIVATE_KEY",
328
+ },
329
+ serviceAccountSubject: {
330
+ type: "string",
331
+ required: false,
332
+ description: "User email to impersonate with domain-wide delegation",
333
+ env: "GOOGLE_CALENDAR_SERVICE_ACCOUNT_SUBJECT",
334
+ },
347
335
  accessToken: {
348
336
  type: "string",
349
337
  required: false,
@@ -22,6 +22,7 @@
22
22
  * the bare ID and normalize. The response always includes a
23
23
  * convenience `contactId` field stripped from `resourceName`.
24
24
  */
25
+ import { googleAccessToken } from "../../_shared/googleAuth.js";
25
26
  // ─── Fields ─────────────────────────────────────────────────────
26
27
  /**
27
28
  * Every `personFields` value supported by People API get/list calls.
@@ -78,45 +79,9 @@ const UPDATABLE_PERSON_FIELDS = new Set([
78
79
  "urls",
79
80
  "userDefined",
80
81
  ]);
81
- // ─── OAuth ───────────────────────────────────────────────────────
82
- const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
83
- const REFRESH_SKEW_MS = 60_000;
84
- async function refreshAccessToken(ctx) {
85
- const cfg = ctx.connection.config;
86
- const { clientId, clientSecret, refreshToken } = cfg;
87
- if (!clientId || !clientSecret || !refreshToken) {
88
- throw new Error("googleContacts: missing clientId/clientSecret/refreshToken. Run the Contacts OAuth helper to seed these.");
89
- }
90
- const body = new URLSearchParams({
91
- client_id: clientId,
92
- client_secret: clientSecret,
93
- refresh_token: refreshToken,
94
- grant_type: "refresh_token",
95
- });
96
- const res = await fetch(TOKEN_ENDPOINT, {
97
- method: "POST",
98
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
99
- body: body.toString(),
100
- });
101
- if (!res.ok) {
102
- throw new Error(`googleContacts: token refresh failed (${res.status}): ${await res.text()}`);
103
- }
104
- const data = (await res.json());
105
- const expiresAt = Date.now() + data.expires_in * 1000;
106
- await ctx.updateConnection({
107
- accessToken: data.access_token,
108
- accessTokenExpiresAt: expiresAt,
109
- });
110
- return data.access_token;
111
- }
82
+ // ─── Auth ────────────────────────────────────────────────────────
112
83
  async function accessToken(ctx) {
113
- const cfg = ctx.connection.config;
114
- if (cfg.accessToken &&
115
- typeof cfg.accessTokenExpiresAt === "number" &&
116
- Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
117
- return cfg.accessToken;
118
- }
119
- return refreshAccessToken(ctx);
84
+ return googleAccessToken(ctx, "googleContacts", SCOPES);
120
85
  }
121
86
  // ─── Request ─────────────────────────────────────────────────────
122
87
  const API_BASE = "https://people.googleapis.com/v1";
@@ -349,9 +314,13 @@ export default function googleContacts(rl) {
349
314
  ],
350
315
  });
351
316
  rl.setConnectionSchema({
352
- clientId: { type: "string", required: true, env: "GOOGLE_CONTACTS_CLIENT_ID" },
353
- clientSecret: { type: "string", required: true, env: "GOOGLE_CONTACTS_CLIENT_SECRET" },
354
- refreshToken: { type: "string", required: true, env: "GOOGLE_CONTACTS_REFRESH_TOKEN" },
317
+ clientId: { type: "string", required: false, env: "GOOGLE_CONTACTS_CLIENT_ID" },
318
+ clientSecret: { type: "string", required: false, env: "GOOGLE_CONTACTS_CLIENT_SECRET" },
319
+ refreshToken: { type: "string", required: false, env: "GOOGLE_CONTACTS_REFRESH_TOKEN" },
320
+ serviceAccountJson: { type: "string", required: false, env: "GOOGLE_CONTACTS_SERVICE_ACCOUNT_JSON" },
321
+ serviceAccountEmail: { type: "string", required: false, env: "GOOGLE_CONTACTS_SERVICE_ACCOUNT_EMAIL" },
322
+ serviceAccountPrivateKey: { type: "string", required: false, env: "GOOGLE_CONTACTS_SERVICE_ACCOUNT_PRIVATE_KEY" },
323
+ serviceAccountSubject: { type: "string", required: false, env: "GOOGLE_CONTACTS_SERVICE_ACCOUNT_SUBJECT" },
355
324
  accessToken: { type: "string", required: false },
356
325
  accessTokenExpiresAt: { type: "number", required: false },
357
326
  });
@@ -36,45 +36,10 @@
36
36
  * with a single request; callers who need to chain multiple edits
37
37
  * atomically can compose them via `document.batchUpdate`.
38
38
  */
39
- // ─── OAuth ───────────────────────────────────────────────────────
40
- const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
41
- const REFRESH_SKEW_MS = 60_000;
42
- async function refreshAccessToken(ctx) {
43
- const cfg = ctx.connection.config;
44
- const { clientId, clientSecret, refreshToken } = cfg;
45
- if (!clientId || !clientSecret || !refreshToken) {
46
- throw new Error("googleDocs: missing clientId/clientSecret/refreshToken. Run the Docs OAuth helper to seed these.");
47
- }
48
- const body = new URLSearchParams({
49
- client_id: clientId,
50
- client_secret: clientSecret,
51
- refresh_token: refreshToken,
52
- grant_type: "refresh_token",
53
- });
54
- const res = await fetch(TOKEN_ENDPOINT, {
55
- method: "POST",
56
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
57
- body: body.toString(),
58
- });
59
- if (!res.ok) {
60
- throw new Error(`googleDocs: token refresh failed (${res.status}): ${await res.text()}`);
61
- }
62
- const data = (await res.json());
63
- const expiresAt = Date.now() + data.expires_in * 1000;
64
- await ctx.updateConnection({
65
- accessToken: data.access_token,
66
- accessTokenExpiresAt: expiresAt,
67
- });
68
- return data.access_token;
69
- }
39
+ import { googleAccessToken } from "../../_shared/googleAuth.js";
40
+ // ─── Auth ────────────────────────────────────────────────────────
70
41
  async function accessToken(ctx) {
71
- const cfg = ctx.connection.config;
72
- if (cfg.accessToken &&
73
- typeof cfg.accessTokenExpiresAt === "number" &&
74
- Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
75
- return cfg.accessToken;
76
- }
77
- return refreshAccessToken(ctx);
42
+ return googleAccessToken(ctx, "googleDocs", SCOPES);
78
43
  }
79
44
  // ─── Request ─────────────────────────────────────────────────────
80
45
  const DOCS_BASE = "https://docs.googleapis.com/v1";
@@ -206,9 +171,13 @@ export default function googleDocs(rl) {
206
171
  ],
207
172
  });
208
173
  rl.setConnectionSchema({
209
- clientId: { type: "string", required: true, env: "GOOGLE_DOCS_CLIENT_ID" },
210
- clientSecret: { type: "string", required: true, env: "GOOGLE_DOCS_CLIENT_SECRET" },
211
- refreshToken: { type: "string", required: true, env: "GOOGLE_DOCS_REFRESH_TOKEN" },
174
+ clientId: { type: "string", required: false, env: "GOOGLE_DOCS_CLIENT_ID" },
175
+ clientSecret: { type: "string", required: false, env: "GOOGLE_DOCS_CLIENT_SECRET" },
176
+ refreshToken: { type: "string", required: false, env: "GOOGLE_DOCS_REFRESH_TOKEN" },
177
+ serviceAccountJson: { type: "string", required: false, env: "GOOGLE_DOCS_SERVICE_ACCOUNT_JSON" },
178
+ serviceAccountEmail: { type: "string", required: false, env: "GOOGLE_DOCS_SERVICE_ACCOUNT_EMAIL" },
179
+ serviceAccountPrivateKey: { type: "string", required: false, env: "GOOGLE_DOCS_SERVICE_ACCOUNT_PRIVATE_KEY" },
180
+ serviceAccountSubject: { type: "string", required: false, env: "GOOGLE_DOCS_SERVICE_ACCOUNT_SUBJECT" },
212
181
  accessToken: { type: "string", required: false },
213
182
  accessTokenExpiresAt: { type: "number", required: false },
214
183
  });
@@ -36,6 +36,7 @@
36
36
  * (Buffer) vs a path (streamed).
37
37
  */
38
38
  import { createReadStream, readFileSync, statSync, writeFileSync } from "node:fs";
39
+ import { googleAccessToken } from "../../_shared/googleAuth.js";
39
40
  // ─── MIME constants ──────────────────────────────────────────────
40
41
  const DRIVE = {
41
42
  FOLDER: "application/vnd.google-apps.folder",
@@ -55,46 +56,9 @@ const DRIVE = {
55
56
  FUSIONTABLE: "application/vnd.google-apps.fusiontable",
56
57
  UNKNOWN: "application/vnd.google-apps.unknown",
57
58
  };
58
- // ─── OAuth ───────────────────────────────────────────────────────
59
- const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
60
- const REFRESH_SKEW_MS = 60_000;
61
- async function refreshAccessToken(ctx) {
62
- const cfg = ctx.connection.config;
63
- const { clientId, clientSecret, refreshToken } = cfg;
64
- if (!clientId || !clientSecret || !refreshToken) {
65
- throw new Error("googleDrive: missing clientId/clientSecret/refreshToken. Run the Google Drive OAuth helper to seed these.");
66
- }
67
- const body = new URLSearchParams({
68
- client_id: clientId,
69
- client_secret: clientSecret,
70
- refresh_token: refreshToken,
71
- grant_type: "refresh_token",
72
- });
73
- const res = await fetch(TOKEN_ENDPOINT, {
74
- method: "POST",
75
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
76
- body: body.toString(),
77
- });
78
- if (!res.ok) {
79
- const text = await res.text();
80
- throw new Error(`googleDrive: token refresh failed (${res.status}): ${text}`);
81
- }
82
- const data = (await res.json());
83
- const expiresAt = Date.now() + data.expires_in * 1000;
84
- await ctx.updateConnection({
85
- accessToken: data.access_token,
86
- accessTokenExpiresAt: expiresAt,
87
- });
88
- return data.access_token;
89
- }
59
+ // ─── Auth ────────────────────────────────────────────────────────
90
60
  async function accessToken(ctx) {
91
- const cfg = ctx.connection.config;
92
- if (cfg.accessToken &&
93
- typeof cfg.accessTokenExpiresAt === "number" &&
94
- Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
95
- return cfg.accessToken;
96
- }
97
- return refreshAccessToken(ctx);
61
+ return googleAccessToken(ctx, "googleDrive", SCOPES);
98
62
  }
99
63
  // ─── Request ─────────────────────────────────────────────────────
100
64
  const API_BASE = "https://www.googleapis.com";
@@ -408,22 +372,46 @@ export default function googleDrive(rl) {
408
372
  rl.setConnectionSchema({
409
373
  clientId: {
410
374
  type: "string",
411
- required: true,
375
+ required: false,
412
376
  description: "Google OAuth2 client ID",
413
377
  env: "GOOGLE_DRIVE_CLIENT_ID",
414
378
  },
415
379
  clientSecret: {
416
380
  type: "string",
417
- required: true,
381
+ required: false,
418
382
  description: "Google OAuth2 client secret",
419
383
  env: "GOOGLE_DRIVE_CLIENT_SECRET",
420
384
  },
421
385
  refreshToken: {
422
386
  type: "string",
423
- required: true,
387
+ required: false,
424
388
  description: "OAuth2 refresh token",
425
389
  env: "GOOGLE_DRIVE_REFRESH_TOKEN",
426
390
  },
391
+ serviceAccountJson: {
392
+ type: "string",
393
+ required: false,
394
+ description: "Google service account JSON credential",
395
+ env: "GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON",
396
+ },
397
+ serviceAccountEmail: {
398
+ type: "string",
399
+ required: false,
400
+ description: "Google service account email",
401
+ env: "GOOGLE_DRIVE_SERVICE_ACCOUNT_EMAIL",
402
+ },
403
+ serviceAccountPrivateKey: {
404
+ type: "string",
405
+ required: false,
406
+ description: "Google service account private key",
407
+ env: "GOOGLE_DRIVE_SERVICE_ACCOUNT_PRIVATE_KEY",
408
+ },
409
+ serviceAccountSubject: {
410
+ type: "string",
411
+ required: false,
412
+ description: "User email to impersonate with domain-wide delegation",
413
+ env: "GOOGLE_DRIVE_SERVICE_ACCOUNT_SUBJECT",
414
+ },
427
415
  accessToken: { type: "string", required: false },
428
416
  accessTokenExpiresAt: { type: "number", required: false },
429
417
  });
@@ -28,46 +28,11 @@
28
28
  * Filtering is intentionally the caller's job — `read` returns
29
29
  * raw rows / objects and callers post-filter.
30
30
  */
31
+ import { googleAccessToken } from "../../_shared/googleAuth.js";
31
32
  const ROW_NUMBER = "row_number";
32
- // ─── OAuth ───────────────────────────────────────────────────────
33
- const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
34
- const REFRESH_SKEW_MS = 60_000;
35
- async function refreshAccessToken(ctx) {
36
- const cfg = ctx.connection.config;
37
- const { clientId, clientSecret, refreshToken } = cfg;
38
- if (!clientId || !clientSecret || !refreshToken) {
39
- throw new Error("googleSheets: missing clientId/clientSecret/refreshToken. Run the Sheets OAuth helper to seed these.");
40
- }
41
- const body = new URLSearchParams({
42
- client_id: clientId,
43
- client_secret: clientSecret,
44
- refresh_token: refreshToken,
45
- grant_type: "refresh_token",
46
- });
47
- const res = await fetch(TOKEN_ENDPOINT, {
48
- method: "POST",
49
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
50
- body: body.toString(),
51
- });
52
- if (!res.ok) {
53
- throw new Error(`googleSheets: token refresh failed (${res.status}): ${await res.text()}`);
54
- }
55
- const data = (await res.json());
56
- const expiresAt = Date.now() + data.expires_in * 1000;
57
- await ctx.updateConnection({
58
- accessToken: data.access_token,
59
- accessTokenExpiresAt: expiresAt,
60
- });
61
- return data.access_token;
62
- }
33
+ // ─── Auth ────────────────────────────────────────────────────────
63
34
  async function accessToken(ctx) {
64
- const cfg = ctx.connection.config;
65
- if (cfg.accessToken &&
66
- typeof cfg.accessTokenExpiresAt === "number" &&
67
- Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
68
- return cfg.accessToken;
69
- }
70
- return refreshAccessToken(ctx);
35
+ return googleAccessToken(ctx, "googleSheets", SCOPES);
71
36
  }
72
37
  // ─── Request ─────────────────────────────────────────────────────
73
38
  const SHEETS_BASE = "https://sheets.googleapis.com";
@@ -387,22 +352,46 @@ export default function googleSheets(rl) {
387
352
  rl.setConnectionSchema({
388
353
  clientId: {
389
354
  type: "string",
390
- required: true,
355
+ required: false,
391
356
  description: "Google OAuth2 client ID",
392
357
  env: "GOOGLE_SHEETS_CLIENT_ID",
393
358
  },
394
359
  clientSecret: {
395
360
  type: "string",
396
- required: true,
361
+ required: false,
397
362
  description: "Google OAuth2 client secret",
398
363
  env: "GOOGLE_SHEETS_CLIENT_SECRET",
399
364
  },
400
365
  refreshToken: {
401
366
  type: "string",
402
- required: true,
367
+ required: false,
403
368
  description: "OAuth2 refresh token",
404
369
  env: "GOOGLE_SHEETS_REFRESH_TOKEN",
405
370
  },
371
+ serviceAccountJson: {
372
+ type: "string",
373
+ required: false,
374
+ description: "Google service account JSON credential",
375
+ env: "GOOGLE_SHEETS_SERVICE_ACCOUNT_JSON",
376
+ },
377
+ serviceAccountEmail: {
378
+ type: "string",
379
+ required: false,
380
+ description: "Google service account email",
381
+ env: "GOOGLE_SHEETS_SERVICE_ACCOUNT_EMAIL",
382
+ },
383
+ serviceAccountPrivateKey: {
384
+ type: "string",
385
+ required: false,
386
+ description: "Google service account private key",
387
+ env: "GOOGLE_SHEETS_SERVICE_ACCOUNT_PRIVATE_KEY",
388
+ },
389
+ serviceAccountSubject: {
390
+ type: "string",
391
+ required: false,
392
+ description: "User email to impersonate with domain-wide delegation",
393
+ env: "GOOGLE_SHEETS_SERVICE_ACCOUNT_SUBJECT",
394
+ },
406
395
  accessToken: { type: "string", required: false },
407
396
  accessTokenExpiresAt: { type: "number", required: false },
408
397
  });