@mcpher/gas-fakes 2.2.5 → 2.2.7

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
@@ -196,7 +196,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
196
196
  - [how libhandler works](libhandler.md)
197
197
  - [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
198
198
  - [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/)
199
+ - [sensitive scopes with local authentication](senstive_scopes.md)
200
200
  - [push test pull](pull-test-push.md)
201
201
  - [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
202
  - [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/)
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,8 +13,9 @@
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.8",
17
17
  "get-stream": "^9.0.1",
18
+ "google-auth-library": "^10.6.2",
18
19
  "googleapis": "^171.4.0",
19
20
  "got": "^14.6.6",
20
21
  "into-stream": "^9.1.0",
@@ -26,6 +27,9 @@
26
27
  "unzipper": "^0.12.3",
27
28
  "zod": "^4.3.6"
28
29
  },
30
+ "overrides": {
31
+ "glob": "^13.0.6"
32
+ },
29
33
  "type": "module",
30
34
  "scripts": {
31
35
  "pub": "npm publish --access public",
@@ -35,7 +39,7 @@
35
39
  },
36
40
  "name": "@mcpher/gas-fakes",
37
41
  "author": "bruce mcpherson",
38
- "version": "2.2.5",
42
+ "version": "2.2.7",
39
43
  "license": "MIT",
40
44
  "main": "main.js",
41
45
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
@@ -44,4 +48,4 @@
44
48
  "bin": {
45
49
  "gas-fakes": "gas-fakes.js"
46
50
  }
47
- }
51
+ }
package/src/cli/setup.js CHANGED
@@ -283,6 +283,7 @@ 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",
286
287
  ];
287
288
  responses.DEFAULT_SCOPES = DEFAULT_SCOPES_VALUES.join(",");
288
289
  responses.EXTRA_SCOPES = manifestScopes
@@ -303,12 +304,12 @@ export async function initializeConfiguration(options = {}) {
303
304
  initial: existingConfig.GOOGLE_SERVICE_ACCOUNT_NAME || "gas-fakes-sa",
304
305
  },
305
306
  {
306
- type: "text",
307
+ type: responses.AUTH_TYPE === "adc" ? "text" : null,
307
308
  name: "CLIENT_CREDENTIAL_FILE",
308
309
  message: "Enter path to OAuth client credentials JSON (optional, required for restricted scopes with ADC)",
309
310
  initial: existingConfig.CLIENT_CREDENTIAL_FILE || "",
310
311
  }
311
- ];
312
+ ];
312
313
 
313
314
  const googleResponses = await prompts(googleQuestions);
314
315
  if (typeof googleResponses.GOOGLE_CLOUD_PROJECT === "undefined") {
@@ -342,10 +343,10 @@ export async function initializeConfiguration(options = {}) {
342
343
  message: "What type of Microsoft account are you using?",
343
344
  choices: [
344
345
  { title: "Consumer (Personal, Outlook.com, Hotmail, etc.)", value: "consumers" },
345
- { title: "Business/Education (Work or School)", value: "organizations" },
346
+ { title: "Business/Education (Work or School) ", value: "organizations" },
346
347
  { title: "Standard Multi-tenant", value: "common" }
347
348
  ],
348
- initial: existingConfig.MS_GRAPH_TENANT_ID === "consumers" ? 0 : (existingConfig.MS_GRAPH_TENANT_ID === "organizations" ? 1 : 2)
349
+ initial: existingConfig.MS_GRAPH_TENANT_ID === "organizations" ? 1 : (existingConfig.MS_GRAPH_TENANT_ID === "common" ? 2 : 0)
349
350
  });
350
351
 
351
352
  if (typeof msAccountType.type === "undefined") {
@@ -604,13 +605,13 @@ export async function authenticateUser(options = {}) {
604
605
  const msScopes = mapGasScopesToMsGraph(gasScopes);
605
606
 
606
607
  try {
608
+ const tenantId = process.env.MS_GRAPH_TENANT_ID || 'consumers';
607
609
  const azCmd = `az config set core.login_experience_v2=off && az login --allow-no-subscriptions --output none`;
608
610
 
609
611
  console.log(`Executing: ${azCmd}`);
610
612
  try {
611
613
  runCommandSync(azCmd);
612
614
  console.log(`\n\x1b[1;32mSuccess!\x1b[0m Azure CLI session discovered.`);
613
- const tenantId = process.env.MS_GRAPH_TENANT_ID || 'consumers';
614
615
  console.log(`Silent fallback is now enabled for: \x1b[1;36m${tenantId}\x1b[0m`);
615
616
  } catch (e) {
616
617
  console.error(`\x1b[1;31mAzure CLI Login failed.\x1b[0m`);
@@ -662,7 +663,11 @@ export async function authenticateUser(options = {}) {
662
663
  ...(EXTRA_SCOPES || "").split(","),
663
664
  ])).filter(s => s).join(",");
664
665
 
665
- console.log(`...requesting scopes: ${scopes}`);
666
+ const adcScopes = AUTH_TYPE === "dwd"
667
+ ? Array.from(new Set((DEFAULT_SCOPES || "").split(","))).filter(s => s).join(",")
668
+ : scopes;
669
+
670
+ console.log(`...requesting scopes: ${adcScopes}`);
666
671
 
667
672
  const driveAccessFlag = "--enable-gdrive-access";
668
673
  const activeConfig = AC || "default";
@@ -684,7 +689,7 @@ export async function authenticateUser(options = {}) {
684
689
  runCommandSync(`gcloud auth login ${driveAccessFlag}`);
685
690
 
686
691
  let clientFlag = "";
687
- if (CLIENT_CREDENTIAL_FILE) {
692
+ if (AUTH_TYPE !== "dwd" && CLIENT_CREDENTIAL_FILE) {
688
693
  const clientPath = path.resolve(process.cwd(), CLIENT_CREDENTIAL_FILE);
689
694
  if (fs.existsSync(clientPath)) {
690
695
  console.log(`...using client credentials from ${clientPath}`);
@@ -693,7 +698,8 @@ export async function authenticateUser(options = {}) {
693
698
  }
694
699
 
695
700
  console.log("Setting up Application Default Credentials (ADC)...");
696
- runCommandSync(`gcloud auth application-default login --scopes="${scopes}" ${clientFlag}`);
701
+ const adcScopeFlag = `--scopes="${adcScopes}"`;
702
+ runCommandSync(`gcloud auth application-default login ${adcScopeFlag} ${clientFlag}`.trim());
697
703
  runCommandSync(`gcloud auth application-default set-quota-project ${projectId}`);
698
704
 
699
705
  // --- DWD Specific Setup (if configured) ---
@@ -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}
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  InteractiveBrowserCredential,
3
- ClientSecretCredential
3
+ ClientSecretCredential,
4
+ useIdentityPlugin
4
5
  } from "@azure/identity";
6
+
5
7
  import { execSync } from 'node:child_process';
6
8
  import { readFile, writeFile } from 'fs/promises';
7
9
  import { existsSync } from 'fs';
@@ -40,20 +42,66 @@ function decodeJWT(token) {
40
42
  return JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf-8'));
41
43
  }
42
44
 
45
+ const customCachePlugin = (options) => {
46
+ return {
47
+ beforeCacheAccess: async (cacheContext) => {
48
+ if (existsSync(TOKEN_CACHE_FILE)) {
49
+ try {
50
+ const data = await readFile(TOKEN_CACHE_FILE, 'utf-8');
51
+ let cache;
52
+ if (data.trim().startsWith('{')) {
53
+ cache = JSON.parse(data);
54
+ } else {
55
+ cache = decodeJWT(data.trim());
56
+ }
57
+ if (cache && cache.msalCache) {
58
+ cacheContext.tokenCache.deserialize(cache.msalCache);
59
+ }
60
+ } catch (e) {
61
+ console.warn(`...failed to load MS Graph token cache: ${e.message}`);
62
+ }
63
+ }
64
+ },
65
+ afterCacheAccess: async (cacheContext) => {
66
+ if (cacheContext.cacheHasChanged) {
67
+ try {
68
+ const msalCache = cacheContext.tokenCache.serialize();
69
+ const cacheData = {
70
+ msalCache,
71
+ token: 'managed-by-msal',
72
+ expiresOn: new Date(Date.now() + 86400000 * 30).getTime() // Keep valid for 30 days
73
+ };
74
+ const jwtString = encodeJWT(cacheData);
75
+ await writeFile(TOKEN_CACHE_FILE, jwtString);
76
+ try {
77
+ chmodSync(TOKEN_CACHE_FILE, 0o600);
78
+ } catch (e) {}
79
+ } catch (e) {
80
+ console.warn(`...failed to save MS Graph token cache: ${e.message}`);
81
+ }
82
+ }
83
+ }
84
+ };
85
+ };
86
+
87
+ useIdentityPlugin((context) => {
88
+ if (context.cachePluginControl) {
89
+ context.cachePluginControl.setPersistence(customCachePlugin);
90
+ }
91
+ });
92
+
43
93
  async function loadTokenCache() {
44
94
  if (existsSync(TOKEN_CACHE_FILE)) {
45
95
  try {
46
96
  const data = await readFile(TOKEN_CACHE_FILE, 'utf-8');
47
97
 
48
98
  let cache;
49
- // Handle legacy plain-text JSON format gracefully
50
99
  if (data.trim().startsWith('{')) {
51
100
  cache = JSON.parse(data);
52
101
  } else {
53
102
  cache = decodeJWT(data.trim());
54
103
  }
55
104
 
56
- // Check if expired (buffer of 5 mins)
57
105
  if (cache && cache.expiresOn && new Date(cache.expiresOn).getTime() > Date.now() + 300000) {
58
106
  return cache.token;
59
107
  }
@@ -70,10 +118,8 @@ async function saveTokenCache(token, expiresOn) {
70
118
  const jwtString = encodeJWT(cacheData);
71
119
  await writeFile(TOKEN_CACHE_FILE, jwtString);
72
120
  try {
73
- chmodSync(TOKEN_CACHE_FILE, 0o600); // Read/Write only for owner
74
- } catch (e) {
75
- // Ignore if chmod fails (e.g. on non-posix)
76
- }
121
+ chmodSync(TOKEN_CACHE_FILE, 0o600);
122
+ } catch (e) {}
77
123
  } catch (e) {
78
124
  console.warn(`...failed to save MS Graph token cache: ${e.message}`);
79
125
  }
@@ -120,7 +166,7 @@ async function isGcpEnv() {
120
166
  * Gets a Microsoft Graph token.
121
167
  */
122
168
  export async function getMsGraphToken(scopes = ['User.Read']) {
123
- const envTenant = process.env.MS_GRAPH_TENANT_ID || 'common';
169
+ const envTenant = process.env.MS_GRAPH_TENANT_ID || 'consumers';
124
170
  const clientId = process.env.MS_GRAPH_CLIENT_ID;
125
171
  const clientSecret = process.env.MS_GRAPH_CLIENT_SECRET;
126
172
  const msAuthType = process.env.MS_AUTH_TYPE;
@@ -230,7 +276,11 @@ export async function getMsGraphToken(scopes = ['User.Read']) {
230
276
  const credentialSilent = new InteractiveBrowserCredential({
231
277
  tenantId: (envTenant && envTenant !== 'common') ? envTenant : 'consumers',
232
278
  clientId,
233
- prompt: promptBehavior
279
+ prompt: promptBehavior,
280
+ tokenCachePersistenceOptions: {
281
+ enabled: true,
282
+ name: 'gas-fakes-msgraph-cache'
283
+ }
234
284
  });
235
285
 
236
286
  try {
@@ -244,7 +294,11 @@ export async function getMsGraphToken(scopes = ['User.Read']) {
244
294
  const credentialInteractive = new InteractiveBrowserCredential({
245
295
  tenantId: (envTenant && envTenant !== 'common') ? envTenant : 'consumers',
246
296
  clientId,
247
- prompt: 'select_account consent'
297
+ prompt: 'select_account consent',
298
+ tokenCachePersistenceOptions: {
299
+ enabled: true,
300
+ name: 'gas-fakes-msgraph-cache'
301
+ }
248
302
  });
249
303
  const tokenResponse = await credentialInteractive.getToken(msScopes);
250
304
  syncLog('...retrieved MS Graph token via interactive login');
package/exgcp.sh DELETED
@@ -1,54 +0,0 @@
1
- #!/bin/bash
2
-
3
- # This script reads the GCP_PROJECT_ID from a .env file
4
- # and exports it as GOOGLE_CLOUD_PROJECT for the current shell session.
5
- #
6
- # Usage: source . ./exgcp.sh
7
-
8
- # Define the path to your .env file relative to the script's location
9
- ENV_FILE="$(dirname "$0")/.env"
10
-
11
- # Check if the .env file exists
12
-
13
- if [ ! -f "$ENV_FILE" ]; then
14
- echo "Error: .env file not found at path: $ENV_FILE"
15
- # Use 'return' instead of 'exit' so it doesn't close the user's terminal when sourced
16
- return 1
17
- fi
18
-
19
- # Read the GCP_PROJECT_ID, remove quotes, and handle potential carriage returns
20
- GOOGLE_CLOUD_PROJECT_VALUE=$(grep -E '^GOOGLE_CLOUD_PROJECT=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
21
- GEMINI_API_KEY_VALUE=$(grep -E '^GEMINI_API_KEY=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
22
- GEMINI_MODEL_VALUE=$(grep -E '^GEMINI_MODEL=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
23
- OMDB_API_KEY_VALUE=$(grep -E '^OMDB_API_KEY=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
24
- # Check if a value was extracted
25
- if [ -z "$GOOGLE_CLOUD_PROJECT_VALUE" ]; then
26
- echo "Error: GOOGLE_CLOUD_PROJECT not found or is empty in $ENV_FILE."
27
- return 1
28
- fi
29
-
30
- if [ -z "GEMINI_API_KEY_VALUE" ]; then
31
- echo "GEMINI_API_KEY not found or is empty in $ENV_FILE."
32
- else
33
- echo "exported: GEMINI_API_KEY"
34
- export GEMINI_API_KEY="$GEMINI_API_KEY_VALUE"
35
- fi
36
-
37
- if [ -z "OMDB_API_KEY_VALUE" ]; then
38
- echo "OMDB_API_KEY not found or is empty in $ENV_FILE."
39
- else
40
- echo "exported: OMDB_API_KEY"
41
- export OMDB_API_KEY="$OMDB_API_KEY_VALUE"
42
- fi
43
-
44
- if [ -z "GEMINI_MODEL_VALUE" ]; then
45
- echo "GEMINI_MODEL not found or is empty in $ENV_FILE."
46
- else
47
- echo "exported: GEMINI_MODEL=$GEMINI_MODEL_VALUE"
48
- export GEMINI_MODEL="$GEMINI_MODEL_VALUE"
49
- fi
50
-
51
- # Export the variable for the current session
52
- export GOOGLE_CLOUD_PROJECT="$GOOGLE_CLOUD_PROJECT_VALUE"
53
-
54
- echo "exported: GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT"
package/introvideo.png DELETED
Binary file
package/logo.png DELETED
Binary file
package/plain-logo.png DELETED
Binary file
package/testlib.sh DELETED
@@ -1,2 +0,0 @@
1
- gas-fakes -l bmFiddler@13EWG4-lPrEf34itxQhAQ7b9JEbmCBfO8uE4Mhr99CHi3Pw65oxXtq-rU \
2
- -s "const sheet=SpreadsheetApp.openById('1h9IGIShgVBVUrUjjawk5MaCEQte_7t32XeEP1Z5jXKQ').getSheets()[0];const fiddler = new bmFiddler.Fiddler(sheet);console.log (fiddler.getData().slice(0, 5));"