1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123import { event } from './event';
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || '';
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || '';
// Parse cookies from request headers
export function parseCookies(): Record<string, string> {
const cookies: Record<string, string> = {};
// Lambda function URLs send cookies as an array
if (Array.isArray((event as any).cookies)) {
(event as any).cookies.forEach((cookie: string) => {
const [name, ...rest] = cookie.split('=');
if (name) {
cookies[name] = rest.join('=');
}
});
return cookies;
}
// Fallback to header parsing for API Gateway
const cookieHeader = event.headers?.cookie || event.headers?.Cookie || '';
cookieHeader.split(';').forEach(cookie => {
const [name, ...rest] = cookie.trim().split('=');
if (name) {
cookies[name] = rest.join('=');
}
});
return cookies;
}
// Get GitHub token from cookie
export function getGitHubToken(): string | null {
const cookies = parseCookies();
return cookies['gh_token'] || null;
}
// Get current user info from cookie
export function getCurrentUser(): { login: string; avatar_url: string; isPro?: boolean } | null {
const cookies = parseCookies();
const userCookie = cookies['gh_user'];
if (!userCookie) return null;
try {
return JSON.parse(decodeURIComponent(userCookie));
} catch {
return null;
}
}
// Check if user has pro status (survives logout)
export function hasProStatus(): boolean {
const cookies = parseCookies();
return cookies['gc_pro'] === '1';
}
// Create secure cookie string
export function createCookie(name: string, value: string, maxAge: number = 86400 * 30): string {
return `${name}=${value}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${maxAge}`;
}
// Create cookie deletion string
export function deleteCookie(name: string): string {
return `${name}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`;
}
// GitHub OAuth URLs - pro users get private repo access
export function getGitHubAuthUrl(isPro: boolean = false): string {
// Pro users get repo + read:org scope for private repo and org access
// GitHub accepts space-separated scopes (URL encoded as %20)
const scopes = isPro ? 'read:user repo read:org' : 'read:user';
return `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&scope=${encodeURIComponent(scopes)}`;
}
// Get pro auth URL (for re-auth after subscribing)
export function getGitHubProAuthUrl(): string {
return getGitHubAuthUrl(true);
}
// Exchange code for token
export async function exchangeCodeForToken(code: string): Promise<{ access_token: string; error?: string }> {
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
}),
});
return response.json();
}
// Fallback token for unauthenticated users (5000 req/hour vs 60)
const GITHUB_FALLBACK_TOKEN = process.env.GITHUB_FALLBACK_TOKEN || '';
// Fetch from GitHub with optional auth
export async function fetchGitHub(url: string, token?: string | null) {
const headers: Record<string, string> = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'EqualifyOpenSource'
};
// Use user's token, or fallback token for unauthenticated users
const authToken = token || GITHUB_FALLBACK_TOKEN;
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(url, { headers });
return response.json();
}
// Fetch current user from GitHub
export async function fetchCurrentUser(token: string) {
return fetchGitHub('https://api.github.com/user', token);
}