Skip to content

Commit b0ca4cc

Browse files
committed
refactor: remove unused guards and tests, restructure tool utilities
- Deleted unused guard functions (isString, isFunction, isBigInt) from guards.ts. - Removed tool-loader.spec.ts and tool-validator.test.ts as they were no longer needed. - Consolidated tool-related exports into a new tools/index.ts file for better organization. - Introduced ToolErrorHandler for standardized error handling during tool execution. - Implemented DefaultToolFactory for loading tool modules dynamically. - Added comprehensive tests for ToolLoader, DefaultToolRegistrar, and tool metadata validation. - Updated tools-manifest.json to reflect changes in tool parameters and descriptions.
1 parent a6dc228 commit b0ca4cc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1121
-1033
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"rules": {
2828
"@typescript-eslint/no-explicit-any": "error",
29+
"@typescript-eslint/explicit-function-return-type": "error",
2930
"@typescript-eslint/strict-boolean-expressions": "warn",
3031
"@typescript-eslint/no-unused-vars": [
3132
"error",

jest.config.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
/** @type {import('ts-jest').JestConfigWithTsJest} **/
2-
module.exports = {
3-
preset: 'ts-jest',
2+
export default {
3+
preset: 'ts-jest/presets/default-esm',
44
testEnvironment: 'node',
5+
extensionsToTreatAsEsm: ['.ts'],
6+
moduleNameMapper: {
7+
'^(\\.{1,2}/.*)\\.js$': '$1',
8+
},
59
transform: {
6-
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.spec.json' }],
10+
'^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tsconfig.spec.json' }],
711
},
812
testPathIgnorePatterns: ['<rootDir>/dist/'],
913
collectCoverageFrom: ['src/**/*.{ts,js}', '!src/**/*.spec.ts', '!src/**/*.test.ts', '!src/**/__fixtures__/**/*'],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"author": "Robert Lindley",
3+
"type": "module",
34
"dependencies": {
45
"@backstage/catalog-client": "^1.9.1",
56
"@backstage/catalog-model": "^1.7.3",

src/api/backstage-catalog-api.ts

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,17 @@ import {
1818
import { CompoundEntityRef, Entity, stringifyEntityRef } from '@backstage/catalog-model';
1919
import axios, { AxiosInstance, isAxiosError } from 'axios';
2020

21-
import { type AuthConfig } from '../auth';
22-
import { AuthManager } from '../auth/auth-manager';
23-
import { securityAuditor, SecurityEventType } from '../auth/security-auditor';
24-
import { CacheManager } from '../cache/cache-manager';
25-
import { logger } from '../utils';
26-
import { isString } from '../utils/guards';
27-
import { JsonApiDocument, JsonApiFormatter } from '../utils/jsonapi-formatter';
28-
import { PaginationHelper, PaginationParams } from '../utils/pagination-helper';
21+
import { AuthConfig, AuthManager, securityAuditor } from '../auth';
22+
import { CacheManager } from '../cache';
23+
import { IBackstageCatalogApi, JsonApiDocument, PaginationParams, SecurityEventType } from '../types';
24+
import { isNonEmptyString, isNumber, isString, JsonApiFormatter, logger, PaginationHelper } from '../utils';
2925

3026
interface BackstageCatalogApiOptions {
3127
baseUrl: string;
3228
auth: AuthConfig;
3329
}
3430

35-
export class BackstageCatalogApi {
31+
export class BackstageCatalogApi implements IBackstageCatalogApi {
3632
private readonly client: AxiosInstance;
3733
private readonly authManager: AuthManager;
3834
private readonly cacheManager: CacheManager;
@@ -60,16 +56,13 @@ export class BackstageCatalogApi {
6056

6157
// Add authorization header if provided
6258
const authHeader = await this.authManager.getAuthorizationHeader();
63-
if (typeof authHeader === 'string' && authHeader.length > 0) {
59+
if (isNonEmptyString(authHeader)) {
6460
config.headers.Authorization = authHeader;
6561
}
6662

6763
// Log security event with explicit fallbacks
68-
const resource = typeof config.url === 'string' && config.url.length > 0 ? String(config.url) : 'unknown';
69-
const action =
70-
typeof config.method === 'string' && config.method.length > 0
71-
? String(config.method).toUpperCase()
72-
: 'UNKNOWN';
64+
const resource = isNonEmptyString(config.url) ? String(config.url) : 'unknown';
65+
const action = isNonEmptyString(config.method) ? String(config.method).toUpperCase() : 'UNKNOWN';
7366

7467
securityAuditor.logEvent({
7568
type: SecurityEventType.AUTH_SUCCESS,
@@ -85,20 +78,11 @@ export class BackstageCatalogApi {
8578
let resource: string = 'unknown';
8679
let action = 'UNKNOWN';
8780
if (isAxiosError(error)) {
88-
resource =
89-
typeof error.config?.url === 'string' && error.config?.url!.length > 0
90-
? String(error.config!.url)
91-
: resource;
92-
action =
93-
typeof error.config?.method === 'string' && error.config?.method!.length > 0
94-
? String(error.config!.method).toUpperCase()
95-
: action;
81+
resource = isNonEmptyString(error.config?.url) ? String(error.config!.url) : resource;
82+
action = isNonEmptyString(error.config?.method) ? String(error.config!.method).toUpperCase() : action;
9683
} else {
97-
resource = typeof config.url === 'string' && config.url.length > 0 ? String(config.url) : resource;
98-
action =
99-
typeof config.method === 'string' && config.method.length > 0
100-
? String(config.method).toUpperCase()
101-
: action;
84+
resource = isNonEmptyString(config.url) ? String(config.url) : resource;
85+
action = isNonEmptyString(config.method) ? String(config.method).toUpperCase() : action;
10286
}
10387

10488
securityAuditor.logEvent({
@@ -118,14 +102,10 @@ export class BackstageCatalogApi {
118102
(error) => {
119103
// Log security events for certain error types. Narrow to axios errors first
120104
if (isAxiosError(error)) {
121-
const resource =
122-
typeof error.config?.url === 'string' && error.config?.url!.length > 0
123-
? String(error.config!.url)
124-
: 'unknown';
125-
const action =
126-
typeof error.config?.method === 'string' && error.config?.method!.length > 0
127-
? String(error.config!.method).toUpperCase()
128-
: 'UNKNOWN';
105+
const resource = isNonEmptyString(error.config?.url) ? String(error.config!.url) : 'unknown';
106+
const action = isNonEmptyString(error.config?.method)
107+
? String(error.config!.method).toUpperCase()
108+
: 'UNKNOWN';
129109

130110
if (error.response?.status === 401) {
131111
securityAuditor.logEvent({
@@ -148,19 +128,15 @@ export class BackstageCatalogApi {
148128
// Fallback for non-axios errors that may still have a response shape
149129
const maybe = error as unknown as { response?: { status?: number } } | undefined;
150130
const maybeResponse = maybe && maybe.response ? maybe.response : undefined;
151-
if (maybeResponse !== undefined && typeof maybeResponse.status === 'number' && maybeResponse.status === 401) {
131+
if (isNumber(maybeResponse?.status) && maybeResponse.status === 401) {
152132
securityAuditor.logEvent({
153133
type: SecurityEventType.UNAUTHORIZED_ACCESS,
154134
resource: 'unknown',
155135
action: 'UNKNOWN',
156136
success: false,
157137
errorMessage: 'Unauthorized access',
158138
});
159-
} else if (
160-
maybeResponse !== undefined &&
161-
typeof maybeResponse.status === 'number' &&
162-
maybeResponse.status === 429
163-
) {
139+
} else if (isNumber(maybeResponse?.status) && maybeResponse.status === 429) {
164140
securityAuditor.logEvent({
165141
type: SecurityEventType.RATE_LIMIT_EXCEEDED,
166142
resource: 'unknown',
@@ -344,7 +320,6 @@ export class BackstageCatalogApi {
344320
*/
345321
async getEntitiesJsonApi(request?: GetEntitiesRequest & PaginationParams): Promise<JsonApiDocument> {
346322
const entities = await this.getEntities(request);
347-
348323
// Convert to JSON:API format
349324
return JsonApiFormatter.entitiesToDocument(entities.items ?? []);
350325
}

src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { BackstageCatalogApi } from './backstage-catalog-api';

src/auth/auth-manager.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios, { AxiosResponse } from 'axios';
22

3-
import { logger } from '../utils';
3+
import { isNonEmptyString, isNumber, logger } from '../utils';
44

55
export interface AuthConfig {
66
type: 'bearer' | 'oauth' | 'api-key' | 'service-account';
@@ -92,7 +92,7 @@ export class AuthManager {
9292
}
9393

9494
private async handleBearerToken(): Promise<TokenInfo> {
95-
if (typeof this.config.token !== 'string' || this.config.token.length === 0) {
95+
if (!isNonEmptyString(this.config.token)) {
9696
throw new Error('Bearer token not configured');
9797
}
9898
return {
@@ -103,21 +103,14 @@ export class AuthManager {
103103

104104
private async handleOAuthRefresh(): Promise<TokenInfo> {
105105
if (
106-
typeof this.config.clientId !== 'string' ||
107-
this.config.clientId.length === 0 ||
108-
typeof this.config.clientSecret !== 'string' ||
109-
this.config.clientSecret.length === 0 ||
110-
typeof this.config.tokenUrl !== 'string' ||
111-
this.config.tokenUrl.length === 0
106+
!isNonEmptyString(this.config.clientId) ||
107+
!isNonEmptyString(this.config.clientSecret) ||
108+
!isNonEmptyString(this.config.tokenUrl)
112109
) {
113110
throw new Error('OAuth configuration incomplete');
114111
}
115112

116-
if (
117-
!this.tokenInfo ||
118-
typeof this.tokenInfo.refreshToken !== 'string' ||
119-
this.tokenInfo.refreshToken.length === 0
120-
) {
113+
if (!isNonEmptyString(this.tokenInfo?.refreshToken)) {
121114
throw new Error('No refresh token available for OAuth');
122115
}
123116

@@ -132,7 +125,7 @@ export class AuthManager {
132125
}
133126

134127
private async handleApiKey(): Promise<TokenInfo> {
135-
if (typeof this.config.apiKey !== 'string' || this.config.apiKey.length === 0) {
128+
if (!isNonEmptyString(this.config.apiKey)) {
136129
throw new Error('API key not configured');
137130
}
138131
return {
@@ -142,7 +135,7 @@ export class AuthManager {
142135
}
143136

144137
private async handleServiceAccount(): Promise<TokenInfo> {
145-
if (typeof this.config.serviceAccountKey !== 'string' || this.config.serviceAccountKey.length === 0) {
138+
if (!isNonEmptyString(this.config.serviceAccountKey)) {
146139
throw new Error('Service account key not configured');
147140
}
148141

@@ -162,15 +155,13 @@ export class AuthManager {
162155
token_type?: string;
163156
};
164157
const expiresAt =
165-
typeof data.expires_in === 'number' && !Number.isNaN(data.expires_in)
166-
? Date.now() + data.expires_in * 1000
167-
: undefined;
158+
isNumber(data.expires_in) && !Number.isNaN(data.expires_in) ? Date.now() + data.expires_in * 1000 : undefined;
168159

169160
return {
170161
accessToken: data.access_token,
171162
refreshToken: data.refresh_token,
172163
expiresAt,
173-
tokenType: typeof data.token_type === 'string' && data.token_type.length > 0 ? data.token_type : 'Bearer',
164+
tokenType: isNonEmptyString(data.token_type) ? data.token_type : 'Bearer',
174165
};
175166
}
176167

src/auth/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
/* eslint-disable import/no-unused-modules */
22
export { type AuthConfig, AuthManager, type TokenInfo } from './auth-manager';
33
export { InputSanitizer, inputSanitizer } from './input-sanitizer';
4-
export { SecurityAuditor, securityAuditor, type SecurityEvent, SecurityEventType } from './security-auditor';
4+
export { SecurityAuditor, securityAuditor } from './security-auditor';

src/auth/input-sanitizer.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { z } from 'zod';
22

3+
import { isObject, isString } from '../utils';
4+
35
export class InputSanitizer {
46
private readonly maxStringLength = 10000;
57
private readonly maxArrayLength = 1000;
@@ -9,7 +11,7 @@ export class InputSanitizer {
911
* Sanitizes a string input
1012
*/
1113
sanitizeString(input: string, fieldName: string): string {
12-
if (typeof input !== 'string') {
14+
if (!isString(input)) {
1315
throw new Error(`Invalid input type for ${fieldName}: expected string, got ${typeof input}`);
1416
}
1517

@@ -58,11 +60,11 @@ export class InputSanitizer {
5860
sanitizeEntityRef(
5961
entityRef: string | { kind: string; namespace: string; name: string }
6062
): string | { kind: string; namespace: string; name: string } {
61-
if (typeof entityRef === 'string') {
63+
if (isString(entityRef)) {
6264
return this.sanitizeString(entityRef, 'entityRef');
6365
}
6466

65-
if (typeof entityRef === 'object' && entityRef !== null) {
67+
if (isObject(entityRef)) {
6668
return {
6769
kind: this.sanitizeString(entityRef.kind, 'entityRef.kind'),
6870
namespace: this.sanitizeString(entityRef.namespace, 'entityRef.namespace'),
@@ -134,4 +136,5 @@ export class InputSanitizer {
134136
}
135137

136138
// Global input sanitizer instance
139+
/* eslint-disable-next-line import/no-unused-modules */
137140
export const inputSanitizer = new InputSanitizer();

src/auth/security-auditor.ts

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,6 @@
11
import { z } from 'zod';
22

3-
export enum SecurityEventType {
4-
AUTH_SUCCESS = 'auth_success',
5-
AUTH_FAILURE = 'auth_failure',
6-
TOKEN_REFRESH = 'token_refresh',
7-
RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded',
8-
INVALID_REQUEST = 'invalid_request',
9-
UNAUTHORIZED_ACCESS = 'unauthorized_access',
10-
}
11-
12-
export interface SecurityEvent {
13-
id: string;
14-
timestamp: Date;
15-
type: SecurityEventType;
16-
userId?: string;
17-
ipAddress?: string;
18-
userAgent?: string;
19-
resource: string;
20-
action: string;
21-
success: boolean;
22-
details?: Record<string, unknown>;
23-
errorMessage?: string;
24-
}
3+
import { ISecurityEvent, SecurityEventType } from '../types';
254

265
const SecurityEventSchema = z.object({
276
id: z.string(),
@@ -38,11 +17,11 @@ const SecurityEventSchema = z.object({
3817
});
3918

4019
export class SecurityAuditor {
41-
private events: SecurityEvent[] = [];
20+
private events: ISecurityEvent[] = [];
4221
private readonly maxEvents = 10000; // Keep last 10k events in memory
4322

44-
logEvent(event: Omit<SecurityEvent, 'id' | 'timestamp'>): void {
45-
const fullEvent: SecurityEvent = {
23+
logEvent(event: Omit<ISecurityEvent, 'id' | 'timestamp'>): void {
24+
const fullEvent: ISecurityEvent = {
4625
...event,
4726
id: this.generateEventId(),
4827
timestamp: new Date(),
@@ -61,7 +40,7 @@ export class SecurityAuditor {
6140
// Log to console in development (use warn so eslint no-console rule permits it)
6241
if (process.env.NODE_ENV !== 'production') {
6342
console.warn(
64-
`[SECURITY] ${event.type}: ${event.resource} - ${event.action} (${event.success ? 'SUCCESS' : 'FAILED'})`
43+
`[SECURITY] ${event.type}: ${event.resource} - ${event.action} (${event.success === true ? 'SUCCESS' : 'FAILED'})`
6544
);
6645
}
6746

@@ -73,15 +52,15 @@ export class SecurityAuditor {
7352
return `sec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
7453
}
7554

76-
private persistEvent(_event: SecurityEvent): void {
55+
private persistEvent(_event: ISecurityEvent): void {
7756
// In a real implementation, this would:
7857
// - Send to SIEM system
7958
// - Write to audit database
8059
// - Send to monitoring service
8160
// For now, we'll just keep in memory
8261
}
8362

84-
getEvents(filter?: { type?: SecurityEventType; userId?: string; since?: Date; limit?: number }): SecurityEvent[] {
63+
getEvents(filter?: { type?: SecurityEventType; userId?: string; since?: Date; limit?: number }): ISecurityEvent[] {
8564
let filtered = this.events;
8665

8766
if (filter && filter.type !== undefined) {
@@ -110,7 +89,7 @@ export class SecurityAuditor {
11089
authSuccessCount: number;
11190
authFailureCount: number;
11291
rateLimitCount: number;
113-
recentEvents: SecurityEvent[];
92+
recentEvents: ISecurityEvent[];
11493
} {
11594
const recentEvents = this.events.slice(-100); // Last 100 events
11695

src/cache/cache-manager.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
1-
/* eslint-disable import/no-unused-modules */
2-
import { logger } from '../utils/logger';
3-
4-
export interface CacheEntry<T = unknown> {
5-
data: T;
6-
timestamp: number;
7-
ttl: number; // Time to live in milliseconds
8-
hits: number;
9-
}
10-
11-
export interface CacheConfig {
12-
defaultTtl: number; // Default TTL in milliseconds
13-
maxSize: number; // Maximum number of entries
14-
cleanupInterval: number; // Cleanup interval in milliseconds
15-
}
1+
import { CacheConfig, CacheEntry } from '../types';
2+
import { isNumber, logger } from '../utils';
163

174
export class CacheManager {
185
private cache = new Map<string, CacheEntry>();
@@ -63,12 +50,12 @@ export class CacheManager {
6350
*/
6451
set<T>(key: string, value: T, ttl?: number): void {
6552
// Evict oldest entries if at capacity
66-
if (typeof this.config.maxSize === 'number' && this.cache.size >= this.config.maxSize) {
53+
if (isNumber(this.config.maxSize) && this.cache.size >= this.config.maxSize) {
6754
logger.debug('Cache at capacity, evicting oldest entry');
6855
this.evictOldest();
6956
}
7057

71-
const finalTtl = typeof ttl === 'number' && !Number.isNaN(ttl) ? ttl : this.config.defaultTtl;
58+
const finalTtl = isNumber(ttl) && !Number.isNaN(ttl) ? ttl : this.config.defaultTtl;
7259
this.cache.set(key, {
7360
data: value,
7461
timestamp: Date.now(),
@@ -180,6 +167,3 @@ export class CacheManager {
180167
}
181168
}
182169
}
183-
184-
// Global cache instance
185-
export const cacheManager = new CacheManager();

0 commit comments

Comments
 (0)