@mcpher/gas-fakes 2.2.6 → 2.3.0

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
 
@@ -168,11 +170,12 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
168
170
  ## Read more docs
169
171
 
170
172
  - [gas fakes intro video](https://youtu.be/oEjpIrkYpEM)
171
- - [getting started](GETTING_STARTED.md) - how to handle authentication for restricted scopes.
173
+ - [getting started](GETTING_STARTED.md) - how to handle authentication for Workspace scopes.
172
174
  - [readme](README.md)
173
175
  - [gas fakes cli](gas-fakes-cli.md)
174
176
  - [ksuite as a back end](ksuite_poc.md)
175
177
  - [msgraph as a back end](msgraph.md)
178
+ - [gas-fakes in serverless containers](https://docs.google.com/presentation/d/1JlXF9T--DD4ERHopyP3WyAMhjRCxxHblgCP5ynxaJ3k/edit?usp=sharing)
176
179
  - [apps script - a lingua franca for workspace platforms](https://ramblings.mcpher.com/apps-script-a-lingua-franca/)
177
180
  - [Apps Script: A ‘Lingua Franca’ for the Multi-Cloud Era](https://ramblings.mcpher.com/apps-script-with-ksuite/)
178
181
  - [running gas-fakes on google cloud run](https://github.com/brucemcpherson/gas-fakes-containers)
@@ -191,17 +194,17 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
191
194
  - [oddities](oddities.md) - a collection of oddities uncovered during this project
192
195
  - [named colors](named-colors.md)
193
196
  - [sandbox](sandbox.md)
194
- - [senstive scopes](senstive_scopes.md)
197
+ - [senstive scopes](workspace_scopes.md)
195
198
  - [using apps script libraries with gas-fakes](libraries.md)
196
199
  - [how libhandler works](libhandler.md)
197
200
  - [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
198
201
  - [named range identity](named-range-identity.md)
199
- - [adc and restricted scopes](https://ramblings.mcpher.com/how-to-allow-access-to-sensitive-scopes-with-application-default-credentials/)
202
+ - [Workspace scopes with local authentication](workspace_scopes.md)
200
203
  - [push test pull](pull-test-push.md)
201
204
  - [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
205
  - [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
206
  - [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/)
207
+ - [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
208
  - [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
206
209
  - [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
207
210
  - [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
@@ -3,7 +3,7 @@
3
3
  "node": ">=20.11.0"
4
4
  },
5
5
  "dependencies": {
6
- "@azure/identity": "^4.13.0",
6
+ "@azure/identity": "^4.13.1",
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",
@@ -13,7 +13,7 @@
13
13
  "archiver": "^7.0.1",
14
14
  "commander": "^14.0.3",
15
15
  "dotenv": "^17.3.1",
16
- "fast-xml-parser": "^5.4.2",
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.6",
42
+ "version": "2.3.0",
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
@@ -285,9 +285,26 @@ export async function initializeConfiguration(options = {}) {
285
285
  "https://www.googleapis.com/auth/cloud-platform",
286
286
  ];
287
287
  responses.DEFAULT_SCOPES = DEFAULT_SCOPES_VALUES.join(",");
288
- responses.EXTRA_SCOPES = manifestScopes
289
- .filter(s => !DEFAULT_SCOPES_VALUES.includes(s))
290
- .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(",");
291
308
 
292
309
  const googleQuestions = [
293
310
  {
@@ -303,12 +320,12 @@ export async function initializeConfiguration(options = {}) {
303
320
  initial: existingConfig.GOOGLE_SERVICE_ACCOUNT_NAME || "gas-fakes-sa",
304
321
  },
305
322
  {
306
- type: "text",
323
+ type: responses.AUTH_TYPE === "adc" ? "text" : null,
307
324
  name: "CLIENT_CREDENTIAL_FILE",
308
- 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)",
309
326
  initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
310
327
  }
311
- ];
328
+ ];
312
329
 
313
330
  const googleResponses = await prompts(googleQuestions);
314
331
  if (typeof googleResponses.GOOGLE_CLOUD_PROJECT === "undefined") {
@@ -342,10 +359,10 @@ export async function initializeConfiguration(options = {}) {
342
359
  message: "What type of Microsoft account are you using?",
343
360
  choices: [
344
361
  { title: "Consumer (Personal, Outlook.com, Hotmail, etc.)", value: "consumers" },
345
- { title: "Business/Education (Work or School)", value: "organizations" },
362
+ { title: "Business/Education (Work or School) ", value: "organizations" },
346
363
  { title: "Standard Multi-tenant", value: "common" }
347
364
  ],
348
- initial: existingConfig.MS_GRAPH_TENANT_ID === "consumers" ? 0 : (existingConfig.MS_GRAPH_TENANT_ID === "organizations" ? 1 : 2)
365
+ initial: existingConfig.MS_GRAPH_TENANT_ID === "organizations" ? 1 : (existingConfig.MS_GRAPH_TENANT_ID === "common" ? 2 : 0)
349
366
  });
350
367
 
351
368
  if (typeof msAccountType.type === "undefined") {
@@ -604,13 +621,13 @@ export async function authenticateUser(options = {}) {
604
621
  const msScopes = mapGasScopesToMsGraph(gasScopes);
605
622
 
606
623
  try {
624
+ const tenantId = process.env.MS_GRAPH_TENANT_ID || 'consumers';
607
625
  const azCmd = `az config set core.login_experience_v2=off && az login --allow-no-subscriptions --output none`;
608
626
 
609
627
  console.log(`Executing: ${azCmd}`);
610
628
  try {
611
629
  runCommandSync(azCmd);
612
630
  console.log(`\n\x1b[1;32mSuccess!\x1b[0m Azure CLI session discovered.`);
613
- const tenantId = process.env.MS_GRAPH_TENANT_ID || 'consumers';
614
631
  console.log(`Silent fallback is now enabled for: \x1b[1;36m${tenantId}\x1b[0m`);
615
632
  } catch (e) {
616
633
  console.error(`\x1b[1;31mAzure CLI Login failed.\x1b[0m`);
@@ -662,7 +679,27 @@ export async function authenticateUser(options = {}) {
662
679
  ...(EXTRA_SCOPES || "").split(","),
663
680
  ])).filter(s => s).join(",");
664
681
 
665
- console.log(`...requesting scopes: ${scopes}`);
682
+ let adcScopes = AUTH_TYPE === "dwd"
683
+ ? Array.from(new Set((DEFAULT_SCOPES || "").split(","))).filter(s => s).join(",")
684
+ : scopes;
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
+
702
+ console.log(`...requesting scopes: ${adcScopes}`);
666
703
 
667
704
  const driveAccessFlag = "--enable-gdrive-access";
668
705
  const activeConfig = AC || "default";
@@ -684,7 +721,7 @@ export async function authenticateUser(options = {}) {
684
721
  runCommandSync(`gcloud auth login ${driveAccessFlag}`);
685
722
 
686
723
  let clientFlag = "";
687
- if (CLIENT_CREDENTIAL_FILE) {
724
+ if (AUTH_TYPE !== "dwd" && CLIENT_CREDENTIAL_FILE) {
688
725
  const clientPath = path.resolve(process.cwd(), CLIENT_CREDENTIAL_FILE);
689
726
  if (fs.existsSync(clientPath)) {
690
727
  console.log(`...using client credentials from ${clientPath}`);
@@ -693,7 +730,8 @@ export async function authenticateUser(options = {}) {
693
730
  }
694
731
 
695
732
  console.log("Setting up Application Default Credentials (ADC)...");
696
- runCommandSync(`gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`);
733
+ const adcScopeFlag = `--scopes="${adcScopes}"`;
734
+ runCommandSync(`gcloud auth application-default login ${adcScopeFlag} ${clientFlag}`.trim());
697
735
  runCommandSync(`gcloud auth application-default set-quota-project ${projectId}`);
698
736
 
699
737
  // --- DWD Specific Setup (if configured) ---
@@ -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) {
@@ -0,0 +1,58 @@
1
+ import { Proxies } from "../../support/proxies.js";
2
+
3
+ /**
4
+ * create a new FakeContainerInfo instance
5
+ * @param {...any} args
6
+ * @returns {FakeContainerInfo}
7
+ */
8
+ export const newFakeContainerInfo = (...args) => {
9
+ return Proxies.guard(new FakeContainerInfo(...args));
10
+ };
11
+
12
+ /**
13
+ * Access to the chart's container position.
14
+ */
15
+ export class FakeContainerInfo {
16
+ /**
17
+ * @param {object} overlayPosition The overlayPosition object from Sheets API
18
+ */
19
+ constructor(overlayPosition) {
20
+ this.__overlayPosition = overlayPosition || {};
21
+ }
22
+
23
+ /**
24
+ * Returns the column index where the drawing is anchored.
25
+ * @returns {number}
26
+ */
27
+ getAnchorColumn() {
28
+ return (this.__overlayPosition.anchorCell?.columnIndex || 0) + 1;
29
+ }
30
+
31
+ /**
32
+ * Returns the row index where the drawing is anchored.
33
+ * @returns {number}
34
+ */
35
+ getAnchorRow() {
36
+ return (this.__overlayPosition.anchorCell?.rowIndex || 0) + 1;
37
+ }
38
+
39
+ /**
40
+ * Returns the horizontal offset in pixels from the anchor column.
41
+ * @returns {number}
42
+ */
43
+ getOffsetX() {
44
+ return this.__overlayPosition.offsetXPixels || 0;
45
+ }
46
+
47
+ /**
48
+ * Returns the vertical offset in pixels from the anchor row.
49
+ * @returns {number}
50
+ */
51
+ getOffsetY() {
52
+ return this.__overlayPosition.offsetYPixels || 0;
53
+ }
54
+
55
+ toString() {
56
+ return "ContainerInfo";
57
+ }
58
+ }
@@ -2,6 +2,7 @@ import { Proxies } from "../../support/proxies.js";
2
2
  import { notYetImplemented, signatureArgs } from "../../support/helpers.js";
3
3
  import { batchUpdate } from "./sheetrangehelpers.js";
4
4
  import { newFakeEmbeddedChartBuilder } from "./fakeembeddedchartbuilder.js";
5
+ import { newFakeContainerInfo } from "./fakecontainerinfo.js";
5
6
 
6
7
  /**
7
8
  * @returns {FakeEmbeddedChart}
@@ -25,13 +26,20 @@ export class FakeEmbeddedChart {
25
26
  const props = [
26
27
  "getAs",
27
28
  "getBlob",
28
- "getContainerInfo",
29
29
  ];
30
30
  props.forEach((f) => {
31
31
  this[f] = () => notYetImplemented(f);
32
32
  });
33
33
  }
34
34
 
35
+ /**
36
+ * Returns information about where the chart is positioned within a sheet.
37
+ * @returns {FakeContainerInfo}
38
+ */
39
+ getContainerInfo() {
40
+ return newFakeContainerInfo(this.__apiChart.position?.overlayPosition);
41
+ }
42
+
35
43
  /**
36
44
  * Returns the ID of this chart.
37
45
  * @returns {number}
@@ -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,7 +191,14 @@ 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
204
  const { tokenInfo: userInfo } = await _getTokenInfo(id.sourceClient);
@@ -166,7 +166,7 @@ async function isGcpEnv() {
166
166
  * Gets a Microsoft Graph token.
167
167
  */
168
168
  export async function getMsGraphToken(scopes = ['User.Read']) {
169
- const envTenant = process.env.MS_GRAPH_TENANT_ID || 'common';
169
+ const envTenant = process.env.MS_GRAPH_TENANT_ID || 'consumers';
170
170
  const clientId = process.env.MS_GRAPH_CLIENT_ID;
171
171
  const clientSecret = process.env.MS_GRAPH_CLIENT_SECRET;
172
172
  const msAuthType = process.env.MS_AUTH_TYPE;
@@ -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,