diff --git a/packages/cli-internal/src/authIdentifiers.ts b/packages/cli-internal/src/authIdentifiers.ts new file mode 100644 index 0000000000..6b72554a4f --- /dev/null +++ b/packages/cli-internal/src/authIdentifiers.ts @@ -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}`; +} diff --git a/packages/cli-internal/src/index.ts b/packages/cli-internal/src/index.ts index 0e69d45032..281b998d92 100644 --- a/packages/cli-internal/src/index.ts +++ b/packages/cli-internal/src/index.ts @@ -25,3 +25,9 @@ export { OpaqueCommandLeafNode, isCommandNodeHidden, } from './InternalCommandNode'; +export { getAuthInstanceService } from './authIdentifiers'; +export { + getSecretStore, + resetSecretStore, + type SecretStore, +} from './secretStore'; diff --git a/packages/cli-module-auth/src/lib/secretStore.ts b/packages/cli-internal/src/secretStore.ts similarity index 87% rename from packages/cli-module-auth/src/lib/secretStore.ts rename to packages/cli-internal/src/secretStore.ts index 55fac878b7..26c1e6ad2b 100644 --- a/packages/cli-module-auth/src/lib/secretStore.ts +++ b/packages/cli-internal/src/secretStore.ts @@ -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; set(service: string, account: string, secret: string): Promise; delete(service: string, account: string): Promise; @@ -54,6 +54,15 @@ class KeytarSecretStore implements SecretStore { } } +async function pathExists(p: string): Promise { + 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 { 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 { 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 { 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; + } + } } } diff --git a/packages/cli-module-actions/src/commands/execute.ts b/packages/cli-module-actions/src/commands/execute.ts index 4ff955096b..f130c597ce 100644 --- a/packages/cli-module-actions/src/commands/execute.ts +++ b/packages/cli-module-actions/src/commands/execute.ts @@ -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); diff --git a/packages/cli-module-actions/src/commands/list.ts b/packages/cli-module-actions/src/commands/list.ts index 601fa6d2c7..2049fc3e44 100644 --- a/packages/cli-module-actions/src/commands/list.ts +++ b/packages/cli-module-actions/src/commands/list.ts @@ -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) { diff --git a/packages/cli-module-actions/src/commands/sourcesAdd.ts b/packages/cli-module-actions/src/commands/sourcesAdd.ts index 6d9ea2b524..0fc4addd5c 100644 --- a/packages/cli-module-actions/src/commands/sourcesAdd.ts +++ b/packages/cli-module-actions/src/commands/sourcesAdd.ts @@ -40,7 +40,7 @@ export default async ({ args, info }: CliCommandContext) => { return; } - await updateInstanceConfig(auth.instanceName, 'pluginSources', [ + await updateInstanceConfig(auth.getInstanceName(), 'pluginSources', [ ...existing, pluginId, ]); diff --git a/packages/cli-module-actions/src/commands/sourcesRemove.ts b/packages/cli-module-actions/src/commands/sourcesRemove.ts index 81a2d5abf3..b680c39684 100644 --- a/packages/cli-module-actions/src/commands/sourcesRemove.ts +++ b/packages/cli-module-actions/src/commands/sourcesRemove.ts @@ -39,7 +39,7 @@ export default async ({ args, info }: CliCommandContext) => { } await updateInstanceConfig( - auth.instanceName, + auth.getInstanceName(), 'pluginSources', existing.filter(s => s !== pluginId), ); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.test.ts b/packages/cli-module-actions/src/lib/resolveAuth.test.ts index 21cf6ebc38..21f494c54a 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.test.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.test.ts @@ -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; 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); diff --git a/packages/cli-module-actions/src/lib/resolveAuth.ts b/packages/cli-module-actions/src/lib/resolveAuth.ts index 18664b1b55..4d2afe6695 100644 --- a/packages/cli-module-actions/src/lib/resolveAuth.ts +++ b/packages/cli-module-actions/src/lib/resolveAuth.ts @@ -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('pluginSources')) ?? []; - return { instance: auth.instance, accessToken, pluginSources }; + return { + baseUrl: auth.getBaseUrl(), + instanceName: auth.getInstanceName(), + accessToken, + pluginSources, + }; } diff --git a/packages/cli-module-auth/package.json b/packages/cli-module-auth/package.json index 94169e998e..b0e2564661 100644 --- a/packages/cli-module-auth/package.json +++ b/packages/cli-module-auth/package.json @@ -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" + } } diff --git a/packages/cli-module-auth/src/commands/login.ts b/packages/cli-module-auth/src/commands/login.ts index b16060f956..6bb8751147 100644 --- a/packages/cli-module-auth/src/commands/login.ts +++ b/packages/cli-module-auth/src/commands/login.ts @@ -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); diff --git a/packages/cli-module-auth/src/commands/logout.ts b/packages/cli-module-auth/src/commands/logout.ts index f79ed2ef35..a02fb3580d 100644 --- a/packages/cli-module-auth/src/commands/logout.ts +++ b/packages/cli-module-auth/src/commands/logout.ts @@ -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) { diff --git a/packages/cli-module-auth/src/commands/show.ts b/packages/cli-module-auth/src/commands/show.ts index 6a5db7f299..275b6ca66d 100644 --- a/packages/cli-module-auth/src/commands/show.ts +++ b/packages/cli-module-auth/src/commands/show.ts @@ -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(/\/$/, ''); diff --git a/packages/cli-module-auth/src/lib/auth.test.ts b/packages/cli-module-auth/src/lib/auth.test.ts index 5ba4ae0295..18e7658e92 100644 --- a/packages/cli-module-auth/src/lib/auth.test.ts +++ b/packages/cli-module-auth/src/lib/auth.test.ts @@ -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; -const mockSecretStore = secretStore as jest.Mocked; +const mockInternalCli = internalCli as jest.Mocked; const mockHttp = http as jest.Mocked; 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 () => { diff --git a/packages/cli-module-auth/src/lib/auth.ts b/packages/cli-module-auth/src/lib/auth.ts index 4b330cf982..1ad108cbf1 100644 --- a/packages/cli-module-auth/src/lib/auth.ts +++ b/packages/cli-module-auth/src/lib/auth.ts @@ -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( diff --git a/packages/cli-module-auth/src/lib/secretStore.test.ts b/packages/cli-module-auth/src/lib/secretStore.test.ts index 6729f7c6a5..f27d364c7c 100644 --- a/packages/cli-module-auth/src/lib/secretStore.test.ts +++ b/packages/cli-module-auth/src/lib/secretStore.test.ts @@ -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(); diff --git a/packages/cli-module-auth/src/lib/storage.ts b/packages/cli-module-auth/src/lib/storage.ts index d66d4fb04a..32c6178696 100644 --- a/packages/cli-module-auth/src/lib/storage.ts +++ b/packages/cli-module-auth/src/lib/storage.ts @@ -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; +}; const METADATA_FILE = 'auth-instances.yaml'; diff --git a/packages/cli-node/report.api.md b/packages/cli-node/report.api.md index eeeb92f5b3..ca10eafff2 100644 --- a/packages/cli-node/report.api.md +++ b/packages/cli-node/report.api.md @@ -88,12 +88,11 @@ export interface BackstagePackageJson { // @public export class CliAuth { - get baseUrl(): string; static create(options?: CliAuthCreateOptions): Promise; getAccessToken(): Promise; + getBaseUrl(): string; getConfig(key: string): Promise; - get instance(): StoredInstance; - get instanceName(): string; + getInstanceName(): string; } // @public @@ -287,17 +286,6 @@ export function runWorkerQueueThreads( results: TResult[]; }>; -// @public (undocumented) -export type StoredInstance = { - name: string; - baseUrl: string; - clientId: string; - issuedAt: number; - accessTokenExpiresAt: number; - selected?: boolean; - config?: Record; -}; - // @public export class SuccessCache { // (undocumented) diff --git a/packages/cli-node/src/auth/CliAuth.test.ts b/packages/cli-node/src/auth/CliAuth.test.ts index 16a9b10dde..3d932e5e34 100644 --- a/packages/cli-node/src/auth/CliAuth.test.ts +++ b/packages/cli-node/src/auth/CliAuth.test.ts @@ -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 () => { diff --git a/packages/cli-node/src/auth/CliAuth.ts b/packages/cli-node/src/auth/CliAuth.ts index 8934e9bd7f..d7eaad6d40 100644 --- a/packages/cli-node/src/auth/CliAuth.ts +++ b/packages/cli-node/src/auth/CliAuth.ts @@ -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 { - 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) { diff --git a/packages/cli-node/src/auth/authIdentifiers.ts b/packages/cli-node/src/auth/authIdentifiers.ts new file mode 100644 index 0000000000..16ddb5b437 --- /dev/null +++ b/packages/cli-node/src/auth/authIdentifiers.ts @@ -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}`; +} diff --git a/packages/cli-node/src/auth/index.ts b/packages/cli-node/src/auth/index.ts index c84c1742d7..d6a1b08d65 100644 --- a/packages/cli-node/src/auth/index.ts +++ b/packages/cli-node/src/auth/index.ts @@ -15,4 +15,3 @@ */ export { CliAuth, type CliAuthCreateOptions } from './CliAuth'; -export { type StoredInstance } from './storage'; diff --git a/packages/cli-node/src/auth/secretStore.ts b/packages/cli-node/src/auth/secretStore.ts index a0f3c81d51..70ef5f2f72 100644 --- a/packages/cli-node/src/auth/secretStore.ts +++ b/packages/cli-node/src/auth/secretStore.ts @@ -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; set(service: string, account: string, secret: string): Promise; @@ -55,6 +55,15 @@ class KeytarSecretStore implements SecretStore { } } +async function pathExists(p: string): Promise { + 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 { 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 { 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 { 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 { if (!singleton) { const keytar = await loadKeytar();