@mcpher/gas-fakes 1.2.23 → 1.2.24
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.RU.md +1 -0
- package/README.md +1 -0
- package/drive_tools.js +20 -0
- package/gas-fakes.js +387 -18
- package/package.json +2 -1
- package/src/services/advforms/fakeadvforms.js +3 -4
- package/src/services/formapp/fakeform.js +85 -16
- package/src/services/formapp/fakeformitem.js +1 -1
- package/src/services/formapp/fakeformresponse.js +2 -2
- package/src/services/formapp/fakeitemresponse.js +1 -1
- package/src/support/auth.js +93 -82
- package/src/support/sxfetch.js +22 -1
package/README.RU.md
CHANGED
|
@@ -359,6 +359,7 @@ const getParentsIterator = ({
|
|
|
359
359
|
- [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
|
|
360
360
|
- [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
|
|
361
361
|
- [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
|
|
362
|
+
- [Power of Google Apps Script: Building MCP Server Tools for Gemini CLI and Google Antigravity in Google Workspace Automation](https://medium.com/google-cloud/power-of-google-apps-script-building-mcp-server-tools-for-gemini-cli-and-google-antigravity-in-71e754e4b740)
|
|
362
363
|
- [A New Era for Google Apps Script: Unlocking the Future of Google Workspace Automation with Natural Language](https://medium.com/google-cloud/a-new-era-for-google-apps-script-unlocking-the-future-of-google-workspace-automation-with-natural-a9cecf87b4c6)
|
|
363
364
|
- [Next-Generation Google Apps Script Development: Leveraging Antigravity and Gemini 3.0](https://medium.com/google-cloud/next-generation-google-apps-script-development-leveraging-antigravity-and-gemini-3-0-c4d5affbc1a8)
|
|
364
365
|
- [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
|
package/README.md
CHANGED
|
@@ -178,6 +178,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
178
178
|
- [Supercharge Your Google Apps Script Caching with GasFlexCache](https://ramblings.mcpher.com/supercharge-your-google-apps-script-caching-with-gasflexcache/)
|
|
179
179
|
- [Fake-Sandbox for Google Apps Script: Granular controls.](https://ramblings.mcpher.com/fake-sandbox-for-google-apps-script-granular-controls/)
|
|
180
180
|
- [A Fake-Sandbox for Google Apps Script: Securely Executing Code Generated by Gemini CLI](https://ramblings.mcpher.com/gas-fakes-sandbox/)
|
|
181
|
+
- [Power of Google Apps Script: Building MCP Server Tools for Gemini CLI and Google Antigravity in Google Workspace Automation](https://medium.com/google-cloud/power-of-google-apps-script-building-mcp-server-tools-for-gemini-cli-and-google-antigravity-in-71e754e4b740)
|
|
181
182
|
- [A New Era for Google Apps Script: Unlocking the Future of Google Workspace Automation with Natural Language](https://medium.com/google-cloud/a-new-era-for-google-apps-script-unlocking-the-future-of-google-workspace-automation-with-natural-a9cecf87b4c6)
|
|
182
183
|
- [Next-Generation Google Apps Script Development: Leveraging Antigravity and Gemini 3.0](https://medium.com/google-cloud/next-generation-google-apps-script-development-leveraging-antigravity-and-gemini-3-0-c4d5affbc1a8)
|
|
183
184
|
- [Modern Google Apps Script Workflow Building on the Cloud](https://medium.com/google-cloud/modern-google-apps-script-workflow-building-on-the-cloud-2255dbd32ac3)
|
package/drive_tools.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const tools = [{ name: "search_files_by_name", schema: {
|
|
4
|
+
description: "Search for files on Google Drive by filename.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
filename: z.string().describe("The name of the file to search for.")
|
|
7
|
+
}
|
|
8
|
+
}, func: (object = {}) => { const { filename } = object;
|
|
9
|
+
const files = DriveApp.getFilesByName(filename);
|
|
10
|
+
const results = [];
|
|
11
|
+
while (files.hasNext()) {
|
|
12
|
+
const file = files.next();
|
|
13
|
+
results.push({
|
|
14
|
+
name: file.getName(),
|
|
15
|
+
id: file.getId(),
|
|
16
|
+
url: file.getUrl(),
|
|
17
|
+
mimeType: file.getMimeType()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return results; } }];
|
package/gas-fakes.js
CHANGED
|
@@ -12,6 +12,9 @@ import dotenv from "dotenv";
|
|
|
12
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
import { parse } from "acorn";
|
|
16
|
+
|
|
17
|
+
import { Auth } from "./src/support/auth.js";
|
|
15
18
|
|
|
16
19
|
// --- Import setup commands ---
|
|
17
20
|
import {
|
|
@@ -22,6 +25,7 @@ import {
|
|
|
22
25
|
|
|
23
26
|
// sync the version with gas fakes code since they share a package.json
|
|
24
27
|
import { createRequire } from "node:module";
|
|
28
|
+
import { access } from "node:fs";
|
|
25
29
|
const require = createRequire(import.meta.url);
|
|
26
30
|
const pjson = require("./package.json");
|
|
27
31
|
const VERSION = pjson.version;
|
|
@@ -30,8 +34,8 @@ const VERSION = pjson.version;
|
|
|
30
34
|
// CONSTANTS & UTILITIES
|
|
31
35
|
// -----------------------------------------------------------------------------
|
|
32
36
|
|
|
33
|
-
const CLI_VERSION = "0.0.
|
|
34
|
-
const MCP_VERSION = "0.0.
|
|
37
|
+
const CLI_VERSION = "0.0.15";
|
|
38
|
+
const MCP_VERSION = "0.0.5";
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
41
|
* Replaces escaped newline characters ('\\n') with actual newlines,
|
|
@@ -40,8 +44,16 @@ const MCP_VERSION = "0.0.4";
|
|
|
40
44
|
* @returns {string} The processed text.
|
|
41
45
|
*/
|
|
42
46
|
function normalizeScriptNewlines(text) {
|
|
43
|
-
const regex = /("[^"]*")|('[^']*')|(`[^`]*`)|(\\n)/g;
|
|
44
|
-
return text.replace(regex, (match, g1, g2, g3, g4) => (g4 ? "\n" : match));
|
|
47
|
+
// const regex = /("[^"]*")|('[^']*')|(`[^`]*`)|(\\n)/g;
|
|
48
|
+
// return text.replace(regex, (match, g1, g2, g3, g4) => (g4 ? "\n" : match));
|
|
49
|
+
|
|
50
|
+
// Updated the above as follows.
|
|
51
|
+
const regex =
|
|
52
|
+
/("(?:[^"\\]|\\.)*")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|(\/\\[rn]\/[dgimsuy]*)|(\/\*[\s\S]*?\*\/)|(\/\/(?:(?!\\n).)*)|(\\n)/g;
|
|
53
|
+
return text.replace(regex, (match, g1, g2, g3, g4, g5, g6, g7) => {
|
|
54
|
+
if (g7) return "\n";
|
|
55
|
+
return match;
|
|
56
|
+
});
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
// -----------------------------------------------------------------------------
|
|
@@ -189,6 +201,7 @@ async function executeGasScript(options) {
|
|
|
189
201
|
useSandbox,
|
|
190
202
|
sandboxConfig,
|
|
191
203
|
args,
|
|
204
|
+
gas_library,
|
|
192
205
|
} = options;
|
|
193
206
|
|
|
194
207
|
let scriptText = filename ? fs.readFileSync(filename, "utf8") : script;
|
|
@@ -197,7 +210,7 @@ async function executeGasScript(options) {
|
|
|
197
210
|
scriptText = scriptText.replace(/\\\s*?\n/g, "\n");
|
|
198
211
|
}
|
|
199
212
|
|
|
200
|
-
|
|
213
|
+
let { mainScript, gasScript } = generateExecutionScript({
|
|
201
214
|
scriptText: normalizeScriptNewlines(scriptText),
|
|
202
215
|
useSandbox,
|
|
203
216
|
sandboxConfig,
|
|
@@ -216,6 +229,18 @@ async function executeGasScript(options) {
|
|
|
216
229
|
configurable: true,
|
|
217
230
|
});
|
|
218
231
|
|
|
232
|
+
if (gas_library && gas_library.length > 0) {
|
|
233
|
+
const libs = gas_library.reduce((ar, { identifier, libScript }) => {
|
|
234
|
+
if (mainScript.includes(identifier)) {
|
|
235
|
+
ar.push(libScript);
|
|
236
|
+
}
|
|
237
|
+
return ar;
|
|
238
|
+
}, []);
|
|
239
|
+
if (libs.length > 0) {
|
|
240
|
+
mainScript = `${libs.join("\n\n")}\n\n${mainScript}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
219
244
|
let res;
|
|
220
245
|
if (args) {
|
|
221
246
|
const gasFunction = new Function("args", mainScript);
|
|
@@ -344,7 +369,7 @@ function registerDefaultTool(server) {
|
|
|
344
369
|
inputSchema: {
|
|
345
370
|
filename: z
|
|
346
371
|
.string()
|
|
347
|
-
.optional()
|
|
372
|
+
.optional()
|
|
348
373
|
.describe(`Path to the file containing Google Apps Script.`),
|
|
349
374
|
script: z
|
|
350
375
|
.string()
|
|
@@ -416,7 +441,7 @@ function registerDefaultTool(server) {
|
|
|
416
441
|
|
|
417
442
|
const schema2 = {
|
|
418
443
|
description: [
|
|
419
|
-
`Use this to create the tools of the MCP server using Google Apps Script as a new file. If a file \`settings.json\` or \`mcp_config.json\` or and so on for loading the MCP servers include \`--tools\` and the tool file to \`gas-fakes\` MCP server, ask the current client which you want to create a new file including the tools or you want to add the tools to the existing tool file. When a new tool file is created, show how to use the tool file as follows. When the created tool file is \`toolFile.js\`, modify the MCP server setting to \`"mcpServers": { "gas-fakes": { "command": "gas-fakes", "args": [ "mcp", "--tools", "{Absolute path}/toolFile.js" ] } }\`. Don't forget to replace \`{Absolute path}\` with the actual path.`,
|
|
444
|
+
`Use this to create the tools of the MCP server using Google Apps Script as a new file. Only when the prompt that a new tool for the MCP server is required is clearly shown, use this tool. If a file \`settings.json\` or \`mcp_config.json\` or and so on for loading the MCP servers include \`--tools\` and the tool file to \`gas-fakes\` MCP server, ask the current client which you want to create a new file including the tools or you want to add the tools to the existing tool file. When a new tool file is created, show how to use the tool file as follows. When the created tool file is \`toolFile.js\`, modify the MCP server setting to \`"mcpServers": { "gas-fakes": { "command": "gas-fakes", "args": [ "mcp", "--tools", "{Absolute path}/toolFile.js" ] } }\`. Don't forget to replace \`{Absolute path}\` with the actual path.`,
|
|
420
445
|
].join("\n"),
|
|
421
446
|
inputSchema: {
|
|
422
447
|
filename: z
|
|
@@ -439,6 +464,24 @@ function registerDefaultTool(server) {
|
|
|
439
464
|
.describe(
|
|
440
465
|
`Generated Google Apps Script. Please be careful of the following rule. For example, when you generated a Google Apps Script like \`function sample(object) { script }\`, please add \`return sample(object);\` to execute the function. This is a very important point. When the prompt says "Create a function", please create a function and add a line of script for calling the function and returning the response like \`return sample(object);\`. Or, you can also create a Google Apps Script without enclosing the script with \`function sample(object) { script }\`. When you want to return the value, show the value using \`console.log\` in the script, or return the value using \`return\`. The simple sample script is \`const { filename } = object; const files = DriveApp.getFilesByName(filename); const ar = []; while (files.hasNext()) { const file = files.next(); ar.push({ filename: file.getName(), fileId: file.getId() }); } return ar;\` This tool is required to be used for creating tools as a new file. When you want to add more tools to the existing file, add them to an array \`tools\` in the file by following the specification of \`tools\`. If you cannot find the array, please create it as a new file.`
|
|
441
466
|
),
|
|
467
|
+
libraries: z
|
|
468
|
+
.array(
|
|
469
|
+
z
|
|
470
|
+
.string()
|
|
471
|
+
.describe(
|
|
472
|
+
[
|
|
473
|
+
`Use this when Google Apps Script libraries are required to be used in the script. The sample values are as follows.`,
|
|
474
|
+
`It supposes that the identity of the library is \`LIB\`.`,
|
|
475
|
+
`- When the library script is the file with path, \`LIB@{filename1}\``,
|
|
476
|
+
`- When the library script is the file direct link, \`LIB@{file URL}\``,
|
|
477
|
+
`- When the library script is the ID (Library project key or library key or script ID or file ID) of the file on Google Drive, \`LIB@{ID}\``,
|
|
478
|
+
].join("\n")
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
.default([])
|
|
482
|
+
.describe(
|
|
483
|
+
`Use this when Google Apps Script libraries are required to be used in the script.`
|
|
484
|
+
),
|
|
442
485
|
})
|
|
443
486
|
.describe("An object for each tool.")
|
|
444
487
|
)
|
|
@@ -457,16 +500,21 @@ function registerDefaultTool(server) {
|
|
|
457
500
|
isError: true,
|
|
458
501
|
};
|
|
459
502
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
503
|
+
|
|
504
|
+
const tool_ar = [];
|
|
505
|
+
for (let i = 0; i < args.length; i++) {
|
|
506
|
+
const { name, schema, gas_script, libraries } = args[i];
|
|
507
|
+
tool_ar.push(
|
|
508
|
+
`{ name: "${name}", schema: ${schema}, func: (object = {}) => { ${gas_library}\n\n${gas_script} }, libraries: [${libraries.join(
|
|
509
|
+
","
|
|
510
|
+
)}] }`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
466
514
|
const tool_script = [
|
|
467
515
|
`import { z } from "zod";`,
|
|
468
516
|
``,
|
|
469
|
-
`const tools = [${tool_ar}];`,
|
|
517
|
+
`const tools = [${tool_ar.join(", ")}];`,
|
|
470
518
|
].join("\n");
|
|
471
519
|
const absolutePath = path.resolve(process.cwd(), args.filename);
|
|
472
520
|
fs.writeFileSync(absolutePath, tool_script);
|
|
@@ -499,7 +547,9 @@ async function registerCustomTools(server, toolsPath) {
|
|
|
499
547
|
|
|
500
548
|
if (!tools || tools.length === 0) return;
|
|
501
549
|
|
|
502
|
-
tools.
|
|
550
|
+
for (let i = 0; i < tools.length; i++) {
|
|
551
|
+
const tool = tools[i];
|
|
552
|
+
|
|
503
553
|
// Extend the custom tool schema with sandbox options
|
|
504
554
|
const extendedSchema = { ...tool.schema };
|
|
505
555
|
extendedSchema.inputSchema = {
|
|
@@ -519,7 +569,24 @@ async function registerCustomTools(server, toolsPath) {
|
|
|
519
569
|
json: z.any().optional().describe("Advanced sandbox JSON configuration."),
|
|
520
570
|
};
|
|
521
571
|
|
|
522
|
-
|
|
572
|
+
let originalFuncStr = tool.func.toString();
|
|
573
|
+
if (tool.libraries && tool.libraries.length > 0) {
|
|
574
|
+
const gas_library = await getLibraries({ libraries: tool.libraries });
|
|
575
|
+
|
|
576
|
+
if (gas_library && gas_library.length > 0) {
|
|
577
|
+
const libs = gas_library.reduce((ar, { identifier, libScript }) => {
|
|
578
|
+
if (originalFuncStr.includes(identifier)) {
|
|
579
|
+
ar.push(libScript);
|
|
580
|
+
}
|
|
581
|
+
return ar;
|
|
582
|
+
}, []);
|
|
583
|
+
if (libs.length > 0) {
|
|
584
|
+
originalFuncStr = `(object = {}) => {\n\n${libs.join(
|
|
585
|
+
"\n\n"
|
|
586
|
+
)}\n\nconst main_gas_fakes = ${originalFuncStr}\n\nreturn main_gas_fakes(object);\n}`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
523
590
|
|
|
524
591
|
const toolHandler = async (opts) => {
|
|
525
592
|
// Wrap the original function string to execute it with args
|
|
@@ -535,7 +602,7 @@ async function registerCustomTools(server, toolsPath) {
|
|
|
535
602
|
};
|
|
536
603
|
|
|
537
604
|
server.registerTool(tool.name, extendedSchema, toolHandler);
|
|
538
|
-
}
|
|
605
|
+
}
|
|
539
606
|
}
|
|
540
607
|
|
|
541
608
|
/**
|
|
@@ -609,6 +676,296 @@ function buildSandboxConfig(options) {
|
|
|
609
676
|
return undefined;
|
|
610
677
|
}
|
|
611
678
|
|
|
679
|
+
// -----------------------------------------------------------------------------
|
|
680
|
+
// PROCESS GAS LIBRARIES
|
|
681
|
+
// -----------------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Helper function to wrap spawn in a Promise.
|
|
685
|
+
* This captures stdout and stderr and resolves or rejects based on the exit code.
|
|
686
|
+
*/
|
|
687
|
+
function spawnCommand(command, args) {
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
const child = spawn(command, args);
|
|
690
|
+
let stdout = "";
|
|
691
|
+
let stderr = "";
|
|
692
|
+
|
|
693
|
+
// Collect data from stdout stream
|
|
694
|
+
child.stdout.on("data", (data) => {
|
|
695
|
+
stdout += data.toString();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Collect data from stderr stream
|
|
699
|
+
child.stderr.on("data", (data) => {
|
|
700
|
+
stderr += data.toString();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Handle command not found or other spawn errors
|
|
704
|
+
child.on("error", (err) => {
|
|
705
|
+
reject(err);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Handle process exit
|
|
709
|
+
child.on("close", (code) => {
|
|
710
|
+
if (code === 0) {
|
|
711
|
+
resolve(stdout.trim());
|
|
712
|
+
} else {
|
|
713
|
+
reject(
|
|
714
|
+
new Error(stderr.trim() || `Command failed with exit code ${code}`)
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function checkForGcloudCli() {
|
|
722
|
+
try {
|
|
723
|
+
// args: ['--version']
|
|
724
|
+
await spawnCommand("gcloud", ["--version"]);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
console.error(
|
|
727
|
+
"\n[Error] Google Cloud SDK (gcloud CLI) not found or failed to run."
|
|
728
|
+
);
|
|
729
|
+
console.error("Please install it by following the official instructions:");
|
|
730
|
+
console.error("https://cloud.google.com/sdk/gcloud");
|
|
731
|
+
// Only exit if it's strictly required to stop execution here
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function getAccessToken(pattern) {
|
|
737
|
+
if (pattern == 1) {
|
|
738
|
+
// Authorization pattern 1
|
|
739
|
+
const auth = await Auth.setAuth(
|
|
740
|
+
["https://www.googleapis.com/auth/drive.readonly"],
|
|
741
|
+
null,
|
|
742
|
+
true
|
|
743
|
+
);
|
|
744
|
+
auth.cachedCredential = null;
|
|
745
|
+
return await auth.getAccessToken();
|
|
746
|
+
} else {
|
|
747
|
+
// Authorization pattern 2
|
|
748
|
+
await checkForGcloudCli();
|
|
749
|
+
try {
|
|
750
|
+
const accessToken = await spawnCommand("gcloud", [
|
|
751
|
+
"auth",
|
|
752
|
+
"print-access-token",
|
|
753
|
+
]);
|
|
754
|
+
return accessToken;
|
|
755
|
+
} catch (error) {
|
|
756
|
+
console.error("\nError obtaining access token:");
|
|
757
|
+
console.error(error.message);
|
|
758
|
+
console.error(
|
|
759
|
+
"Please ensure you are authenticated with gcloud CLI. Run 'gcloud auth application-default login'."
|
|
760
|
+
);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function fetchScriptFileFromGoogleDrive(sourcePath, pattern = 1) {
|
|
767
|
+
try {
|
|
768
|
+
const accessToken = await getAccessToken(pattern);
|
|
769
|
+
const url = `https://www.googleapis.com/drive/v3/files/${sourcePath}/export?mimeType=${encodeURIComponent(
|
|
770
|
+
"application/vnd.google-apps.script+json"
|
|
771
|
+
)}`;
|
|
772
|
+
|
|
773
|
+
const response = await fetch(url, {
|
|
774
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (!response.ok) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
`HTTP error ${response.status} fetching Drive ID "${sourcePath}".`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const text = await response.json();
|
|
784
|
+
if (text.files && text.files.length > 0) {
|
|
785
|
+
// GAS projects can have multiple files; filter for server-side JS and join them.
|
|
786
|
+
return text.files
|
|
787
|
+
.filter((e) => e.type === "server_js")
|
|
788
|
+
.map((e) => e.source)
|
|
789
|
+
.join("\n\n");
|
|
790
|
+
}
|
|
791
|
+
} catch (err) {
|
|
792
|
+
if (pattern == 1) {
|
|
793
|
+
return await fetchScriptFileFromGoogleDrive(sourcePath, 2);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// If it wasn't a Drive ID or auth failed, fall through to error
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Could not retrieve script from "${sourcePath}". Ensure it is a valid path, URL, or Drive ID, and that you are authenticated.`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Fetches the source code for a library from a local file, a URL, or Google Drive.
|
|
805
|
+
*
|
|
806
|
+
* @param {string} sourcePath - The file path, URL, or Drive File ID of the library.
|
|
807
|
+
* @returns {Promise<string>} The source code of the library.
|
|
808
|
+
* @throws {Error} If the source cannot be found, HTTP fails, or auth fails.
|
|
809
|
+
*/
|
|
810
|
+
async function fetchLibrarySource(sourcePath) {
|
|
811
|
+
// 1. Check Local File
|
|
812
|
+
if (fs.existsSync(sourcePath)) {
|
|
813
|
+
return fs.readFileSync(sourcePath, "utf8");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// 2. Check URL
|
|
817
|
+
// Note: URL.canParse requires Node.js v18.17.0+ or v20+
|
|
818
|
+
if (URL.canParse(sourcePath)) {
|
|
819
|
+
const response = await fetch(sourcePath);
|
|
820
|
+
if (!response.ok) {
|
|
821
|
+
throw new Error(`HTTP error ${response.status} fetching ${sourcePath}`);
|
|
822
|
+
}
|
|
823
|
+
return await response.text();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 3. Check Google Drive (via gcloud)
|
|
827
|
+
try {
|
|
828
|
+
return await fetchScriptFileFromGoogleDrive(sourcePath);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`No valid source code found for "${sourcePath}". Error message: ${err.message}`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Wraps raw library source code into a GAS-style namespace using an IIFE.
|
|
838
|
+
* It uses AST parsing to export top-level functions and 'var' variables.
|
|
839
|
+
*
|
|
840
|
+
* @param {string} identifier - The library namespace identifier (e.g., "TableApp").
|
|
841
|
+
* @param {string} source - The raw JavaScript source code.
|
|
842
|
+
* @returns {string} The wrapped, executable JavaScript code.
|
|
843
|
+
*/
|
|
844
|
+
function generateLibraryWrapper(identifier, source) {
|
|
845
|
+
try {
|
|
846
|
+
const ast = parse(source, { ecmaVersion: 2020 });
|
|
847
|
+
|
|
848
|
+
// Extract top-level FunctionDeclarations and VariableDeclarations (var only)
|
|
849
|
+
const exportNames = ast.body.reduce((names, node) => {
|
|
850
|
+
if (node.type === "FunctionDeclaration") {
|
|
851
|
+
names.push(node.id.name);
|
|
852
|
+
} else if (node.type === "VariableDeclaration" && node.kind === "var") {
|
|
853
|
+
names.push(...node.declarations.map((d) => d.id.name));
|
|
854
|
+
}
|
|
855
|
+
return names;
|
|
856
|
+
}, []);
|
|
857
|
+
|
|
858
|
+
if (exportNames.length === 0) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`No top-level functions or var variables found to export in library "${identifier}".`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return [
|
|
865
|
+
`var ${identifier} = (function () {`,
|
|
866
|
+
`var ${identifier};`,
|
|
867
|
+
source,
|
|
868
|
+
`\n`,
|
|
869
|
+
`if (this && this.${identifier}) { ${identifier} = this.${identifier}; }`,
|
|
870
|
+
`return { ${exportNames.join(", ")} };`,
|
|
871
|
+
`}).call({});`,
|
|
872
|
+
].join("\n");
|
|
873
|
+
|
|
874
|
+
// Or, use the following script.
|
|
875
|
+
// return [
|
|
876
|
+
// `var ${identifier} = (function () {`,
|
|
877
|
+
// source,
|
|
878
|
+
// `\n`,
|
|
879
|
+
// `return { ${exportNames.join(", ")} };`,
|
|
880
|
+
// `})();`,
|
|
881
|
+
// ].join("\n");
|
|
882
|
+
} catch (err) {
|
|
883
|
+
console.error(`Error processing library "${identifier}": ${err.message}`);
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Processes library arguments, fetches source code, merges duplicates,
|
|
890
|
+
* and generates namespaced wrapper scripts.
|
|
891
|
+
*
|
|
892
|
+
* This function handles the format "Identifier@Source". If the same Identifier
|
|
893
|
+
* is provided multiple times, the sources are concatenated in order.
|
|
894
|
+
*
|
|
895
|
+
* @param {object} options - The CLI options object.
|
|
896
|
+
* @param {string[]} [options.libraries] - Array of library strings (e.g., ["Lib@./file.js"]).
|
|
897
|
+
* @returns {Promise<string|null>} The combined string of all library scripts, or null if none provided.
|
|
898
|
+
*/
|
|
899
|
+
async function getLibraries(options) {
|
|
900
|
+
const { libraries } = options;
|
|
901
|
+
|
|
902
|
+
if (!libraries || !Array.isArray(libraries) || libraries.length === 0) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// 1. Parse and Fetch (Concurrent)
|
|
907
|
+
// We map arguments to Promises to fetch all libraries in parallel.
|
|
908
|
+
const fetchPromises = libraries.map(async (libArg) => {
|
|
909
|
+
const splitIndex = libArg.indexOf("@");
|
|
910
|
+
|
|
911
|
+
if (splitIndex === -1) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`Invalid library format: "${libArg}". Expected format: 'Identifier@Source'.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const identifier = libArg.substring(0, splitIndex).trim();
|
|
918
|
+
const sourcePath = libArg.substring(splitIndex + 1).trim();
|
|
919
|
+
|
|
920
|
+
if (!identifier || !sourcePath) {
|
|
921
|
+
throw new Error(
|
|
922
|
+
`Invalid library argument: "${libArg}". Identifier or Source is missing.`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
const source = await fetchLibrarySource(sourcePath);
|
|
928
|
+
return { identifier, source };
|
|
929
|
+
} catch (err) {
|
|
930
|
+
throw new Error(`Failed to load library "${identifier}": ${err.message}`);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
let fetchedLibs;
|
|
935
|
+
try {
|
|
936
|
+
fetchedLibs = await Promise.all(fetchPromises);
|
|
937
|
+
} catch (err) {
|
|
938
|
+
// If any fetch fails, log the error and exit
|
|
939
|
+
console.error(`Error: ${err.message}`);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// 2. Merge libraries with the same Identifier
|
|
944
|
+
// Using a Map to maintain insertion order and group sources
|
|
945
|
+
const mergedLibsMap = new Map();
|
|
946
|
+
|
|
947
|
+
for (const { identifier, source } of fetchedLibs) {
|
|
948
|
+
if (mergedLibsMap.has(identifier)) {
|
|
949
|
+
const existingSource = mergedLibsMap.get(identifier);
|
|
950
|
+
mergedLibsMap.set(identifier, existingSource + "\n\n" + source);
|
|
951
|
+
} else {
|
|
952
|
+
mergedLibsMap.set(identifier, source);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// 3. Generate Wrappers
|
|
957
|
+
// Convert the merged sources into wrapped IIFE strings
|
|
958
|
+
const wrappedScripts = [];
|
|
959
|
+
for (const [identifier, source] of mergedLibsMap) {
|
|
960
|
+
wrappedScripts.push({
|
|
961
|
+
identifier,
|
|
962
|
+
libScript: generateLibraryWrapper(identifier, source),
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return wrappedScripts;
|
|
967
|
+
}
|
|
968
|
+
|
|
612
969
|
/**
|
|
613
970
|
* Sets up and runs the command-line interface.
|
|
614
971
|
*/
|
|
@@ -618,7 +975,11 @@ async function main() {
|
|
|
618
975
|
program
|
|
619
976
|
.name("gas-fakes")
|
|
620
977
|
.description("A CLI tool to execute Google Apps Script with fakes/mocks.")
|
|
621
|
-
.version(
|
|
978
|
+
.version(
|
|
979
|
+
VERSION,
|
|
980
|
+
"-v, --version",
|
|
981
|
+
"Display the current version of gas-fakes"
|
|
982
|
+
);
|
|
622
983
|
|
|
623
984
|
// Default command to execute a script
|
|
624
985
|
program
|
|
@@ -661,6 +1022,11 @@ async function main() {
|
|
|
661
1022
|
`Arguments for the function of Google Apps Script. Provide it as a JSON string. The name of the argument is "args" as a fixed name. For example, when the function of GAS is \`function sample(args) { script }\`, you can provide the arguments like \`-a '{"key": "value"}'\`.`,
|
|
662
1023
|
null
|
|
663
1024
|
)
|
|
1025
|
+
.option(
|
|
1026
|
+
"-l, --libraries <string...>",
|
|
1027
|
+
`Libraries. You can run the Google Apps Script with libraries. When you use 2 libraries "Lib1" and "Lib" which are the identifiers of library, provide '--libraries "Lib1@{filename}" --libraries "Lib2@{file URL}"'.`,
|
|
1028
|
+
null
|
|
1029
|
+
)
|
|
664
1030
|
.action(async (options) => {
|
|
665
1031
|
const { filename, script, env, gfsettings } = options;
|
|
666
1032
|
if (!filename && !script) {
|
|
@@ -705,6 +1071,8 @@ async function main() {
|
|
|
705
1071
|
}
|
|
706
1072
|
}
|
|
707
1073
|
|
|
1074
|
+
const gas_library = await getLibraries(options);
|
|
1075
|
+
|
|
708
1076
|
await executeGasScript({
|
|
709
1077
|
filename,
|
|
710
1078
|
script,
|
|
@@ -713,6 +1081,7 @@ async function main() {
|
|
|
713
1081
|
sandboxConfig,
|
|
714
1082
|
gfSettings: settingsPath,
|
|
715
1083
|
args,
|
|
1084
|
+
gas_library,
|
|
716
1085
|
});
|
|
717
1086
|
});
|
|
718
1087
|
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"@mcpher/gas-flex-cache": "^1.1.3",
|
|
8
8
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
9
9
|
"@sindresorhus/is": "^7.0.1",
|
|
10
|
+
"acorn": "^8.15.0",
|
|
10
11
|
"archiver": "^7.0.1",
|
|
11
12
|
"commander": "^14.0.1",
|
|
12
13
|
"dotenv": "^17.2.3",
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"name": "@mcpher/gas-fakes",
|
|
35
36
|
"author": "bruce mcpherson",
|
|
36
|
-
"version": "1.2.
|
|
37
|
+
"version": "1.2.24",
|
|
37
38
|
"license": "MIT",
|
|
38
39
|
"main": "main.js",
|
|
39
40
|
"description": "A proof of concept implementation of Apps Script Environment on Node",
|
|
@@ -42,8 +42,7 @@ class FakeAdvForms {
|
|
|
42
42
|
"newFormInfo": [
|
|
43
43
|
"title",
|
|
44
44
|
"documentTitle",
|
|
45
|
-
"description"
|
|
46
|
-
"state"
|
|
45
|
+
"description"
|
|
47
46
|
],
|
|
48
47
|
"newUpdateSettingsRequest": [
|
|
49
48
|
"settings",
|
|
@@ -53,7 +52,7 @@ class FakeAdvForms {
|
|
|
53
52
|
"emailCollectionType",
|
|
54
53
|
"quizSettings"
|
|
55
54
|
],
|
|
56
|
-
"newQuizSettings"
|
|
55
|
+
"newQuizSettings": [
|
|
57
56
|
"isQuiz"
|
|
58
57
|
],
|
|
59
58
|
"newItem": [
|
|
@@ -188,7 +187,7 @@ class FakeAdvForms {
|
|
|
188
187
|
"properties",
|
|
189
188
|
"youtubeUri"
|
|
190
189
|
],
|
|
191
|
-
|
|
190
|
+
|
|
192
191
|
}
|
|
193
192
|
|
|
194
193
|
Reflect.ownKeys(propLists).forEach(p => {
|
|
@@ -3,15 +3,18 @@ import { newFakeFormItem } from './fakeformitem.js';
|
|
|
3
3
|
import { newFakeFormResponse } from './fakeformresponse.js';
|
|
4
4
|
import { newFakeGridItem } from './fakegriditem.js';
|
|
5
5
|
import { newFakeCheckboxGridItem } from './fakecheckboxgriditem.js';
|
|
6
|
+
import { DestinationType } from '../enums/formsenums.js';
|
|
6
7
|
import { newFakeSectionHeaderItem } from './fakesectionheaderitem.js';
|
|
7
8
|
import { newFakeScaleItem } from './fakescaleitem.js';
|
|
8
|
-
import './formitems.js'; // Import for side effects (item class registration)
|
|
9
|
+
//import './formitems.js'; // Import for side effects (item class registration)
|
|
9
10
|
import { newFakeMultipleChoiceItem } from './fakemultiplechoiceitem.js';
|
|
10
11
|
import { newFakeCheckboxItem } from './fakecheckboxitem.js';
|
|
11
12
|
import { newFakeListItem } from './fakelistitem.js';
|
|
12
13
|
import { newFakePageBreakItem } from './fakepagebreakitem.js';
|
|
13
14
|
import { newFakeTextItem } from './faketextitem.js';
|
|
14
|
-
|
|
15
|
+
import { signatureArgs } from '../../support/helpers.js';
|
|
16
|
+
import {Utils} from '../../support/utils.js';
|
|
17
|
+
const { is } = Utils
|
|
15
18
|
export const newFakeForm = (...args) => {
|
|
16
19
|
return Proxies.guard(new FakeForm(...args));
|
|
17
20
|
};
|
|
@@ -29,14 +32,28 @@ export class FakeForm {
|
|
|
29
32
|
// Store the resource provided at creation as the single source of truth for this instance.
|
|
30
33
|
this.__id = resource.formId;
|
|
31
34
|
this.__file = DriveApp.getFileById(this.__id);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.__publishedState = resource.settings?.state !== 'INACTIVE';
|
|
35
|
+
this.__destinationId = resource.linkedSheetId || null;
|
|
36
|
+
this.__destinationType = this.__destinationId ? DestinationType.SPREADSHEET : null;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
get __resource() {
|
|
38
40
|
return Forms.Form.get(this.__id);
|
|
39
41
|
}
|
|
42
|
+
get __publishSettings() {
|
|
43
|
+
return this.__resource.publishSettings;
|
|
44
|
+
}
|
|
45
|
+
isAcceptingResponses() {
|
|
46
|
+
return this.__publishSettings.isAcceptingResponses;
|
|
47
|
+
}
|
|
48
|
+
isPublished() {
|
|
49
|
+
return this.__publishSettings.isPublished;
|
|
50
|
+
}
|
|
51
|
+
setPublished(enabled) {
|
|
52
|
+
throw new Error('setPublished is not yet implemented in the fake environment.');
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
40
57
|
saveAndClose() {
|
|
41
58
|
// this is a no-op in fake environment since it is stateless
|
|
42
59
|
}
|
|
@@ -244,6 +261,24 @@ export class FakeForm {
|
|
|
244
261
|
return this.__addItem(itemResource, newFakeTextItem);
|
|
245
262
|
}
|
|
246
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Gets the ID of the form's response destination.
|
|
266
|
+
* @returns {string | null} The destination ID, or null if no destination is set.
|
|
267
|
+
*/
|
|
268
|
+
getDestinationId() {
|
|
269
|
+
return this.__resource.linkedSheetId || null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Gets the type of the form's response destination.
|
|
274
|
+
* @returns {import('../enums/formsenums.js').DestinationType | null} The destination type, or null if no destination is set.
|
|
275
|
+
*/
|
|
276
|
+
getDestinationType() {
|
|
277
|
+
if (this.getDestinationId()) {
|
|
278
|
+
return DestinationType.SPREADSHEET;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
247
282
|
|
|
248
283
|
|
|
249
284
|
/**
|
|
@@ -338,14 +373,6 @@ export class FakeForm {
|
|
|
338
373
|
return responses.sort((a, b) => a.getTimestamp() - b.getTimestamp());
|
|
339
374
|
}
|
|
340
375
|
|
|
341
|
-
/**
|
|
342
|
-
* Gets whether the form is accepting responses.
|
|
343
|
-
* @returns {boolean} true if the form is accepting responses; false otherwise.
|
|
344
|
-
*/
|
|
345
|
-
isPublished() {
|
|
346
|
-
// ACTIVE is the state for accepting responses. The default if not set is ACTIVE.
|
|
347
|
-
return this.__publishedState;
|
|
348
|
-
}
|
|
349
376
|
|
|
350
377
|
/**
|
|
351
378
|
* Sets the name of the form file in Google Drive.
|
|
@@ -415,7 +442,32 @@ export class FakeForm {
|
|
|
415
442
|
return itemToMove;
|
|
416
443
|
}
|
|
417
444
|
/**
|
|
418
|
-
*
|
|
445
|
+
* Unlinks the form from its response destination.
|
|
446
|
+
* @returns {FakeForm} The form, for chaining.
|
|
447
|
+
*/
|
|
448
|
+
removeDestination() {
|
|
449
|
+
// This is not supported by the REST API, so we manage it internally for the fake.
|
|
450
|
+
this.__destinationId = null;
|
|
451
|
+
this.__destinationType = null;
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Sets the destination for form responses.
|
|
456
|
+
* @param {import('../enums/formsenums.js').DestinationType} type The type of destination.
|
|
457
|
+
* @param {string} id The ID of the destination (spreadsheet ID).
|
|
458
|
+
* @returns {FakeForm} The form, for chaining.
|
|
459
|
+
*/
|
|
460
|
+
setDestination(type, id) {
|
|
461
|
+
if (type !== DestinationType.SPREADSHEET) {
|
|
462
|
+
throw new Error('Only SPREADSHEET destination type is supported.');
|
|
463
|
+
}
|
|
464
|
+
// This is not supported by the REST API, so we manage it internally for the fake.
|
|
465
|
+
this.__destinationId = id;
|
|
466
|
+
this.__destinationType = DestinationType.SPREADSHEET;
|
|
467
|
+
return this;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Sets whether the form is published responses.
|
|
419
471
|
* @param {boolean} enabled true if the form should accept responses; false otherwise.
|
|
420
472
|
* @returns {FakeForm} The form, for chaining.
|
|
421
473
|
*/
|
|
@@ -423,6 +475,7 @@ export class FakeForm {
|
|
|
423
475
|
throw new Error('setPublished is not yet implemented in the fake environment.');
|
|
424
476
|
}
|
|
425
477
|
|
|
478
|
+
|
|
426
479
|
__update(updateRequest) {
|
|
427
480
|
const batchRequest = Forms.newBatchUpdateFormRequest()
|
|
428
481
|
.setRequests([updateRequest])
|
|
@@ -430,6 +483,16 @@ export class FakeForm {
|
|
|
430
483
|
return this;
|
|
431
484
|
}
|
|
432
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Sets whether the form is accepting responses.
|
|
488
|
+
* @param {boolean} enabled true if the form should accept responses; false otherwise.
|
|
489
|
+
* @returns {FakeForm} The form, for chaining.
|
|
490
|
+
*/
|
|
491
|
+
setAcceptingResponses(enabled) {
|
|
492
|
+
// The REST API does not expose a way to set this. The fake will manage it internally.
|
|
493
|
+
throw new Error('setAcceptingResponses is not yet implemented in the fake environment.');
|
|
494
|
+
}
|
|
495
|
+
|
|
433
496
|
/**
|
|
434
497
|
* Sets the title of the form.
|
|
435
498
|
* @param {string} title The new title for the form.
|
|
@@ -481,10 +544,16 @@ export class FakeForm {
|
|
|
481
544
|
* shorten url no longer supported by google
|
|
482
545
|
* @returns {string} The form URL.
|
|
483
546
|
*/
|
|
484
|
-
shortenFormUrl() {
|
|
547
|
+
shortenFormUrl(url) {
|
|
548
|
+
// just validate the atgs would work on apps script
|
|
549
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'shortenFormUrl');
|
|
550
|
+
if (nargs !== 1 || !is.nonEmptyString(url)) {
|
|
551
|
+
matchThrow();
|
|
552
|
+
}
|
|
553
|
+
// apps script expects a url, but we return the published url and just ignore the url anyway
|
|
485
554
|
return this.getPublishedUrl()
|
|
486
555
|
}
|
|
487
|
-
|
|
556
|
+
|
|
488
557
|
toString() {
|
|
489
558
|
return 'Form';
|
|
490
559
|
}
|
|
@@ -30,10 +30,10 @@ export class FakeFormResponse {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Gets the unique ID for this form response.
|
|
33
|
-
* @returns {
|
|
33
|
+
* @returns {number} the unique hex ID converted to a decimal number
|
|
34
34
|
*/
|
|
35
35
|
getId() {
|
|
36
|
-
return this.__resource.responseId;
|
|
36
|
+
return parseInt(this.__resource.responseId, 16);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
package/src/support/auth.js
CHANGED
|
@@ -1,119 +1,127 @@
|
|
|
1
|
-
import { GoogleAuth } from
|
|
2
|
-
import is from
|
|
3
|
-
import { createHash } from
|
|
4
|
-
import { syncLog } from
|
|
1
|
+
import { GoogleAuth } from "google-auth-library";
|
|
2
|
+
import is from "@sindresorhus/is";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { syncLog } from "./workersync/synclogger.js";
|
|
5
5
|
|
|
6
|
-
const _authScopes = new Set([])
|
|
6
|
+
const _authScopes = new Set([]);
|
|
7
7
|
|
|
8
8
|
// all this stuff gets populated by the initial synced fxInit
|
|
9
|
-
let _auth = null
|
|
10
|
-
let _projectId = null
|
|
11
|
-
let _tokenInfo = null
|
|
9
|
+
let _auth = null;
|
|
10
|
+
let _projectId = null;
|
|
11
|
+
let _tokenInfo = null;
|
|
12
12
|
let _accessToken = null;
|
|
13
13
|
let _tokenExpiresAt = null;
|
|
14
|
-
let _manifest = null
|
|
15
|
-
let _clasp = null
|
|
16
|
-
|
|
17
|
-
let _settings = null
|
|
18
|
-
const setManifest = (manifest) => _manifest = manifest
|
|
19
|
-
const setClasp = (clasp) => _clasp = clasp
|
|
20
|
-
const getManifest = () => _manifest
|
|
21
|
-
const getClasp = () => _clasp
|
|
22
|
-
const getSettings = () => _settings
|
|
23
|
-
const getScriptId = () => getSettings().scriptId
|
|
24
|
-
const getDocumentId = () => getSettings().documentId
|
|
25
|
-
const setProjectId = (projectId) => _projectId = projectId
|
|
26
|
-
const setAccessToken = (accessToken) => _accessToken = accessToken
|
|
27
|
-
const setSettings = (settings) => _settings = settings
|
|
28
|
-
const getCachePath = () => getSettings().cache
|
|
29
|
-
const getPropertiesPath = () => getSettings().properties
|
|
30
|
-
const setTokenExpiresAt = (expiresAt) => _tokenExpiresAt = expiresAt;
|
|
14
|
+
let _manifest = null;
|
|
15
|
+
let _clasp = null;
|
|
16
|
+
|
|
17
|
+
let _settings = null;
|
|
18
|
+
const setManifest = (manifest) => (_manifest = manifest);
|
|
19
|
+
const setClasp = (clasp) => (_clasp = clasp);
|
|
20
|
+
const getManifest = () => _manifest;
|
|
21
|
+
const getClasp = () => _clasp;
|
|
22
|
+
const getSettings = () => _settings;
|
|
23
|
+
const getScriptId = () => getSettings().scriptId;
|
|
24
|
+
const getDocumentId = () => getSettings().documentId;
|
|
25
|
+
const setProjectId = (projectId) => (_projectId = projectId);
|
|
26
|
+
const setAccessToken = (accessToken) => (_accessToken = accessToken);
|
|
27
|
+
const setSettings = (settings) => (_settings = settings);
|
|
28
|
+
const getCachePath = () => getSettings().cache;
|
|
29
|
+
const getPropertiesPath = () => getSettings().properties;
|
|
30
|
+
const setTokenExpiresAt = (expiresAt) => (_tokenExpiresAt = expiresAt);
|
|
31
31
|
const setTokenInfo = (tokenInfo) => {
|
|
32
32
|
_tokenInfo = tokenInfo;
|
|
33
33
|
// set expiry time with a 60 second buffer
|
|
34
34
|
if (tokenInfo && tokenInfo.expires_in) {
|
|
35
|
-
setTokenExpiresAt(Date.now() + (
|
|
35
|
+
setTokenExpiresAt(Date.now() + (tokenInfo.expires_in - 60) * 1000);
|
|
36
36
|
} else {
|
|
37
37
|
// no expiry info, so we'll have to fetch a new one next time
|
|
38
38
|
setTokenExpiresAt(0);
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
};
|
|
41
41
|
const getTokenInfo = () => {
|
|
42
|
-
if (!_tokenInfo) throw `token info isnt set yet
|
|
43
|
-
return _tokenInfo
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const getTimeZone = () => getManifest().timeZone
|
|
47
|
-
const getUserId = () => getTokenInfo().sub
|
|
48
|
-
const getTokenScopes = () => getTokenInfo().scope
|
|
49
|
-
const getHashedUserId = () => createHash('md5').update(getUserId() + 'hud').digest().toString('hex')
|
|
42
|
+
if (!_tokenInfo) throw `token info isnt set yet`;
|
|
43
|
+
return _tokenInfo;
|
|
44
|
+
};
|
|
50
45
|
|
|
46
|
+
const getTimeZone = () => getManifest().timeZone;
|
|
47
|
+
const getUserId = () => getTokenInfo().sub;
|
|
48
|
+
const getTokenScopes = () => getTokenInfo().scope;
|
|
49
|
+
const getHashedUserId = () =>
|
|
50
|
+
createHash("md5")
|
|
51
|
+
.update(getUserId() + "hud")
|
|
52
|
+
.digest()
|
|
53
|
+
.toString("hex");
|
|
51
54
|
|
|
52
55
|
const getAccessToken = () => _accessToken;
|
|
53
56
|
|
|
54
|
-
const isTokenExpired = () =>
|
|
57
|
+
const isTokenExpired = () =>
|
|
58
|
+
!_accessToken || !_tokenExpiresAt || Date.now() >= _tokenExpiresAt;
|
|
55
59
|
/**
|
|
56
60
|
* we'll be using adc credentials so no need for any special auth here
|
|
57
61
|
* the idea here is to keep addign scopes to any auth so we have them all
|
|
58
62
|
* @param {string[]} [scopes=[]] the required scopes will be added to existing scopes already asked for
|
|
63
|
+
* @param {string} [keyFile=null]
|
|
64
|
+
* @param {boolean} [mcpLoading=false] When the MCP server is loading, this value is true. By this, the invalid values can be hidden while the MCP server is loading. This is important for using Google Antigravity.
|
|
59
65
|
* @returns {GoogleAuth.auth}
|
|
60
66
|
*/
|
|
61
|
-
let _authClient = null
|
|
62
|
-
const getAuthClient = () => _authClient
|
|
63
|
-
const setAuth = async (scopes = [], keyFile = null) => {
|
|
64
|
-
const hasCurrentAuth = hasAuth() && scopes.every(s => _authScopes.has(s));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
let _authClient = null;
|
|
68
|
+
const getAuthClient = () => _authClient;
|
|
69
|
+
const setAuth = async (scopes = [], keyFile = null, mcpLoading = false) => {
|
|
70
|
+
const hasCurrentAuth = hasAuth() && scopes.every((s) => _authScopes.has(s));
|
|
71
|
+
|
|
67
72
|
if (!hasCurrentAuth) {
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
if (!mcpLoading) {
|
|
74
|
+
syncLog(`...initializing auth and discovering project ID`);
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
// 1. Create the GoogleAuth manager instance (this instance has getProjectId)
|
|
71
78
|
_auth = new GoogleAuth({ scopes });
|
|
72
|
-
|
|
79
|
+
|
|
73
80
|
// 2. Use the manager to get the authenticated client (this is passed to API methods)
|
|
74
81
|
_authClient = await _auth.getClient();
|
|
75
|
-
|
|
82
|
+
|
|
76
83
|
// 3. Use the manager to reliably get the project ID
|
|
77
84
|
_projectId = await _auth.getProjectId();
|
|
78
|
-
|
|
85
|
+
|
|
79
86
|
if (!_projectId) {
|
|
80
|
-
throw new Error(
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Failed to get project ID from Application Default Credentials."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!mcpLoading) {
|
|
93
|
+
syncLog(`...discovered and set projectId to ${_projectId}`);
|
|
81
94
|
}
|
|
82
|
-
|
|
83
|
-
syncLog(`...discovered and set projectId to ${_projectId}`);
|
|
84
|
-
scopes.forEach(s => _authScopes.add(s));
|
|
95
|
+
scopes.forEach((s) => _authScopes.add(s));
|
|
85
96
|
}
|
|
86
97
|
return getAuth();
|
|
87
98
|
};
|
|
88
99
|
|
|
89
|
-
|
|
90
100
|
/**
|
|
91
101
|
* if we're doing a fetch on drive API we need a special header
|
|
92
102
|
*/
|
|
93
103
|
const googify = (options = {}) => {
|
|
94
|
-
const { headers } = options
|
|
104
|
+
const { headers } = options;
|
|
95
105
|
|
|
96
106
|
// no auth, therefore no need
|
|
97
|
-
if (!headers || !hasAuth()) return options
|
|
107
|
+
if (!headers || !hasAuth()) return options;
|
|
98
108
|
|
|
99
109
|
// if no authorization, we dont need this either
|
|
100
|
-
if (!Reflect.has(headers, "Authorization")) return options
|
|
110
|
+
if (!Reflect.has(headers, "Authorization")) return options;
|
|
101
111
|
|
|
102
112
|
// we'll need the projectID for this
|
|
103
113
|
// note - you must add the x-goog-user-project header, otherwise it'll use some nonexistent project
|
|
104
114
|
// see https://cloud.google.com/docs/authentication/rest#set-billing-project
|
|
105
115
|
// this has been syncified
|
|
106
|
-
const projectId = getProjectId()
|
|
116
|
+
const projectId = getProjectId();
|
|
107
117
|
return {
|
|
108
118
|
...options,
|
|
109
119
|
headers: {
|
|
110
120
|
"x-goog-user-project": projectId,
|
|
111
|
-
...headers
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
121
|
+
...headers,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
117
125
|
|
|
118
126
|
/**
|
|
119
127
|
* this would have been set up when manifest was imported
|
|
@@ -121,56 +129,59 @@ const googify = (options = {}) => {
|
|
|
121
129
|
*/
|
|
122
130
|
const getProjectId = () => {
|
|
123
131
|
if (is.null(_projectId) || is.undefined(_projectId)) {
|
|
124
|
-
throw new Error(
|
|
132
|
+
throw new Error(
|
|
133
|
+
"Project id not set - this means that the fxInit wasnt run"
|
|
134
|
+
);
|
|
125
135
|
}
|
|
126
|
-
return _projectId
|
|
127
|
-
}
|
|
136
|
+
return _projectId;
|
|
137
|
+
};
|
|
128
138
|
|
|
129
|
-
/**
|
|
139
|
+
/**
|
|
130
140
|
* @returns {Boolean} checks to see if auth has bee initialized yet
|
|
131
141
|
*/
|
|
132
|
-
const hasAuth = () => Boolean(_auth)
|
|
142
|
+
const hasAuth = () => Boolean(_auth);
|
|
133
143
|
|
|
134
144
|
/**
|
|
135
145
|
* @returns {GoogleAuth.auth}
|
|
136
146
|
*/
|
|
137
147
|
const getAuth = () => {
|
|
138
|
-
if (!hasAuth())
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
if (!hasAuth())
|
|
149
|
+
throw new Error(`auth hasnt been intialized with setAuth yet`);
|
|
150
|
+
return _auth;
|
|
151
|
+
};
|
|
142
152
|
|
|
143
153
|
/**
|
|
144
154
|
* why is this here ?
|
|
145
155
|
* because when we syncit, we import auth for each method and it needs this
|
|
146
156
|
* if it was somewhere else we'd need to import that too.
|
|
147
|
-
* we can't serialize a return object
|
|
157
|
+
* we can't serialize a return object
|
|
148
158
|
* so we just select a few props from it
|
|
149
|
-
* @param {SyncApiResponse} result
|
|
150
|
-
* @returns
|
|
159
|
+
* @param {SyncApiResponse} result
|
|
160
|
+
* @returns
|
|
151
161
|
*/
|
|
152
162
|
export const responseSyncify = (result) => {
|
|
153
163
|
if (!result) {
|
|
154
164
|
return {
|
|
155
165
|
status: 503, // Service Unavailable, a good representation for a worker-level failure
|
|
156
|
-
statusText:
|
|
157
|
-
error: {
|
|
166
|
+
statusText: "Worker Error: No response object received from API call",
|
|
167
|
+
error: {
|
|
168
|
+
message: "Worker Error: No response object received from API call",
|
|
169
|
+
},
|
|
158
170
|
};
|
|
159
171
|
}
|
|
160
172
|
return {
|
|
161
173
|
status: result.status,
|
|
162
174
|
statusText: result.statusText,
|
|
163
175
|
responseUrl: result.request?.responseURL,
|
|
164
|
-
error: result.data?.error
|
|
176
|
+
error: result.data?.error,
|
|
165
177
|
};
|
|
166
|
-
}
|
|
167
|
-
|
|
178
|
+
};
|
|
168
179
|
|
|
169
180
|
/**
|
|
170
181
|
* these are the ones that have been so far requested
|
|
171
182
|
* @returns {Set}
|
|
172
183
|
*/
|
|
173
|
-
const getAuthedScopes = () => _authScopes
|
|
184
|
+
const getAuthedScopes = () => _authScopes;
|
|
174
185
|
|
|
175
186
|
export const Auth = {
|
|
176
187
|
getAuth,
|
|
@@ -198,5 +209,5 @@ export const Auth = {
|
|
|
198
209
|
getTimeZone,
|
|
199
210
|
setAccessToken,
|
|
200
211
|
isTokenExpired,
|
|
201
|
-
getAuthClient
|
|
202
|
-
}
|
|
212
|
+
getAuthClient,
|
|
213
|
+
};
|
package/src/support/sxfetch.js
CHANGED
|
@@ -20,10 +20,31 @@ export const sxFetch = async (Auth, url, options, responseFields) => {
|
|
|
20
20
|
|
|
21
21
|
// Always fetch as a buffer to prevent corruption of binary data like images.
|
|
22
22
|
// The caller (UrlFetchApp/HttpResponse) will be responsible for decoding to text if needed.
|
|
23
|
+
// we need to fiddle with options as apps script is diffeent
|
|
24
|
+
let fixedOptions = {}
|
|
25
|
+
if (options) {
|
|
26
|
+
fixedOptions = { ...options }
|
|
27
|
+
Object.keys(fixedOptions).forEach(k => {
|
|
28
|
+
if (k.match(/Content-Type/i)) {
|
|
29
|
+
fixedOptions.contentType = fixedOptions[k]
|
|
30
|
+
delete fixedOptions[k]
|
|
31
|
+
}
|
|
32
|
+
if (k.match(/payload/i)) {
|
|
33
|
+
fixedOptions.body = fixedOptions[k]
|
|
34
|
+
delete fixedOptions[k]
|
|
35
|
+
}
|
|
36
|
+
if(k.match(/muteHttpExceptions/i)) {
|
|
37
|
+
fixedOptions.throwHttpErrors = !fixedOptions[k]
|
|
38
|
+
delete fixedOptions[k]
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
const response = await got(url, {
|
|
24
|
-
...
|
|
44
|
+
...fixedOptions,
|
|
25
45
|
responseType: 'buffer'
|
|
26
46
|
})
|
|
47
|
+
|
|
27
48
|
// we cant return the response from this as it cant be serialized
|
|
28
49
|
// so we;ll extract oout the fields required
|
|
29
50
|
const result = responseFields.reduce((p, c) => {
|