@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 +19 -5
- package/package.json +4 -4
- package/src/cli/lib-manager.js +3 -1
- package/src/cli/setup.js +38 -6
- package/src/services/scriptapp/app.js +3 -1
- package/src/support/auth.js +29 -3
- package/src/support/sxauth.js +18 -0
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
|
-
|
|
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
|
|
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](
|
|
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
|
-
- [
|
|
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
|
|
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.
|
|
10
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
11
11
|
"@sindresorhus/is": "^7.2.0",
|
|
12
12
|
"acorn": "^8.16.0",
|
|
13
|
-
"archiver": "^
|
|
13
|
+
"archiver": "^4.0.2",
|
|
14
14
|
"commander": "^14.0.3",
|
|
15
15
|
"dotenv": "^17.3.1",
|
|
16
|
-
"fast-xml-parser": "^5.5.
|
|
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.
|
|
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",
|
package/src/cli/lib-manager.js
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|
package/src/support/auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
package/src/support/sxauth.js
CHANGED
|
@@ -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,
|