Skip to content

Commit b0fa745

Browse files
authored
fix: Fix incorrect storage of id_token, add missing awaits for some async operations( #1121)
* fix: await settings writes across create/clone flows Ensure project settings persistence is awaited before command and MCP clone flows return success. This removes fire-and-forget writes so I/O failures are surfaced in-band instead of being lost. Add assertions that create/clone produce a persisted .clasp.json and add a real filesystem test that setProjectId propagates write failures. * fix: handle invalid --project and --ignore paths Convert explicit missing config and ignore paths into controlled CLI errors instead of bubbling raw fs.stat ENOENT output. Treat both ENOENT and ENOTDIR as user path-not-found cases so directory/file mixups are handled consistently. Cover both error paths with real filesystem tests and validate the exact CLI messages via direct command execution. * fix: align MCP pull/clone response wording Update MCP pull_project and clone_project status text to match the actual operation names. Correct create_project and clone_project error text that previously referenced push. Keep this scoped to user-facing response copy without changing command behavior. * Fix open-web-app deployment label formatting Correct the deployment choice label in open-web-app by removing an extra closing brace from the rendered string. This prevents malformed output in the interactive deployment picker. * Update tail-logs options in README command list Remove stale --open and --setup flags from the tail-logs command summary so it matches the current CLI implementation. This keeps the command index aligned with supported options. * chore: format files.ts to satisfy biome check Apply biome formatting adjustments in Files core implementation to eliminate the pre-existing check failure. This is a non-functional change that only normalizes whitespace, indentation, and line wrapping. Keeps the combined PR green under npm run check. * fix: await logout credential deletion Await credential store deletion in the logout command so write failures are surfaced before success output is emitted. Add a regression test with an unwritable auth file to verify logout exits with an error and does not print JSON success when deletion fails. * fix: persist correct id_token on token refresh Stop writing access tokens into the id_token field when OAuth tokens refresh for loaded credentials and post-login credentials. Add auth-level regression tests that trigger refresh events through both code paths and verify the persisted id_token value matches token.id_token. Include import ordering updates in the new auth test to satisfy lint checks.
1 parent b225233 commit b0fa745

15 files changed

Lines changed: 250 additions & 30 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ clasp
135135

136136
> **NOTE**: These commands require you to add your [Project ID](#projectid-optional).
137137
138-
- [`clasp tail-logs [--json] [--open] [--setup] [--watch] [--simplified]`](#logs)
138+
- [`clasp tail-logs [--json] [--watch] [--simplified]`](#logs)
139139
- [`clasp list-apis`](#apis)
140140
- [`clasp enable-api<api>`](#apis)
141141
- [`clasp disable-api <api>`](#apis)

src/auth/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export async function getAuthorizedOAuth2Client(
149149
...savedCredentials,
150150
expiry_date: tokens.expiry_date,
151151
access_token: tokens.access_token,
152-
id_token: tokens.access_token,
152+
id_token: tokens.id_token,
153153
};
154154
await store.save(userKey!, refreshedCredentials);
155155
});
@@ -217,7 +217,7 @@ async function saveOauthClientCredentials(store: CredentialStore, userKey: strin
217217
...savedCredentials,
218218
expiry_date: tokens.expiry_date,
219219
access_token: tokens.access_token,
220-
id_token: tokens.access_token,
220+
id_token: tokens.id_token,
221221
};
222222
debug('Saving refreshed credentials for user %s', userKey);
223223
await store.save(userKey, refreshedCredentials);

src/commands/clone-script.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export const command = new Command('clone-script')
9595
const files = await clasp.files.pull(versionNumber);
9696
// After successfully pulling files, update the local project settings (e.g., .clasp.json)
9797
// to reflect the cloned scriptId and other relevant configurations.
98-
clasp.project.updateSettings();
98+
await clasp.project.updateSettings();
9999
return files;
100100
});
101101

src/commands/create-script.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export const command = new Command('create-script')
141141
const files = await withSpinner(spinnerMsg, async () => {
142142
const files = await clasp.files.pull();
143143
// Update the local .clasp.json with the new scriptId and other settings.
144-
clasp.project.updateSettings();
144+
await clasp.project.updateSettings();
145145
return files;
146146
});
147147

src/commands/logout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const command = new Command('logout').description('Logout of clasp').acti
4141
return;
4242
}
4343

44-
auth.credentialStore?.delete(auth.user);
44+
await auth.credentialStore?.delete(auth.user);
4545

4646
if (options.json) {
4747
console.log(JSON.stringify({success: true}, null, 2));

src/commands/open-webapp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const command = new Command('open-web-app')
4444
const choices = deployments.results.map(deployment => {
4545
const description = ellipsize(deployment.deploymentConfig?.description ?? '', 30);
4646
const versionNumber = (deployment.deploymentConfig?.versionNumber?.toString() ?? 'HEAD').padEnd(4);
47-
const name = `${description}@${versionNumber}} - ${deployment.deploymentId}`;
47+
const name = `${description}@${versionNumber} - ${deployment.deploymentId}`;
4848
return {
4949
name: name,
5050
value: deployment.deploymentId,

src/core/clasp.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,15 @@ async function findProjectRootdDir(configFilePath?: string) {
246246
debug('Searching for project root');
247247
if (configFilePath) {
248248
debug('Checking for config file at %s', configFilePath);
249-
const info = await fs.stat(configFilePath);
249+
let info: Awaited<ReturnType<typeof fs.stat>>;
250+
try {
251+
info = await fs.stat(configFilePath);
252+
} catch (error) {
253+
if (isPathNotFoundError(error)) {
254+
throw new Error(`Invalid --project path: ${configFilePath}. File or directory does not exist.`);
255+
}
256+
throw error;
257+
}
250258
if (info.isDirectory()) {
251259
debug('Is directory, trying file');
252260
configFilePath = path.join(configFilePath, '.clasp.json');
@@ -279,7 +287,15 @@ async function findIgnoreFile(projectDir: string, configFilePath?: string) {
279287
debug('Searching for ignore file');
280288
if (configFilePath) {
281289
debug('Checking for ignore file at %s', configFilePath);
282-
const info = await fs.stat(configFilePath);
290+
let info: Awaited<ReturnType<typeof fs.stat>>;
291+
try {
292+
info = await fs.stat(configFilePath);
293+
} catch (error) {
294+
if (isPathNotFoundError(error)) {
295+
throw new Error(`Invalid --ignore path: ${configFilePath}. File or directory does not exist.`);
296+
}
297+
throw error;
298+
}
283299
if (info.isDirectory()) {
284300
debug('Is directory, trying file');
285301
configFilePath = path.join(configFilePath, '.claspignore');
@@ -328,3 +344,10 @@ function firstValue<T>(values: T | T[] | undefined): T | undefined {
328344
}
329345
return values as T | undefined;
330346
}
347+
348+
function isPathNotFoundError(error: unknown): error is NodeJS.ErrnoException {
349+
if (!error || typeof error !== 'object') {
350+
return false;
351+
}
352+
return (error as NodeJS.ErrnoException).code === 'ENOENT' || (error as NodeJS.ErrnoException).code === 'ENOTDIR';
353+
}

src/core/files.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,11 @@ function parentDirs(file: string) {
5656
return parentDirs;
5757
}
5858
function isInside(parentPath: string, childPath: string): boolean {
59-
6059
const relative = path.relative(parentPath, childPath);
6160

62-
return (
63-
64-
relative !== '' &&
65-
66-
!relative.startsWith('..') &&
67-
68-
!path.isAbsolute(relative)
69-
70-
);
71-
61+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
7262
}
7363

74-
75-
7664
async function getLocalFiles(rootDir: string, ignorePatterns: string[], recursive: boolean) {
7765
debug('Collecting files in %s', rootDir);
7866
let fdirBuilder = new fdir().withBasePath().withRelativePaths();
@@ -207,7 +195,7 @@ export class Files {
207195
this.options = options;
208196
}
209197

210-
/**
198+
/**
211199
* Fetches the content of a script project from Google Drive.
212200
*/
213201
async fetchRemote(versionNumber?: number): Promise<ProjectFile[]> {
@@ -241,7 +229,7 @@ export class Files {
241229
// This prevents traversal (../../) and prefix attacks (/foo/bar vs /foo/bar1)
242230
if (!isInside(absoluteContentDir, resolvedPath)) {
243231
throw new Error(
244-
`Security Error: Remote file name "${f.name}" attempts to write outside the project directory.`
232+
`Security Error: Remote file name "${f.name}" attempts to write outside the project directory.`,
245233
);
246234
}
247235

@@ -601,4 +589,3 @@ function extractSyntaxError(error: GaxiosError, files: ProjectFile[]) {
601589
snippet = preLines + '\n' + errLine + '\n' + postLines;
602590
return {message, snippet}; // Return the formatted message and snippet.
603591
}
604-

src/core/project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ export class Project {
470470
debug('Setting project ID %s in file %s', projectId, this.options.configFilePath);
471471
assertScriptConfigured(this.options);
472472
this.options.project.projectId = projectId;
473-
this.updateSettings();
473+
await this.updateSettings();
474474
}
475475

476476
/**

src/mcp/server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export function buildMcpServer(auth: AuthInfo) {
197197
content: [
198198
{
199199
type: 'text',
200-
text: `Error pushing project: ${err.message}`,
200+
text: `Error cloning project: ${err.message}`,
201201
},
202202
],
203203
};
@@ -294,7 +294,7 @@ export function buildMcpServer(auth: AuthInfo) {
294294
content: [
295295
{
296296
type: 'text',
297-
text: `Error pushing project: ${err.message}`,
297+
text: `Error creating project: ${err.message}`,
298298
},
299299
],
300300
};
@@ -365,7 +365,7 @@ export function buildMcpServer(auth: AuthInfo) {
365365
// Pull files from the specified remote script ID.
366366
const files = await clasp.files.pull();
367367
// Create/update the .clasp.json file with the cloned script's ID and settings.
368-
clasp.project.updateSettings();
368+
await clasp.project.updateSettings();
369369

370370
const fileList: Array<TextContent> = files.map(file => ({
371371
type: 'text',
@@ -394,7 +394,7 @@ export function buildMcpServer(auth: AuthInfo) {
394394
content: [
395395
{
396396
type: 'text',
397-
text: `Error pushing project: ${err.message}`,
397+
text: `Error cloning project: ${err.message}`,
398398
},
399399
],
400400
};

0 commit comments

Comments
 (0)