mirror of
https://github.com/openclaw/clawhub.git
synced 2026-07-22 03:35:29 -04:00
fix: handle GitHub API rate limits in account age check
The GitHub account lookup uses unauthenticated requests (60 req/hr per IP). Since this runs server-side in Convex, all users share the same IP and quickly exhaust the rate limit, causing "GitHub account lookup failed" errors during skill publish. - Detect 403/429 responses and surface a clear rate-limit message - Support optional GITHUB_TOKEN env var for authenticated requests (5,000 req/hr) Fixes #155 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,11 +105,80 @@ describe('requireGitHubAccountAge', () => {
|
||||
githubFetchedAt: 0,
|
||||
})
|
||||
const runMutation = vi.fn()
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: false })
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 404 })
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await expect(
|
||||
requireGitHubAccountAge({ runQuery, runMutation } as never, 'users:1' as never),
|
||||
).rejects.toThrow(/GitHub account lookup failed/i)
|
||||
})
|
||||
|
||||
it('throws rate-limit error on 403', async () => {
|
||||
const runQuery = vi.fn().mockResolvedValue({
|
||||
_id: 'users:1',
|
||||
handle: 'steipete',
|
||||
githubCreatedAt: undefined,
|
||||
githubFetchedAt: 0,
|
||||
})
|
||||
const runMutation = vi.fn()
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 403 })
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await expect(
|
||||
requireGitHubAccountAge({ runQuery, runMutation } as never, 'users:1' as never),
|
||||
).rejects.toThrow(/rate limit exceeded/i)
|
||||
})
|
||||
|
||||
it('throws rate-limit error on 429', async () => {
|
||||
const runQuery = vi.fn().mockResolvedValue({
|
||||
_id: 'users:1',
|
||||
handle: 'steipete',
|
||||
githubCreatedAt: undefined,
|
||||
githubFetchedAt: 0,
|
||||
})
|
||||
const runMutation = vi.fn()
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 429 })
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await expect(
|
||||
requireGitHubAccountAge({ runQuery, runMutation } as never, 'users:1' as never),
|
||||
).rejects.toThrow(/rate limit exceeded/i)
|
||||
})
|
||||
|
||||
it('includes Authorization header when GITHUB_TOKEN is set', async () => {
|
||||
vi.useFakeTimers()
|
||||
const now = new Date('2026-02-02T12:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
|
||||
vi.stubEnv('GITHUB_TOKEN', 'ghp_test123')
|
||||
|
||||
const runQuery = vi.fn().mockResolvedValue({
|
||||
_id: 'users:1',
|
||||
handle: 'steipete',
|
||||
githubCreatedAt: undefined,
|
||||
githubFetchedAt: now.getTime() - 2 * ONE_DAY_MS,
|
||||
})
|
||||
const runMutation = vi.fn()
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
created_at: '2020-01-01T00:00:00Z',
|
||||
}),
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await requireGitHubAccountAge({ runQuery, runMutation } as never, 'users:1' as never)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://api.github.com/users/steipete',
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'User-Agent': 'clawhub',
|
||||
Authorization: 'Bearer ghp_test123',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,10 +24,23 @@ export async function requireGitHubAccountAge(ctx: ActionCtx, userId: Id<'users'
|
||||
const stale = !createdAt || now - fetchedAt > FETCH_TTL_MS
|
||||
|
||||
if (stale) {
|
||||
const headers: Record<string, string> = { 'User-Agent': 'clawhub' }
|
||||
const token = process.env.GITHUB_TOKEN
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${GITHUB_API}/users/${encodeURIComponent(handle)}`, {
|
||||
headers: { 'User-Agent': 'clawhub' },
|
||||
headers,
|
||||
})
|
||||
if (!response.ok) throw new ConvexError('GitHub account lookup failed')
|
||||
if (!response.ok) {
|
||||
if (response.status === 403 || response.status === 429) {
|
||||
throw new ConvexError(
|
||||
'GitHub API rate limit exceeded — please try again in a few minutes',
|
||||
)
|
||||
}
|
||||
throw new ConvexError('GitHub account lookup failed')
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as GitHubUser
|
||||
const parsed = payload.created_at ? Date.parse(payload.created_at) : Number.NaN
|
||||
|
||||
Reference in New Issue
Block a user