mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
217 lines
9.2 KiB
TypeScript
217 lines
9.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { handleAuthorize } from '@/handlers/authorize'
|
|
import { hashKey } from '@/lib/kv'
|
|
|
|
import { createMockKV, mockKVGet } from './helpers'
|
|
|
|
const mockKV = createMockKV()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('handleAuthorize', () => {
|
|
it('shows region picker when no _region param', async () => {
|
|
const request = new Request(
|
|
'https://oauth.hanzo.ai/oauth/authorize/?client_id=abc&redirect_uri=http://localhost:3000/callback&response_type=code'
|
|
)
|
|
const response = await handleAuthorize(request, mockKV)
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(response.headers.get('content-type')).toContain('text/html')
|
|
|
|
const html = await response.text()
|
|
expect(html).toContain('Authenticate with Insights')
|
|
expect(html).toContain('US Cloud')
|
|
expect(html).toContain('EU Cloud')
|
|
})
|
|
|
|
it('redirects to US authorize with translated client_id and proxy callback when _region=us', async () => {
|
|
const mapping = {
|
|
us_client_id: 'us_real_id',
|
|
eu_client_id: 'eu_real_id',
|
|
redirect_uris: ['http://localhost:3000/callback'],
|
|
created_at: Date.now(),
|
|
}
|
|
mockKVGet(mockKV, (_key: string, type?: unknown) => {
|
|
if (type === 'json') {
|
|
return Promise.resolve(mapping)
|
|
}
|
|
return Promise.resolve(null)
|
|
})
|
|
|
|
const request = new Request(
|
|
'https://oauth.hanzo.ai/oauth/authorize/?client_id=us_real_id&redirect_uri=http://localhost:3000/callback&response_type=code&_region=us'
|
|
)
|
|
const response = await handleAuthorize(request, mockKV)
|
|
|
|
expect(response.status).toBe(302)
|
|
const location = new URL(response.headers.get('location')!)
|
|
expect(location.origin).toBe('https://us.hanzo.ai')
|
|
expect(location.pathname).toBe('/oauth/authorize/')
|
|
expect(location.searchParams.get('client_id')).toBe('us_real_id')
|
|
expect(location.searchParams.get('redirect_uri')).toBe('https://oauth.hanzo.ai/oauth/callback/')
|
|
expect(location.searchParams.has('_region')).toBe(false)
|
|
})
|
|
|
|
it('redirects to EU authorize with translated client_id and proxy callback when _region=eu', async () => {
|
|
const mapping = {
|
|
us_client_id: 'us_real_id',
|
|
eu_client_id: 'eu_real_id',
|
|
redirect_uris: ['http://localhost:3000/callback'],
|
|
created_at: Date.now(),
|
|
}
|
|
mockKVGet(mockKV, (_key: string, type?: unknown) => {
|
|
if (type === 'json') {
|
|
return Promise.resolve(mapping)
|
|
}
|
|
return Promise.resolve(null)
|
|
})
|
|
|
|
const request = new Request(
|
|
'https://oauth.hanzo.ai/oauth/authorize/?client_id=us_real_id&redirect_uri=http://localhost:3000/callback&response_type=code&_region=eu'
|
|
)
|
|
const response = await handleAuthorize(request, mockKV)
|
|
|
|
expect(response.status).toBe(302)
|
|
const location = new URL(response.headers.get('location')!)
|
|
expect(location.origin).toBe('https://eu.hanzo.ai')
|
|
expect(location.pathname).toBe('/oauth/authorize/')
|
|
expect(location.searchParams.get('client_id')).toBe('eu_real_id')
|
|
expect(location.searchParams.get('redirect_uri')).toBe('https://oauth.hanzo.ai/oauth/callback/')
|
|
expect(location.searchParams.has('_region')).toBe(false)
|
|
})
|
|
|
|
it('rejects unregistered redirect_uri to prevent open redirects', async () => {
|
|
const mapping = {
|
|
us_client_id: 'us_id',
|
|
eu_client_id: 'eu_id',
|
|
redirect_uris: ['http://localhost:3000/callback'],
|
|
created_at: Date.now(),
|
|
}
|
|
mockKVGet(mockKV, (_key: string, type?: unknown) => {
|
|
if (type === 'json') {
|
|
return Promise.resolve(mapping)
|
|
}
|
|
return Promise.resolve(null)
|
|
})
|
|
|
|
const request = new Request(
|
|
'https://oauth.hanzo.ai/oauth/authorize/?client_id=us_id&redirect_uri=https://attacker.com/steal&response_type=code&state=abc&_region=eu'
|
|
)
|
|
const response = await handleAuthorize(request, mockKV)
|
|
|
|
expect(response.status).toBe(400)
|
|
const data = (await response.json()) as Record<string, unknown>
|
|
expect(data.error).toBe('invalid_request')
|
|
expect(data.error_description).toBe('redirect_uri is not registered for this client')
|
|
})
|
|
|
|
it('stores region selection and callback redirect_uri keyed by state and client_id', async () => {
|
|
const mapping = {
|
|
us_client_id: 'us_id',
|
|
eu_client_id: 'eu_id',
|
|
redirect_uris: ['http://localhost:3000/callback'],
|
|
created_at: Date.now(),
|
|
}
|
|
mockKVGet(mockKV, (_key: string, type?: unknown) => {
|
|
if (type === 'json') {
|
|
return Promise.resolve(mapping)
|
|
}
|
|
return Promise.resolve(null)
|
|
})
|
|
|
|
const request = new Request(
|
|
'https://oauth.hanzo.ai/oauth/authorize/?client_id=us_id&redirect_uri=http://localhost:3000/callback&response_type=code&state=abc123&_region=eu'
|
|
)
|
|
await handleAuthorize(request, mockKV)
|
|
|
|
const putCalls = vi.mocked(mockKV.put).mock.calls
|
|
|
|
const stateHash = await hashKey('abc123')
|
|
const clientHash = await hashKey('us_id')
|
|
|
|
// Region selection stored by both state and client_id
|
|
const regionByState = putCalls.find(([key]) => (key as string) === `region:${stateHash}`)
|
|
const regionByClient = putCalls.find(([key]) => (key as string) === `region:${clientHash}`)
|
|
expect(regionByState).toBeTruthy()
|
|
expect(regionByState![1]).toBe('eu')
|
|
expect(regionByClient).toBeTruthy()
|
|
expect(regionByClient![1]).toBe('eu')
|
|
|
|
// Callback redirect_uri stored by both state and client_id
|
|
const callbackByState = putCalls.find(([key]) => (key as string) === `callback:${stateHash}`)
|
|
const callbackByClient = putCalls.find(([key]) => (key as string) === `callback:${clientHash}`)
|
|
expect(callbackByState).toBeTruthy()
|
|
expect(callbackByState![1]).toBe('http://localhost:3000/callback')
|
|
expect(callbackByClient).toBeTruthy()
|
|
expect(callbackByClient![1]).toBe('http://localhost:3000/callback')
|
|
})
|
|
|
|
it('writes bounded-length KV keys even for very large state values', async () => {
|
|
const mapping = {
|
|
us_client_id: 'us_id',
|
|
eu_client_id: 'eu_id',
|
|
redirect_uris: ['http://localhost:3000/callback'],
|
|
created_at: Date.now(),
|
|
}
|
|
mockKVGet(mockKV, (_key: string, type?: unknown) => {
|
|
if (type === 'json') {
|
|
return Promise.resolve(mapping)
|
|
}
|
|
return Promise.resolve(null)
|
|
})
|
|
|
|
// A JWT-shaped state well above Cloudflare's 512-byte KV key limit.
|
|
const longState = 'x'.repeat(2000)
|
|
const request = new Request(
|
|
`https://oauth.hanzo.ai/oauth/authorize/?client_id=us_id&redirect_uri=http://localhost:3000/callback&response_type=code&state=${longState}&_region=us`
|
|
)
|
|
const response = await handleAuthorize(request, mockKV)
|
|
|
|
expect(response.status).toBe(302)
|
|
const putCalls = vi.mocked(mockKV.put).mock.calls
|
|
expect(putCalls.length).toBeGreaterThan(0)
|
|
for (const [key] of putCalls) {
|
|
expect((key as string).length).toBeLessThanOrEqual(512)
|
|
}
|
|
})
|
|
|
|
it('passes redirect_uri through without interception for legacy clients (no stored redirect_uris)', async () => {
|
|
const mapping = { us_client_id: 'us_id', eu_client_id: 'eu_id', created_at: Date.now() }
|
|
mockKVGet(mockKV, (_key: string, type?: unknown) => {
|
|
if (type === 'json') {
|
|
return Promise.resolve(mapping)
|
|
}
|
|
return Promise.resolve(null)
|
|
})
|
|
|
|
const request = new Request(
|
|
'https://oauth.hanzo.ai/oauth/authorize/?client_id=us_id&redirect_uri=http://localhost:3000/callback&response_type=code&_region=eu'
|
|
)
|
|
const response = await handleAuthorize(request, mockKV)
|
|
|
|
expect(response.status).toBe(302)
|
|
const location = new URL(response.headers.get('location')!)
|
|
expect(location.searchParams.get('redirect_uri')).toBe('http://localhost:3000/callback')
|
|
|
|
// No callback redirect_uri should be stored
|
|
const putCalls = vi.mocked(mockKV.put).mock.calls
|
|
const callbackPuts = putCalls.filter(([key]) => (key as string).startsWith('callback:'))
|
|
expect(callbackPuts).toHaveLength(0)
|
|
})
|
|
|
|
it('sets security and cache headers on region picker page', async () => {
|
|
const request = new Request('https://oauth.hanzo.ai/oauth/authorize/?client_id=abc&response_type=code')
|
|
const response = await handleAuthorize(request, mockKV)
|
|
|
|
expect(response.headers.get('x-frame-options')).toBe('DENY')
|
|
expect(response.headers.get('x-content-type-options')).toBe('nosniff')
|
|
expect(response.headers.get('referrer-policy')).toBe('no-referrer')
|
|
expect(response.headers.get('cache-control')).toBe(
|
|
'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800'
|
|
)
|
|
})
|
|
})
|