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:
@@ -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