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.
- package/dist/plugins/_shared/googleAuth.js +113 -0
- package/dist/plugins/gmail/src/index.js +30 -42
- package/dist/plugins/googleCalendar/src/index.js +30 -42
- package/dist/plugins/googleContacts/src/index.js +10 -41
- package/dist/plugins/googleDocs/src/index.js +10 -41
- package/dist/plugins/googleDrive/src/index.js +30 -42
- package/dist/plugins/googleSheets/src/index.js +30 -41
- package/dist/plugins/googleSlides/src/index.js +10 -41
- package/dist/plugins/googleTasks/src/index.js +10 -41
- package/dist/plugins/linear/src/index.js +986 -128
- package/package.json +1 -1
|
@@ -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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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:
|
|
353
|
-
clientSecret: { type: "string", required:
|
|
354
|
-
refreshToken: { type: "string", required:
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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:
|
|
210
|
-
clientSecret: { type: "string", required:
|
|
211
|
-
refreshToken: { type: "string", required:
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
});
|