From b8f72c858598b4c6f7a433024adcd4b39986d649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 3 Nov 2021 14:34:17 +0100 Subject: [PATCH] feat: make KeytarStore the default This changes the default publisher login store to be the system's default, using keytar. If a FileStore is detected, there will be a migration step the first time a store gets open. Set VSCE_STORE=file to always force using the FileStore. Closes: #325 --- src/store.ts | 60 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/src/store.ts b/src/store.ts index 0d439b1e..3bc61238 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,21 +1,18 @@ import * as fs from 'fs'; import * as path from 'path'; -import { promisify } from 'util'; import { home } from 'osenv'; import { read, getGalleryAPI, getSecurityRolesAPI, log } from './util'; import { validatePublisher } from './validation'; import { readManifest } from './package'; import * as keytar from 'keytar'; -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); - export interface IPublisher { readonly name: string; readonly pat: string; } export interface IStore extends Iterable { + readonly size: number; get(name: string): IPublisher | undefined; add(publisher: IPublisher): Promise; delete(name: string): Promise; @@ -26,7 +23,7 @@ export class FileStore implements IStore { static async open(path: string = FileStore.DefaultPath): Promise { try { - const rawStore = await readFile(path, 'utf8'); + const rawStore = await fs.promises.readFile(path, 'utf8'); return new FileStore(path, JSON.parse(rawStore).publishers); } catch (err) { if (err.code === 'ENOENT') { @@ -39,10 +36,22 @@ export class FileStore implements IStore { } } - private constructor(private readonly path: string, private publishers: IPublisher[]) {} + get size(): number { + return this.publishers.length; + } + + private constructor(readonly path: string, private publishers: IPublisher[]) {} private async save(): Promise { - await writeFile(this.path, JSON.stringify({ publishers: this.publishers }), { mode: '0600' }); + await fs.promises.writeFile(this.path, JSON.stringify({ publishers: this.publishers }), { mode: '0600' }); + } + + async deleteStore(): Promise { + try { + await fs.promises.unlink(this.path); + } catch { + // noop + } } get(name: string): IPublisher | undefined { @@ -74,6 +83,10 @@ export class KeytarStore implements IStore { ); } + get size(): number { + return this.publishers.length; + } + private constructor(private readonly serviceName: string, private publishers: IPublisher[]) {} get(name: string): IPublisher { @@ -131,10 +144,33 @@ async function requestPAT(publisherName: string): Promise { return pat; } +async function openDefaultStore(): Promise { + if (/^file$/i.test(process.env['VSCE_STORE'] ?? '')) { + return await FileStore.open(); + } + + const keytarStore = await KeytarStore.open(); + const fileStore = await FileStore.open(); + + // migrate from file store + if (fileStore.size) { + for (const publisher of fileStore) { + await keytarStore.add(publisher); + } + + await fileStore.deleteStore(); + log.info( + `Migrated ${fileStore.size} publishers to system credential manager. Deleted local store '${fileStore.path}'.` + ); + } + + return keytarStore; +} + export async function getPublisher(publisherName: string): Promise { validatePublisher(publisherName); - const store = await FileStore.open(); + const store = await openDefaultStore(); let publisher = store.get(publisherName); if (publisher) { @@ -151,7 +187,7 @@ export async function getPublisher(publisherName: string): Promise { export async function loginPublisher(publisherName: string): Promise { validatePublisher(publisherName); - const store = await FileStore.open(); + const store = await openDefaultStore(); let publisher = store.get(publisherName); if (publisher) { @@ -173,7 +209,7 @@ export async function loginPublisher(publisherName: string): Promise export async function logoutPublisher(publisherName: string): Promise { validatePublisher(publisherName); - const store = await FileStore.open(); + const store = await openDefaultStore(); const publisher = store.get(publisherName); if (!publisher) { @@ -194,13 +230,13 @@ export async function deletePublisher(publisherName: string): Promise { const api = await getGalleryAPI(publisher.pat); await api.deletePublisher(publisherName); - const store = await FileStore.open(); + const store = await openDefaultStore(); await store.delete(publisherName); log.done(`Deleted publisher '${publisherName}'.`); } export async function listPublishers(): Promise { - const store = await FileStore.open(); + const store = await openDefaultStore(); for (const publisher of store) { console.log(publisher.name);