@mcpher/gas-fakes 2.1.1 → 2.2.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/.claspignore CHANGED
@@ -3,4 +3,4 @@ run.js
3
3
  node_modules/
4
4
  package.json
5
5
  package-lock.json
6
- .gemini/
6
+ .gemini/gasmess/
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Run Native Apps Script code anywhere with gas-fakes
2
2
 
3
- I use clasp/antigravity to develop Google Apps Script (GAS) applications, but when using GAS native services, there's way too much back and forwards to the GAS IDE going while testing. I set myself the ambition of implementing a fake version of the GAS runtime environment on Node so I could at least do some testing and debugging of Apps Scripts locally on Node.
3
+ ## Google Apps Script, meet Local Development.
4
4
 
5
- This is a proof of concept so I've implemented a growing subset of number of services and methods. There are a rigorous set of tests for all emulated classes and methods to make sure the same code produces the same result on both Node and Apps Script. Please report any inconsistencies in the issues of this repo.
5
+ gas-fakes is a powerful emulation layer that lets you run Apps Script projects on Node.js as if they were native. By translating GAS service calls into granular Google API requests, it provides a secure, high-speed sandbox for local debugging and automated testing.
6
+
7
+ Built for the modern stack, it features plug-and-play containerization—allowing you to package your scripts as portable microservices or isolated workers. Coupled with automated identity management, gas-fakes handles the heavy lifting of OAuth and credential cycling, enabling your scripts to act on behalf of users or service accounts without manual intervention. It’s the missing link for building robust, scalable Google Workspace automations and AI-driven workflows.
6
8
 
7
9
 
8
10
  ## Getting started as a package user
@@ -30,44 +32,24 @@ gas-fakes -s "const files=DriveApp.getRootFolder().searchFiles('title contains
30
32
 
31
33
  For details see [gas fakes cli](gas-fakes-cli.md)
32
34
 
33
- ### Settings
34
-
35
- The optional `gasfakes.json` file holds various location and behavior parameters for your local Node environment. It is not required on GAS, as you can't change anything over there. If you don't provide this file, `gas-fakes` will create one for you with sensible defaults.
36
-
37
- ```json
38
- {
39
- "manifest": "./appsscript.json",
40
- "clasp": "./.clasp.json",
41
- "documentId": null,
42
- "cache": "/tmp/gas-fakes/cache",
43
- "properties": "/tmp/gas-fakes/properties",
44
- "scriptId": "a-unique-id-for-your-local-project"
45
- }
46
- ```
47
-
48
- | property | type | default | description |
49
- | ---------- | ------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
- | manifest | string | ./appsscript.json | the manifest path and name relative to your main module |
51
- | clasp | string | ./clasp.json | where to look for an optional clasp file |
52
- | documentId | string | null | a bound document id. This will allow testing of container bound script. The documentId will become your activeDocument (for the appropriate service) |
53
- | cache | string | /tmp/gas-fakes/cache | gas-fakes uses a local file to emulate apps script's CacheService. This is where it should put the files |
54
- | properties | string | /tmp/gas-fakes/properties | gas-fakes uses a local file to emulate apps script's PropertiesService. This is where it should put the files. You may want to put it somewhere other than /tmp to avoid accidental deletion, but don't put it in a place that'll get commited to public git repo |
55
- | scriptId | string | from clasp, or some random value | If you have a clasp file, it'll pick up the scriptId from there. If not you can enter your scriptId manually, or just leave it to create a fake one. It's use for the moment is to return something useful from ScriptApp.getScriptId() and to partition the cache and properties stores |
56
-
57
- ### Troubleshooting: Missing Environment Tags
35
+ ### Configuration
58
36
 
59
- If you see a warning or error like `Project '...' lacks an 'environment' tag`, it means your Google Cloud Organization has a policy requiring projects to be designated with an environment tag (e.g., `Development`, `Production`).
37
+ Configuration for your local Node environment is handled via environment variables, typically stored in a `.env` file and managed by the `gas-fakes init` process.
60
38
 
61
- To resolve this, you need to bind an environment tag to your project. Replace `YOUR_ORG_ID` and `YOUR_PROJECT_ID` with your actual identifiers:
39
+ | Environment Variable | Default | Description |
40
+ |---|---|---|
41
+ | `GF_MANIFEST_PATH` | `./appsscript.json` | Path to the `appsscript.json` manifest file. |
42
+ | `GF_CLASP_PATH` | `./.clasp.json` | Path to the `.clasp.json` file. |
43
+ | `GF_SCRIPT_ID` | from clasp, or random | Discovered from `.clasp.json` or generated as a random UUID during `gas-fakes init`. Used for `ScriptApp.getScriptId()` and partitioning stores. |
44
+ | `GF_DOCUMENT_ID` | `null` | A bound document ID for testing container-bound scripts. |
45
+ | `GF_CACHE_PATH` | `/tmp/gas-fakes/cache` | Path for `CacheService` local file emulation. |
46
+ | `GF_PROPERTIES_PATH` | `/tmp/gas-fakes/properties` | Path for `PropertiesService` local file emulation. |
47
+ | `GF_PLATFORM_AUTH` | `google` | Comma-separated list of backends to initialize (`google`, `ksuite`). |
48
+ | `AUTH_TYPE` | `dwd` | Google auth type: `dwd` (Domain-Wide Delegation) or `adc` (Application Default Credentials). |
49
+ | `LOG_DESTINATION` | `CONSOLE` | Logging destination: `CONSOLE`, `CLOUD`, `BOTH`, or `NONE`. |
50
+ | `STORE_TYPE` | `FILE` | Internal storage type for properties/cache: `FILE` (local) or `UPSTASH` (Redis). |
62
51
 
63
- ```bash
64
- # Bind the 'Development' environment tag to your project
65
- gcloud resource-manager tags bindings create \
66
- --tag-value=YOUR_ORG_ID/environment/Development \
67
- --parent=//cloudresourcemanager.googleapis.com/projects/YOUR_PROJECT_ID
68
- ```
69
52
 
70
- *Note: The tag key `environment` and the value `Development` must already exist at the organization level. If they don't, you (or your admin) will need to create them first using `gcloud resource-manager tags keys create` and `gcloud resource-manager tags values create`.*
71
53
 
72
54
  ### Cloud Logging Integration
73
55
 
@@ -123,7 +105,7 @@ If you have used Logging to cloud, you can get a link to the log data like this.
123
105
  console.log ('....example cloud log link for this session',Logger.__cloudLogLink)
124
106
  ```
125
107
 
126
- It contains a cloud logging query that will display any logging done in this session - the filter is based on the scriptId (from gasfakes.json), the projectId and userId (from Auth), as well as the start and end time of the session.
108
+ It contains a cloud logging query that will display any logging done in this session - the filter is based on the scriptId (from the environment), the projectId and userId (from Auth), as well as the start and end time of the session.
127
109
 
128
110
  #### A note on .env location
129
111
 
@@ -147,6 +129,20 @@ Logger.__logDestination="BOTH"
147
129
 
148
130
  Do whichever one suits you best.
149
131
 
132
+ ### Troubleshooting: Missing Environment Tags
133
+
134
+ If you see a warning or error like `Project '...' lacks an 'environment' tag`, it means your Google Cloud Organization has a policy requiring projects to be designated with an environment tag (e.g., `Development`, `Production`).
135
+
136
+ You can ignore this, but you can resolve it if you want to keep things tidy. You need to bind an environment tag to your project. Replace `YOUR_ORG_ID` and `YOUR_PROJECT_ID` with your actual identifiers:
137
+ ```bash
138
+ # Bind the 'Development' environment tag to your project
139
+ gcloud resource-manager tags bindings create \
140
+ --tag-value=YOUR_ORG_ID/environment/Development \
141
+ --parent=//cloudresourcemanager.googleapis.com/projects/YOUR_PROJECT_ID
142
+ ```
143
+
144
+ *Note: The tag key `environment` and the value `Development` must already exist at the organization level. If they don't, you (or your admin) will need to create them first using `gcloud resource-manager tags keys create` and `gcloud resource-manager tags values create`.*
145
+
150
146
  ### Pushing files to GAS
151
147
 
152
148
  There are a couple of syntactical differences between Node and Apps Script. Not in the body of the code but in how the IDE executes. The 2 main ones are
@@ -170,9 +166,12 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
170
166
  - [gas fakes cli](gas-fakes-cli.md)
171
167
  - [ksuite poc](ksuite_poc.md)
172
168
  - [apps script - a lingua franca for workspace platforms](https://ramblings.mcpher.com/apps-script-a-lingua-franca/)
169
+ - [Apps Script: A ‘Lingua Franca’ for the Multi-Cloud Era](https://ramblings.mcpher.com/apps-script-with-ksuite/)
173
170
  - [running gas-fakes on google cloud run](https://github.com/brucemcpherson/gas-fakes-containers)
174
171
  - [running gas-fakes on google kubernetes engine](https://github.com/brucemcpherson/gas-fakes-containers)
175
172
  - [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
173
+ - [running gas-fakes on Azure ACA](https://github.com/brucemcpherson/gas-fakes-containers)
174
+ - [Yes – you can run native apps script code on Azure ACA as well!](https://ramblings.mcpher.com/yes-you-can-run-native-apps-script-code-on-azure-aca-as-well/)
176
175
  - [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
177
176
  - [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
178
177
  - [Inside the volatile world of a Google Document](https://ramblings.mcpher.com/inside-the-volatile-world-of-a-google-document/)
package/package.json CHANGED
@@ -3,8 +3,10 @@
3
3
  "node": ">=20.11.0"
4
4
  },
5
5
  "dependencies": {
6
+ "@azure/identity": "^4.13.0",
6
7
  "@mcpher/fake-gasenum": "^1.0.6",
7
8
  "@mcpher/gas-flex-cache": "^1.1.5",
9
+ "@microsoft/microsoft-graph-client": "^3.0.7",
8
10
  "@modelcontextprotocol/sdk": "^1.26.0",
9
11
  "@sindresorhus/is": "^7.2.0",
10
12
  "acorn": "^8.15.0",
@@ -39,7 +41,7 @@
39
41
  },
40
42
  "name": "@mcpher/gas-fakes",
41
43
  "author": "bruce mcpherson",
42
- "version": "2.1.1",
44
+ "version": "2.2.1",
43
45
  "license": "MIT",
44
46
  "main": "main.js",
45
47
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
package/src/cli/app.js CHANGED
@@ -32,11 +32,6 @@ export async function main() {
32
32
  "A string containing the Google Apps Script."
33
33
  )
34
34
  .option("-e, --env <path>", "Path to a custom .env file.", "./.env")
35
- .option(
36
- "-g, --gfsettings <path>",
37
- "Path to a gasfakes.json settings file.",
38
- "./gasfakes.json"
39
- )
40
35
  .option("-x, --sandbox", "Run the script in a basic sandbox.")
41
36
  .option(
42
37
  "-w, --whitelistRead <string>",
@@ -70,7 +65,7 @@ export async function main() {
70
65
  null
71
66
  )
72
67
  .action(async (options) => {
73
- const { filename, script, env, gfsettings } = options;
68
+ const { filename, script, env } = options;
74
69
 
75
70
  // If no script provided and no sub-command matched, show help
76
71
  if (!filename && !script) {
@@ -94,10 +89,6 @@ export async function main() {
94
89
  dotenv.config({ path: envPath, quiet: true });
95
90
  }
96
91
 
97
- const settingsPath = path.resolve(process.cwd(), gfsettings);
98
- console.log(`...using gasfakes settings file in ${settingsPath}`);
99
- process.env.GF_SETTINGS_PATH = settingsPath;
100
-
101
92
  const sandboxConfig = buildSandboxConfig(options);
102
93
  const useSandbox = !!options.sandbox || !!sandboxConfig;
103
94
 
@@ -124,7 +115,6 @@ export async function main() {
124
115
  display: options.display,
125
116
  useSandbox,
126
117
  sandboxConfig,
127
- gfSettings: settingsPath,
128
118
  args,
129
119
  gas_library,
130
120
  });
@@ -156,7 +156,6 @@ export async function executeGasScript(options) {
156
156
  filename,
157
157
  script,
158
158
  display,
159
- gfSettings,
160
159
  useSandbox,
161
160
  sandboxConfig,
162
161
  args,
@@ -181,12 +180,6 @@ export async function executeGasScript(options) {
181
180
  );
182
181
  }
183
182
 
184
- Object.defineProperty(globalThis, "settingsPath", {
185
- value: gfSettings,
186
- writable: true,
187
- configurable: true,
188
- });
189
-
190
183
  if (gas_library && gas_library.length > 0) {
191
184
  const libs = gas_library.reduce((ar, { identifier, libScript }) => {
192
185
  if (mainScript.includes(identifier)) {
package/src/cli/setup.js CHANGED
@@ -3,11 +3,36 @@ import dotenv from "dotenv";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
5
  import os from "os";
6
+ import { randomUUID } from "node:crypto";
6
7
  import { execSync } from "child_process";
7
8
  import { checkForGcloudCli, runCommandSync } from "./utils.js";
8
9
 
9
10
  // --- Utility Functions ---
10
11
 
12
+ /**
13
+ * Retrieves the scriptId and its source.
14
+ * @returns {{scriptId: string, source: string}}
15
+ */
16
+ function getScriptIdInfo() {
17
+ if (process.env.GF_SCRIPT_ID) {
18
+ return { scriptId: process.env.GF_SCRIPT_ID, source: "env (GF_SCRIPT_ID)" };
19
+ }
20
+
21
+ const claspPath = process.env.GF_CLASP_PATH || "./.clasp.json";
22
+ if (fs.existsSync(claspPath)) {
23
+ try {
24
+ const clasp = JSON.parse(fs.readFileSync(claspPath, "utf8"));
25
+ if (clasp.scriptId) {
26
+ return { scriptId: clasp.scriptId, source: `clasp (${claspPath})` };
27
+ }
28
+ } catch (e) {
29
+ // Ignore parsing errors
30
+ }
31
+ }
32
+
33
+ return { scriptId: "not set (will be random at runtime)", source: "none" };
34
+ }
35
+
11
36
  /**
12
37
  * Recursively searches for .env files starting from a directory.
13
38
  * @param {string} dir - Start directory
@@ -118,7 +143,80 @@ export async function initializeConfiguration(options = {}) {
118
143
  if (typeof platforms === "string") platforms = platforms.split(",");
119
144
  responses.GF_PLATFORM_AUTH = platforms.join(",");
120
145
 
121
- // --- Step 2: Google Workspace Configuration ---
146
+ // --- Step 2: Gas-Fakes Behavior Configuration ---
147
+ console.log("\n--- Configuring Gas-Fakes paths and behavior ---");
148
+ const gasFakesQuestions = [
149
+ {
150
+ type: "text",
151
+ name: "GF_MANIFEST_PATH",
152
+ message: "Path to appsscript.json",
153
+ initial: existingConfig.GF_MANIFEST_PATH || "./appsscript.json",
154
+ },
155
+ {
156
+ type: "text",
157
+ name: "GF_CLASP_PATH",
158
+ message: "Path to .clasp.json",
159
+ initial: existingConfig.GF_CLASP_PATH || "./.clasp.json",
160
+ },
161
+ {
162
+ type: "text",
163
+ name: "GF_SCRIPT_ID",
164
+ message: (prev, values) => {
165
+ const claspPath = values.GF_CLASP_PATH || "./.clasp.json";
166
+ let hint = "";
167
+ if (fs.existsSync(claspPath)) {
168
+ try {
169
+ const clasp = JSON.parse(fs.readFileSync(claspPath, "utf8"));
170
+ if (clasp.scriptId) {
171
+ hint = ` (found in ${claspPath}: ${clasp.scriptId})`;
172
+ }
173
+ } catch (e) {}
174
+ }
175
+ if (!hint && !existingConfig.GF_SCRIPT_ID) {
176
+ hint = " (no ID found; a random one will be generated)";
177
+ }
178
+ return `Script ID (optional, overrides .clasp.json)${hint}`;
179
+ },
180
+ initial: (prev, values) => {
181
+ if (existingConfig.GF_SCRIPT_ID) return existingConfig.GF_SCRIPT_ID;
182
+ const claspPath = values.GF_CLASP_PATH || "./.clasp.json";
183
+ if (fs.existsSync(claspPath)) {
184
+ try {
185
+ const clasp = JSON.parse(fs.readFileSync(claspPath, "utf8"));
186
+ if (clasp.scriptId) return clasp.scriptId;
187
+ } catch (e) {}
188
+ }
189
+ return randomUUID();
190
+ },
191
+ },
192
+ {
193
+ type: "text",
194
+ name: "GF_DOCUMENT_ID",
195
+ message: "Document ID (optional, for container-bound scripts)",
196
+ initial: existingConfig.GF_DOCUMENT_ID || "",
197
+ },
198
+ {
199
+ type: "text",
200
+ name: "GF_CACHE_PATH",
201
+ message: "Cache storage path",
202
+ initial: existingConfig.GF_CACHE_PATH || "/tmp/gas-fakes/cache",
203
+ },
204
+ {
205
+ type: "text",
206
+ name: "GF_PROPERTIES_PATH",
207
+ message: "Properties storage path",
208
+ initial: existingConfig.GF_PROPERTIES_PATH || "/tmp/gas-fakes/properties",
209
+ }
210
+ ];
211
+
212
+ const gasFakesResponses = await prompts(gasFakesQuestions);
213
+ if (typeof gasFakesResponses.GF_MANIFEST_PATH === "undefined") {
214
+ console.log("Initialization cancelled.");
215
+ return;
216
+ }
217
+ Object.assign(responses, gasFakesResponses);
218
+
219
+ // --- Step 3: Google Workspace Configuration ---
122
220
  if (platforms.includes("google")) {
123
221
  console.log("\n--- Configuring Google Workspace backend ---");
124
222
 
@@ -146,18 +244,18 @@ export async function initializeConfiguration(options = {}) {
146
244
  }
147
245
 
148
246
  // Discover Scopes from appsscript.json
149
- const manifestPath = path.resolve(process.cwd(), "appsscript.json");
247
+ const manifestPath = path.resolve(process.cwd(), responses.GF_MANIFEST_PATH);
150
248
  let manifestScopes = [];
151
249
  if (fs.existsSync(manifestPath)) {
152
250
  try {
153
251
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
154
252
  manifestScopes = manifest.oauthScopes || [];
155
- console.log(`...discovered ${manifestScopes.length} scopes in appsscript.json`);
253
+ console.log(`...discovered ${manifestScopes.length} scopes in ${responses.GF_MANIFEST_PATH}`);
156
254
  } catch (err) {
157
- console.warn("...warning: failed to parse appsscript.json. Using default scopes only.");
255
+ console.warn(`...warning: failed to parse ${responses.GF_MANIFEST_PATH}. Using default scopes only.`);
158
256
  }
159
257
  } else {
160
- console.log("...appsscript.json not found. Using default scopes only.");
258
+ console.log(`${responses.GF_MANIFEST_PATH} not found. Using default scopes only.`);
161
259
  }
162
260
 
163
261
  const DEFAULT_SCOPES_VALUES = [
@@ -184,9 +282,9 @@ export async function initializeConfiguration(options = {}) {
184
282
  initial: existingConfig.GOOGLE_SERVICE_ACCOUNT_NAME || "gas-fakes-sa",
185
283
  },
186
284
  {
187
- type: responses.AUTH_TYPE === "adc" ? "text" : null,
285
+ type: "text",
188
286
  name: "CLIENT_CREDENTIAL_FILE",
189
- message: "Enter path to OAuth client credentials JSON (optional, required for restricted scopes)",
287
+ message: "Enter path to OAuth client credentials JSON (optional, required for restricted scopes with ADC)",
190
288
  initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
191
289
  }
192
290
  ];
@@ -199,7 +297,7 @@ export async function initializeConfiguration(options = {}) {
199
297
  Object.assign(responses, googleResponses);
200
298
  }
201
299
 
202
- // --- Step 3: Infomaniak KSuite Configuration ---
300
+ // --- Step 4: Infomaniak KSuite Configuration ---
203
301
  if (platforms.includes("ksuite")) {
204
302
  console.log("\n--- Configuring Infomaniak KSuite backend ---");
205
303
  const ksuiteQuestions = [
@@ -224,7 +322,7 @@ export async function initializeConfiguration(options = {}) {
224
322
  Object.assign(responses, ksuiteResponses);
225
323
  }
226
324
 
227
- // --- Step 4: Shared Remaining Config ---
325
+ // --- Step 5: Shared Remaining Config ---
228
326
  const remainingQuestions = [
229
327
  {
230
328
  type: "toggle",
@@ -335,6 +433,9 @@ export async function authenticateUser(options = {}) {
335
433
  }
336
434
  dotenv.config({ path: envPath, quiet: true });
337
435
 
436
+ const { scriptId, source } = getScriptIdInfo();
437
+ console.log(`...using scriptId: ${scriptId} (source: ${source})`);
438
+
338
439
  let platforms = (process.env.GF_PLATFORM_AUTH || "google").split(",");
339
440
 
340
441
  // If specific backend requested via CLI, only auth that one
@@ -363,6 +464,7 @@ export async function authenticateUser(options = {}) {
363
464
 
364
465
  const {
365
466
  GOOGLE_CLOUD_PROJECT,
467
+ GCP_PROJECT_ID,
366
468
  DEFAULT_SCOPES,
367
469
  EXTRA_SCOPES,
368
470
  CLIENT_CREDENTIAL_FILE,
@@ -371,9 +473,9 @@ export async function authenticateUser(options = {}) {
371
473
  GOOGLE_SERVICE_ACCOUNT_NAME
372
474
  } = process.env;
373
475
 
374
- const projectId = GOOGLE_CLOUD_PROJECT;
476
+ const projectId = GOOGLE_CLOUD_PROJECT || GCP_PROJECT_ID;
375
477
  if (!projectId) {
376
- console.error("Error: GOOGLE_CLOUD_PROJECT is not set.");
478
+ console.error("Error: Project ID not set. Please run 'gas-fakes init' first.");
377
479
  continue;
378
480
  }
379
481
 
@@ -387,33 +489,38 @@ export async function authenticateUser(options = {}) {
387
489
  const driveAccessFlag = "--enable-gdrive-access";
388
490
  const activeConfig = AC || "default";
389
491
 
390
- if (AUTH_TYPE === "adc") {
391
- let clientFlag = "";
392
- if (CLIENT_CREDENTIAL_FILE) {
393
- const clientPath = path.resolve(process.cwd(), CLIENT_CREDENTIAL_FILE);
394
- if (fs.existsSync(clientPath)) clientFlag = `--client-id-file="${clientPath}"`;
395
- }
396
-
397
- console.log("Revoking previous ADC credentials...");
398
- try { execSync("gcloud auth application-default revoke --quiet", { stdio: "ignore", shell: true }); } catch (e) {}
492
+ // --- Common Google Login (Normal Auth Dialog) ---
493
+ console.log("Revoking previous user credentials...");
494
+ try { execSync("gcloud auth revoke --quiet", { stdio: "ignore", shell: true }); } catch (e) {}
495
+ try { execSync("gcloud auth application-default revoke --quiet", { stdio: "ignore", shell: true }); } catch (e) {}
399
496
 
400
- console.log(`Setting up gcloud config: ${activeConfig}`);
401
- try { execSync(`gcloud config configurations describe "${activeConfig}"`, { stdio: "ignore", shell: true }); }
402
- catch (e) { runCommandSync(`gcloud config configurations create "${activeConfig}"`); }
403
- runCommandSync(`gcloud config configurations activate "${activeConfig}"`);
404
-
405
- runCommandSync(`gcloud config set project ${projectId}`);
406
- runCommandSync(`gcloud config set billing/quota_project ${projectId}`);
407
-
408
- console.log("Logging in...");
409
- runCommandSync(`gcloud auth login ${driveAccessFlag}`);
410
-
411
- console.log("Setting up ADC...");
412
- runCommandSync(`gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`);
413
- runCommandSync(`gcloud auth application-default set-quota-project ${projectId}`);
497
+ console.log(`Setting up gcloud config: ${activeConfig}`);
498
+ try { execSync(`gcloud config configurations describe "${activeConfig}"`, { stdio: "ignore", shell: true }); }
499
+ catch (e) { runCommandSync(`gcloud config configurations create "${activeConfig}"`); }
500
+ runCommandSync(`gcloud config configurations activate "${activeConfig}"`);
501
+
502
+ runCommandSync(`gcloud config set project ${projectId}`);
503
+ runCommandSync(`gcloud config set billing/quota_project ${projectId}`);
504
+
505
+ console.log("Initiating user login...");
506
+ runCommandSync(`gcloud auth login ${driveAccessFlag}`);
507
+
508
+ let clientFlag = "";
509
+ if (CLIENT_CREDENTIAL_FILE) {
510
+ const clientPath = path.resolve(process.cwd(), CLIENT_CREDENTIAL_FILE);
511
+ if (fs.existsSync(clientPath)) {
512
+ console.log(`...using client credentials from ${clientPath}`);
513
+ clientFlag = `--client-id-file="${clientPath}"`;
514
+ }
414
515
  }
415
516
 
517
+ console.log("Setting up Application Default Credentials (ADC)...");
518
+ runCommandSync(`gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`);
519
+ runCommandSync(`gcloud auth application-default set-quota-project ${projectId}`);
520
+
521
+ // --- DWD Specific Setup (if configured) ---
416
522
  if (AUTH_TYPE === "dwd") {
523
+ console.log("\n--- Performing Domain-Wide Delegation (DWD) Setup ---");
417
524
  const current_user = execSync("gcloud config get-value account", { shell: true }).toString().trim();
418
525
  const sa_email = `${GOOGLE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
419
526
 
@@ -446,7 +553,7 @@ export async function authenticateUser(options = {}) {
446
553
  }
447
554
 
448
555
  /**
449
- * Handles the 'enableAPIs' command to enable or disable necessary Google Cloud services based on options.
556
+ * Handles the 'enableAPIs' command to enable or disable required Google Cloud services based on options.
450
557
  * @param {object} options Options object provided by commander.js.
451
558
  */
452
559
  export function enableGoogleAPIs(options) {
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import './support/env-loader.js';
2
+ import './services/stores/app.js'
2
3
  import './services/scriptapp/app.js'
3
4
  import './services/driveapp/app.js'
4
5
  import './services/logger/app.js'
@@ -27,5 +28,3 @@ import './services/slidesapp/app.js'
27
28
  import './services/mimetype/app.js'
28
29
  import './services/lock/app.js'
29
30
  import './services/libhandlerapp/app.js'
30
- // should be last
31
- import './services/stores/app.js'
@@ -74,7 +74,7 @@ class FakeDocumentApp {
74
74
  return this.openById(match[1]);
75
75
  }
76
76
  /**
77
- * note that this in gas-fakes uses the documentId from gasfakes.json config file
77
+ * note that this in gas-fakes uses the GF_DOCUMENT_ID from the environment
78
78
  * Returns the document to which the script is container-bound. To interact with document to which the script is not container-bound, use openById(id) or openByUrl(url) instead.
79
79
  * @returns {Document}
80
80
  */
@@ -25,11 +25,13 @@ export const getCurrentNr = (data) => {
25
25
  .filter(key => key.startsWith(shadowPrefix))
26
26
  .reduce((p, c) => {
27
27
  // strangly there's another level of .namedRanges property
28
- if (data.namedRanges[c].namedRanges.length !== 1) {
29
- // TODO I dont know if this true yet we'll need to investigate
30
- throw new Error(`expected only 1 nr match but got ${data.namedRanges[c].namedRanges.length}`)
28
+ const nrs = data.namedRanges[c].namedRanges;
29
+ if (nrs.length > 1) {
30
+ // This can happen if a batchUpdate retries due to timeout but the first attempt actually succeeded.
31
+ // The duplicates will be automatically cleaned up by the caller (makeElementMap).
32
+ process.stderr.write(`...warning: found ${nrs.length} named ranges for ${c} - duplicates will be cleaned up\n`);
31
33
  }
32
- data.namedRanges[c].namedRanges.forEach(r => {
34
+ nrs.forEach(r => {
33
35
  p.push(r)
34
36
  })
35
37
  return p
@@ -114,9 +114,7 @@ export class FakeFormItem {
114
114
  }
115
115
 
116
116
  getId() {
117
- const resource = this.__resource;
118
- const hexId = resource.questionItem?.question?.questionId || this.__itemId;
119
- return Utils.fromHex(hexId);
117
+ return Utils.fromHex(this.__itemId);
120
118
  }
121
119
 
122
120
  getIndex() {
@@ -59,7 +59,7 @@ const limitMode = (mode) => {
59
59
  const requireAllScopes = (mode) => {
60
60
  limitMode(mode)
61
61
  ensureInit()
62
- return checkScopesMatch(Array.from(Auth.getAuthedScopes().keys()))
62
+ return checkScopesMatch(Array.from(Auth.getAuthedScopes()))
63
63
  }
64
64
 
65
65
  /**
@@ -86,11 +86,28 @@ const checkScopesMatch = (required) => {
86
86
  ensureInit()
87
87
  const scopes = Auth.getTokenScopes()
88
88
 
89
+ // console.log('...DEBUG: scopes type:', typeof scopes, 'content:', scopes);
90
+
89
91
  // now we're syncronous all the way
90
- const tokened = new Set((typeof scopes === 'string' ? scopes : "").split(" "))
92
+ // normalize tokened scopes by removing trailing slashes.
93
+ // Handle both space and comma separation.
94
+ let scopeList = [];
95
+ if (Array.isArray(scopes)) {
96
+ scopeList = scopes;
97
+ } else if (typeof scopes === 'string') {
98
+ scopeList = scopes.split(/[ ,]/);
99
+ } else if (scopes && typeof scopes === 'object') {
100
+ // If it's a non-null object, maybe it's serializable but has a toString?
101
+ scopeList = String(scopes).split(/[ ,]/);
102
+ }
103
+
104
+ const tokened = new Set(scopeList.map(s => s.trim().replace(/\/$/, "")).filter(s => s))
91
105
 
92
106
  // see which ones are missing
93
107
  const missing = required.filter(s => {
108
+ // normalized required scope
109
+ const ns = s.trim().replace(/\/$/, "")
110
+
94
111
  // setting this scope causes gcloud to block
95
112
  // seem to manage without them anyway
96
113
  const ignores = [
@@ -99,13 +116,19 @@ const checkScopesMatch = (required) => {
99
116
  "https://www.googleapis.com/auth/presentations",
100
117
  "https://www.googleapis.com/auth/forms"
101
118
  ]
102
- const hasIgnore = ignores.includes(s)
119
+ const hasIgnore = ignores.some(i => i.replace(/\/$/, "") === ns)
103
120
  if (hasIgnore) {
104
121
  slogger.warn('...ignoring requested scope for adc as google blocks it outside apps script' + s)
105
122
  }
106
- // if drive is authorized and drive.readonly is required that's okay too
107
- // if drive.readonly is authorized and drive is requested thats not
108
- return !(hasIgnore || tokened.has(s.replace(/\.readonly$/, "")))
123
+
124
+ // a scope is satisfied if:
125
+ // 1. It is explicitly in the tokened set
126
+ // 2. It is a .readonly scope AND the base scope is in the tokened set (e.g. drive satisfy drive.readonly)
127
+
128
+ const baseNs = ns.replace(/\.readonly$/, "")
129
+ const isSatisfied = tokened.has(ns) || (ns.endsWith(".readonly") && tokened.has(baseNs))
130
+
131
+ return !(hasIgnore || isSatisfied)
109
132
  })
110
133
 
111
134
  if (missing.length) {
@@ -178,6 +201,9 @@ if (typeof globalThis[name] === typeof undefined) {
178
201
  __proxies: Proxies,
179
202
  get __registeredServices() {
180
203
  return Proxies.getRegisteredServices()
204
+ },
205
+ get __loadedServices() {
206
+ return Proxies.getLoadedServices()
181
207
  }
182
208
  }
183
209
 
@@ -200,6 +226,12 @@ if (typeof globalThis[name] === typeof undefined) {
200
226
  const handler = {
201
227
  get(_, prop, receiver) {
202
228
  if (prop === 'isFake') return true;
229
+
230
+ // BRIDGE: Inform LoadedRegistry about ScriptApp being loaded
231
+ if (prop !== '__behavior' && prop !== '__proxies') {
232
+ Proxies.__addLoaded(name);
233
+ }
234
+
203
235
  const app = getApp(prop);
204
236
  return Reflect.get(app, prop, receiver);
205
237
  },
@@ -216,4 +248,6 @@ if (typeof globalThis[name] === typeof undefined) {
216
248
  writable: false,
217
249
  });
218
250
 
251
+ // Manually add ScriptApp to service registry
252
+ Proxies.__addService(name);
219
253
  }
@@ -268,10 +268,38 @@ class FakeBehavior {
268
268
  this.__idWhitelist = null
269
269
 
270
270
  // individually settable services
271
- const services = ScriptApp.__registeredServices
272
- this.__sandboxService = {}
273
- services.forEach(f => this.__sandboxService[f] = newFakeSandboxService(this, f))
274
-
271
+ // BRIDGE: Use a Proxy to dynamically handle services, even those registered later
272
+ this.__sandboxService = new Proxy({}, {
273
+ get: (target, name) => {
274
+ if (typeof name !== 'string' || name.startsWith('__')) return target[name]
275
+
276
+ // EXCLUSION: CacheService and PropertiesService are NOT intended to be sandboxed
277
+ if (name === 'CacheService' || name === 'PropertiesService') return undefined;
278
+
279
+ if (!target[name]) {
280
+ target[name] = newFakeSandboxService(this, name)
281
+ }
282
+ return target[name]
283
+ },
284
+ ownKeys: (target) => {
285
+ // When asked for keys, ensure all registered services are present in the target
286
+ if (globalThis.ScriptApp?.__registeredServices) {
287
+ globalThis.ScriptApp.__registeredServices.forEach(s => {
288
+ // EXCLUSION: CacheService and PropertiesService are NOT intended to be sandboxed
289
+ if (s !== 'CacheService' && s !== 'PropertiesService' && !target[s]) {
290
+ target[s] = newFakeSandboxService(this, s)
291
+ }
292
+ })
293
+ }
294
+ return Reflect.ownKeys(target)
295
+ },
296
+ getOwnPropertyDescriptor: (target, name) => {
297
+ if (typeof name === 'string' && !name.startsWith('__') && name !== 'CacheService' && name !== 'PropertiesService' && !target[name]) {
298
+ target[name] = newFakeSandboxService(this, name)
299
+ }
300
+ return Reflect.getOwnPropertyDescriptor(target, name)
301
+ }
302
+ })
275
303
  }
276
304
  newIdWhitelistItem(id) {
277
305
  return newFakeIdWhitelistItem(id)
@@ -352,7 +380,7 @@ class FakeBehavior {
352
380
  }
353
381
  resetGmail() {
354
382
  this.__createdGmailIds.clear();
355
- this.__allowedGmailIds.clear();
383
+ this.__allowedIds.clear();
356
384
  return this;
357
385
  }
358
386
  resetCalendar() {
@@ -35,7 +35,7 @@ export const newFakeSpreadsheetApp = (...args) => {
35
35
  */
36
36
  export class FakeSpreadsheetApp {
37
37
  constructor() {
38
- // in the context of gas-fakes we start with the activespreadsheet being the one mentioned in gasfakes.json
38
+ // in the context of gas-fakes we start with the activespreadsheet being the one mentioned in the environment (GF_DOCUMENT_ID)
39
39
  this.__activeSpreadsheet = null
40
40
  const enumProps = [
41
41
  "AutoFillSeries", // AutoFillSeries An enumeration of the types of series used to calculate auto-filled values.
@@ -19,22 +19,18 @@ let _cacheApp = null
19
19
  /**
20
20
  * @returns {FakeService}
21
21
  */
22
- const registerApp = (_app, name, kind) => {
23
- if (typeof globalThis[name] === typeof undefined) {
24
-
25
-
26
- const getApp = () => {
27
- // if it hasnt been intialized yet then do that
28
- if (!_app) {
29
- _app = newFakeService(kind)
30
- }
31
- // this is the actual driveApp we'll return from the proxy
32
- return _app
22
+ const registerApp = (name, kind) => {
23
+ let instance = null;
24
+ const getApp = () => {
25
+ // if it hasnt been intialized yet then do that
26
+ if (!instance) {
27
+ instance = newFakeService(kind)
33
28
  }
34
- Proxies.registerProxy(name, getApp)
29
+ // this is the actual driveApp we'll return from the proxy
30
+ return instance
35
31
  }
32
+ Proxies.registerProxy(name, getApp)
36
33
  }
37
34
 
38
- _propertiesApp = registerApp(_propertiesApp, 'PropertiesService', 'PROPERTIES')
39
- _cacheApp = registerApp(_cacheApp, 'CacheService', 'CACHE')
40
-
35
+ registerApp('PropertiesService', 'PROPERTIES')
36
+ registerApp('CacheService', 'CACHE')
@@ -4,6 +4,7 @@ import { Proxies } from '../../support/proxies.js';
4
4
  import { FakeDocument } from './fakedocument.js';
5
5
  import { FakeElement } from './fakeelement.js';
6
6
  import { FakeNamespace } from './fakenamespace.js';
7
+ import { FakeFormat } from './fakeformat.js';
7
8
 
8
9
  /**
9
10
  * Parses the given XML string and returns a Document object.
@@ -42,6 +43,22 @@ const getNamespace = (prefix, uri) => {
42
43
  return new FakeNamespace(prefix, uri);
43
44
  };
44
45
 
46
+ /**
47
+ * Returns a Format object for pretty printing.
48
+ * @return {FakeFormat} The format object.
49
+ */
50
+ const getPrettyFormat = () => {
51
+ return new FakeFormat({ pretty: true });
52
+ };
53
+
54
+ /**
55
+ * Returns a Format object for raw output.
56
+ * @return {FakeFormat} The format object.
57
+ */
58
+ const getRawFormat = () => {
59
+ return new FakeFormat({ pretty: false });
60
+ };
61
+
45
62
  // Singleton app object
46
63
  let _app = null;
47
64
 
@@ -52,6 +69,8 @@ if (typeof globalThis[name] === typeof undefined) {
52
69
  _app = {
53
70
  parse,
54
71
  getNamespace,
72
+ getPrettyFormat,
73
+ getRawFormat,
55
74
  toString: () => name
56
75
  };
57
76
  }
@@ -0,0 +1,53 @@
1
+ import { XMLBuilder } from 'fast-xml-parser';
2
+
3
+ /**
4
+ * Fake Format class for XmlService
5
+ */
6
+ export class FakeFormat {
7
+ constructor(options = {}) {
8
+ this._options = options;
9
+ }
10
+
11
+ /**
12
+ * Formats the given document as an XML string.
13
+ * @param {FakeDocument} document The document to format.
14
+ * @return {string} The formatted XML string.
15
+ */
16
+ format(document) {
17
+ if (!document || typeof document.getRootElement !== 'function') {
18
+ throw new Error("XmlService: Invalid document provided to format().");
19
+ }
20
+
21
+ const root = document.getRootElement();
22
+ const builderOptions = {
23
+ ignoreAttributes: false,
24
+ attributeNamePrefix: "@_",
25
+ textNodeName: "#text",
26
+ format: this._options.pretty || false,
27
+ indentBy: this._options.pretty ? " " : "",
28
+ suppressEmptyNode: false // GAS usually outputs <tag></tag> or <tag/>? Actually GAS uses <tag/> for empty elements usually.
29
+ };
30
+
31
+ const builder = new XMLBuilder(builderOptions);
32
+
33
+ // Construct the object for building
34
+ // We access the internal _data property of FakeElement
35
+ const data = {
36
+ [root.getQualifiedName()]: root._data
37
+ };
38
+
39
+ let xml = builder.build(data);
40
+
41
+ // GAS adds XML declaration followed by a line separator (\r\n)
42
+ const declaration = '<?xml version="1.0" encoding="UTF-8"?>\r\n';
43
+
44
+ if (this._options.pretty) {
45
+ // GAS pretty format uses \r\n and has a newline after declaration
46
+ // XMLBuilder uses \n, so we replace it.
47
+ return declaration + xml.replace(/\n/g, '\r\n') + '\r\n';
48
+ } else {
49
+ // Raw format - declaration, compact XML, and trailing newline
50
+ return declaration + xml + '\r\n';
51
+ }
52
+ }
53
+ }
@@ -67,13 +67,17 @@ const setTokenScopes = (scopes, platform = _platform) => {
67
67
  const getTokenScopes = () => {
68
68
  const id = _getIdentity();
69
69
  if (id.tokenScopes) return id.tokenScopes;
70
- if (_platform === 'ksuite') return ""; // KSuite doesn't use standard OAuth scopes in same way
70
+ if (_platform === 'ksuite') return "";
71
71
 
72
- // Google fallback
73
- return getAccessTokenInfo().then(info => {
74
- id.tokenScopes = info.tokenInfo.scope;
75
- return id.tokenScopes;
76
- });
72
+ // If we have an authClient, we might be able to discover them
73
+ if (id.authClient) {
74
+ return getAccessTokenInfo().then(info => {
75
+ id.tokenScopes = info.tokenInfo.scope;
76
+ return id.tokenScopes;
77
+ });
78
+ }
79
+
80
+ return "";
77
81
  };
78
82
 
79
83
  const getHashedUserId = () =>
@@ -219,6 +223,12 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
219
223
  return { access_token: token };
220
224
  };
221
225
 
226
+ dwdClient.invalidateToken = function () {
227
+ this._token = null;
228
+ this._expiresAt = 0;
229
+ this.credentials = { access_token: 'dummy' };
230
+ };
231
+
222
232
  id.authClient = dwdClient
223
233
  }
224
234
  } catch (error) {
@@ -290,8 +300,17 @@ export const responseSyncify = (result) => {
290
300
 
291
301
  // Helper to populate identity from sxInit response
292
302
  const setIdentity = (platform, data) => {
303
+ if (!data) return;
304
+ // slogger.warn(`...DEBUG: Auth.setIdentity for platform=${platform}. data keys=${Object.keys(data).join(',')}`);
293
305
  const id = _getIdentity(platform);
294
306
  Object.assign(id, data);
307
+ // slogger.warn(`...DEBUG: Auth.setIdentity result for platform=${platform}. id.tokenScopes=${id.tokenScopes}`);
308
+ };
309
+
310
+ const getAuthedScopes = () => {
311
+ const id = _getIdentity('google');
312
+ const scopes = id.tokenScopes || "";
313
+ return new Set((typeof scopes === 'string' ? scopes : "").split(" ").filter(s => s));
295
314
  };
296
315
 
297
316
  export const Auth = {
@@ -305,6 +324,7 @@ export const Auth = {
305
324
  getUserId,
306
325
  getAccessToken,
307
326
  getTokenScopes,
327
+ getAuthedScopes,
308
328
  getScriptId,
309
329
  getDocumentId,
310
330
  setSettings,
@@ -134,4 +134,7 @@ export const Proxies = {
134
134
  guard,
135
135
  blanketProxy,
136
136
  getRegisteredServices: () => Array.from(serviceRegistry),
137
- }
137
+ getLoadedServices: () => Array.from(loadedRegistry),
138
+ __addService: (name) => serviceRegistry.add(name),
139
+ __addLoaded: (name) => loadedRegistry.add(name),
140
+ }
@@ -15,17 +15,16 @@ import { KSuiteDrive } from './ksuite/kdrive.js';
15
15
  let _loggedSummary = false;
16
16
 
17
17
  /**
18
- * initialize ke stuff at the beginning such as manifest content and settings
18
+ * initialize key stuff at the beginning such as manifest content and settings
19
19
  * @param {object} p pargs
20
- * @param {string} p.manifestPath where to finfd the manifest by default
21
- * @param {string} p.authPath import the auth code
20
+ * @param {string} p.manifestPath where to find the manifest by default
22
21
  * @param {string} p.claspPath where to find the clasp file by default
23
22
  * @param {string} p.settingsPath where to find the settings file
24
23
  * @param {string} p.cachePath the cache files
25
24
  * @param {string} p.propertiesPath the properties file location
26
25
  * @param {string} p.fakeId a fake script id to use if one isnt in the settings
27
26
  * @param {string[]} [p.platformAuth] list of platforms to authenticate
28
- * @return {object} the finalized vesions of all the above
27
+ * @return {object} the finalized versions of all the above
29
28
  */
30
29
  export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath, propertiesPath, fakeId, platformAuth }) => {
31
30
 
@@ -34,6 +33,7 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
34
33
 
35
34
  // get a file and parse if it exists
36
35
  const getIfExists = async (file) => {
36
+ if (!file) return {};
37
37
  try {
38
38
  const content = await readFile(file, { encoding: 'utf8' })
39
39
  return JSON.parse(content)
@@ -42,30 +42,21 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
42
42
  }
43
43
  }
44
44
 
45
- const settingsDir = path.dirname(settingsPath)
46
- const _settings = await getIfExists(settingsPath)
47
- const settings = { ..._settings }
45
+ const manifestFile = process.env.GF_MANIFEST_PATH || manifestPath;
46
+ const claspFile = process.env.GF_CLASP_PATH || claspPath;
48
47
 
49
- settings.manifest = settings.manifest || manifestPath
50
- settings.clasp = settings.clasp || claspPath
51
48
  const [manifest, clasp] = await Promise.all([
52
- getIfExists(path.resolve(settingsDir, settings.manifest)),
53
- getIfExists(path.resolve(settingsDir, settings.clasp))
49
+ getIfExists(manifestFile),
50
+ getIfExists(claspFile)
54
51
  ])
55
52
 
56
- settings.scriptId = settings.scriptId || clasp.scriptId || fakeId
57
- settings.documentId = settings.documentId || null
58
- settings.cache = settings.cache || cachePath
59
- settings.properties = settings.properties || propertiesPath
60
-
61
- const strSet = JSON.stringify(settings, null, 2)
62
- if (JSON.stringify(_settings, null, 2) !== strSet) {
63
- try {
64
- await mkdir(settingsDir, { recursive: true })
65
- await writeFile(settingsPath, strSet, { flag: 'w' })
66
- } catch (err) {
67
- syncWarn(`...unable to write settings file: ${err}`)
68
- }
53
+ const settings = {
54
+ manifest: manifestFile,
55
+ clasp: claspFile,
56
+ scriptId: process.env.GF_SCRIPT_ID || clasp.scriptId || fakeId,
57
+ documentId: process.env.GF_DOCUMENT_ID || null,
58
+ cache: process.env.GF_CACHE_PATH || cachePath,
59
+ properties: process.env.GF_PROPERTIES_PATH || propertiesPath
69
60
  }
70
61
 
71
62
  const identities = {};
@@ -73,6 +64,9 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
73
64
  // --- Google Auth Block ---
74
65
  if (platforms.includes('google') || platforms.includes('workspace')) {
75
66
  try {
67
+ // Ensure platform is set for info discovery
68
+ Auth.setPlatform('google');
69
+
76
70
  const scopes = manifest.oauthScopes || []
77
71
  const mandatoryScopes = [
78
72
  "openid",
@@ -105,7 +99,7 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
105
99
  activeUser,
106
100
  effectiveUser,
107
101
  projectId: Auth.getProjectId(),
108
- tokenScopes: effectiveInfo.tokenInfo.scopes,
102
+ tokenScopes: effectiveInfo.tokenInfo.scopes || effectiveInfo.tokenInfo.scope,
109
103
  authMethod: Auth.getAuthMethod('google')
110
104
  };
111
105
 
@@ -125,6 +119,7 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
125
119
  syncWarn("ksuite requested in platformAuth but KSUITE_TOKEN is missing from environment.");
126
120
  } else {
127
121
  try {
122
+ Auth.setPlatform('ksuite');
128
123
  const kDrive = new KSuiteDrive(kToken);
129
124
  const accountId = await kDrive.getAccountId();
130
125
 
@@ -152,6 +147,10 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
152
147
  }
153
148
  }
154
149
 
150
+ // Restore default platform context based on authorized backends
151
+ const defaultPlatform = platforms[0] === 'google' ? 'workspace' : platforms[0];
152
+ Auth.setPlatform(defaultPlatform);
153
+
155
154
  // Final Summary Report (Concise, single instance)
156
155
  if (!_loggedSummary) {
157
156
  const summary = Object.keys(identities).map(p => {
@@ -166,7 +165,9 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
166
165
  }).join(', ');
167
166
 
168
167
  if (summary) {
168
+ const scriptIdSource = process.env.GF_SCRIPT_ID ? 'env' : (clasp.scriptId ? 'clasp' : 'random');
169
169
  syncLog(`...authorized backends: ${summary}`);
170
+ syncLog(`...using scriptId: ${settings.scriptId} (source: ${scriptIdSource})`);
170
171
  _loggedSummary = true;
171
172
  }
172
173
  }
@@ -36,7 +36,7 @@ export const sxRetry = async (Auth, tag, func, options = {}) => {
36
36
  response = err.response;
37
37
  }
38
38
 
39
- const redoCodes = [429, 500, 503, 408, 401];
39
+ const redoCodes = [429, 500, 502, 503, 408, 401];
40
40
  const networkErrorCodes = [
41
41
  'ETIMEDOUT',
42
42
  'ECONNRESET',
@@ -23,7 +23,6 @@ import { callSync } from "./workersync/synchronizer.js";
23
23
 
24
24
  const manifestDefaultPath = "./appsscript.json";
25
25
  const claspDefaultPath = "./.clasp.json";
26
- const settingsDefaultPath = process.env.GF_SETTINGS_PATH || "./gasfakes.json";
27
26
  const propertiesDefaultPath = "/tmp/gas-fakes/properties";
28
27
  const cacheDefaultPath = "/tmp/gas-fakes/cache";
29
28
 
@@ -271,18 +270,16 @@ const fxUnzipper = ({ blob }) => {
271
270
  * initialize all the stuff at the beginning such as manifest content and settings
272
271
  * and register them all in Auth object for future reference
273
272
  * @param {object} p pargs
274
- * @param {string} p.manifestPath where to finfd the manifest by default
273
+ * @param {string} p.manifestPath where to find the manifest by default
275
274
  * @param {string} p.claspPath where to find the clasp file by default
276
- * @param {string} p.settingsPath where to find the settings file
277
275
  * @param {string} p.cachePath the cache files
278
276
  * @param {string} p.propertiesPath the properties file location
279
277
  * @param {string[]} [p.platformAuth] list of platforms to authenticate
280
- * @return {object} the finalized vesions of all the above
278
+ * @return {object} the finalized versions of all the above
281
279
  */
282
280
  export const fxInit = ({
283
281
  manifestPath = manifestDefaultPath,
284
282
  claspPath = claspDefaultPath,
285
- settingsPath = settingsDefaultPath,
286
283
  cachePath = cacheDefaultPath,
287
284
  propertiesPath = propertiesDefaultPath,
288
285
  platformAuth
@@ -296,7 +293,6 @@ export const fxInit = ({
296
293
  // because this is all run in a synced subprocess it's not an async result
297
294
  const synced = callSync("sxInit", {
298
295
  claspPath: resolve(claspPath),
299
- settingsPath: resolve(settingsPath),
300
296
  manifestPath: resolve(manifestPath),
301
297
  mainDir,
302
298
  cachePath,
@@ -317,9 +313,12 @@ export const fxInit = ({
317
313
  Auth.setClasp(clasp);
318
314
  Auth.setManifest(manifest);
319
315
 
316
+ // console.log(`...DEBUG: fxInit identities received keys=${Object.keys(identities || {}).join(',')}`);
317
+
320
318
  // Populate all identities
321
319
  if (identities) {
322
320
  Object.keys(identities).forEach(p => {
321
+ // console.log(`...DEBUG: fxInit populating identity for ${p}. scopes=${identities[p].tokenScopes}`);
323
322
  Auth.setIdentity(p, identities[p]);
324
323
  });
325
324
  }
package/gasfakes.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "manifest": "./appsscript.json",
3
- "clasp": "./.clasp.json",
4
- "scriptId": "28780abe-aaec-4526-a0bf-9d5c104c68b8",
5
- "documentId": "1h9IGIShgVBVUrUjjawk5MaCEQte_7t32XeEP1Z5jXKQ",
6
- "cache": "/tmp/gas-fakes/cache",
7
- "properties": "/tmp/gas-fakes/properties"
8
- }