Skip to content

Commit 854fca6

Browse files
committed
revert: restore auth-detector files swept in by a shared-checkout race
d118990 was meant to carry only the Stripe e2e timeout fix. Another session working issue #170 in the same checkout had staged the removal of src/auth-detector.ts and tests/auth-detector.test.ts, and `git commit` takes the whole index, not just the paths passed to `git add`. The deletion rode along into dev and into release PR #172. Restoring both files here so the release ships only what was reviewed. The #170 removal is legitimate work and lands on its own branch, with its own review.
1 parent d118990 commit 854fca6

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

src/auth-detector.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { AuthStrategy } from './auth/types';
2+
import { BasicAuthStrategy } from './auth/basic';
3+
import { BearerTokenStrategy } from './auth/bearer';
4+
import { ApiKeyStrategy } from './auth/api-key';
5+
6+
export interface SecurityScheme {
7+
type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect';
8+
scheme?: string;
9+
name?: string;
10+
in?: string;
11+
flows?: Record<string, unknown>;
12+
}
13+
14+
export interface DetectedAuth {
15+
type: 'basic' | 'bearer' | 'apiKey';
16+
strategy: AuthStrategy;
17+
headerName?: string;
18+
}
19+
20+
const PRIORITY: Array<'basic' | 'bearer' | 'apiKey'> = ['basic', 'bearer', 'apiKey'];
21+
22+
export function detectAuthFromSpec(
23+
schemes: Record<string, SecurityScheme> | undefined,
24+
): DetectedAuth | null {
25+
if (!schemes || Object.keys(schemes).length === 0) return null;
26+
27+
const detected: DetectedAuth[] = [];
28+
29+
for (const scheme of Object.values(schemes)) {
30+
if (scheme.type === 'http' && scheme.scheme === 'basic') {
31+
detected.push({
32+
type: 'basic',
33+
strategy: new BasicAuthStrategy(),
34+
});
35+
} else if (scheme.type === 'http' && scheme.scheme === 'bearer') {
36+
detected.push({
37+
type: 'bearer',
38+
strategy: new BearerTokenStrategy(async (config) => {
39+
return config.password;
40+
}),
41+
});
42+
} else if (scheme.type === 'apiKey' && scheme.name && scheme.in === 'header') {
43+
detected.push({
44+
type: 'apiKey',
45+
strategy: new ApiKeyStrategy(scheme.name, ''),
46+
headerName: scheme.name,
47+
});
48+
} else if (scheme.type === 'oauth2') {
49+
detected.push({
50+
type: 'bearer',
51+
strategy: new BearerTokenStrategy(async (config) => {
52+
return config.password;
53+
}),
54+
});
55+
}
56+
}
57+
58+
if (detected.length === 0) return null;
59+
60+
for (const prio of PRIORITY) {
61+
const match = detected.find(d => d.type === prio);
62+
63+
if (match) return match;
64+
}
65+
66+
return detected[0]!;
67+
}
68+
69+
export async function fetchSecuritySchemes(
70+
specUrl: string,
71+
headers?: Record<string, string>,
72+
): Promise<Record<string, SecurityScheme> | null> {
73+
try {
74+
const res = await fetch(specUrl, {
75+
headers: { Accept: 'application/json', ...headers },
76+
});
77+
78+
if (!res.ok) return null;
79+
80+
const spec = await res.json() as Record<string, unknown>;
81+
const components = spec.components as Record<string, unknown> | undefined;
82+
83+
return (components?.securitySchemes as Record<string, SecurityScheme>) ?? null;
84+
} catch {
85+
return null;
86+
}
87+
}

tests/auth-detector.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, test, expect } from 'bun:test';
2+
import { detectAuthFromSpec, type SecurityScheme } from '../src/auth-detector';
3+
import { BasicAuthStrategy } from '../src/auth/basic';
4+
5+
describe('detectAuthFromSpec()', () => {
6+
test('detects basic auth', () => {
7+
const schemes: Record<string, SecurityScheme> = {
8+
basicAuth: { type: 'http', scheme: 'basic' },
9+
};
10+
const result = detectAuthFromSpec(schemes);
11+
expect(result).not.toBeNull();
12+
expect(result!.type).toBe('basic');
13+
expect(result!.strategy).toBeInstanceOf(BasicAuthStrategy);
14+
});
15+
16+
test('detects bearer auth', () => {
17+
const schemes: Record<string, SecurityScheme> = {
18+
bearerAuth: { type: 'http', scheme: 'bearer' },
19+
};
20+
const result = detectAuthFromSpec(schemes);
21+
expect(result).not.toBeNull();
22+
expect(result!.type).toBe('bearer');
23+
});
24+
25+
test('detects apiKey auth with header name', () => {
26+
const schemes: Record<string, SecurityScheme> = {
27+
apiKey: { type: 'apiKey', name: 'X-API-Key', in: 'header' },
28+
};
29+
const result = detectAuthFromSpec(schemes);
30+
expect(result).not.toBeNull();
31+
expect(result!.type).toBe('apiKey');
32+
expect(result!.headerName).toBe('X-API-Key');
33+
});
34+
35+
test('returns null for empty schemes', () => {
36+
const result = detectAuthFromSpec({});
37+
expect(result).toBeNull();
38+
});
39+
40+
test('returns null for undefined schemes', () => {
41+
const result = detectAuthFromSpec(undefined);
42+
expect(result).toBeNull();
43+
});
44+
45+
test('prefers basic auth when multiple schemes present', () => {
46+
const schemes: Record<string, SecurityScheme> = {
47+
apiKey: { type: 'apiKey', name: 'X-Key', in: 'header' },
48+
basicAuth: { type: 'http', scheme: 'basic' },
49+
};
50+
const result = detectAuthFromSpec(schemes);
51+
expect(result!.type).toBe('basic');
52+
});
53+
54+
test('detects oauth2 as bearer type', () => {
55+
const schemes: Record<string, SecurityScheme> = {
56+
oauth: {
57+
type: 'oauth2',
58+
flows: {
59+
clientCredentials: {
60+
tokenUrl: 'https://auth.example.com/token',
61+
scopes: {},
62+
},
63+
},
64+
},
65+
};
66+
const result = detectAuthFromSpec(schemes);
67+
expect(result).not.toBeNull();
68+
expect(result!.type).toBe('bearer');
69+
});
70+
});

0 commit comments

Comments
 (0)