@google/clasp
Version:
Develop Apps Script Projects locally
138 lines (137 loc) • 5.79 kB
JavaScript
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file implements the `CredentialStore` interface, providing a file-based
// mechanism for storing and managing user credentials. It handles different
// file formats for compatibility with older versions of clasp.
import fs from 'fs';
function hasLegacyLocalCredentials(store) {
return store.token && store.oauth2ClientSettings;
}
function hasLegacyGlobalCredentials(store) {
return !!store.access_token;
}
/**
* Implements the `CredentialStore` interface using a local JSON file.
* This class handles saving, loading, and deleting OAuth 2.0 credentials
* for different users. It also supports migrating credentials from older
* clasp file formats.
*/
export class FileCredentialStore {
constructor(filePath) {
this.filePath = filePath;
}
/**
* Saves credentials for a given user.
* If credentials are provided as undefined, it effectively removes the user's credentials.
* @param {string} user - The identifier for the user.
* @param {StoredCredential | undefined} credentials - The credentials to save, or undefined to clear.
* @returns {Promise<void>}
*/
async save(user, credentials) {
const store = this.readFile();
if (!store.tokens) {
store.tokens = {};
}
store.tokens[user] = credentials;
this.writeFile(store);
}
/**
* Deletes credentials for a specific user.
* If deleting the 'default' user, it also cleans up legacy credential formats.
* @param {string} user - The identifier for the user whose credentials are to be deleted.
* @returns {Promise<void>}
*/
async delete(user) {
let store = this.readFile();
if (!store.tokens) {
store.tokens = {};
}
store.tokens[user] = undefined;
if (user === 'default') {
// If the 'default' user's token is deleted, we also clean up any potential
// top-level V1 credential keys to ensure a clean state and prevent
// V1 credentials from being loaded unintentionally after a V3 'default' delete.
store = {
tokens: store.tokens, // Keep other named tokens if they exist
};
}
this.writeFile(store);
}
/**
* Deletes all stored credentials by clearing the tokens map.
* @returns {Promise<void>}
*/
async deleteAll() {
await this.writeFile({
tokens: {},
});
}
/**
* Loads credentials for a given user.
* It supports loading credentials from the current format as well as
* attempting to load from legacy V1 local and global file formats
* if the user is 'default' and no V3 credentials are found.
* @param {string} user - The identifier for the user.
* @returns {Promise<StoredCredential | null>} The stored credentials if found, otherwise null.
*/
async load(user) {
var _a, _b, _c;
const store = this.readFile();
const credentials = (_a = store.tokens) === null || _a === void 0 ? void 0 : _a[user];
if (credentials) {
return credentials; // Modern V3 token found for the user.
}
// The following logic attempts to load legacy V1 credentials
// ONLY if the requested user is 'default' and no V3 'default' token was found.
if (user !== 'default') {
return null; // For non-default users, only V3 tokens are considered.
}
// Check for V1 local file format (usually from older .clasprc.json in project root)
if (hasLegacyLocalCredentials(store)) {
// Convert V1 local format to StoredCredential format.
return {
type: 'authorized_user',
...store.token, // Spread V1 token properties
client_id: (_b = store.oauth2ClientSettings) === null || _b === void 0 ? void 0 : _b.clientId,
client_secret: (_c = store.oauth2ClientSettings) === null || _c === void 0 ? void 0 : _c.clientSecret,
};
}
// Check for V1 global file format (usually from older ~/.clasprc.json)
if (hasLegacyGlobalCredentials(store)) {
// Convert V1 global format to StoredCredential format.
// Note: Default client_id and client_secret are used here as global V1 didn't store them.
return {
type: 'authorized_user',
access_token: store.access_token, // Map V1 fields
refresh_token: store.refresh_token,
expiry_date: store.exprity_date,
token_type: store.token_type,
client_id: '1072944905499-vm2v2i5dvn0a0d2o4ca36i1vge8cvbn0.apps.googleusercontent.com',
client_secret: 'v6V3fKV_zWU7iw1DrpO1rknX',
};
}
return null;
}
readFile() {
if (fs.existsSync(this.filePath)) {
// TODO - use promises
const content = fs.readFileSync(this.filePath, { encoding: 'utf8' });
return JSON.parse(content);
}
return {};
}
writeFile(store) {
fs.writeFileSync(this.filePath, JSON.stringify(store, null, 2));
}
}