mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-28 15:51:29 +08:00
2657f82ef1
* feat(auth): add contextual OAuth workspace grants * fix(auth): register RFC 9396 detail types * fix(openapi): restore delegated CLI auth profile * fix(store): recover paid capacity fulfillment * fix(auth): close OAuth contract gaps * fix(store): resume verified x402 settlements * chore(deps): update zpan cloud sdk to 2.5.2 * test(store): cover verified settlement recovery * fix(auth): localize standard consent scopes * fix(docker): include dependency patches before install * fix(store): harden x402 purchase responses * test(auth): cover OAuth authorization boundaries * docs: add PR 550 verification screenshots * chore: remove temporary verification screenshots * docs(openapi): document exhausted capacity response
153 lines
6.5 KiB
TypeScript
153 lines
6.5 KiB
TypeScript
import { env } from 'cloudflare:workers'
|
|
import { WORKSPACE_AUTHORIZATION_DETAIL_TYPE } from '@shared/oauth'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { createApp } from '../../app'
|
|
import { createAuth } from '../../auth'
|
|
import { createCloudflarePlatform } from '../../platform/cloudflare'
|
|
|
|
async function buildApp() {
|
|
const platform = createCloudflarePlatform(env)
|
|
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET, 'http://localhost')
|
|
return createApp(platform, auth)
|
|
}
|
|
|
|
describe('[CF] Auth API', () => {
|
|
it('POST /api/auth/sign-up/email creates user', async () => {
|
|
const app = await buildApp()
|
|
const res = await app.request('/api/auth/sign-up/email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: 'CF Test', email: `cf-${Date.now()}@example.com`, password: 'password123456' }),
|
|
})
|
|
expect(res.status).toBe(200)
|
|
const body = (await res.json()) as { user: { email: string } }
|
|
expect(body.user.email).toContain('@example.com')
|
|
})
|
|
|
|
it('POST /api/auth/sign-in/email signs in', async () => {
|
|
const app = await buildApp()
|
|
const email = `cf-signin-${Date.now()}@example.com`
|
|
await app.request('/api/auth/sign-up/email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: 'CF Test', email, password: 'password123456' }),
|
|
})
|
|
const res = await app.request('/api/auth/sign-in/email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password: 'password123456' }),
|
|
})
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('set-cookie')).toBeTruthy()
|
|
})
|
|
|
|
it('accepts official Workers commit and branch aliases but rejects unrelated workers.dev', async () => {
|
|
const platform = createCloudflarePlatform(env)
|
|
const configuredOrigin = 'https://zpan-staging.saltbo.workers.dev'
|
|
const commitOrigin = 'https://99dc50ae-zpan.saltbo.workers.dev'
|
|
const branchOrigin = 'https://feat-x402-paid-agent-uploads-zpan.saltbo.workers.dev'
|
|
const auth = await createAuth(platform.db, env.BETTER_AUTH_SECRET, configuredOrigin, [configuredOrigin])
|
|
const app = createApp(platform, auth)
|
|
const email = `cf-preview-${Date.now()}@example.com`
|
|
const password = 'password123456'
|
|
const signUp = await app.request(`${configuredOrigin}/api/auth/sign-up/email`, {
|
|
method: 'POST',
|
|
headers: { Origin: configuredOrigin, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: 'CF Preview User', email, password }),
|
|
})
|
|
expect(signUp.status).toBe(200)
|
|
|
|
for (const origin of [commitOrigin, branchOrigin]) {
|
|
const signIn = await app.request(`${origin}/api/auth/sign-in/email`, {
|
|
method: 'POST',
|
|
headers: { Origin: origin, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password, callbackURL: `${origin}/files` }),
|
|
})
|
|
expect(signIn.status, await signIn.clone().text()).toBe(200)
|
|
}
|
|
|
|
const unrelatedOrigin = 'https://unrelated-worker.other-account.workers.dev'
|
|
const rejected = await app.request(`${unrelatedOrigin}/api/auth/sign-in/email`, {
|
|
method: 'POST',
|
|
headers: { Origin: unrelatedOrigin, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password, callbackURL: `${unrelatedOrigin}/files` }),
|
|
})
|
|
expect(rejected.status).toBe(403)
|
|
})
|
|
|
|
it('completes managed OAuth consent on D1', async () => {
|
|
const app = await buildApp()
|
|
const signUp = await app.request('/api/auth/sign-up/email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: 'CF OAuth',
|
|
email: `cf-oauth-${Date.now()}@example.com`,
|
|
password: 'password123456',
|
|
}),
|
|
})
|
|
const cookie = signUp.headers
|
|
.getSetCookie()
|
|
.map((value) => value.split(';', 1)[0])
|
|
.join('; ')
|
|
const registration = await app.request('/api/auth/oauth2/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
client_name: 'CF Consent Test Client',
|
|
redirect_uris: ['https://broker.example.com/callback'],
|
|
grant_types: ['authorization_code', 'refresh_token'],
|
|
response_types: ['code'],
|
|
token_endpoint_auth_method: 'none',
|
|
scope: 'openid offline_access objects:read quota:read',
|
|
}),
|
|
})
|
|
const registered = (await registration.json()) as { client_id: string }
|
|
expect(registration.status).toBe(201)
|
|
const params = new URLSearchParams({
|
|
client_id: registered.client_id,
|
|
redirect_uri: 'https://broker.example.com/callback',
|
|
response_type: 'code',
|
|
scope: 'openid offline_access objects:read quota:read',
|
|
state: 'cf-oauth',
|
|
code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
|
|
code_challenge_method: 'S256',
|
|
authorization_details: JSON.stringify([{ type: WORKSPACE_AUTHORIZATION_DETAIL_TYPE }]),
|
|
})
|
|
const authorize = await app.request(`/api/auth/oauth2/authorize?${params}`, {
|
|
headers: { Cookie: cookie, Origin: 'http://localhost' },
|
|
})
|
|
const consentLocation = authorize.headers.get('location')
|
|
expect(authorize.status).toBe(302)
|
|
expect(consentLocation).toMatch(/^\/oauth\/consent\?/)
|
|
|
|
const oauthQuery = consentLocation?.slice(consentLocation.indexOf('?') + 1) ?? ''
|
|
const contextResponse = await app.request(`/api/oauth-consent?oauthQuery=${encodeURIComponent(oauthQuery)}`, {
|
|
headers: { Cookie: cookie },
|
|
})
|
|
const context = (await contextResponse.json()) as { workspaces: Array<{ id: string }> }
|
|
expect(contextResponse.status).toBe(200)
|
|
expect(context.workspaces).toHaveLength(1)
|
|
|
|
const consent = await app.request('/api/oauth-consent', {
|
|
method: 'POST',
|
|
headers: { Cookie: cookie, Origin: 'http://localhost', 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
accept: true,
|
|
oauthQuery,
|
|
workspaceIds: [context.workspaces[0].id],
|
|
}),
|
|
})
|
|
const consentBody = await consent.text()
|
|
|
|
expect(consent.status, consentBody).toBe(200)
|
|
expect(JSON.parse(consentBody)).toMatchObject({
|
|
url: expect.stringMatching(/^https:\/\/broker\.example\.com\/callback\?code=/),
|
|
})
|
|
})
|
|
|
|
// Wrong password test is covered by Node tests (auth.test.ts).
|
|
// Better Auth throws an unhandled rejection internally on auth failure
|
|
// that leaks into the Miniflare isolate, causing a false test failure.
|
|
})
|