@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
package/src/cli/setup.js CHANGED
@@ -41,6 +41,16 @@ async function findEnvFiles(dir) {
41
41
  export async function initializeConfiguration(options = {}) {
42
42
  let envPath;
43
43
 
44
+ // first step is to check we have a manifest file if we are doing auth type dwd
45
+ if (options.authType === "dwd") {
46
+ const manifestPath = path.resolve(process.cwd(), "appsscript.json");
47
+ if (!fs.existsSync(manifestPath)) {
48
+ console.log("No manifest file found. Please create an appsscript.json file in the current directory with required scopes.");
49
+ return;
50
+ }
51
+ }
52
+
53
+ // need to figure out which env file we are operating on
44
54
  if (options.env) {
45
55
  envPath = path.resolve(process.cwd(), options.env);
46
56
  console.log(`-> Using specified .env path: ${envPath}`);
@@ -90,23 +100,57 @@ export async function initializeConfiguration(options = {}) {
90
100
  existingConfig = dotenv.parse(fs.readFileSync(envPath));
91
101
  }
92
102
 
93
- console.log("--------------------------------------------------");
94
- console.log("Configuring .env for gas-fakes");
95
- console.log("Press Enter to accept the default value in brackets.");
96
- console.log("Use Space to select/deselect scopes.");
97
- console.log("--------------------------------------------------");
103
+ const responses = {};
104
+ const DEFAULT_SCOPES_VALUES = [
105
+ "https://www.googleapis.com/auth/userinfo.email",
106
+ "openid",
107
+ "https://www.googleapis.com/auth/cloud-platform",
108
+ ];
109
+ console.log(
110
+ "\nThe following default scopes are required for basic operations and will be enabled automatically:"
111
+ );
112
+ DEFAULT_SCOPES_VALUES.forEach((scope) => console.log(` - ${scope}`));
113
+ responses.DEFAULT_SCOPES = DEFAULT_SCOPES_VALUES;
114
+
115
+ if (options.authType === "dwd") {
116
+ responses.AUTH_TYPE = "dwd";
117
+ // we need to get the scopes from the manifest file
118
+ const manifestPath = path.resolve(process.cwd(), "appsscript.json");
119
+ const manifest = JSON.parse(fs.readFileSync(manifestPath));
120
+ const scopes = manifest?.oauthScopes;
121
+ // remove any in manifest that are default anyway
122
+ responses.EXTRA_SCOPES = (scopes || []).filter(scope => !DEFAULT_SCOPES_VALUES.includes(scope));
123
+
124
+ // if we are doing dwd we need to ask for the service account name
125
+ const serviceAccountNameQuestion = {
126
+ type: "text",
127
+ name: "GOOGLE_SERVICE_ACCOUNT_NAME",
128
+ message: "Enter a service account name (it will be created if it doesnt already exist)",
129
+ initial: existingConfig.GOOGLE_SERVICE_ACCOUNT_NAME || "gas-fakes-sa",
130
+ };
131
+ const serviceAccountNameResponse = await prompts(serviceAccountNameQuestion);
132
+ if (typeof serviceAccountNameResponse.GOOGLE_SERVICE_ACCOUNT_NAME === "undefined") {
133
+ console.log("Initialization cancelled.");
134
+ return;
135
+ }
136
+ Object.assign(responses, serviceAccountNameResponse);
137
+ } else {
138
+ responses.AUTH_TYPE = "adc";
139
+ console.log("--------------------------------------------------");
140
+ console.log("Configuring .env for gas-fakes");
141
+ console.log("Press Enter to accept the default value in brackets.");
142
+ console.log("Use Space to select/deselect scopes.");
143
+ console.log("--------------------------------------------------");
144
+ }
145
+
98
146
 
99
- const existingExtraScopes = existingConfig.EXTRA_SCOPES
100
- ? existingConfig.EXTRA_SCOPES.split(",").filter((s) => s)
101
- : [];
102
147
 
103
- const responses = {};
104
148
 
105
149
  // --- Stage 1: Basic Info ---
106
150
  const basicInfoQuestions = [
107
151
  {
108
152
  type: "text",
109
- name: "GCP_PROJECT_ID",
153
+ name: "GOOGLE_CLOUD_PROJECT",
110
154
  message: "Enter your GCP Project ID",
111
155
  initial:
112
156
  existingConfig.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT,
@@ -121,7 +165,7 @@ export async function initializeConfiguration(options = {}) {
121
165
  ];
122
166
 
123
167
  const basicInfoResponses = await prompts(basicInfoQuestions);
124
- if (typeof basicInfoResponses.GCP_PROJECT_ID === "undefined") {
168
+ if (typeof basicInfoResponses.GOOGLE_CLOUD_PROJECT === "undefined") {
125
169
  console.log("Initialization cancelled.");
126
170
  return;
127
171
  }
@@ -129,153 +173,125 @@ export async function initializeConfiguration(options = {}) {
129
173
 
130
174
  // --- Stage 2: Scopes ---
131
175
 
132
- const DEFAULT_SCOPES_VALUES = [
133
- "https://www.googleapis.com/auth/userinfo.email",
134
- "openid",
135
- "https://www.googleapis.com/auth/cloud-platform",
136
- ];
137
- console.log(
138
- "\nThe following default scopes are required for basic operations and will be enabled automatically:"
139
- );
140
- DEFAULT_SCOPES_VALUES.forEach((scope) => console.log(` - ${scope}`));
141
- responses.DEFAULT_SCOPES = DEFAULT_SCOPES_VALUES;
142
-
143
- const extraScopeQuestion = {
144
- type: "multiselect",
145
- name: "EXTRA_SCOPES",
146
- message: "Select any extra scopes your script requires",
147
- choices: [
148
- {
149
- title: "Workspace resources",
150
- value: "https://www.googleapis.com/auth/drive",
151
- },
152
- /*
153
- {
154
- title: "Sheets (full access)",
155
- value: "https://www.googleapis.com/auth/spreadsheets",
156
- },
157
- {
158
- title: "Docs (full access)",
159
- value: "https://www.googleapis.com/auth/documents",
160
- },
161
- {
162
- title: "Forms (full access)",
163
- value: "https://www.googleapis.com/auth/forms",
164
- },
165
- {
166
- title: "Gmail (send mail)",
167
- value: "https://www.googleapis.com/auth/gmail.send",
168
- },
169
- {
170
- title: "Gmail (full access)",
171
- value: "https://www.googleapis.com/auth/gmail.modify",
172
- },
173
- */
174
- {
175
- sensitivity: "sensitive",
176
- title: "Calendar (full access)",
177
- value: "https://www.googleapis.com/auth/calendar",
178
- },
179
- {
180
- // actually labels are not sensitive
181
- title: "Gmail labels",
182
- value: "https://www.googleapis.com/auth/gmail.labels",
183
- },
184
- {
185
- sensitivity: "sensitive",
186
- title: "Gmail compose",
187
- value: "https://www.googleapis.com/auth/gmail.compose",
188
- },
189
- {
190
- sensitivity: "sensitive",
191
- title: "Gmail modify",
192
- value: "https://www.googleapis.com/auth/gmail.modify",
193
- },
194
- {
195
- sensitivity: "sensitive",
196
- title: "Gmail send",
197
- value: "https://www.googleapis.com/auth/gmail.send",
198
- },
199
- ].map((scope) => ({
200
- ...scope,
201
- title: scope.sensitivity
202
- ? `[${scope.sensitivity}] ${scope.title}`
203
- : scope.title,
204
- // because we always need drive for ant extra scopes
205
- selected:
206
- existingExtraScopes.length > 0
207
- ? existingExtraScopes.includes(scope.value)
208
- : scope.value === "https://www.googleapis.com/auth/drive",
209
- })),
210
- hint: "- Use space to select/deselect. Press Enter to submit.",
211
- };
212
-
213
- // Check for any kind of sensitivity
214
- const sensitiveScopesList = extraScopeQuestion.choices.filter(
215
- (scope) => scope.sensitivity
216
- );
217
-
218
- const extraScopeResponses = await prompts(extraScopeQuestion);
176
+ // we only need this if we doing adc
177
+ if (options.authType === "adc") {
178
+ const existingExtraScopes = existingConfig.EXTRA_SCOPES
179
+ ? existingConfig.EXTRA_SCOPES.split(",").filter((s) => s)
180
+ : [];
181
+ const extraScopeQuestion = {
182
+ type: "multiselect",
183
+ name: "EXTRA_SCOPES",
184
+ message: "Select any extra scopes your script requires",
185
+ choices: [
186
+ {
187
+ title: "Workspace resources",
188
+ value: "https://www.googleapis.com/auth/drive",
189
+ },
190
+ {
191
+ sensitivity: "sensitive",
192
+ title: "Calendar (full access)",
193
+ value: "https://www.googleapis.com/auth/calendar",
194
+ },
195
+ {
196
+ // actually labels are not sensitive
197
+ title: "Gmail labels",
198
+ value: "https://www.googleapis.com/auth/gmail.labels",
199
+ },
200
+ {
201
+ sensitivity: "sensitive",
202
+ title: "Gmail compose",
203
+ value: "https://www.googleapis.com/auth/gmail.compose",
204
+ },
205
+ {
206
+ sensitivity: "sensitive",
207
+ title: "Gmail modify",
208
+ value: "https://www.googleapis.com/auth/gmail.modify",
209
+ },
210
+ {
211
+ sensitivity: "sensitive",
212
+ title: "Gmail send",
213
+ value: "https://www.googleapis.com/auth/gmail.send",
214
+ },
215
+ ].map((scope) => ({
216
+ ...scope,
217
+ title: scope.sensitivity
218
+ ? `[${scope.sensitivity}] ${scope.title}`
219
+ : scope.title,
220
+ // because we always need drive for ant extra scopes
221
+ selected:
222
+ existingExtraScopes.length > 0
223
+ ? existingExtraScopes.includes(scope.value)
224
+ : scope.value === "https://www.googleapis.com/auth/drive",
225
+ })),
226
+ hint: "- Use space to select/deselect. Press Enter to submit.",
227
+ };
228
+
229
+ // Check for any kind of sensitivity
230
+ const sensitiveScopesList = extraScopeQuestion.choices.filter(
231
+ (scope) => scope.sensitivity
232
+ );
219
233
 
220
- if (typeof extraScopeResponses.EXTRA_SCOPES === "undefined") {
221
- console.log("Initialization cancelled.");
222
- return;
223
- }
224
- Object.assign(responses, extraScopeResponses);
234
+ const extraScopeResponses = options.authType === "adc" ? await prompts(extraScopeQuestion) : {};
225
235
 
226
- const selectedExtraScopes = responses.EXTRA_SCOPES || [];
236
+ if (typeof extraScopeResponses.EXTRA_SCOPES === "undefined") {
237
+ console.log("Initialization cancelled.");
238
+ return;
239
+ }
240
+ Object.assign(responses, extraScopeResponses);
227
241
 
228
- const usesSensitiveScopes = sensitiveScopesList.some((s) =>
229
- selectedExtraScopes.includes(s.value)
230
- );
242
+ const selectedExtraScopes = responses.EXTRA_SCOPES || [];
231
243
 
232
- if (usesSensitiveScopes) {
233
- console.log("\n--------------------------------------------------");
234
- console.log(
235
- "You have selected sensitive or restricted scopes. Google requires an OAuth client credential file for these."
236
- );
237
- console.log(
238
- "See the getting started guide https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md for how."
244
+ const usesSensitiveScopes = sensitiveScopesList.some((s) =>
245
+ selectedExtraScopes.includes(s.value)
239
246
  );
240
- console.log("--------------------------------------------------");
241
- }
242
247
 
243
- const clientCredentialQuestion = {
244
- type: "text",
245
- name: "CLIENT_CREDENTIAL_FILE",
246
- message: usesSensitiveScopes
247
- ? "Enter the path and filename for your OAuth client credentials JSON"
248
- : "Enter path to OAuth client credentials JSON (optional)",
249
- initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
250
- validate: (input) => {
251
- const trimmedInput = input.trim();
252
-
253
- if (usesSensitiveScopes) {
254
- if (trimmedInput === "") {
255
- return "This field is required for the selected sensitive scopes.";
256
- }
257
- } else {
258
- if (trimmedInput === "") {
259
- return true;
248
+ if (usesSensitiveScopes) {
249
+ console.log("\n--------------------------------------------------");
250
+ console.log(
251
+ "You have selected sensitive or restricted scopes. Google requires an OAuth client credential file for these."
252
+ );
253
+ console.log(
254
+ "See the getting started guide https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md for how."
255
+ );
256
+ console.log("--------------------------------------------------");
257
+ }
258
+
259
+ const clientCredentialQuestion = {
260
+ type: "text",
261
+ name: "CLIENT_CREDENTIAL_FILE",
262
+ message: usesSensitiveScopes
263
+ ? "Enter the path and filename for your OAuth client credentials JSON"
264
+ : "Enter path to OAuth client credentials JSON (optional)",
265
+ initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
266
+ validate: (input) => {
267
+ const trimmedInput = input.trim();
268
+
269
+ if (usesSensitiveScopes) {
270
+ if (trimmedInput === "") {
271
+ return "This field is required for the selected sensitive scopes.";
272
+ }
273
+ } else {
274
+ if (trimmedInput === "") {
275
+ return true;
276
+ }
260
277
  }
261
- }
262
278
 
263
- const resolvedPath = path.resolve(process.cwd(), trimmedInput);
264
- if (!fs.existsSync(resolvedPath)) {
265
- return `File not found at '${resolvedPath}'. Please check the path and try again.`;
266
- }
279
+ const resolvedPath = path.resolve(process.cwd(), trimmedInput);
280
+ if (!fs.existsSync(resolvedPath)) {
281
+ return `File not found at '${resolvedPath}'. Please check the path and try again.`;
282
+ }
267
283
 
268
- return true;
269
- },
270
- };
284
+ return true;
285
+ },
286
+ };
271
287
 
272
- const clientCredentialResponse = await prompts(clientCredentialQuestion);
273
- if (typeof clientCredentialResponse.CLIENT_CREDENTIAL_FILE === "undefined") {
274
- console.log("Initialization cancelled.");
275
- return;
288
+ const clientCredentialResponse = await prompts(clientCredentialQuestion);
289
+ if (typeof clientCredentialResponse.CLIENT_CREDENTIAL_FILE === "undefined") {
290
+ console.log("Initialization cancelled.");
291
+ return;
292
+ }
293
+ Object.assign(responses, clientCredentialResponse);
276
294
  }
277
- Object.assign(responses, clientCredentialResponse);
278
-
279
295
  // --- Stage 3: Remaining Config ---
280
296
  const defaultScopesDisplay = `\n - Default: [${responses.DEFAULT_SCOPES.join(
281
297
  ", "
@@ -285,6 +301,7 @@ export async function initializeConfiguration(options = {}) {
285
301
  ? `\n - Extra: [${responses.EXTRA_SCOPES.join(", ")}]`
286
302
  : "\n - Extra: [None]";
287
303
 
304
+
288
305
  const remainingQuestions = [
289
306
  {
290
307
  type: "toggle",
@@ -307,8 +324,8 @@ export async function initializeConfiguration(options = {}) {
307
324
  existingConfig.LOG_DESTINATION
308
325
  ) > -1
309
326
  ? ["CONSOLE", "CLOUD", "BOTH", "NONE"].indexOf(
310
- existingConfig.LOG_DESTINATION
311
- )
327
+ existingConfig.LOG_DESTINATION
328
+ )
312
329
  : 0,
313
330
  },
314
331
  {
@@ -321,7 +338,7 @@ export async function initializeConfiguration(options = {}) {
321
338
  ],
322
339
  initial:
323
340
  ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE?.toUpperCase()) >
324
- -1
341
+ -1
325
342
  ? ["FILE", "UPSTASH"].indexOf(existingConfig.STORE_TYPE.toUpperCase())
326
343
  : 0,
327
344
  },
@@ -414,6 +431,7 @@ export async function initializeConfiguration(options = {}) {
414
431
  * Handles the 'auth' command to authenticate with Google Cloud.
415
432
  */
416
433
  export async function authenticateUser() {
434
+
417
435
  // First, check if gcloud CLI is available.
418
436
  await checkForGcloudCli();
419
437
 
@@ -429,18 +447,30 @@ export async function authenticateUser() {
429
447
  dotenv.config({ path: envPath, quiet: true });
430
448
 
431
449
  const {
450
+ // still supported for backwards compatibility
432
451
  GCP_PROJECT_ID,
452
+
453
+ GOOGLE_CLOUD_PROJECT,
454
+
455
+ // the scopes would have been set up in the init process whether or not they came form the manifest
433
456
  DEFAULT_SCOPES,
434
457
  EXTRA_SCOPES,
458
+
459
+ // this would be required for sensitive scopes if using adc
435
460
  CLIENT_CREDENTIAL_FILE,
436
461
  AC,
462
+ AUTH_TYPE,
463
+ GOOGLE_SERVICE_ACCOUNT_NAME
437
464
  } = process.env;
438
465
 
439
- if (!GCP_PROJECT_ID) {
440
- console.error("Error: GCP_PROJECT_ID is not set in your .env file.");
466
+ const projectId = GOOGLE_CLOUD_PROJECT || GCP_PROJECT_ID;
467
+ if (!projectId) {
468
+ console.error("Error: GOOGLE_CLOUD_PROJECT is not set in your .env file.");
441
469
  process.exit(1);
442
470
  }
443
471
 
472
+ // question for Kanshi -- why all this stuff? it's all in the .env file....
473
+ /*
444
474
  const defaultScopes =
445
475
  DEFAULT_SCOPES ||
446
476
  "https://www.googleapis.com/auth/userinfo.email,openid,https://www.googleapis.com/auth/cloud-platform";
@@ -452,73 +482,79 @@ export async function authenticateUser() {
452
482
  if (extraScopes && extraScopes.length > 0) {
453
483
  scopes += (extraScopes.startsWith(",") ? "" : ",") + extraScopes;
454
484
  }
455
-
485
+ */
486
+ const scopes = Array.from(new Set([DEFAULT_SCOPES.split(",").concat(EXTRA_SCOPES.split(","))].filter(d => d))).join(",")
456
487
  const driveAccessFlag = "--enable-gdrive-access";
457
488
 
458
489
  console.log(`...requesting scopes ${scopes}`);
459
490
 
460
491
  let clientFlag = "";
461
- if (CLIENT_CREDENTIAL_FILE) {
462
- console.log("...attempting to use enhanced client credentials");
492
+ const activeConfig = AC || "default";
493
+ //--- specific to adc auth type
494
+ if (AUTH_TYPE === "adc") {
463
495
 
464
- let clientPath = CLIENT_CREDENTIAL_FILE;
465
- if (!path.isAbsolute(clientPath)) {
466
- clientPath = path.join(rootDirectory, clientPath);
467
- }
496
+ if (CLIENT_CREDENTIAL_FILE) {
497
+ console.log("...attempting to use enhanced client credentials");
498
+
499
+ let clientPath = CLIENT_CREDENTIAL_FILE;
500
+ if (!path.isAbsolute(clientPath)) {
501
+ clientPath = path.join(rootDirectory, clientPath);
502
+ }
468
503
 
469
- if (fs.existsSync(clientPath)) {
470
- clientFlag = `--client-id-file="${clientPath}"`;
504
+ if (fs.existsSync(clientPath)) {
505
+ clientFlag = `--client-id-file="${clientPath}"`;
506
+ } else {
507
+ console.error(
508
+ `Error: Client credential file specified in .env not found at '${clientPath}'`
509
+ );
510
+ process.exit(1);
511
+ }
471
512
  } else {
472
- console.error(
473
- `Error: Client credential file specified in .env not found at '${clientPath}'`
513
+ console.log(
514
+ "\n...CLIENT_CREDENTIAL_FILE is not set. Using default Application Default Credentials (ADC)."
515
+ );
516
+ console.log(
517
+ "...if you have requested any sensitive scopes, you'll see 'This app is blocked message.'"
518
+ );
519
+ console.log(
520
+ "...To allow them see - https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md\n"
474
521
  );
475
- process.exit(1);
476
522
  }
477
- } else {
478
- console.log(
479
- "\n...CLIENT_CREDENTIAL_FILE is not set. Using default Application Default Credentials (ADC)."
480
- );
481
- console.log(
482
- "...if you have requested any sensitive scopes, you'll see 'This app is blocked message.'"
483
- );
484
- console.log(
485
- "...To allow them see - https://github.com/brucemcpherson/gas-fakes/blob/main/GETTING_STARTED.md\n"
486
- );
487
- }
488
523
 
489
- const projectId = GCP_PROJECT_ID;
490
- const activeConfig = AC || "default";
491
524
 
492
- console.log("Revoking previous credentials...");
493
- try {
494
- execSync("gcloud auth revoke --quiet", { stdio: "ignore", shell: true });
495
- } catch (e) {
496
- /* ignore */
497
- }
498
- try {
499
- execSync("gcloud auth application-default revoke --quiet", {
500
- stdio: "ignore",
501
- shell: true,
502
- });
503
- } catch (e) {
504
- /* ignore */
505
- }
506
525
 
507
- console.log(`Ensuring gcloud configuration '${activeConfig}' exists...`);
508
- try {
509
- execSync(`gcloud config configurations describe "${activeConfig}"`, {
510
- stdio: "ignore",
511
- shell: true,
512
- });
513
- console.log(`Configuration '${activeConfig}' already exists.`);
514
- } catch (error) {
515
- console.log(`Configuration '${activeConfig}' not found. Creating it...`);
516
- runCommandSync(`gcloud config configurations create "${activeConfig}"`);
517
- }
526
+ console.log("Revoking previous credentials...");
527
+ try {
528
+ execSync("gcloud auth revoke --quiet", { stdio: "ignore", shell: true });
529
+ } catch (e) {
530
+ /* ignore */
531
+ }
532
+ try {
533
+ execSync("gcloud auth application-default revoke --quiet", {
534
+ stdio: "ignore",
535
+ shell: true,
536
+ });
537
+ } catch (e) {
538
+ /* ignore */
539
+ }
518
540
 
519
- console.log(`Activating gcloud configuration: ${activeConfig}`);
520
- runCommandSync(`gcloud config configurations activate "${activeConfig}"`);
541
+ console.log(`Ensuring gcloud configuration '${activeConfig}' exists...`);
542
+ try {
543
+ execSync(`gcloud config configurations describe "${activeConfig}"`, {
544
+ stdio: "ignore",
545
+ shell: true,
546
+ });
547
+ console.log(`Configuration '${activeConfig}' already exists.`);
548
+ } catch (error) {
549
+ console.log(`Configuration '${activeConfig}' not found. Creating it...`);
550
+ runCommandSync(`gcloud config configurations create "${activeConfig}"`);
551
+ }
552
+
553
+ console.log(`Activating gcloud configuration: ${activeConfig}`);
554
+ runCommandSync(`gcloud config configurations activate "${activeConfig}"`);
555
+ }
521
556
 
557
+ //--- need this for both auth types
522
558
  console.log(`Setting project to: ${projectId}`);
523
559
  runCommandSync(`gcloud config set project ${projectId}`);
524
560
  runCommandSync(`gcloud config set billing/quota_project ${projectId}`);
@@ -546,36 +582,89 @@ export async function authenticateUser() {
546
582
  process.exit(1);
547
583
  }
548
584
 
549
- console.log("Initiating Application Default Credentials (ADC) login...");
550
- runCommandSync(
551
- `gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`
552
- );
553
- runCommandSync(
554
- `gcloud auth application-default set-quota-project ${projectId}`
555
- );
585
+ //--- specific to adc auth type
586
+ if (AUTH_TYPE === "adc") {
587
+ console.log("Initiating Application Default Credentials (ADC) login...");
588
+ runCommandSync(
589
+ `gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`
590
+ );
591
+ runCommandSync(
592
+ `gcloud auth application-default set-quota-project ${projectId}`
593
+ );
556
594
 
557
- // --- Verification ---
558
- console.log("\nVerifying configuration...");
595
+ // --- Verification ---
596
+ console.log("\nVerifying configuration...");
559
597
 
560
- const gcloudConfigDir =
561
- process.env.CLOUDSDK_CONFIG || path.join(os.homedir(), ".config", "gcloud");
562
- const activeConfigPath = path.join(gcloudConfigDir, "active_config");
598
+ const gcloudConfigDir =
599
+ process.env.CLOUDSDK_CONFIG || path.join(os.homedir(), ".config", "gcloud");
600
+ const activeConfigPath = path.join(gcloudConfigDir, "active_config");
563
601
 
564
- let currentConfig = "unknown";
565
- if (fs.existsSync(activeConfigPath)) {
566
- currentConfig = fs.readFileSync(activeConfigPath, "utf8").trim();
567
- } else {
568
- console.warn(
569
- `Warning: Could not find active_config file at ${activeConfigPath}`
602
+ let currentConfig = "unknown";
603
+ if (fs.existsSync(activeConfigPath)) {
604
+ currentConfig = fs.readFileSync(activeConfigPath, "utf8").trim();
605
+ } else {
606
+ console.warn(
607
+ `Warning: Could not find active_config file at ${activeConfigPath}`
608
+ );
609
+ }
610
+
611
+ const currentProject = execSync("gcloud config get project", { shell: true })
612
+ .toString()
613
+ .trim();
614
+ console.log(
615
+ `Active config is ${currentConfig} - project is ${currentProject}`
570
616
  );
571
617
  }
618
+ let sa_email = ""
619
+ // -- specifc to dwd auth type
620
+ if (AUTH_TYPE === "dwd") {
621
+ console.log("Initiating keyless domain-wide delegation authentication...");
622
+ const current_user = execSync("gcloud config get-value account", { shell: true })
623
+ .toString()
624
+ .trim();
625
+
626
+ sa_email = `${GOOGLE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
627
+ console.log(`Service account email: ${sa_email}`);
628
+ console.log(`Current user: ${current_user}`);
629
+
630
+ // Service Account Lifecycle
631
+ let existing_sa = false
632
+ try {
633
+ execSync(`gcloud iam service-accounts describe "${sa_email}"`, { shell: true });
634
+ existing_sa = true;
635
+ } catch (error) {
636
+ /* ignore */
637
+ }
638
+ if (existing_sa) {
639
+ const serviceAccountNameQuestion = {
640
+ type: "select",
641
+ name: "ROTATE_OR_REPLACE",
642
+ message: "Select an action for the existing service account",
643
+ initial: 0,
644
+ choices: [
645
+ { title: "Keep/rotate existing Service Account", value: "keep" },
646
+ { title: "Replace/Recreate Service Account", value: "replace" },
647
+ ],
648
+ };
649
+
650
+ const { ROTATE_OR_REPLACE } = await prompts(serviceAccountNameQuestion);
651
+ if (ROTATE_OR_REPLACE === "replace") {
652
+ console.log("Replacing existing service account...");
653
+ runCommandSync(`gcloud iam service-accounts delete "${sa_email}" --quiet --format=none`);
654
+ runCommandSync(`gcloud iam service-accounts create "${GOOGLE_SERVICE_ACCOUNT_NAME}" --display-name "${GOOGLE_SERVICE_ACCOUNT_NAME}" --quiet --format=none`);
655
+ }
656
+ } else {
657
+ console.log("Creating new service account...");
658
+ runCommandSync(`gcloud iam service-accounts create "${GOOGLE_SERVICE_ACCOUNT_NAME}" --display-name "${GOOGLE_SERVICE_ACCOUNT_NAME}" --quiet --format=none`);
659
+ }
572
660
 
573
- const currentProject = execSync("gcloud config get project", { shell: true })
574
- .toString()
575
- .trim();
576
- console.log(
577
- `Active config is ${currentConfig} - project is ${currentProject}`
578
- );
661
+ // set service account permissions
662
+ runCommandSync(`gcloud projects add-iam-policy-binding "${projectId}" --member="serviceAccount:${sa_email}" --role="roles/editor" --quiet --format=none`);
663
+ runCommandSync(`gcloud iam service-accounts add-iam-policy-binding "${sa_email}" --member="serviceAccount:${sa_email}" --role="roles/iam.serviceAccountTokenCreator" --quiet --format=none`);
664
+ runCommandSync(`gcloud iam service-accounts add-iam-policy-binding "${sa_email}" --member="user:${current_user}" --role="roles/iam.serviceAccountTokenCreator" --quiet --format=none`);
665
+ runCommandSync(`gcloud projects add-iam-policy-binding "${projectId}" --member="serviceAccount:${sa_email}" --role="roles/logging.logWriter" --quiet --format=none`);
666
+
667
+ }
579
668
 
580
669
  console.log("\nFetching token information...");
581
670
  const userToken = execSync("gcloud auth print-access-token", { shell: true })
@@ -588,6 +677,7 @@ export async function authenticateUser() {
588
677
  .toString()
589
678
  .trim();
590
679
 
680
+
591
681
  console.log("\n...user token scopes");
592
682
  runCommandSync(
593
683
  `curl https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${userToken}`
@@ -598,6 +688,18 @@ export async function authenticateUser() {
598
688
  `curl https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${appDefaultToken}`
599
689
  );
600
690
  console.log("\nAuthentication process finished.");
691
+
692
+ if (AUTH_TYPE === "dwd") {
693
+ const saUniqueId = execSync(`gcloud iam service-accounts describe "${sa_email}" --format="value(uniqueId)"`, { shell: true })
694
+ .toString()
695
+ .trim();
696
+ console.log("\nNow you need to ensure that workspace admin console has the scopes and client ID added to Domain-Wide Delegation");
697
+ console.log("Enter the following in Admin Console: https://admin.google.com/ac/owl/domainwidedelegation");
698
+ console.log(`
699
+ Client ID: ${saUniqueId}
700
+ Scopes: ${scopes}
701
+ `);
702
+ }
601
703
  }
602
704
 
603
705
  /**