@ariadng/sheets 0.4.0 → 0.4.2
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/cli.cjs +1610 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +0 -6
- package/dist/cli.js +1318 -595
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +858 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +356 -0
- package/dist/index.d.ts +355 -10
- package/dist/index.js +810 -11
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/dist/api/index.d.ts +0 -60
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -347
- package/dist/api/index.js.map +0 -1
- package/dist/auth/constants.d.ts +0 -13
- package/dist/auth/constants.d.ts.map +0 -1
- package/dist/auth/constants.js +0 -21
- package/dist/auth/constants.js.map +0 -1
- package/dist/auth/index.d.ts +0 -13
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -22
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/oauth.d.ts +0 -32
- package/dist/auth/oauth.d.ts.map +0 -1
- package/dist/auth/oauth.js +0 -80
- package/dist/auth/oauth.js.map +0 -1
- package/dist/auth/service-account.d.ts +0 -18
- package/dist/auth/service-account.d.ts.map +0 -1
- package/dist/auth/service-account.js +0 -92
- package/dist/auth/service-account.js.map +0 -1
- package/dist/auth/user-auth.d.ts +0 -24
- package/dist/auth/user-auth.d.ts.map +0 -1
- package/dist/auth/user-auth.js +0 -230
- package/dist/auth/user-auth.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/http/index.d.ts +0 -19
- package/dist/http/index.d.ts.map +0 -1
- package/dist/http/index.js +0 -68
- package/dist/http/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -200
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -16
- package/dist/types/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,811 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// src/types/index.ts
|
|
2
|
+
var SheetsError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(error) {
|
|
7
|
+
super(error.message);
|
|
8
|
+
this.name = "SheetsError";
|
|
9
|
+
this.code = error.code;
|
|
10
|
+
this.status = error.status;
|
|
11
|
+
this.details = error.details;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/http/index.ts
|
|
16
|
+
var BASE_URL = "https://sheets.googleapis.com/v4";
|
|
17
|
+
var MAX_RETRIES = 3;
|
|
18
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
19
|
+
var HttpClient = class {
|
|
20
|
+
getAccessToken;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.getAccessToken = options.getAccessToken;
|
|
23
|
+
}
|
|
24
|
+
async request(path2, options = {}) {
|
|
25
|
+
const { method = "GET", body, params } = options;
|
|
26
|
+
let url = `${BASE_URL}${path2}`;
|
|
27
|
+
if (params) {
|
|
28
|
+
const searchParams = new URLSearchParams(params);
|
|
29
|
+
url += `?${searchParams.toString()}`;
|
|
30
|
+
}
|
|
31
|
+
let lastError = null;
|
|
32
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
const accessToken = await this.getAccessToken();
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method,
|
|
37
|
+
headers: {
|
|
38
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
39
|
+
"Content-Type": "application/json"
|
|
40
|
+
},
|
|
41
|
+
body: body ? JSON.stringify(body) : void 0
|
|
42
|
+
});
|
|
43
|
+
if (response.status === 429) {
|
|
44
|
+
const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
45
|
+
await this.sleep(backoffMs);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
if (data.error) {
|
|
51
|
+
throw new SheetsError({
|
|
52
|
+
code: data.error.code,
|
|
53
|
+
message: data.error.message,
|
|
54
|
+
status: data.error.status
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
58
|
+
}
|
|
59
|
+
return data;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
lastError = error;
|
|
62
|
+
if (error instanceof SheetsError && error.code !== 429 && error.code !== 500 && error.code !== 503) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
66
|
+
const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
67
|
+
await this.sleep(backoffMs);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw lastError || new Error("Request failed after retries");
|
|
72
|
+
}
|
|
73
|
+
sleep(ms) {
|
|
74
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/auth/constants.ts
|
|
79
|
+
var OAUTH_CLIENT_ID = "344941894490-jmdvo5ghomqi7vuisfrf80hfassk1ma5.apps.googleusercontent.com";
|
|
80
|
+
var OAUTH_CLIENT_SECRET = "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";
|
|
81
|
+
var OAUTH_REDIRECT_URI = "http://localhost:8085/callback";
|
|
82
|
+
var OAUTH_CALLBACK_PORT = 8085;
|
|
83
|
+
var OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
84
|
+
var OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
85
|
+
var OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
86
|
+
var OAUTH_SCOPES = [
|
|
87
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
88
|
+
"https://www.googleapis.com/auth/userinfo.email"
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// src/auth/oauth.ts
|
|
92
|
+
var OAuthAuth = class {
|
|
93
|
+
config;
|
|
94
|
+
cachedToken;
|
|
95
|
+
expiresAt;
|
|
96
|
+
constructor(config) {
|
|
97
|
+
this.config = config;
|
|
98
|
+
this.cachedToken = config.accessToken;
|
|
99
|
+
this.expiresAt = config.expiresAt || 0;
|
|
100
|
+
}
|
|
101
|
+
async getAccessToken() {
|
|
102
|
+
if (!this.canRefresh()) {
|
|
103
|
+
return this.cachedToken;
|
|
104
|
+
}
|
|
105
|
+
if (this.isExpired()) {
|
|
106
|
+
await this.refreshToken();
|
|
107
|
+
}
|
|
108
|
+
return this.cachedToken;
|
|
109
|
+
}
|
|
110
|
+
canRefresh() {
|
|
111
|
+
return !!(this.config.refreshToken && this.config.clientId && this.config.clientSecret && this.expiresAt > 0);
|
|
112
|
+
}
|
|
113
|
+
isExpired() {
|
|
114
|
+
return Date.now() >= this.expiresAt - 6e4;
|
|
115
|
+
}
|
|
116
|
+
async refreshToken() {
|
|
117
|
+
if (!this.config.refreshToken || !this.config.clientId || !this.config.clientSecret) {
|
|
118
|
+
throw new Error("Token expired and missing refresh credentials (refreshToken, clientId, clientSecret)");
|
|
119
|
+
}
|
|
120
|
+
const response = await fetch(OAUTH_TOKEN_URL, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
123
|
+
body: new URLSearchParams({
|
|
124
|
+
client_id: this.config.clientId,
|
|
125
|
+
client_secret: this.config.clientSecret,
|
|
126
|
+
refresh_token: this.config.refreshToken,
|
|
127
|
+
grant_type: "refresh_token"
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const error = await response.text();
|
|
132
|
+
throw new Error(`Token refresh failed: ${error}`);
|
|
133
|
+
}
|
|
134
|
+
const tokenResponse = await response.json();
|
|
135
|
+
this.cachedToken = tokenResponse.access_token;
|
|
136
|
+
this.expiresAt = Date.now() + tokenResponse.expires_in * 1e3;
|
|
137
|
+
if (tokenResponse.refresh_token) {
|
|
138
|
+
this.config.refreshToken = tokenResponse.refresh_token;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get current token state for persistence in automation tools
|
|
143
|
+
* Returns updated tokens after any refresh operations
|
|
144
|
+
*/
|
|
145
|
+
getTokenState() {
|
|
146
|
+
return {
|
|
147
|
+
accessToken: this.cachedToken,
|
|
148
|
+
refreshToken: this.config.refreshToken,
|
|
149
|
+
expiresAt: this.expiresAt
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/auth/service-account.ts
|
|
155
|
+
import * as crypto from "crypto";
|
|
156
|
+
import * as fs from "fs/promises";
|
|
157
|
+
var TOKEN_URI = "https://oauth2.googleapis.com/token";
|
|
158
|
+
var SCOPE = "https://www.googleapis.com/auth/spreadsheets";
|
|
159
|
+
var TOKEN_LIFETIME_SECONDS = 3600;
|
|
160
|
+
var ServiceAccountAuth = class {
|
|
161
|
+
config;
|
|
162
|
+
credentials = null;
|
|
163
|
+
cachedToken = null;
|
|
164
|
+
tokenExpiresAt = 0;
|
|
165
|
+
constructor(config) {
|
|
166
|
+
this.config = config;
|
|
167
|
+
}
|
|
168
|
+
async getAccessToken() {
|
|
169
|
+
if (this.cachedToken && Date.now() < this.tokenExpiresAt - 6e4) {
|
|
170
|
+
return this.cachedToken;
|
|
171
|
+
}
|
|
172
|
+
await this.loadCredentials();
|
|
173
|
+
const jwt = this.createJwt();
|
|
174
|
+
const token = await this.exchangeJwtForToken(jwt);
|
|
175
|
+
this.cachedToken = token.access_token;
|
|
176
|
+
this.tokenExpiresAt = Date.now() + token.expires_in * 1e3;
|
|
177
|
+
return this.cachedToken;
|
|
178
|
+
}
|
|
179
|
+
async loadCredentials() {
|
|
180
|
+
if (this.credentials) return;
|
|
181
|
+
if (this.config.credentials) {
|
|
182
|
+
this.credentials = this.config.credentials;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!this.config.credentialsPath) {
|
|
186
|
+
throw new Error("Service account requires credentialsPath or credentials");
|
|
187
|
+
}
|
|
188
|
+
const content = await fs.readFile(this.config.credentialsPath, "utf-8");
|
|
189
|
+
this.credentials = JSON.parse(content);
|
|
190
|
+
}
|
|
191
|
+
createJwt() {
|
|
192
|
+
if (!this.credentials) {
|
|
193
|
+
throw new Error("Credentials not loaded");
|
|
194
|
+
}
|
|
195
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
196
|
+
const header = {
|
|
197
|
+
alg: "RS256",
|
|
198
|
+
typ: "JWT"
|
|
199
|
+
};
|
|
200
|
+
const payload = {
|
|
201
|
+
iss: this.credentials.client_email,
|
|
202
|
+
scope: SCOPE,
|
|
203
|
+
aud: TOKEN_URI,
|
|
204
|
+
iat: now,
|
|
205
|
+
exp: now + TOKEN_LIFETIME_SECONDS
|
|
206
|
+
};
|
|
207
|
+
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
|
208
|
+
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
|
209
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
210
|
+
const sign = crypto.createSign("RSA-SHA256");
|
|
211
|
+
sign.update(signatureInput);
|
|
212
|
+
const signature = sign.sign(this.credentials.private_key);
|
|
213
|
+
const encodedSignature = this.base64UrlEncode(signature);
|
|
214
|
+
return `${signatureInput}.${encodedSignature}`;
|
|
215
|
+
}
|
|
216
|
+
base64UrlEncode(input) {
|
|
217
|
+
const buffer = typeof input === "string" ? Buffer.from(input) : input;
|
|
218
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
219
|
+
}
|
|
220
|
+
async exchangeJwtForToken(jwt) {
|
|
221
|
+
const response = await fetch(TOKEN_URI, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
225
|
+
},
|
|
226
|
+
body: new URLSearchParams({
|
|
227
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
228
|
+
assertion: jwt
|
|
229
|
+
})
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
const error = await response.text();
|
|
233
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
234
|
+
}
|
|
235
|
+
return await response.json();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/auth/user-auth.ts
|
|
240
|
+
import * as crypto2 from "crypto";
|
|
241
|
+
import * as fs2 from "fs/promises";
|
|
242
|
+
import * as http from "http";
|
|
243
|
+
import * as os from "os";
|
|
244
|
+
import * as path from "path";
|
|
245
|
+
import { exec } from "child_process";
|
|
246
|
+
var CONFIG_DIR = path.join(os.homedir(), ".sheets");
|
|
247
|
+
var TOKENS_FILE = path.join(CONFIG_DIR, "tokens.json");
|
|
248
|
+
function generatePKCE() {
|
|
249
|
+
const codeVerifier = crypto2.randomBytes(32).toString("base64url");
|
|
250
|
+
const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
251
|
+
return { codeVerifier, codeChallenge };
|
|
252
|
+
}
|
|
253
|
+
function openBrowser(url) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const platform = process.platform;
|
|
256
|
+
let command;
|
|
257
|
+
if (platform === "darwin") {
|
|
258
|
+
command = `open "${url}"`;
|
|
259
|
+
} else if (platform === "win32") {
|
|
260
|
+
command = `start "" "${url}"`;
|
|
261
|
+
} else {
|
|
262
|
+
command = `xdg-open "${url}"`;
|
|
263
|
+
}
|
|
264
|
+
exec(command, (error) => {
|
|
265
|
+
if (error) {
|
|
266
|
+
reject(new Error(`Failed to open browser: ${error.message}`));
|
|
267
|
+
} else {
|
|
268
|
+
resolve();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function startCallbackServer() {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
let timeoutId;
|
|
276
|
+
const server = http.createServer((req, res) => {
|
|
277
|
+
const url = new URL(req.url || "", `http://localhost:${OAUTH_CALLBACK_PORT}`);
|
|
278
|
+
if (url.pathname === "/callback") {
|
|
279
|
+
const code = url.searchParams.get("code");
|
|
280
|
+
const error = url.searchParams.get("error");
|
|
281
|
+
if (error) {
|
|
282
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
283
|
+
res.end("<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>");
|
|
284
|
+
clearTimeout(timeoutId);
|
|
285
|
+
server.close();
|
|
286
|
+
reject(new Error(`Authorization error: ${error}`));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (code) {
|
|
290
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
291
|
+
res.end("<html><body><h1>Authorization Successful</h1><p>You can close this window.</p></body></html>");
|
|
292
|
+
clearTimeout(timeoutId);
|
|
293
|
+
server.close();
|
|
294
|
+
resolve(code);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
298
|
+
res.end("<html><body><h1>Missing Code</h1></body></html>");
|
|
299
|
+
} else {
|
|
300
|
+
res.writeHead(404);
|
|
301
|
+
res.end();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
server.on("error", (err) => {
|
|
305
|
+
clearTimeout(timeoutId);
|
|
306
|
+
reject(new Error(`Callback server error: ${err.message}`));
|
|
307
|
+
});
|
|
308
|
+
server.listen(OAUTH_CALLBACK_PORT, () => {
|
|
309
|
+
});
|
|
310
|
+
timeoutId = setTimeout(() => {
|
|
311
|
+
server.close();
|
|
312
|
+
reject(new Error("Authorization timeout"));
|
|
313
|
+
}, 5 * 60 * 1e3);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function getAuthorizationUrl(codeChallenge, clientId) {
|
|
317
|
+
const params = new URLSearchParams({
|
|
318
|
+
client_id: clientId || OAUTH_CLIENT_ID,
|
|
319
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
320
|
+
response_type: "code",
|
|
321
|
+
scope: OAUTH_SCOPES.join(" "),
|
|
322
|
+
code_challenge: codeChallenge,
|
|
323
|
+
code_challenge_method: "S256",
|
|
324
|
+
access_type: "offline",
|
|
325
|
+
prompt: "consent"
|
|
326
|
+
});
|
|
327
|
+
return `${OAUTH_AUTH_URL}?${params.toString()}`;
|
|
328
|
+
}
|
|
329
|
+
async function exchangeCodeForTokens(code, codeVerifier, credentials) {
|
|
330
|
+
const response = await fetch(OAUTH_TOKEN_URL, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
333
|
+
body: new URLSearchParams({
|
|
334
|
+
client_id: credentials?.clientId || OAUTH_CLIENT_ID,
|
|
335
|
+
client_secret: credentials?.clientSecret || OAUTH_CLIENT_SECRET,
|
|
336
|
+
code,
|
|
337
|
+
code_verifier: codeVerifier,
|
|
338
|
+
grant_type: "authorization_code",
|
|
339
|
+
redirect_uri: OAUTH_REDIRECT_URI
|
|
340
|
+
})
|
|
341
|
+
});
|
|
342
|
+
if (!response.ok) {
|
|
343
|
+
const error = await response.text();
|
|
344
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
345
|
+
}
|
|
346
|
+
return await response.json();
|
|
347
|
+
}
|
|
348
|
+
async function refreshAccessToken(refreshToken) {
|
|
349
|
+
const response = await fetch(OAUTH_TOKEN_URL, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
352
|
+
body: new URLSearchParams({
|
|
353
|
+
client_id: OAUTH_CLIENT_ID,
|
|
354
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
355
|
+
refresh_token: refreshToken,
|
|
356
|
+
grant_type: "refresh_token"
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
const error = await response.text();
|
|
361
|
+
throw new Error(`Token refresh failed: ${error}`);
|
|
362
|
+
}
|
|
363
|
+
return await response.json();
|
|
364
|
+
}
|
|
365
|
+
async function getUserInfo(accessToken) {
|
|
366
|
+
const response = await fetch(OAUTH_USERINFO_URL, {
|
|
367
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
368
|
+
});
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
throw new Error("Failed to get user info");
|
|
371
|
+
}
|
|
372
|
+
return await response.json();
|
|
373
|
+
}
|
|
374
|
+
async function ensureConfigDir() {
|
|
375
|
+
try {
|
|
376
|
+
await fs2.mkdir(CONFIG_DIR, { recursive: true });
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function loadStoredTokens() {
|
|
381
|
+
try {
|
|
382
|
+
const content = await fs2.readFile(TOKENS_FILE, "utf-8");
|
|
383
|
+
return JSON.parse(content);
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function saveTokens(tokens) {
|
|
389
|
+
await ensureConfigDir();
|
|
390
|
+
await fs2.writeFile(TOKENS_FILE, JSON.stringify(tokens, null, 2));
|
|
391
|
+
}
|
|
392
|
+
async function deleteTokens() {
|
|
393
|
+
try {
|
|
394
|
+
await fs2.unlink(TOKENS_FILE);
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function login(credentials) {
|
|
399
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
400
|
+
const authUrl = getAuthorizationUrl(codeChallenge, credentials?.clientId);
|
|
401
|
+
console.log("Opening browser for Google login...");
|
|
402
|
+
const codePromise = startCallbackServer();
|
|
403
|
+
await openBrowser(authUrl);
|
|
404
|
+
console.log("Waiting for authorization...");
|
|
405
|
+
const code = await codePromise;
|
|
406
|
+
console.log("Exchanging code for tokens...");
|
|
407
|
+
const tokenResponse = await exchangeCodeForTokens(code, codeVerifier, credentials);
|
|
408
|
+
const userInfo = await getUserInfo(tokenResponse.access_token);
|
|
409
|
+
const tokens = {
|
|
410
|
+
accessToken: tokenResponse.access_token,
|
|
411
|
+
refreshToken: tokenResponse.refresh_token || "",
|
|
412
|
+
expiresAt: Date.now() + tokenResponse.expires_in * 1e3,
|
|
413
|
+
email: userInfo.email
|
|
414
|
+
};
|
|
415
|
+
await saveTokens(tokens);
|
|
416
|
+
return tokens;
|
|
417
|
+
}
|
|
418
|
+
var UserAuth = class {
|
|
419
|
+
tokens = null;
|
|
420
|
+
async getAccessToken() {
|
|
421
|
+
if (!this.tokens) {
|
|
422
|
+
this.tokens = await loadStoredTokens();
|
|
423
|
+
}
|
|
424
|
+
if (!this.tokens) {
|
|
425
|
+
throw new Error('Not logged in. Run "sheets login" first.');
|
|
426
|
+
}
|
|
427
|
+
if (Date.now() >= this.tokens.expiresAt - 6e4) {
|
|
428
|
+
if (!this.tokens.refreshToken) {
|
|
429
|
+
throw new Error('Token expired and no refresh token. Run "sheets login" again.');
|
|
430
|
+
}
|
|
431
|
+
const tokenResponse = await refreshAccessToken(this.tokens.refreshToken);
|
|
432
|
+
this.tokens.accessToken = tokenResponse.access_token;
|
|
433
|
+
this.tokens.expiresAt = Date.now() + tokenResponse.expires_in * 1e3;
|
|
434
|
+
if (tokenResponse.refresh_token) {
|
|
435
|
+
this.tokens.refreshToken = tokenResponse.refresh_token;
|
|
436
|
+
}
|
|
437
|
+
await saveTokens(this.tokens);
|
|
438
|
+
}
|
|
439
|
+
return this.tokens.accessToken;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// src/auth/index.ts
|
|
444
|
+
function createAuthProvider(config) {
|
|
445
|
+
switch (config.type) {
|
|
446
|
+
case "oauth":
|
|
447
|
+
return new OAuthAuth(config);
|
|
448
|
+
case "service-account":
|
|
449
|
+
return new ServiceAccountAuth(config);
|
|
450
|
+
case "user":
|
|
451
|
+
return new UserAuth();
|
|
452
|
+
default:
|
|
453
|
+
throw new Error(`Unknown auth type: ${config.type}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/api/index.ts
|
|
458
|
+
function columnLetterToNumber(letters) {
|
|
459
|
+
let result = 0;
|
|
460
|
+
for (let i = 0; i < letters.length; i++) {
|
|
461
|
+
result = result * 26 + (letters.charCodeAt(i) - 64);
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
function columnNumberToLetter(num) {
|
|
466
|
+
let result = "";
|
|
467
|
+
while (num > 0) {
|
|
468
|
+
const remainder = (num - 1) % 26;
|
|
469
|
+
result = String.fromCharCode(65 + remainder) + result;
|
|
470
|
+
num = Math.floor((num - 1) / 26);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
function parseA1Range(range) {
|
|
475
|
+
const cellRef = range.includes("!") ? range.split("!")[1] : range;
|
|
476
|
+
const firstCell = cellRef.split(":")[0];
|
|
477
|
+
const match = firstCell.match(/^([A-Z]+)(\d+)$/i);
|
|
478
|
+
if (!match) {
|
|
479
|
+
return { startRow: 1, startCol: 1 };
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
startCol: columnLetterToNumber(match[1].toUpperCase()),
|
|
483
|
+
startRow: parseInt(match[2], 10)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
var SheetsClient = class {
|
|
487
|
+
http;
|
|
488
|
+
constructor(options) {
|
|
489
|
+
const authProvider = createAuthProvider(options.auth);
|
|
490
|
+
this.http = new HttpClient({
|
|
491
|
+
getAccessToken: () => authProvider.getAccessToken()
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Get a spreadsheet by ID
|
|
496
|
+
*/
|
|
497
|
+
async getSpreadsheet(spreadsheetId) {
|
|
498
|
+
return this.http.request(`/spreadsheets/${spreadsheetId}`);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get list of sheets in a spreadsheet
|
|
502
|
+
*/
|
|
503
|
+
async getSheets(spreadsheetId) {
|
|
504
|
+
const spreadsheet = await this.getSpreadsheet(spreadsheetId);
|
|
505
|
+
return spreadsheet.sheets.map((sheet) => sheet.properties);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Read cell values from a range
|
|
509
|
+
*/
|
|
510
|
+
async getValues(spreadsheetId, range, options) {
|
|
511
|
+
const params = {};
|
|
512
|
+
if (options?.valueRenderOption) {
|
|
513
|
+
params.valueRenderOption = options.valueRenderOption;
|
|
514
|
+
}
|
|
515
|
+
if (options?.dateTimeRenderOption) {
|
|
516
|
+
params.dateTimeRenderOption = options.dateTimeRenderOption;
|
|
517
|
+
}
|
|
518
|
+
if (options?.majorDimension) {
|
|
519
|
+
params.majorDimension = options.majorDimension;
|
|
520
|
+
}
|
|
521
|
+
const encodedRange = encodeURIComponent(range);
|
|
522
|
+
const response = await this.http.request(
|
|
523
|
+
`/spreadsheets/${spreadsheetId}/values/${encodedRange}`,
|
|
524
|
+
{ params }
|
|
525
|
+
);
|
|
526
|
+
return this.normalizeValueRange(response);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Read cell formulas from a range
|
|
530
|
+
*/
|
|
531
|
+
async getFormulas(spreadsheetId, range) {
|
|
532
|
+
return this.getValues(spreadsheetId, range, { valueRenderOption: "FORMULA" });
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Read multiple ranges at once
|
|
536
|
+
*/
|
|
537
|
+
async batchGetValues(spreadsheetId, ranges, options) {
|
|
538
|
+
const params = {
|
|
539
|
+
ranges: ranges.join(",")
|
|
540
|
+
};
|
|
541
|
+
if (options?.valueRenderOption) {
|
|
542
|
+
params.valueRenderOption = options.valueRenderOption;
|
|
543
|
+
}
|
|
544
|
+
if (options?.dateTimeRenderOption) {
|
|
545
|
+
params.dateTimeRenderOption = options.dateTimeRenderOption;
|
|
546
|
+
}
|
|
547
|
+
if (options?.majorDimension) {
|
|
548
|
+
params.majorDimension = options.majorDimension;
|
|
549
|
+
}
|
|
550
|
+
const response = await this.http.request(
|
|
551
|
+
`/spreadsheets/${spreadsheetId}/values:batchGet`,
|
|
552
|
+
{ params }
|
|
553
|
+
);
|
|
554
|
+
return {
|
|
555
|
+
spreadsheetId: response.spreadsheetId,
|
|
556
|
+
valueRanges: (response.valueRanges || []).map((vr) => this.normalizeValueRange(vr))
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Clear values from a single range
|
|
561
|
+
*/
|
|
562
|
+
async clearValues(spreadsheetId, range) {
|
|
563
|
+
const encodedRange = encodeURIComponent(range);
|
|
564
|
+
return this.http.request(
|
|
565
|
+
`/spreadsheets/${spreadsheetId}/values/${encodedRange}:clear`,
|
|
566
|
+
{ method: "POST" }
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Clear values from multiple ranges
|
|
571
|
+
*/
|
|
572
|
+
async batchClearValues(spreadsheetId, ranges) {
|
|
573
|
+
return this.http.request(
|
|
574
|
+
`/spreadsheets/${spreadsheetId}/values:batchClear`,
|
|
575
|
+
{ method: "POST", body: { ranges } }
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Write values to a range (or starting cell)
|
|
580
|
+
* Range can be "A1" or "A1:D10" - data array determines actual extent
|
|
581
|
+
*/
|
|
582
|
+
async updateValues(spreadsheetId, range, values, options) {
|
|
583
|
+
const params = {
|
|
584
|
+
valueInputOption: options?.valueInputOption || "USER_ENTERED"
|
|
585
|
+
};
|
|
586
|
+
if (options?.includeValuesInResponse) {
|
|
587
|
+
params.includeValuesInResponse = "true";
|
|
588
|
+
}
|
|
589
|
+
if (options?.responseValueRenderOption) {
|
|
590
|
+
params.responseValueRenderOption = options.responseValueRenderOption;
|
|
591
|
+
}
|
|
592
|
+
if (options?.responseDateTimeRenderOption) {
|
|
593
|
+
params.responseDateTimeRenderOption = options.responseDateTimeRenderOption;
|
|
594
|
+
}
|
|
595
|
+
const encodedRange = encodeURIComponent(range);
|
|
596
|
+
return this.http.request(
|
|
597
|
+
`/spreadsheets/${spreadsheetId}/values/${encodedRange}`,
|
|
598
|
+
{
|
|
599
|
+
method: "PUT",
|
|
600
|
+
params,
|
|
601
|
+
body: {
|
|
602
|
+
majorDimension: options?.majorDimension || "ROWS",
|
|
603
|
+
values
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Write to multiple ranges in one request
|
|
610
|
+
*/
|
|
611
|
+
async batchUpdateValues(spreadsheetId, data, options) {
|
|
612
|
+
return this.http.request(
|
|
613
|
+
`/spreadsheets/${spreadsheetId}/values:batchUpdate`,
|
|
614
|
+
{
|
|
615
|
+
method: "POST",
|
|
616
|
+
body: {
|
|
617
|
+
valueInputOption: options?.valueInputOption || "USER_ENTERED",
|
|
618
|
+
data: data.map((d) => ({
|
|
619
|
+
range: d.range,
|
|
620
|
+
majorDimension: options?.majorDimension || "ROWS",
|
|
621
|
+
values: d.values
|
|
622
|
+
})),
|
|
623
|
+
includeValuesInResponse: options?.includeValuesInResponse || false,
|
|
624
|
+
responseValueRenderOption: options?.responseValueRenderOption,
|
|
625
|
+
responseDateTimeRenderOption: options?.responseDateTimeRenderOption
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Append rows after the last row of detected table
|
|
632
|
+
*/
|
|
633
|
+
async appendValues(spreadsheetId, range, values, options) {
|
|
634
|
+
const params = {
|
|
635
|
+
valueInputOption: options?.valueInputOption || "USER_ENTERED"
|
|
636
|
+
};
|
|
637
|
+
if (options?.insertDataOption) {
|
|
638
|
+
params.insertDataOption = options.insertDataOption;
|
|
639
|
+
}
|
|
640
|
+
if (options?.includeValuesInResponse) {
|
|
641
|
+
params.includeValuesInResponse = "true";
|
|
642
|
+
}
|
|
643
|
+
if (options?.responseValueRenderOption) {
|
|
644
|
+
params.responseValueRenderOption = options.responseValueRenderOption;
|
|
645
|
+
}
|
|
646
|
+
if (options?.responseDateTimeRenderOption) {
|
|
647
|
+
params.responseDateTimeRenderOption = options.responseDateTimeRenderOption;
|
|
648
|
+
}
|
|
649
|
+
const encodedRange = encodeURIComponent(range);
|
|
650
|
+
return this.http.request(
|
|
651
|
+
`/spreadsheets/${spreadsheetId}/values/${encodedRange}:append`,
|
|
652
|
+
{
|
|
653
|
+
method: "POST",
|
|
654
|
+
params,
|
|
655
|
+
body: {
|
|
656
|
+
majorDimension: options?.majorDimension || "ROWS",
|
|
657
|
+
values
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Search for values matching a query across sheets
|
|
664
|
+
*/
|
|
665
|
+
async searchValues(spreadsheetId, query, options) {
|
|
666
|
+
const caseSensitive = options?.caseSensitive ?? false;
|
|
667
|
+
const exactMatch = options?.exactMatch ?? false;
|
|
668
|
+
const useRegex = options?.regex ?? false;
|
|
669
|
+
const limit = options?.limit;
|
|
670
|
+
const matchType = useRegex ? "regex" : exactMatch ? "exact" : "contains";
|
|
671
|
+
let matcher;
|
|
672
|
+
if (useRegex) {
|
|
673
|
+
const flags = caseSensitive ? "" : "i";
|
|
674
|
+
const regex = new RegExp(query, flags);
|
|
675
|
+
matcher = (cellValue) => regex.test(cellValue);
|
|
676
|
+
} else if (exactMatch) {
|
|
677
|
+
if (caseSensitive) {
|
|
678
|
+
matcher = (cellValue) => cellValue === query;
|
|
679
|
+
} else {
|
|
680
|
+
const lowerQuery = query.toLowerCase();
|
|
681
|
+
matcher = (cellValue) => cellValue.toLowerCase() === lowerQuery;
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
if (caseSensitive) {
|
|
685
|
+
matcher = (cellValue) => cellValue.includes(query);
|
|
686
|
+
} else {
|
|
687
|
+
const lowerQuery = query.toLowerCase();
|
|
688
|
+
matcher = (cellValue) => cellValue.toLowerCase().includes(lowerQuery);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const matches = [];
|
|
692
|
+
const allSheets = await this.getSheets(spreadsheetId);
|
|
693
|
+
let sheetsToSearch;
|
|
694
|
+
if (options?.sheetIndex !== void 0) {
|
|
695
|
+
const sheet = allSheets.find((s) => s.index === options.sheetIndex);
|
|
696
|
+
if (!sheet) {
|
|
697
|
+
throw new Error(`Sheet index ${options.sheetIndex} not found.`);
|
|
698
|
+
}
|
|
699
|
+
sheetsToSearch = [sheet];
|
|
700
|
+
} else if (options?.gid !== void 0) {
|
|
701
|
+
const sheet = allSheets.find((s) => s.sheetId === options.gid);
|
|
702
|
+
if (!sheet) {
|
|
703
|
+
throw new Error(`Sheet with gid ${options.gid} not found.`);
|
|
704
|
+
}
|
|
705
|
+
sheetsToSearch = [sheet];
|
|
706
|
+
} else if (options?.range && options.range.includes("!")) {
|
|
707
|
+
sheetsToSearch = [];
|
|
708
|
+
} else {
|
|
709
|
+
sheetsToSearch = allSheets.filter((s) => !s.hidden);
|
|
710
|
+
}
|
|
711
|
+
if (options?.range && options.range.includes("!")) {
|
|
712
|
+
const valueRange = await this.getValues(spreadsheetId, options.range);
|
|
713
|
+
const sheetName = options.range.split("!")[0].replace(/^'|'$/g, "").replace(/''/g, "'");
|
|
714
|
+
const sheet = allSheets.find((s) => s.title === sheetName);
|
|
715
|
+
const { startRow, startCol } = parseA1Range(options.range);
|
|
716
|
+
this.collectMatches(
|
|
717
|
+
valueRange,
|
|
718
|
+
sheetName,
|
|
719
|
+
sheet?.sheetId ?? 0,
|
|
720
|
+
startRow,
|
|
721
|
+
startCol,
|
|
722
|
+
matcher,
|
|
723
|
+
matches,
|
|
724
|
+
limit
|
|
725
|
+
);
|
|
726
|
+
} else {
|
|
727
|
+
for (const sheet of sheetsToSearch) {
|
|
728
|
+
if (limit && matches.length >= limit) break;
|
|
729
|
+
const escapedTitle = sheet.title.replace(/'/g, "''");
|
|
730
|
+
const range = options?.range ? `'${escapedTitle}'!${options.range}` : `'${escapedTitle}'`;
|
|
731
|
+
try {
|
|
732
|
+
const valueRange = await this.getValues(spreadsheetId, range);
|
|
733
|
+
const { startRow, startCol } = parseA1Range(valueRange.range);
|
|
734
|
+
this.collectMatches(
|
|
735
|
+
valueRange,
|
|
736
|
+
sheet.title,
|
|
737
|
+
sheet.sheetId,
|
|
738
|
+
startRow,
|
|
739
|
+
startCol,
|
|
740
|
+
matcher,
|
|
741
|
+
matches,
|
|
742
|
+
limit
|
|
743
|
+
);
|
|
744
|
+
} catch {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
query,
|
|
751
|
+
matchType,
|
|
752
|
+
caseSensitive,
|
|
753
|
+
totalMatches: matches.length,
|
|
754
|
+
matches
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
collectMatches(valueRange, sheetName, sheetId, startRow, startCol, matcher, matches, limit) {
|
|
758
|
+
for (let rowIndex = 0; rowIndex < valueRange.values.length; rowIndex++) {
|
|
759
|
+
if (limit && matches.length >= limit) return;
|
|
760
|
+
const row = valueRange.values[rowIndex];
|
|
761
|
+
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
|
762
|
+
if (limit && matches.length >= limit) return;
|
|
763
|
+
const cell = row[colIndex];
|
|
764
|
+
const cellValue = cell.value;
|
|
765
|
+
if (cellValue == null) continue;
|
|
766
|
+
const stringValue = String(cellValue);
|
|
767
|
+
if (matcher(stringValue)) {
|
|
768
|
+
const actualRow = startRow + rowIndex;
|
|
769
|
+
const actualCol = startCol + colIndex;
|
|
770
|
+
matches.push({
|
|
771
|
+
sheet: sheetName,
|
|
772
|
+
sheetId,
|
|
773
|
+
address: columnNumberToLetter(actualCol) + actualRow,
|
|
774
|
+
row: actualRow,
|
|
775
|
+
column: actualCol,
|
|
776
|
+
value: cellValue
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
normalizeValueRange(raw) {
|
|
783
|
+
const values = (raw.values || []).map(
|
|
784
|
+
(row) => row.map((cell) => ({
|
|
785
|
+
value: cell
|
|
786
|
+
}))
|
|
787
|
+
);
|
|
788
|
+
return {
|
|
789
|
+
range: raw.range,
|
|
790
|
+
majorDimension: raw.majorDimension || "ROWS",
|
|
791
|
+
values
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
function createClient(options) {
|
|
796
|
+
return new SheetsClient(options);
|
|
797
|
+
}
|
|
798
|
+
export {
|
|
799
|
+
HttpClient,
|
|
800
|
+
OAuthAuth,
|
|
801
|
+
ServiceAccountAuth,
|
|
802
|
+
SheetsClient,
|
|
803
|
+
SheetsError,
|
|
804
|
+
UserAuth,
|
|
805
|
+
createAuthProvider,
|
|
806
|
+
createClient,
|
|
807
|
+
deleteTokens,
|
|
808
|
+
loadStoredTokens,
|
|
809
|
+
login
|
|
810
|
+
};
|
|
12
811
|
//# sourceMappingURL=index.js.map
|