@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.
- package/.gcloudignore +24 -0
- package/README.md +5 -5
- package/appsscript.json +101 -0
- package/exgcp.sh +4 -4
- package/package.json +5 -6
- package/src/cli/app.js +9 -1
- package/src/cli/lib-manager.js +0 -1
- package/src/cli/setup.js +330 -228
- package/src/services/advcalendar/clapis.js +4 -2
- package/src/services/advdocs/docapis.js +4 -3
- package/src/services/advdrive/drapis.js +8 -5
- package/src/services/advforms/formsapis.js +4 -3
- package/src/services/advgmail/gmailapis.js +4 -3
- package/src/services/advsheets/shapis.js +5 -10
- package/src/services/advslides/slapis.js +4 -3
- package/src/services/driveapp/fakedriveapp.js +10 -2
- package/src/services/logger/fakelogger.js +6 -3
- package/src/services/scriptapp/app.js +16 -11
- package/src/services/scriptapp/behavior.js +132 -107
- package/src/services/session/fakesession.js +24 -9
- package/src/services/slidesapp/fakepresentation.js +19 -8
- package/src/services/slidesapp/fakeslide.js +45 -20
- package/src/services/spreadsheetapp/fakesheet.js +9 -7
- package/src/services/stores/fakestores.js +20 -18
- package/src/services/stores/gasflex.js +13 -14
- package/src/services/urlfetchapp/app.js +3 -3
- package/src/support/auth.js +227 -55
- package/src/support/slogger.js +42 -0
- package/src/support/sxauth.js +42 -39
- package/src/support/sxcalendar.js +9 -43
- package/src/support/sxdocs.js +6 -42
- package/src/support/sxdrive.js +19 -76
- package/src/support/sxfetch.js +36 -30
- package/src/support/sxforms.js +9 -40
- package/src/support/sxgmail.js +9 -37
- package/src/support/sxretry.js +79 -0
- package/src/support/sxsheets.js +6 -37
- package/src/support/sxslides.js +5 -36
- package/src/support/sxtoken.js +15 -0
- package/src/support/syncit.js +27 -10
- package/src/support/workersync/sxfunctions.js +2 -0
- package/src/support/workersync/synclogger.js +22 -5
- package/src/support/workersync/worker.js +8 -11
- package/README.RU.md +0 -373
- package/env.setup.template +0 -16
- package/run.js +0 -35
- package/setup.js +0 -689
- package/src/Code.js +0 -3
- package/src/appsscript.json +0 -5
package/src/support/auth.js
CHANGED
|
@@ -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
|
-
|
|
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 = () =>
|
|
48
|
-
const
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
_projectId = await _auth.getProjectId();
|
|
167
|
+
//mayLog(`[Auth] DWD Scopes: ${payload.scope}`)
|
|
85
168
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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(
|
|
302
|
+
const hasAuth = () => Boolean(_authClient);
|
|
303
|
+
|
|
143
304
|
|
|
144
305
|
/**
|
|
145
|
-
* @returns {
|
|
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
|
-
|
|
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
|
};
|
package/src/support/slogger.js
CHANGED
|
@@ -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,
|
package/src/support/sxauth.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
66
|
-
} else {
|
|
67
|
-
syncError(`Failed in sxCalendar for ${prop}.${method}`, error);
|
|
34
|
+
return true;
|
|
68
35
|
}
|
|
69
|
-
return
|
|
36
|
+
return false;
|
|
70
37
|
}
|
|
71
|
-
|
|
72
|
-
}
|
|
38
|
+
});
|
|
73
39
|
};
|
package/src/support/sxdocs.js
CHANGED
|
@@ -5,12 +5,9 @@
|
|
|
5
5
|
* note
|
|
6
6
|
* - arguments and returns must be serializable ie. primitives or plain objects
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
};
|