@mcpher/gas-fakes 2.2.7 → 2.3.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/README.md CHANGED
@@ -19,8 +19,10 @@ For a complete guide on how to set up your local environment for authentication
19
19
  Collaborators should fork the repo and use the local versions of these files - see collaborators info.
20
20
 
21
21
  ### Use exactly the same code as in Apps Script
22
+ ## Usage
23
+ Just as on Apps Script, everything is executed synchronously so you don't need to bother with handling Promises/async/await. Just write normal Apps Script code. Usually you would have an associated App Script project if that's your eventual target. Just like Apps Script, you need a manifest file (appsscript.json) so you can be sure that the correct scopes are authorized and asked for.
22
24
 
23
- Just as on Apps Script, everything is executed synchronously so you don't need to bother with handling Promises/async/await. Just write normal Apps Script code. Usually you would have an associated App Script project if that's your eventual target. Just like Apps Script, you need a manifest file (appsscript.json) so you can be sure that the correct scopes are authroized and asked for.
25
+ > **Note on `appsscript.json`:** For full fidelity with live Apps Script, you should have a manifest available. However, as a workaround for "pure" `gas-fakes` projects where there is no intention to run in live Apps Script, if `appsscript.json` is missing or has no scopes, the runtime will automatically emulate one using the `DEFAULT_SCOPES` and `EXTRA_SCOPES` defined in your `.env` file, and will default the script's `timeZone` to `America/New_York` (or `GF_TIMEZONE`).
24
26
 
25
27
  # gas-fakes-cli
26
28
 
@@ -49,6 +51,17 @@ Configuration for your local Node environment is handled via environment variabl
49
51
  | `LOG_DESTINATION` | `CONSOLE` | Logging destination: `CONSOLE`, `CLOUD`, `BOTH`, or `NONE`. |
50
52
  | `STORE_TYPE` | `FILE` | Internal storage type for properties/cache: `FILE` (local) or `UPSTASH` (Redis). |
51
53
 
54
+ ### Note on Consumer Accounts and ADC
55
+
56
+ If you are using a consumer account (gmail.com) or do not have access to Domain-Wide Delegation (DWD), you must use Application Default Credentials (`AUTH_TYPE="adc"`) to authenticate with Google Workspace assets.
57
+
58
+ **Important Security Note:** Due to recent Workspace security changes, you **must** create and provide a custom OAuth2 client credentials JSON file during initialization (`gas-fakes init`) to avoid `403 Access Blocked` errors when using Workspace scopes (like Gmail or Drive).
59
+
60
+ - **Guide:** [How to allow access to Workspace scopes with Application Default Credentials](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
61
+ - **Link to Create Credentials:** [Google Cloud Console Credentials Page](https://console.cloud.google.com/apis/credentials)
62
+
63
+ **Security Warning:** Always add your client credentials JSON file to your `.gitignore` to prevent it from being committed to GitHub or other source control.
64
+
52
65
 
53
66
 
54
67
  ### Cloud Logging Integration
@@ -168,11 +181,12 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
168
181
  ## Read more docs
169
182
 
170
183
  - [gas fakes intro video](https://youtu.be/oEjpIrkYpEM)
171
- - [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
184
+ - [getting started](GETTING_STARTED.md) - how to handle authentication for Workspace scopes.
172
185
  - [readme](README.md)
173
186
  - [gas fakes cli](gas-fakes-cli.md)
174
187
  - [ksuite as a back end](ksuite_poc.md)
175
188
  - [msgraph as a back end](msgraph.md)
189
+ - [gas-fakes in serverless containers](https://docs.google.com/presentation/d/1JlXF9T--DD4ERHopyP3WyAMhjRCxxHblgCP5ynxaJ3k/edit?usp=sharing)
176
190
  - [apps script - a lingua franca for workspace platforms](https://ramblings.mcpher.com/apps-script-a-lingua-franca/)
177
191
  - [Apps Script: A ‘Lingua Franca’ for the Multi-Cloud Era](https://ramblings.mcpher.com/apps-script-with-ksuite/)
178
192
  - [running gas-fakes on google cloud run](https://github.com/brucemcpherson/gas-fakes-containers)
@@ -191,17 +205,17 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
191
205
  - [oddities](oddities.md) - a collection of oddities uncovered during this project
192
206
  - [named colors](named-colors.md)
193
207
  - [sandbox](sandbox.md)
194
- - [senstive scopes](senstive_scopes.md)
208
+ - [senstive scopes](workspace_scopes.md)
195
209
  - [using apps script libraries with gas-fakes](libraries.md)
196
210
  - [how libhandler works](libhandler.md)
197
211
  - [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
198
212
  - [named range identity](named-range-identity.md)
199
- - [sensitive scopes with local authentication](senstive_scopes.md)
213
+ - [Workspace scopes with local authentication](workspace_scopes.md)
200
214
  - [push test pull](pull-test-push.md)
201
215
  - [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
202
216
  - [gas-fakes-cli now has built in mcp server and gemini extension](https://ramblings.mcpher.com/gas-fakes-cli-now-has-built-in-mcp-server-and-gemini-extension/)
203
217
  - [gas-fakes CLI: Run apps script code directly from your terminal](https://ramblings.mcpher.com/gas-fakes-cli-run-apps-script-code-directly-from-your-terminal/)
204
- - [How to allow access to sensitive scopes with Application Default Credentials](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
218
+ - [How to allow access to Workspace scopes with Application Default Credentials](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
205
219
  - [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
206
220
  - [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
207
221
  - [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
package/package.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "@mcpher/fake-gasenum": "^1.0.6",
8
8
  "@mcpher/gas-flex-cache": "^1.1.5",
9
9
  "@microsoft/microsoft-graph-client": "^3.0.7",
10
- "@modelcontextprotocol/sdk": "^1.27.1",
10
+ "@modelcontextprotocol/sdk": "^1.28.0",
11
11
  "@sindresorhus/is": "^7.2.0",
12
12
  "acorn": "^8.16.0",
13
- "archiver": "^7.0.1",
13
+ "archiver": "^4.0.2",
14
14
  "commander": "^14.0.3",
15
15
  "dotenv": "^17.3.1",
16
- "fast-xml-parser": "^5.5.8",
16
+ "fast-xml-parser": "^5.5.9",
17
17
  "get-stream": "^9.0.1",
18
18
  "google-auth-library": "^10.6.2",
19
19
  "googleapis": "^171.4.0",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "name": "@mcpher/gas-fakes",
41
41
  "author": "bruce mcpherson",
42
- "version": "2.2.7",
42
+ "version": "2.3.1",
43
43
  "license": "MIT",
44
44
  "main": "main.js",
45
45
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
@@ -6,8 +6,10 @@ import { checkForGcloudCli, spawnCommand } from "./utils.js";
6
6
  async function getAccessToken(pattern) {
7
7
  if (pattern == 1) {
8
8
  // Authorization pattern 1
9
+ // We use cloud-platform as it provides sufficient access for Drive API fetching
10
+ // while avoiding the 'well-known client ID' block that targets Workspace scopes like drive.readonly.
9
11
  const auth = await Auth.setAuth(
10
- ["https://www.googleapis.com/auth/drive.readonly"],
12
+ ["https://www.googleapis.com/auth/cloud-platform"],
11
13
  true
12
14
  );
13
15
  auth.cachedCredential = null;
package/src/cli/setup.js CHANGED
@@ -283,12 +283,28 @@ export async function initializeConfiguration(options = {}) {
283
283
  "https://www.googleapis.com/auth/userinfo.email",
284
284
  "openid",
285
285
  "https://www.googleapis.com/auth/cloud-platform",
286
- "https://www.googleapis.com/auth/drive.readonly",
287
286
  ];
288
287
  responses.DEFAULT_SCOPES = DEFAULT_SCOPES_VALUES.join(",");
289
- responses.EXTRA_SCOPES = manifestScopes
290
- .filter(s => !DEFAULT_SCOPES_VALUES.includes(s))
291
- .join(",");
288
+
289
+ let extraScopes = manifestScopes.filter(s => !DEFAULT_SCOPES_VALUES.includes(s));
290
+
291
+ // Restricted scopes that trigger blocks for the default 'well-known' ADC client ID
292
+ const restrictedMatch = (s) =>
293
+ s.includes("auth/drive") ||
294
+ s.includes("auth/spreadsheets") ||
295
+ s.includes("auth/documents") ||
296
+ s.includes("auth/forms") ||
297
+ s.includes("auth/presentations") ||
298
+ s.includes("auth/script.external_request");
299
+
300
+ if (responses.AUTH_TYPE === "adc" && !responses.CLIENT_CREDENTIAL_FILE) {
301
+ const toSkip = extraScopes.filter(restrictedMatch);
302
+ if (toSkip.length > 0) {
303
+ console.log(`\n\x1b[1;33mWarning: ADC requested with Workspace scopes (${toSkip.map(s => s.split("/").pop()).join(", ")}). Google now blocks these scopes when using the default gcloud CLI client ID. You MUST provide a custom OAuth client credential file.\x1b[0m`);
304
+ }
305
+ }
306
+
307
+ responses.EXTRA_SCOPES = extraScopes.join(",");
292
308
 
293
309
  const googleQuestions = [
294
310
  {
@@ -306,7 +322,7 @@ export async function initializeConfiguration(options = {}) {
306
322
  {
307
323
  type: responses.AUTH_TYPE === "adc" ? "text" : null,
308
324
  name: "CLIENT_CREDENTIAL_FILE",
309
- message: "Enter path to OAuth client credentials JSON (optional, required for restricted scopes with ADC)",
325
+ message: "Enter path to OAuth client credentials JSON (optional, required for Workspace scopes with ADC)",
310
326
  initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
311
327
  }
312
328
  ];
@@ -663,10 +679,26 @@ export async function authenticateUser(options = {}) {
663
679
  ...(EXTRA_SCOPES || "").split(","),
664
680
  ])).filter(s => s).join(",");
665
681
 
666
- const adcScopes = AUTH_TYPE === "dwd"
682
+ let adcScopes = AUTH_TYPE === "dwd"
667
683
  ? Array.from(new Set((DEFAULT_SCOPES || "").split(","))).filter(s => s).join(",")
668
684
  : scopes;
669
685
 
686
+ const hasClientId = CLIENT_CREDENTIAL_FILE && fs.existsSync(path.resolve(process.cwd(), CLIENT_CREDENTIAL_FILE));
687
+ if (AUTH_TYPE !== "dwd" && !hasClientId) {
688
+ const scopeList = adcScopes.split(",");
689
+ const workspaceScopes = scopeList.filter(s =>
690
+ s.includes("auth/drive") ||
691
+ s.includes("auth/spreadsheets") ||
692
+ s.includes("auth/documents") ||
693
+ s.includes("auth/forms") ||
694
+ s.includes("auth/presentations")
695
+ );
696
+
697
+ if (workspaceScopes.length > 0) {
698
+ console.warn(`\n\x1b[1;33mWarning: Workspace scopes (${workspaceScopes.map(s => s.split("/").pop()).join(", ")}) requested with ADC and no CLIENT_CREDENTIAL_FILE. This is expected to be blocked by Google.\x1b[0m`);
699
+ }
700
+ }
701
+
670
702
  console.log(`...requesting scopes: ${adcScopes}`);
671
703
 
672
704
  const driveAccessFlag = "--enable-gdrive-access";
@@ -114,7 +114,9 @@ const checkScopesMatch = (required) => {
114
114
  "https://www.googleapis.com/auth/script.external_request",
115
115
  "https://www.googleapis.com/auth/documents",
116
116
  "https://www.googleapis.com/auth/presentations",
117
- "https://www.googleapis.com/auth/forms"
117
+ "https://www.googleapis.com/auth/forms",
118
+ "https://www.googleapis.com/auth/drive",
119
+ "https://www.googleapis.com/auth/spreadsheets"
118
120
  ]
119
121
  const hasIgnore = ignores.some(i => i.replace(/\/$/, "") === ns)
120
122
  if (hasIgnore) {
@@ -167,6 +167,22 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
167
167
 
168
168
  if (!useDwd) {
169
169
  id.authMethod = 'adc'
170
+
171
+ const workspaceScopes = scopes.filter(s =>
172
+ s.includes('auth/drive') ||
173
+ s.includes('auth/spreadsheets') ||
174
+ s.includes('auth/documents') ||
175
+ s.includes('auth/forms') ||
176
+ s.includes('auth/presentations')
177
+ )
178
+
179
+ const creds = await id.auth.getCredentials();
180
+ const isDefaultClient = creds && creds.client_id === '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com';
181
+
182
+ if (isDefaultClient && workspaceScopes.length > 0) {
183
+ throw new Error(`ADC error: Google no longer allows the use of the default gcloud client ID for regular Workspace scopes (${workspaceScopes.map(s => s.split('/').pop()).join(', ')}). You must use a custom client credential file. See https://docs.cloud.google.com/docs/authentication/troubleshoot-adc#access_blocked_when_using_scopes`);
184
+ }
185
+
170
186
  id.authClient = await id.auth.getClient({ scopes })
171
187
  id.sourceClient = id.authClient
172
188
  } else {
@@ -175,11 +191,21 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
175
191
  id.authMethod = 'dwd'
176
192
  const targetPrincipal = `${saName}@${id.projectId}.iam.gserviceaccount.com`
177
193
 
178
- const sourceScopes = scopes.filter(s => s === 'openid' || s === 'https://www.googleapis.com/auth/userinfo.email')
194
+ // For DWD source client, we only need identity and enough scope to sign JWT
195
+ // cloud-platform is sufficient for IAM signJwt and avoid Drive-related blocks
196
+ const sourceScopes = scopes.filter(s =>
197
+ s === 'openid' ||
198
+ s === 'https://www.googleapis.com/auth/userinfo.email' ||
199
+ s === 'https://www.googleapis.com/auth/cloud-platform'
200
+ )
201
+
179
202
  id.sourceClient = await id.auth.getClient(sourceScopes.length > 0 ? { scopes: sourceScopes } : {})
180
203
 
181
- const { tokenInfo: userInfo } = await _getTokenInfo(id.sourceClient);
182
- const userEmail = process.env.GOOGLE_WORKSPACE_SUBJECT || userInfo.email
204
+ let userEmail = process.env.GOOGLE_WORKSPACE_SUBJECT;
205
+ if (!userEmail) {
206
+ const { tokenInfo: userInfo } = await _getTokenInfo(id.sourceClient);
207
+ userEmail = userInfo.email;
208
+ }
183
209
 
184
210
  const dwdClient = new OAuth2Client()
185
211
  dwdClient._token = null
@@ -52,6 +52,24 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
52
52
  getIfExists(claspFile)
53
53
  ])
54
54
 
55
+ // Emulate manifest scopes from .env if missing or empty
56
+ if (!manifest.oauthScopes || manifest.oauthScopes.length === 0) {
57
+ const envScopes = Array.from(new Set([
58
+ ...(process.env.DEFAULT_SCOPES || "").split(","),
59
+ ...(process.env.EXTRA_SCOPES || "").split(",")
60
+ ])).map(s => s.trim()).filter(s => s);
61
+
62
+ if (envScopes.length > 0) {
63
+ manifest.oauthScopes = envScopes;
64
+ if (!manifest.timeZone) {
65
+ manifest.timeZone = process.env.GF_TIMEZONE || "America/New_York";
66
+ }
67
+ if (!_loggedSummary) {
68
+ syncLog(`...appsscript.json missing or missing scopes. Emulating manifest using scopes from .env file`);
69
+ }
70
+ }
71
+ }
72
+
55
73
  const settings = {
56
74
  manifest: manifestFile,
57
75
  clasp: claspFile,