Ctrl K
ComunidadGuías…

server/http/app-auth-mounts.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { forbidden, notFound } from '@server/domain/errors'
import { listApplications } from '@server/usecases/applications'
import { type ConfigzOptions, getConfig } from '@server/usecases/configz'
import type { WalletRepository } from '@server/usecases/ports'
import type { Context } from 'hono'
import { getAddress } from 'viem'
import type { AgentConfiguration } from './app-types'
import { getDeps } from './middleware/deps'

export function mountAgentConfiguration(configuration: AgentConfiguration): AgentConfiguration {
  const issuer = mountAgentIssuer(configuration.issuer)
  return {
    ...configuration,
    issuer,
    default_location: mountAgentUrl(configuration.default_location, issuer),
    endpoints: Object.fromEntries(
      Object.entries(configuration.endpoints).map(([key, value]) => [key, mountAgentUrl(value, issuer)]),
    ),
  }
}

export function mountAgentIssuer(issuer: string) {
  const url = new URL(issuer)
  if (url.pathname === '/api/auth' || url.pathname.endsWith('/api/auth')) return issuer
  return `${issuer.replace(/\/$/, '')}/api/auth`
}

export function mountAgentUrl(value: string, issuer: string) {
  const url = new URL(value)
  const path = url.pathname.startsWith('/api/auth/') ? url.pathname.slice('/api/auth'.length) : url.pathname
  return `${issuer}${path}${url.search}`
}

export function oauthClientCorsOrigins() {
  return async ({ path, context }: { path: string; context: Context }) => {
    if (!isOAuthClientCorsPath(path)) return []

    const deps = getDeps(context)
    const issuer = new URL(context.req.url).origin
    const origins = new Set<string>()
    let page = 1

    for (;;) {
      const result = await listApplications(deps, issuer, {
        page,
        pageSize: 100,
      })
      for (const application of result.items) {
        if (application.disabled) continue
        for (const origin of application.corsOrigins) origins.add(origin)
      }
      if (result.pagination.page >= result.pagination.totalPages) break
      page += 1
    }

    return [...origins]
  }
}

export function isOAuthClientCorsPath(path: string) {
  return oauthClientCorsPaths.has(path)
}

export function isPublicCorsPath(path: string) {
  return path.startsWith('/api/public/') || publicOAuthMetadataPaths.has(path)
}

export function isIdentityProviderCallbackPath(path: string) {
  return /^\/api\/auth\/callback\/[^/]+$/.test(path)
}

export const publicIssuerMetadataPaths = [
  '/.well-known/openid-configuration/api/auth',
  '/.well-known/oauth-authorization-server/api/auth',
] as const

const publicOAuthMetadataPaths = new Set([
  '/api/auth/.well-known/openid-configuration',
  '/api/auth/.well-known/oauth-authorization-server',
  '/api/auth/jwks',
  ...publicIssuerMetadataPaths,
])

const oauthClientCorsPaths = new Set([
  '/api/auth/.well-known/openid-configuration',
  '/api/auth/.well-known/oauth-authorization-server',
  '/api/auth/jwks',
  '/api/auth/oauth2/token',
  '/api/auth/oauth2/userinfo',
  '/api/auth/oauth2/revoke',
  '/api/auth/oauth2/introspect',
])

export async function requireHostedAuthMethodEnabled(c: Context, options: ConfigzOptions) {
  const path = c.req.path
  if (path === '/api/auth/request-password-reset') throw notFound()
  if (!isManagedHostedAuthPath(path)) return

  const config = await getConfig(getDeps(c), options)
  if (!config.signIn.passwordEnabled && passwordAuthPaths.has(path)) {
    throw forbidden('Password authentication is disabled.')
  }
  if (!config.signIn.signupEnabled && path === '/api/auth/sign-up/email') {
    throw forbidden('Sign up is disabled.')
  }
  if (!config.signIn.emailOtpEnabled && (emailOtpAuthPaths.has(path) || (await isBlockedEmailOtpRequest(c)))) {
    throw forbidden('Email code authentication is disabled.')
  }
  if (!config.signIn.socialLoginEnabled && path === '/api/auth/sign-in/social') {
    throw forbidden('Social authentication is disabled.')
  }
  if (web3WalletAuthPaths.has(path) && !config.builtInProviders.web3Wallet.enabled) {
    throw forbidden('Web3 wallet authentication is disabled.')
  }
}

const passwordAuthPaths = new Set([
  '/api/auth/sign-in/email',
  '/api/auth/sign-in/username',
  '/api/auth/sign-up/email',
  '/api/auth/email-otp/request-password-reset',
  '/api/auth/email-otp/reset-password',
  '/api/auth/change-password',
])

const emailOtpAuthPaths = new Set(['/api/auth/sign-in/email-otp'])
const web3WalletAuthPaths = new Set(['/api/auth/siwe/nonce', '/api/auth/siwe/get-nonce', '/api/auth/siwe/verify'])

export function isManagedHostedAuthPath(path: string) {
  return (
    passwordAuthPaths.has(path) ||
    emailOtpAuthPaths.has(path) ||
    web3WalletAuthPaths.has(path) ||
    path === '/api/auth/email-otp/send-verification-otp' ||
    path === '/api/auth/sign-in/social'
  )
}

export async function requireLinkedSiweWallet(c: Context, wallets: WalletRepository) {
  if (c.req.path !== '/api/auth/siwe/verify') return

  const body = await c.req.raw
    .clone()
    .json()
    .catch(() => null)
  if (!body || typeof body !== 'object') return

  const record = body as Record<string, unknown>
  const walletAddress = typeof record.walletAddress === 'string' ? toWalletAddress(record.walletAddress) : null
  const chainId = typeof record.chainId === 'number' ? record.chainId : 1
  if (!walletAddress) return

  const linked = await wallets.findWalletAddress(walletAddress, chainId)
  const linkedOnAnotherChain = linked ? null : await wallets.findAnyWalletAddress(walletAddress)
  if (!linked && !linkedOnAnotherChain) {
    throw forbidden('This wallet is not linked to an existing account.')
  }
}

export function toWalletAddress(value: string) {
  try {
    return getAddress(value)
  } catch {
    return null
  }
}

export async function isBlockedEmailOtpRequest(c: Context) {
  if (c.req.path !== '/api/auth/email-otp/send-verification-otp') return false

  const body = await c.req.raw
    .clone()
    .json()
    .catch(() => null)
  return !(body && typeof body === 'object' && 'type' in body && body.type === 'email-verification')
}