@mcpher/gas-fakes 1.2.32 → 2.0.1

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.
Files changed (49) hide show
  1. package/.gcloudignore +24 -0
  2. package/README.md +5 -5
  3. package/appsscript.json +101 -0
  4. package/exgcp.sh +4 -4
  5. package/package.json +5 -6
  6. package/src/cli/app.js +9 -1
  7. package/src/cli/lib-manager.js +0 -1
  8. package/src/cli/setup.js +330 -228
  9. package/src/services/advcalendar/clapis.js +4 -2
  10. package/src/services/advdocs/docapis.js +4 -3
  11. package/src/services/advdrive/drapis.js +8 -5
  12. package/src/services/advforms/formsapis.js +4 -3
  13. package/src/services/advgmail/gmailapis.js +4 -3
  14. package/src/services/advsheets/shapis.js +5 -10
  15. package/src/services/advslides/slapis.js +4 -3
  16. package/src/services/driveapp/fakedriveapp.js +10 -2
  17. package/src/services/logger/fakelogger.js +6 -3
  18. package/src/services/scriptapp/app.js +16 -11
  19. package/src/services/scriptapp/behavior.js +132 -107
  20. package/src/services/session/fakesession.js +24 -9
  21. package/src/services/slidesapp/fakepresentation.js +19 -8
  22. package/src/services/slidesapp/fakeslide.js +45 -20
  23. package/src/services/spreadsheetapp/fakesheet.js +9 -7
  24. package/src/services/stores/fakestores.js +20 -18
  25. package/src/services/stores/gasflex.js +13 -14
  26. package/src/services/urlfetchapp/app.js +3 -3
  27. package/src/support/auth.js +227 -55
  28. package/src/support/slogger.js +42 -0
  29. package/src/support/sxauth.js +42 -39
  30. package/src/support/sxcalendar.js +9 -43
  31. package/src/support/sxdocs.js +6 -42
  32. package/src/support/sxdrive.js +19 -76
  33. package/src/support/sxfetch.js +36 -30
  34. package/src/support/sxforms.js +9 -40
  35. package/src/support/sxgmail.js +9 -37
  36. package/src/support/sxretry.js +79 -0
  37. package/src/support/sxsheets.js +6 -37
  38. package/src/support/sxslides.js +5 -36
  39. package/src/support/sxtoken.js +15 -0
  40. package/src/support/syncit.js +27 -10
  41. package/src/support/workersync/sxfunctions.js +2 -0
  42. package/src/support/workersync/synclogger.js +22 -5
  43. package/src/support/workersync/worker.js +8 -11
  44. package/README.RU.md +0 -373
  45. package/env.setup.template +0 -16
  46. package/run.js +0 -35
  47. package/setup.js +0 -689
  48. package/src/Code.js +0 -3
  49. package/src/appsscript.json +0 -5
@@ -1,20 +1,28 @@
1
- import { GoogleAuth } from "google-auth-library";
1
+ import { GoogleAuth, JWT, Impersonated, OAuth2Client } from "google-auth-library";
2
2
  import is from "@sindresorhus/is";
3
3
  import { createHash } from "node:crypto";
4
- import { syncLog } from "./workersync/synclogger.js";
4
+ import { syncLog, syncError } from "./workersync/synclogger.js";
5
+ import fs from "node:fs";
5
6
 
6
7
  const _authScopes = new Set([]);
7
8
 
8
9
  // all this stuff gets populated by the initial synced fxInit
9
10
  let _auth = null;
10
11
  let _projectId = null;
11
- let _tokenInfo = null;
12
12
  let _accessToken = null;
13
13
  let _tokenExpiresAt = null;
14
14
  let _manifest = null;
15
15
  let _clasp = null;
16
+ let _activeUser = null;
17
+ let _effectiveUser = null;
18
+ let _tokenScopes = null;
19
+
16
20
 
17
21
  let _settings = null;
22
+ const getActiveUser = () => _activeUser
23
+ const getEffectiveUser = () => _effectiveUser
24
+ const setActiveUser = (user) => (_activeUser = user);
25
+ const setEffectiveUser = (user) => (_effectiveUser = user)
18
26
  const setManifest = (manifest) => (_manifest = manifest);
19
27
  const setClasp = (clasp) => (_clasp = clasp);
20
28
  const getManifest = () => _manifest;
@@ -27,75 +35,227 @@ const setAccessToken = (accessToken) => (_accessToken = accessToken);
27
35
  const setSettings = (settings) => (_settings = settings);
28
36
  const getCachePath = () => getSettings().cache;
29
37
  const getPropertiesPath = () => getSettings().properties;
30
- const setTokenExpiresAt = (expiresAt) => (_tokenExpiresAt = expiresAt);
31
- const setTokenInfo = (tokenInfo) => {
32
- _tokenInfo = tokenInfo;
33
- // set expiry time with a 60 second buffer
34
- if (tokenInfo && tokenInfo.expires_in) {
35
- setTokenExpiresAt(Date.now() + (tokenInfo.expires_in - 60) * 1000);
36
- } else {
37
- // no expiry info, so we'll have to fetch a new one next time
38
- setTokenExpiresAt(0);
39
- }
40
- };
41
- const getTokenInfo = () => {
42
- if (!_tokenInfo) throw `token info isnt set yet`;
43
- return _tokenInfo;
44
- };
38
+
45
39
 
46
40
  const getTimeZone = () => getManifest().timeZone;
47
- const getUserId = () => getTokenInfo().sub;
48
- const getTokenScopes = () => getTokenInfo().scope;
41
+ const getUserId = () => getEffectiveUser().id;
42
+ const setTokenScopes = (scopes) => (_tokenScopes = Array.isArray(scopes) ? scopes.join(" ") : scopes);
43
+ const getTokenScopes = () => {
44
+ if (_tokenScopes) return _tokenScopes;
45
+ // If not cached, we have to return the promise (which might break synchronous callers like ScriptApp)
46
+ return getAccessTokenInfo().then(info => {
47
+ _tokenScopes = info.tokenInfo.scope;
48
+ return _tokenScopes;
49
+ });
50
+ };
49
51
  const getHashedUserId = () =>
50
52
  createHash("md5")
51
53
  .update(getUserId() + "hud")
52
54
  .digest()
53
55
  .toString("hex");
54
56
 
55
- const getAccessToken = () => _accessToken;
57
+ const _getTokenInfo = async (client) => {
58
+ const tokenResponse = await client.getAccessToken();
59
+ const token = tokenResponse.token;
60
+ const tokenInfo = await client.getTokenInfo(token);
61
+ return {
62
+ tokenInfo,
63
+ token
64
+ }
65
+ }
66
+ // this is the effective user's token info
67
+ // - In DWD: The impersonated user (the one we are acting as)
68
+ // - In ADC: The actual signed-in user
69
+ const getAccessTokenInfo = async () => {
70
+ if (!hasAuth()) throw `auth isnt set yet`;
71
+ return _getTokenInfo(_authClient);
72
+ }
73
+
74
+ // this is the active identity's token info
75
+ // - In DWD: The service account itself
76
+ // - In ADC: Same as the effective user
77
+ const getSourceAccessTokenInfo = async () => {
78
+ if (!_sourceClient) throw `source auth isnt set yet`;
79
+ return _getTokenInfo(_sourceClient);
80
+ }
81
+
82
+
83
+ // this is the access token allowed to do the work - the one for the effective user
84
+ // - In DWD: The impersonated user
85
+ // - In ADC: The actual signed-in user
86
+ const getAccessToken = async () => {
87
+ if (!hasAuth()) throw `auth isnt set yet`;
88
+ return (await getAccessTokenInfo()).token;
89
+ }
90
+
56
91
 
57
92
  const isTokenExpired = () =>
58
93
  !_accessToken || !_tokenExpiresAt || Date.now() >= _tokenExpiresAt;
59
- /**
60
- * we'll be using adc credentials so no need for any special auth here
61
- * the idea here is to keep addign scopes to any auth so we have them all
62
- * @param {string[]} [scopes=[]] the required scopes will be added to existing scopes already asked for
63
- * @param {string} [keyFile=null]
64
- * @param {boolean} [mcpLoading=false] When the MCP server is loading, this value is true. By this, the invalid values can be hidden while the MCP server is loading. This is important for using Google Antigravity.
65
- * @returns {GoogleAuth.auth}
66
- */
94
+
95
+ // the auth client is the one that has the scopes to do the work
96
+ // under adc, this is the source client
97
+ // under service account, this is the impersonated client
67
98
  let _authClient = null;
99
+ let _sourceClient = null;
68
100
  const getAuthClient = () => _authClient;
69
- const setAuth = async (scopes = [], keyFile = null, mcpLoading = false) => {
70
- const hasCurrentAuth = hasAuth() && scopes.every((s) => _authScopes.has(s));
101
+ const getSourceClient = () => _sourceClient;
71
102
 
72
- if (!hasCurrentAuth) {
73
- if (!mcpLoading) {
74
- syncLog(`...initializing auth and discovering project ID`);
75
- }
76
103
 
77
- // 1. Create the GoogleAuth manager instance (this instance has getProjectId)
78
- _auth = new GoogleAuth({ scopes });
104
+ // We'll support ADC, or workload identity
105
+ // and the service account must be allowed to impersonate the user
106
+ const setAuth = async (scopes = [], mcpLoading = false) => {
107
+ const mayLog = mcpLoading ? () => null : syncLog;
108
+ mayLog(`...initializing auth`)
109
+
110
+ try {
111
+ _auth = new GoogleAuth()
112
+ _projectId = await _auth.getProjectId()
113
+ mayLog(`...discovered project ID: ${_projectId}`)
114
+
115
+ // steering for auth type
116
+ // 1. if AUTH_TYPE is DWD, use DWD
117
+ // 2. if AUTH_TYPE is ADC, use ADC
118
+ // 3. if AUTH_TYPE is not set, use DWD if saName is present, else ADC
119
+ const saName = process.env.GOOGLE_SERVICE_ACCOUNT_NAME
120
+ const authType = process.env.AUTH_TYPE?.toLowerCase() || 'dwd'
121
+ const useDwd = authType === 'dwd' || (authType !== 'adc' && saName)
122
+
123
+ if (!useDwd) {
124
+ mayLog(`...using ADC`)
125
+ _authClient = await _auth.getClient({
126
+ scopes
127
+ })
128
+ _sourceClient = _authClient
129
+ } else {
130
+ mayLog(`...using service account: ${saName}`)
131
+ const targetPrincipal = `${saName}@${_projectId}.iam.gserviceaccount.com`
132
+ mayLog(`...attempting to use service account: ${targetPrincipal}`)
133
+
134
+ /// _sourceClient is the identity of the person/thing running the code
135
+ _sourceClient = await _auth.getClient()
136
+
137
+ // now to get who the real user is
138
+ const { tokenInfo: userInfo } = await getSourceAccessTokenInfo()
139
+ mayLog(`...user verified as: ${userInfo.email}`);
140
+
141
+ // DWD Subject priority:
142
+ // 1. GOOGLE_WORKSPACE_SUBJECT
143
+ // 2. Caller identity (from getSourceAccessTokenInfo)
144
+ const saEmail = targetPrincipal
145
+ const userEmail = process.env.GOOGLE_WORKSPACE_SUBJECT || userInfo.email
146
+
147
+ const dwdClient = new OAuth2Client()
148
+ dwdClient._token = null
149
+ dwdClient._expiresAt = 0
150
+
151
+ dwdClient.getAccessToken = async function () {
152
+ if (this._token && Date.now() < this._expiresAt - 60000) {
153
+ return { token: this._token }
154
+ }
79
155
 
80
- // 2. Use the manager to get the authenticated client (this is passed to API methods)
81
- _authClient = await _auth.getClient();
156
+ const iat = Math.floor(Date.now() / 1000)
157
+ const exp = iat + 3600
158
+ const payload = {
159
+ iss: saEmail,
160
+ sub: userEmail,
161
+ aud: "https://oauth2.googleapis.com/token",
162
+ iat,
163
+ exp,
164
+ scope: scopes.join(' ')
165
+ }
82
166
 
83
- // 3. Use the manager to reliably get the project ID
84
- _projectId = await _auth.getProjectId();
167
+ //mayLog(`[Auth] DWD Scopes: ${payload.scope}`)
85
168
 
86
- if (!_projectId) {
87
- throw new Error(
88
- "Failed to get project ID from Application Default Credentials."
89
- );
169
+ // Sign the JWT via IAM API
170
+ // Note: The caller must have 'Service Account Token Creator' role on the target SA
171
+ const signUrl = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${saEmail}:signJwt`
172
+ const signResponse = await _sourceClient.request({
173
+ url: signUrl,
174
+ method: 'POST',
175
+ data: {
176
+ payload: JSON.stringify(payload)
177
+ }
178
+ })
179
+
180
+ const { signedJwt } = signResponse.data
181
+
182
+ // Exchange JWT for access token
183
+ const tokenUrl = "https://oauth2.googleapis.com/token"
184
+ const tokenResponse = await fetch(tokenUrl, {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
187
+ body: new URLSearchParams({
188
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
189
+ assertion: signedJwt
190
+ })
191
+ })
192
+
193
+ if (!tokenResponse.ok) {
194
+ const errorText = await tokenResponse.text()
195
+ throw new Error(`Failed to exchange JWT for token: ${errorText}`)
196
+ }
197
+
198
+ const tokenData = await tokenResponse.json()
199
+ this._token = tokenData.access_token
200
+ this._expiresAt = Date.now() + (tokenData.expires_in * 1000)
201
+ this.credentials = { access_token: this._token, expiry_date: this._expiresAt }
202
+
203
+ return { token: this._token }
204
+ }
205
+
206
+ // override request to ensure we use our token but leverage the existing transporter
207
+ const originalRequest = dwdClient.request.bind(dwdClient)
208
+ dwdClient.request = async function (options) {
209
+ // Ensure token is fresh
210
+ await this.getAccessToken()
211
+ return originalRequest(options)
212
+ }
213
+
214
+ _authClient = dwdClient
215
+ _authClient.targetPrincipal = saEmail
216
+ _authClient.invalidateToken = function () {
217
+ this._token = null
218
+ this._expiresAt = 0
219
+ this.credentials = null
220
+ }
221
+
222
+ mayLog(`...using Domain-Wide Delegation for user: ${userEmail}`)
223
+
224
+ // check we can get an access token - this will trigger the signJwt flow
225
+ const { tokenInfo } = await getAccessTokenInfo()
226
+ mayLog(`...sa (acting as user) verified as: ${tokenInfo.email}`);
90
227
  }
91
228
 
92
- if (!mcpLoading) {
93
- syncLog(`...discovered and set projectId to ${_projectId}`);
229
+
230
+ } catch (error) {
231
+ mayLog(`...auth failed - check you are logged in with 'gcloud auth login' and have enabled workload identity: ${error}`)
232
+ throw error
233
+ }
234
+ return getAuth()
235
+ }
236
+
237
+ /**
238
+ * force a token refresh on next request
239
+ */
240
+ const invalidateToken = () => {
241
+ if (hasAuth()) {
242
+ const client = getAuthClient();
243
+ if (client.invalidateToken) {
244
+ client.invalidateToken();
245
+ } else {
246
+ client.credentials = null;
94
247
  }
95
- scopes.forEach((s) => _authScopes.add(s));
96
248
  }
97
- return getAuth();
98
- };
249
+ }
250
+ /**
251
+ * we'll be using adc credentials so no need for any special auth here
252
+ * the idea here is to keep addign scopes to any auth so we have them all
253
+ * @param {string[]} [scopes=[]] the required scopes will be added to existing scopes already asked for
254
+ * @param {string} [keyFile=null]
255
+ * @param {boolean} [mcpLoading=false] When the MCP server is loading, this value is true. By this, the invalid values can be hidden while the MCP server is loading. This is important for using Google Antigravity.
256
+ * @returns {GoogleAuth.auth}
257
+ */
258
+
99
259
 
100
260
  /**
101
261
  * if we're doing a fetch on drive API we need a special header
@@ -139,17 +299,22 @@ const getProjectId = () => {
139
299
  /**
140
300
  * @returns {Boolean} checks to see if auth has bee initialized yet
141
301
  */
142
- const hasAuth = () => Boolean(_auth);
302
+ const hasAuth = () => Boolean(_authClient);
303
+
143
304
 
144
305
  /**
145
- * @returns {GoogleAuth.auth}
306
+ * @returns {import("google-auth-library").AuthClient}
146
307
  */
147
308
  const getAuth = () => {
148
309
  if (!hasAuth())
149
310
  throw new Error(`auth hasnt been intialized with setAuth yet`);
150
- return _auth;
311
+
312
+ // Simply return the client we've already prepared/patched
313
+ return getAuthClient();
151
314
  };
152
315
 
316
+
317
+
153
318
  /**
154
319
  * why is this here ?
155
320
  * because when we syncit, we import auth for each method and it needs this
@@ -192,7 +357,6 @@ export const Auth = {
192
357
  googify,
193
358
  setProjectId,
194
359
  getUserId,
195
- setTokenInfo,
196
360
  getAccessToken,
197
361
  getTokenScopes,
198
362
  getScriptId,
@@ -200,7 +364,6 @@ export const Auth = {
200
364
  setSettings,
201
365
  getCachePath,
202
366
  getPropertiesPath,
203
- getTokenInfo,
204
367
  getHashedUserId,
205
368
  setManifest,
206
369
  setClasp,
@@ -210,4 +373,13 @@ export const Auth = {
210
373
  setAccessToken,
211
374
  isTokenExpired,
212
375
  getAuthClient,
376
+ getSourceClient,
377
+ getAccessTokenInfo,
378
+ getSourceAccessTokenInfo,
379
+ setActiveUser,
380
+ getActiveUser,
381
+ setEffectiveUser,
382
+ getEffectiveUser,
383
+ invalidateToken,
384
+ setTokenScopes
213
385
  };
@@ -3,8 +3,50 @@
3
3
  * used for all internal gas-fakes logging
4
4
  */
5
5
 
6
+ import fs from 'node:fs';
7
+
6
8
  const q = process.env.QUIET || false
7
9
  export const isQuiet = q.toString().toLowerCase()=== "true";
10
+ const isCloudRun = !!process.env.K_SERVICE;
11
+
12
+ const getTimestamp = () => new Date().toISOString();
13
+
14
+ const formatRawMessage = (args) => {
15
+ return args.map(arg => {
16
+ if (arg === null) return 'null';
17
+ if (arg === undefined) return 'undefined';
18
+ if (typeof arg === 'object') {
19
+ try {
20
+ return JSON.stringify(arg);
21
+ } catch (e) {
22
+ return '[Complex Object]';
23
+ }
24
+ }
25
+ return String(arg);
26
+ }).join(' ');
27
+ };
28
+
29
+ const writeLog = (stream, severity, prefix, args) => {
30
+ const message = formatRawMessage(args);
31
+ if (isCloudRun) {
32
+ // Structured logging for Cloud Run
33
+ const entry = {
34
+ severity: severity,
35
+ message: `${prefix}${message}`,
36
+ timestamp: getTimestamp()
37
+ };
38
+ fs.writeSync(stream, JSON.stringify(entry) + '\n');
39
+ } else {
40
+ // Human-readable logging for local use
41
+ fs.writeSync(stream, `${getTimestamp()} ${prefix}${message}\n`);
42
+ }
43
+ };
44
+
45
+ // Monkey-patch console to ensure all logs (including from libraries) are timestamped and synchronized
46
+ globalThis.console.log = (...args) => writeLog(1, 'DEFAULT', '', args);
47
+ globalThis.console.error = (...args) => writeLog(2, 'ERROR', '[Error] ', args);
48
+ globalThis.console.warn = (...args) => writeLog(1, 'WARNING', '[Warn] ', args);
49
+ globalThis.console.info = (...args) => writeLog(1, 'INFO', '[Info] ', args);
8
50
 
9
51
  export const slogger = {
10
52
  log: isQuiet ? () => {} : console.log,
@@ -8,8 +8,7 @@
8
8
  import got from 'got';
9
9
  import { Auth } from './auth.js';
10
10
  import { syncError, syncLog } from './workersync/synclogger.js';
11
- import { homedir } from 'os';
12
- import { access, readFile, writeFile, mkdir } from 'fs/promises';
11
+ import { readFile, writeFile, mkdir } from 'fs/promises';
13
12
  import path from 'path'
14
13
 
15
14
 
@@ -86,54 +85,58 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
86
85
  // get the required scopes and set them
87
86
  const scopes = manifest.oauthScopes || []
88
87
 
89
- // Initialize auth. This is async and will discover the project ID.
88
+ // Initialize auth.
90
89
  const auth = await Auth.setAuth(scopes);
90
+
91
+ // static things we need to get into the main thread we can do now
91
92
  const projectId = Auth.getProjectId();
92
- let accessToken = null
93
93
 
94
- // need to handle an expired refresh token
95
- try {
96
- accessToken = await auth.getAccessToken()
97
- } catch (error) {
94
+ // the active user is the person we are (ADC) or pretending to be (Workload identity)
95
+ const [activeInfo, effectiveInfo] = await Promise.all([
96
+ Auth.getSourceAccessTokenInfo(),
97
+ Auth.getAccessTokenInfo()])
98
98
 
99
+ /// these all jst exist in this sub process so we need to send them back to parent process
100
+ /// we'll send back the token, but it should be refreshed dynamically to handle expiry
101
+ const activeUser = {
102
+ id: activeInfo.tokenInfo.sub,
103
+ email: activeInfo.tokenInfo.email,
104
+ token: activeInfo.token
99
105
  }
100
- let tokenInfo = null
101
- try {
102
- tokenInfo = await got(`https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${accessToken}`).json()
103
- } catch (err) {
104
- syncError(`Application default credentials needs attention`)
105
- if (error.code === 400 && error.message.includes('invalid_grant')) {
106
- // Log a specific, actionable error for the developer/operator
107
- syncError("ADC 'invalid_grant' Error: The underlying Application Default Credentials have expired or been revoked.");
108
- syncError("Helpful note: in your admin console under security/access amd data control there are a couple of settings to fiddle with token life")
109
- syncError("Use your settaccount.sh to reauthenticate");
110
- throw new Error ('failed to get access token info : Use your settaccount.sh to reauthenticate')
111
- }
112
- throw error; // Re-throw any other errors
106
+ const effectiveUser = {
107
+ id: effectiveInfo.tokenInfo.sub,
108
+ email: effectiveInfo.tokenInfo.email,
109
+ token: effectiveInfo.token
113
110
  }
114
111
 
115
- /// these all jst exist in this sub process so we need to send them back to parent process
112
+ //syncLog(`[Auth] Active User TokenInfo: ${JSON.stringify(activeInfo.tokenInfo)}`)
113
+ //syncLog(`[Auth] Effective User TokenInfo: ${JSON.stringify(effectiveInfo.tokenInfo)}`)
114
+ // check that mandatory scopes have been allowed
115
+ const effectiveScopes = effectiveInfo.tokenInfo.scopes
116
+ // we strictly need the effective user ID (the one we are impersonating)
117
+ // but the active user (the SA) might not have a 'sub' on Cloud Run
118
+ if (!effectiveUser.id) {
119
+ const isOpenid = effectiveScopes.includes('openid')
120
+ throw new Error(`...unable to figure out effective user id - openid scope was ${isOpenid ? '' : 'not'} granted`)
121
+ }
122
+ if (!activeUser.email || !effectiveUser.email) {
123
+ const isEmail = effectiveScopes.includes('https://www.googleapis.com/auth/userinfo.email')
124
+ throw new Error(`...unable to figure out user email - userinfo.email scope was ${isEmail ? '' : 'not'} granted`)
125
+ }
126
+ const allowedScopes = new Set(effectiveScopes)
127
+ const missingScopes = scopes.filter(scope => !allowedScopes.has(scope))
128
+ if (missingScopes.length > 0) {
129
+ syncError(`...these scopes were asked for but not granted: ${missingScopes.join(', ')}`)
130
+ }
116
131
  return {
117
- scopes,
132
+ // these will be the scopes we're allowed to get
133
+ scopes: effectiveScopes,
134
+ activeUser,
135
+ effectiveUser,
118
136
  projectId,
119
- tokenInfo,
120
- accessToken, // also return the token itself
121
137
  settings,
122
138
  manifest,
123
- clasp
139
+ clasp,
124
140
  }
125
141
  }
126
142
 
127
- export const sxRefreshToken = async (Auth) => {
128
- const auth = Auth.getAuth();
129
- // force a refresh by clearing the cached credential
130
- auth.cachedCredential = null;
131
- const accessToken = await auth.getAccessToken();
132
- const tokenInfo = await got(`https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${accessToken}`).json();
133
-
134
- // update the worker's auth state
135
- Auth.setAccessToken(accessToken);
136
- Auth.setTokenInfo(tokenInfo);
137
-
138
- return { accessToken, tokenInfo };
139
- };
@@ -6,12 +6,9 @@
6
6
  * - arguments and returns must be serializable ie. primitives or plain objects
7
7
  */
8
8
 
9
- import { responseSyncify } from './auth.js';
10
- import { syncWarn, syncError } from './workersync/synclogger.js';
9
+ import { sxRetry } from './sxretry.js';
11
10
  import { getCalendarApiClient } from '../services/advcalendar/clapis.js';
12
11
 
13
- const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
14
-
15
12
  /**
16
13
  * sync a call to calendar api
17
14
  * @param {object} Auth the auth object
@@ -25,49 +22,18 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
25
22
  export const sxCalendar = async (Auth, { prop, method, params, options = {} }) => {
26
23
 
27
24
  const apiClient = getCalendarApiClient();
28
-
29
- const maxRetries = 7;
30
- let delay = 1777;
25
+ const tag = `sxCalendar for ${prop}.${method}`;
31
26
 
32
27
  const { noLog404, ...validParams } = params || {};
33
28
 
34
- for (let i = 0; i < maxRetries; i++) {
35
- let response;
36
- let error;
37
-
38
- try {
39
- const callish = apiClient[prop];
40
- response = await callish[method](validParams, options);
41
- } catch (err) {
42
- error = err;
43
- response = err.response;
44
- }
45
-
46
- const isRetryable = [429, 500, 503].includes(response?.status) ||
47
- error?.code == 429 ||
48
- (response?.status === 403 && (
49
- error?.message?.toLowerCase().includes('usage limit') ||
50
- error?.message?.toLowerCase().includes('rate limit') ||
51
- error?.errors?.some(e => ['rateLimitExceeded', 'userRateLimitExceeded', 'calendarUsageLimitsExceeded'].includes(e.reason))
52
- ));
53
-
54
- if (isRetryable && i < maxRetries - 1) {
55
- // add a random jitter to avoid thundering herd
56
- const jitter = Math.floor(Math.random() * 1000);
57
- syncWarn(`Retryable error on Calendar API call ${prop}.${method} (status: ${response?.status}). Retrying in ${delay + jitter}ms...`);
58
- await sleep(delay + jitter);
59
- delay *= 2;
60
- continue;
61
- }
62
-
63
- if (error || isRetryable) {
29
+ return sxRetry(Auth, tag, async () => {
30
+ return apiClient[prop][method](validParams, options);
31
+ }, {
32
+ skipLog: (error, response) => {
64
33
  if (noLog404 && (response?.status === 404 || error?.code === 404 || response?.status === 400 || error?.code === 400)) {
65
- // Suppress logging for expected 404s (or 400s which can happen if already deleted)
66
- } else {
67
- syncError(`Failed in sxCalendar for ${prop}.${method}`, error);
34
+ return true;
68
35
  }
69
- return { data: null, response: responseSyncify(response) };
36
+ return false;
70
37
  }
71
- return { data: response.data, response: responseSyncify(response) };
72
- }
38
+ });
73
39
  };
@@ -5,12 +5,9 @@
5
5
  * note
6
6
  * - arguments and returns must be serializable ie. primitives or plain objects
7
7
  */
8
- import { responseSyncify } from './auth.js';
9
- import { syncWarn, syncError, syncLog } from './workersync/synclogger.js';
8
+ import { sxRetry } from './sxretry.js';
10
9
  import { getDocsApiClient } from '../services/advdocs/docapis.js';
11
10
 
12
- const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
13
-
14
11
  /**
15
12
  * sync a call to docs api
16
13
  * @param {object} Auth the auth object
@@ -22,44 +19,11 @@ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
22
19
  * @return {import('./sxdrive.js').SxResult} from the Docs api
23
20
  */
24
21
  export const sxDocs = async (Auth, { prop, method, params, options = {} }) => {
25
- const apiClient = getDocsApiClient();
26
- // rate limit on docs is higher that the others so we may as well have a bigger delay
27
- const maxRetries = 7;
28
- let delay = 2789;
29
- //syncLog(JSON.stringify({ prop, method, params, options }))
30
- for (let i = 0; i < maxRetries; i++) {
31
- let response;
32
- let error;
33
-
34
- try {
35
- const callish = apiClient[prop]
36
- response = await callish[method](params, options);
37
- } catch (err) {
38
- error = err;
39
- response = err.response;
40
- }
41
22
 
42
- const isRetryable = [429, 500, 503].includes(response?.status) || error?.code == 429;
43
-
44
- if (isRetryable && i < maxRetries - 1) {
45
- // add a random jitter to avoid thundering herd
46
- const jitter = Math.floor(Math.random() * 1000);
47
- syncWarn(`Retryable error on Docs API call ${prop}.${method} (status: ${response?.status}). Retrying in ${delay + jitter}ms...`);
48
- await sleep(delay + jitter);
49
- delay *= 2;
50
- continue;
51
- }
23
+ const apiClient = getDocsApiClient();
24
+ const tag = `sxDocs for ${prop}.${method}`;
52
25
 
53
- if (error || isRetryable) {
54
- syncError(`Failed in sxDocs for ${prop}.${method}`, error);
55
- return {
56
- data: null,
57
- response: responseSyncify(response)
58
- };
59
- }
60
- return {
61
- data: response.data,
62
- response: responseSyncify(response)
63
- };
64
- }
26
+ return sxRetry(Auth, tag, async () => {
27
+ return apiClient[prop][method](params, options);
28
+ });
65
29
  };