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:
Patrik Oldsberg
2026-03-17 17:10:01 +01:00
parent da8e6603a4
commit 2b90358730
23 changed files with 160 additions and 89 deletions
+2 -14
View File
@@ -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)
+2 -3
View File
@@ -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 () => {
+7 -11
View File
@@ -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}`;
}
-1
View File
@@ -15,4 +15,3 @@
*/
export { CliAuth, type CliAuthCreateOptions } from './CliAuth';
export { type StoredInstance } from './storage';
+23 -6
View File
@@ -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();