mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-28 15:51:29 +08:00
d22227ed2f
* feat: add delegated agent oauth provider Agent-Profile: https://agent-kanban.dev/agents/e0a1ce35687e48ef * test(auth): cover delegated OAuth configuration * fix(auth): route OAuth metadata through worker * fix(auth): advertise canonical OAuth issuer * test: cover agent oauth provider integration Agent-Profile: https://agent-kanban.dev/agents/e0a1ce35687e48ef * test(auth): cover managed OAuth consent flow --------- Co-authored-by: Ravi Shah <ravi-shah@mails.agent-kanban.dev> Co-authored-by: saltbo <saltbo@foxmail.com>
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
import type { oauthProvider } from '@better-auth/oauth-provider'
|
|
import { APIError } from 'better-auth'
|
|
import {
|
|
AGENT_OAUTH_ACCESS_TOKEN_SECONDS,
|
|
AGENT_OAUTH_CLIENT_ID,
|
|
AGENT_OAUTH_REFRESH_TOKEN_SECONDS,
|
|
AGENT_OAUTH_SCOPES,
|
|
} from '../../shared/agent-oauth'
|
|
import { createOrgRepo } from '../adapters/repos/org'
|
|
import type { Database } from '../platform/interface'
|
|
import type { AgentOAuthGateway } from '../usecases/ports'
|
|
|
|
type AgentOAuthOrgLookup = Pick<ReturnType<typeof createOrgRepo>, 'findPersonalOrg' | 'getMemberRole'>
|
|
type AgentOAuthProviderOptions = Parameters<typeof oauthProvider>[0]
|
|
|
|
export function createAgentOAuthProviderOptions(input: {
|
|
db: Database
|
|
agentOAuth: AgentOAuthGateway
|
|
orgs?: AgentOAuthOrgLookup
|
|
}): AgentOAuthProviderOptions {
|
|
const orgs = input.orgs ?? createOrgRepo(input.db)
|
|
return {
|
|
disableJwtPlugin: true,
|
|
loginPage: '/sign-in',
|
|
consentPage: '/settings/agent-access',
|
|
accessTokenExpiresIn: AGENT_OAUTH_ACCESS_TOKEN_SECONDS,
|
|
refreshTokenExpiresIn: AGENT_OAUTH_REFRESH_TOKEN_SECONDS,
|
|
grantTypes: ['authorization_code', 'refresh_token'],
|
|
scopes: [...AGENT_OAUTH_SCOPES],
|
|
advertisedMetadata: { scopes_supported: [...AGENT_OAUTH_SCOPES] },
|
|
cachedTrustedClients: new Set([AGENT_OAUTH_CLIENT_ID]),
|
|
silenceWarnings: {
|
|
oauthAuthServerConfig: true,
|
|
openidConfig: true,
|
|
},
|
|
postLogin: {
|
|
page: '/settings/agent-access',
|
|
shouldRedirect: async () => false,
|
|
consentReferenceId: async ({ user, session, scopes }) => {
|
|
const clientScopes = scopes.filter((scope) => scope !== 'openid' && scope !== 'profile' && scope !== 'email')
|
|
const grantableScopes = new Set<string>(AGENT_OAUTH_SCOPES)
|
|
if (clientScopes.some((scope) => !grantableScopes.has(scope))) {
|
|
throw new APIError('BAD_REQUEST', { error: 'invalid_scope', error_description: 'Scope is not grantable' })
|
|
}
|
|
const orgId = typeof session.activeOrganizationId === 'string' ? session.activeOrganizationId : null
|
|
const selectedOrgId = orgId || (await orgs.findPersonalOrg(user.id))
|
|
if (!selectedOrgId) {
|
|
throw new APIError('BAD_REQUEST', {
|
|
error: 'invalid_request',
|
|
error_description: 'A workspace is required for Agent OAuth',
|
|
})
|
|
}
|
|
const role = await orgs.getMemberRole(selectedOrgId, user.id)
|
|
if (!role && selectedOrgId !== (await orgs.findPersonalOrg(user.id))) {
|
|
throw new APIError('FORBIDDEN', {
|
|
error: 'access_denied',
|
|
error_description: 'Workspace access is required for Agent OAuth',
|
|
})
|
|
}
|
|
return selectedOrgId
|
|
},
|
|
},
|
|
customAccessTokenClaims: async ({ user, referenceId, scopes, metadata }) => {
|
|
if (metadata?.client_id && metadata.client_id !== AGENT_OAUTH_CLIENT_ID) return {}
|
|
if (!user?.id || !referenceId) {
|
|
throw new APIError('BAD_REQUEST', {
|
|
error: 'invalid_grant',
|
|
error_description: 'Agent OAuth grant is missing workspace context',
|
|
})
|
|
}
|
|
await input.agentOAuth.assertLiveGrant(input.db, {
|
|
userId: user.id,
|
|
clientId: AGENT_OAUTH_CLIENT_ID,
|
|
orgId: referenceId,
|
|
scopes,
|
|
})
|
|
return {
|
|
zpan_org_id: referenceId,
|
|
zpan_actor: 'agent_oauth',
|
|
}
|
|
},
|
|
}
|
|
}
|