Address PR review feedback
- Convert CliAuth getters to methods (getInstanceName, getBaseUrl) so options can be added in the future - Remove StoredInstance from cli-node public API, hiding instance details - Move secretStore to cli-internal for re-use, refactoring from fs-extra to node:fs - Add shared getAuthInstanceService helper in cli-internal for constructing secret-store service keys - Define StoredInstance locally in cli-module-auth instead of importing from cli-node - Update all consumers and tests for the new method-based API Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/** Returns the secret-store service key for a given auth instance. */
|
||||
export function getAuthInstanceService(instanceName: string): string {
|
||||
return `backstage-cli:auth-instance:${instanceName}`;
|
||||
}
|
||||
@@ -25,3 +25,9 @@ export {
|
||||
OpaqueCommandLeafNode,
|
||||
isCommandNodeHidden,
|
||||
} from './InternalCommandNode';
|
||||
export { getAuthInstanceService } from './authIdentifiers';
|
||||
export {
|
||||
getSecretStore,
|
||||
resetSecretStore,
|
||||
type SecretStore,
|
||||
} from './secretStore';
|
||||
|
||||
+22
-5
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
type SecretStore = {
|
||||
export type SecretStore = {
|
||||
get(service: string, account: string): Promise<string | undefined>;
|
||||
set(service: string, account: string, secret: string): Promise<void>;
|
||||
delete(service: string, account: string): Promise<void>;
|
||||
@@ -54,6 +54,15 @@ class KeytarSecretStore implements SecretStore {
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class FileSecretStore implements SecretStore {
|
||||
private readonly baseDir: string;
|
||||
constructor() {
|
||||
@@ -73,17 +82,25 @@ class FileSecretStore implements SecretStore {
|
||||
}
|
||||
async get(service: string, account: string): Promise<string | undefined> {
|
||||
const file = this.filePath(service, account);
|
||||
if (!(await fs.pathExists(file))) return undefined;
|
||||
if (!(await pathExists(file))) {
|
||||
return undefined;
|
||||
}
|
||||
return await fs.readFile(file, 'utf8');
|
||||
}
|
||||
async set(service: string, account: string, secret: string): Promise<void> {
|
||||
const file = this.filePath(service, account);
|
||||
await fs.ensureDir(path.dirname(file));
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, secret, { encoding: 'utf8', mode: 0o600 });
|
||||
}
|
||||
async delete(service: string, account: string): Promise<void> {
|
||||
const file = this.filePath(service, account);
|
||||
await fs.remove(file);
|
||||
try {
|
||||
await fs.unlink(file);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { accessToken, instance } = await resolveAuth(instanceFlag);
|
||||
const { accessToken, baseUrl } = await resolveAuth(instanceFlag);
|
||||
|
||||
const client = new ActionsClient(instance.baseUrl, accessToken);
|
||||
const client = new ActionsClient(baseUrl, accessToken);
|
||||
const actions = await client.listForPlugin(actionId);
|
||||
const action = actions.find(a => a.id === actionId);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
args,
|
||||
);
|
||||
|
||||
const { accessToken, pluginSources, instance } = await resolveAuth(
|
||||
const { accessToken, pluginSources, baseUrl } = await resolveAuth(
|
||||
instanceFlag,
|
||||
);
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new ActionsClient(instance.baseUrl, accessToken);
|
||||
const client = new ActionsClient(baseUrl, accessToken);
|
||||
const actions = await client.list(pluginSources);
|
||||
|
||||
if (!actions.length) {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateInstanceConfig(auth.instanceName, 'pluginSources', [
|
||||
await updateInstanceConfig(auth.getInstanceName(), 'pluginSources', [
|
||||
...existing,
|
||||
pluginId,
|
||||
]);
|
||||
|
||||
@@ -39,7 +39,7 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
}
|
||||
|
||||
await updateInstanceConfig(
|
||||
auth.instanceName,
|
||||
auth.getInstanceName(),
|
||||
'pluginSources',
|
||||
existing.filter(s => s !== pluginId),
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { resolveAuth } from './resolveAuth';
|
||||
import { CliAuth, type StoredInstance } from '@backstage/cli-node';
|
||||
import { CliAuth } from '@backstage/cli-node';
|
||||
|
||||
jest.mock('@backstage/cli-node', () => {
|
||||
const actual = jest.requireActual('@backstage/cli-node');
|
||||
@@ -28,23 +28,14 @@ jest.mock('@backstage/cli-node', () => {
|
||||
const mockCreate = CliAuth.create as jest.MockedFunction<typeof CliAuth.create>;
|
||||
|
||||
describe('resolveAuth', () => {
|
||||
const mockInstance: StoredInstance = {
|
||||
name: 'production',
|
||||
baseUrl: 'https://backstage.example.com',
|
||||
clientId: 'my-client',
|
||||
issuedAt: Date.now(),
|
||||
accessTokenExpiresAt: Date.now() + 3600_000,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('resolves auth with the selected instance and stored token', async () => {
|
||||
mockCreate.mockResolvedValue({
|
||||
instance: mockInstance,
|
||||
instanceName: mockInstance.name,
|
||||
baseUrl: mockInstance.baseUrl,
|
||||
getInstanceName: jest.fn().mockReturnValue('production'),
|
||||
getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'),
|
||||
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
|
||||
getConfig: jest.fn().mockResolvedValue(['catalog', 'scaffolder']),
|
||||
} as unknown as CliAuth);
|
||||
@@ -53,7 +44,8 @@ describe('resolveAuth', () => {
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith({ instanceName: undefined });
|
||||
expect(result).toEqual({
|
||||
instance: mockInstance,
|
||||
baseUrl: 'https://backstage.example.com',
|
||||
instanceName: 'production',
|
||||
accessToken: 'test-access-token',
|
||||
pluginSources: ['catalog', 'scaffolder'],
|
||||
});
|
||||
@@ -61,9 +53,8 @@ describe('resolveAuth', () => {
|
||||
|
||||
it('passes instance name flag to CliAuth.create', async () => {
|
||||
mockCreate.mockResolvedValue({
|
||||
instance: mockInstance,
|
||||
instanceName: mockInstance.name,
|
||||
baseUrl: mockInstance.baseUrl,
|
||||
getInstanceName: jest.fn().mockReturnValue('staging'),
|
||||
getBaseUrl: jest.fn().mockReturnValue('https://staging.example.com'),
|
||||
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
|
||||
getConfig: jest.fn().mockResolvedValue([]),
|
||||
} as unknown as CliAuth);
|
||||
@@ -75,9 +66,8 @@ describe('resolveAuth', () => {
|
||||
|
||||
it('throws when getAccessToken fails', async () => {
|
||||
mockCreate.mockResolvedValue({
|
||||
instance: mockInstance,
|
||||
instanceName: mockInstance.name,
|
||||
baseUrl: mockInstance.baseUrl,
|
||||
getInstanceName: jest.fn().mockReturnValue('production'),
|
||||
getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'),
|
||||
getAccessToken: jest
|
||||
.fn()
|
||||
.mockRejectedValue(
|
||||
@@ -93,9 +83,8 @@ describe('resolveAuth', () => {
|
||||
|
||||
it('returns empty plugin sources when none are configured', async () => {
|
||||
mockCreate.mockResolvedValue({
|
||||
instance: mockInstance,
|
||||
instanceName: mockInstance.name,
|
||||
baseUrl: mockInstance.baseUrl,
|
||||
getInstanceName: jest.fn().mockReturnValue('production'),
|
||||
getBaseUrl: jest.fn().mockReturnValue('https://backstage.example.com'),
|
||||
getAccessToken: jest.fn().mockResolvedValue('test-access-token'),
|
||||
getConfig: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as CliAuth);
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CliAuth, type StoredInstance } from '@backstage/cli-node';
|
||||
import { CliAuth } from '@backstage/cli-node';
|
||||
|
||||
export async function resolveAuth(instanceFlag?: string): Promise<{
|
||||
instance: StoredInstance;
|
||||
baseUrl: string;
|
||||
instanceName: string;
|
||||
accessToken: string;
|
||||
pluginSources: string[];
|
||||
}> {
|
||||
@@ -25,5 +26,10 @@ export async function resolveAuth(instanceFlag?: string): Promise<{
|
||||
const accessToken = await auth.getAccessToken();
|
||||
const pluginSources = (await auth.getConfig<string[]>('pluginSources')) ?? [];
|
||||
|
||||
return { instance: auth.instance, accessToken, pluginSources };
|
||||
return {
|
||||
baseUrl: auth.getBaseUrl(),
|
||||
instanceName: auth.getInstanceName(),
|
||||
accessToken,
|
||||
pluginSources,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"bin": "bin/backstage-cli-module-auth",
|
||||
"files": [
|
||||
"dist",
|
||||
"bin"
|
||||
@@ -50,6 +51,5 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"keytar": "^7.9.0"
|
||||
},
|
||||
"bin": "bin/backstage-cli-module-auth"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
getInstanceByName,
|
||||
StoredInstance,
|
||||
} from '../lib/storage';
|
||||
import { getSecretStore } from '../lib/secretStore';
|
||||
import { getSecretStore, getAuthInstanceService } from '@internal/cli';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'node:path';
|
||||
@@ -321,7 +321,7 @@ async function persistInstance(options: {
|
||||
const { instanceName, backendBaseUrl, clientId, token } = options;
|
||||
const secretStore = await getSecretStore();
|
||||
await withMetadataLock(async () => {
|
||||
const service = `backstage-cli:auth-instance:${instanceName}`;
|
||||
const service = getAuthInstanceService(instanceName);
|
||||
await secretStore.set(service, 'accessToken', token.access_token);
|
||||
if (token.refresh_token) {
|
||||
await secretStore.set(service, 'refreshToken', token.refresh_token);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { cli } from 'cleye';
|
||||
import type { CliCommandContext } from '@backstage/cli-node';
|
||||
import { getSecretStore } from '../lib/secretStore';
|
||||
import { getSecretStore, getAuthInstanceService } from '@internal/cli';
|
||||
import {
|
||||
removeInstance,
|
||||
withMetadataLock,
|
||||
@@ -47,7 +47,7 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
await withMetadataLock(async () => {
|
||||
const instance = await getInstanceByName(instanceName);
|
||||
const secretStore = await getSecretStore();
|
||||
const service = `backstage-cli:auth-instance:${instanceName}`;
|
||||
const service = getAuthInstanceService(instanceName);
|
||||
const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? '';
|
||||
|
||||
if (refreshToken) {
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async ({ args, info }: CliCommandContext) => {
|
||||
const auth = await CliAuth.create({ instanceName: instanceFlag });
|
||||
const accessToken = await auth.getAccessToken();
|
||||
|
||||
const authBase = new URL('/api/auth', auth.baseUrl)
|
||||
const authBase = new URL('/api/auth', auth.getBaseUrl())
|
||||
.toString()
|
||||
.replace(/\/$/, '');
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
import { accessTokenNeedsRefresh, refreshAccessToken } from './auth';
|
||||
import * as storage from './storage';
|
||||
import * as secretStore from './secretStore';
|
||||
import * as internalCli from '@internal/cli';
|
||||
import * as http from './http';
|
||||
|
||||
jest.mock('./storage');
|
||||
jest.mock('./secretStore');
|
||||
jest.mock('@internal/cli');
|
||||
jest.mock('./http');
|
||||
|
||||
const mockStorage = storage as jest.Mocked<typeof storage>;
|
||||
const mockSecretStore = secretStore as jest.Mocked<typeof secretStore>;
|
||||
const mockInternalCli = internalCli as jest.Mocked<typeof internalCli>;
|
||||
const mockHttp = http as jest.Mocked<typeof http>;
|
||||
|
||||
describe('auth', () => {
|
||||
@@ -95,7 +95,10 @@ describe('auth', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSecretStore.getSecretStore.mockResolvedValue(mockSecretStoreInstance);
|
||||
mockInternalCli.getSecretStore.mockResolvedValue(mockSecretStoreInstance);
|
||||
mockInternalCli.getAuthInstanceService.mockImplementation(
|
||||
(name: string) => `backstage-cli:auth-instance:${name}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully refresh access token', async () => {
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod/v3';
|
||||
import type { StoredInstance } from '@backstage/cli-node';
|
||||
import { upsertInstance, withMetadataLock, getInstanceByName } from './storage';
|
||||
import { getSecretStore } from './secretStore';
|
||||
import {
|
||||
type StoredInstance,
|
||||
upsertInstance,
|
||||
withMetadataLock,
|
||||
getInstanceByName,
|
||||
} from './storage';
|
||||
import { getSecretStore, getAuthInstanceService } from '@internal/cli';
|
||||
import { httpJson } from './http';
|
||||
|
||||
const TokenResponseSchema = z.object({
|
||||
@@ -40,7 +44,7 @@ export async function refreshAccessToken(
|
||||
return withMetadataLock(async () => {
|
||||
const instance = await getInstanceByName(instanceName);
|
||||
|
||||
const service = `backstage-cli:auth-instance:${instanceName}`;
|
||||
const service = getAuthInstanceService(instanceName);
|
||||
const refreshToken = (await secretStore.get(service, 'refreshToken')) ?? '';
|
||||
if (!refreshToken) {
|
||||
throw new Error(
|
||||
|
||||
@@ -21,7 +21,7 @@ jest.mock('keytar', () => {
|
||||
import fs from 'fs-extra';
|
||||
import path from 'node:path';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { getSecretStore, resetSecretStore } from './secretStore';
|
||||
import { getSecretStore, resetSecretStore } from '@internal/cli';
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
|
||||
@@ -22,8 +22,15 @@ import lockfile from 'proper-lockfile';
|
||||
import YAML from 'yaml';
|
||||
import { z } from 'zod/v3';
|
||||
|
||||
export type { StoredInstance } from '@backstage/cli-node';
|
||||
import type { StoredInstance } from '@backstage/cli-node';
|
||||
export type StoredInstance = {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
clientId: string;
|
||||
issuedAt: number;
|
||||
accessTokenExpiresAt: number;
|
||||
selected?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const METADATA_FILE = 'auth-instances.yaml';
|
||||
|
||||
|
||||
@@ -88,12 +88,11 @@ export interface BackstagePackageJson {
|
||||
|
||||
// @public
|
||||
export class CliAuth {
|
||||
get baseUrl(): string;
|
||||
static create(options?: CliAuthCreateOptions): Promise<CliAuth>;
|
||||
getAccessToken(): Promise<string>;
|
||||
getBaseUrl(): string;
|
||||
getConfig<T = unknown>(key: string): Promise<T | undefined>;
|
||||
get instance(): StoredInstance;
|
||||
get instanceName(): string;
|
||||
getInstanceName(): string;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -287,17 +286,6 @@ export function runWorkerQueueThreads<TItem, TResult, TContext>(
|
||||
results: TResult[];
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type StoredInstance = {
|
||||
name: string;
|
||||
baseUrl: string;
|
||||
clientId: string;
|
||||
issuedAt: number;
|
||||
accessTokenExpiresAt: number;
|
||||
selected?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class SuccessCache {
|
||||
// (undocumented)
|
||||
|
||||
@@ -58,9 +58,8 @@ describe('CliAuth', () => {
|
||||
const auth = await CliAuth.create();
|
||||
|
||||
expect(mockStorage.getSelectedInstance).toHaveBeenCalledWith(undefined);
|
||||
expect(auth.instance).toEqual(mockInstance);
|
||||
expect(auth.instanceName).toBe('production');
|
||||
expect(auth.baseUrl).toBe('https://backstage.example.com');
|
||||
expect(auth.getInstanceName()).toBe('production');
|
||||
expect(auth.getBaseUrl()).toBe('https://backstage.example.com');
|
||||
});
|
||||
|
||||
it('resolves a named instance when specified', async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
accessTokenNeedsRefresh,
|
||||
} from './storage';
|
||||
import { getSecretStore, type SecretStore } from './secretStore';
|
||||
import { getAuthInstanceService } from './authIdentifiers';
|
||||
import { httpJson } from './httpJson';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -73,18 +74,13 @@ export class CliAuth {
|
||||
this.#secretStore = secretStore;
|
||||
}
|
||||
|
||||
/** The resolved instance metadata. */
|
||||
get instance(): StoredInstance {
|
||||
return this.#instance;
|
||||
}
|
||||
|
||||
/** Shorthand for `instance.name`. */
|
||||
get instanceName(): string {
|
||||
/** Returns the name of the resolved auth instance. */
|
||||
getInstanceName(): string {
|
||||
return this.#instance.name;
|
||||
}
|
||||
|
||||
/** Shorthand for `instance.baseUrl`. */
|
||||
get baseUrl(): string {
|
||||
/** Returns the base URL of the resolved auth instance. */
|
||||
getBaseUrl(): string {
|
||||
return this.#instance.baseUrl;
|
||||
}
|
||||
|
||||
@@ -97,7 +93,7 @@ export class CliAuth {
|
||||
await this.#refreshAccessToken();
|
||||
}
|
||||
|
||||
const service = `backstage-cli:auth-instance:${this.#instance.name}`;
|
||||
const service = getAuthInstanceService(this.#instance.name);
|
||||
const token = await this.#secretStore.get(service, 'accessToken');
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
@@ -116,7 +112,7 @@ export class CliAuth {
|
||||
}
|
||||
|
||||
async #refreshAccessToken(): Promise<void> {
|
||||
const service = `backstage-cli:auth-instance:${this.#instance.name}`;
|
||||
const service = getAuthInstanceService(this.#instance.name);
|
||||
const refreshToken =
|
||||
(await this.#secretStore.get(service, 'refreshToken')) ?? '';
|
||||
if (!refreshToken) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/** @internal */
|
||||
export function getAuthInstanceService(instanceName: string): string {
|
||||
return `backstage-cli:auth-instance:${instanceName}`;
|
||||
}
|
||||
@@ -15,4 +15,3 @@
|
||||
*/
|
||||
|
||||
export { CliAuth, type CliAuthCreateOptions } from './CliAuth';
|
||||
export { type StoredInstance } from './storage';
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
/** @public */
|
||||
/** @internal */
|
||||
export type SecretStore = {
|
||||
get(service: string, account: string): Promise<string | undefined>;
|
||||
set(service: string, account: string, secret: string): Promise<void>;
|
||||
@@ -55,6 +55,15 @@ class KeytarSecretStore implements SecretStore {
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.stat(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class FileSecretStore implements SecretStore {
|
||||
private readonly baseDir: string;
|
||||
constructor() {
|
||||
@@ -74,23 +83,31 @@ class FileSecretStore implements SecretStore {
|
||||
}
|
||||
async get(service: string, account: string): Promise<string | undefined> {
|
||||
const file = this.filePath(service, account);
|
||||
if (!(await fs.pathExists(file))) return undefined;
|
||||
if (!(await pathExists(file))) {
|
||||
return undefined;
|
||||
}
|
||||
return await fs.readFile(file, 'utf8');
|
||||
}
|
||||
async set(service: string, account: string, secret: string): Promise<void> {
|
||||
const file = this.filePath(service, account);
|
||||
await fs.ensureDir(path.dirname(file));
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, secret, { encoding: 'utf8', mode: 0o600 });
|
||||
}
|
||||
async delete(service: string, account: string): Promise<void> {
|
||||
const file = this.filePath(service, account);
|
||||
await fs.remove(file);
|
||||
try {
|
||||
await fs.unlink(file);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let singleton: SecretStore | undefined;
|
||||
|
||||
/** @public */
|
||||
/** @internal */
|
||||
export async function getSecretStore(): Promise<SecretStore> {
|
||||
if (!singleton) {
|
||||
const keytar = await loadKeytar();
|
||||
|
||||
Reference in New Issue
Block a user