diff --git a/apps/backend/package.json b/apps/backend/package.json index 4b7406c..4c7c7d1 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -10,7 +10,9 @@ "@hono/zod-validator": "^0.5.0", "@repo/db": "workspace:*", "@repo/oidc-provider": "workspace:*", + "@repo/tus": "workspace:*", "@trpc/server": "11.1.2", + "dayjs": "^1.11.12", "hono": "^4.7.10", "ioredis": "5.4.1", "jose": "^6.0.11", @@ -19,6 +21,7 @@ "node-cron": "^4.0.7", "oidc-provider": "^9.1.1", "superjson": "^2.2.2", + "transliteration": "^2.3.5", "valibot": "^1.1.0", "zod": "^3.25.23" }, diff --git a/apps/backend/src/upload/tus.ts b/apps/backend/src/upload/tus.ts index 55d8950..d98c7fe 100644 --- a/apps/backend/src/upload/tus.ts +++ b/apps/backend/src/upload/tus.ts @@ -1,7 +1,7 @@ import { Server, Upload } from '@repo/tus'; import { prisma } from '@repo/db'; import { getFilenameWithoutExt } from '../utils/file'; -import { nanoid } from 'nanoid-cjs'; +import { nanoid } from 'nanoid'; import { slugify } from 'transliteration'; import { StorageManager } from './storage.adapter'; diff --git a/packages/oidc-provider/README.md b/packages/oidc-provider/README.md new file mode 100644 index 0000000..611f14d --- /dev/null +++ b/packages/oidc-provider/README.md @@ -0,0 +1,158 @@ +# OIDC Provider + +OpenID Connect Provider 实现,支持标准的 OIDC 协议流程。 + +## 特性 + +- 完整的 OIDC 协议支持 +- 密码认证策略 +- 会话管理 +- 令牌管理(访问令牌、刷新令牌、ID令牌) +- PKCE 支持 +- 可自定义的存储适配器 + +## 快速开始 + +### 1. 安装 + +```bash +npm install @nice/oidc-provider +``` + +### 2. 配置 + +```typescript +import { createOIDCProvider } from '@nice/oidc-provider/middleware/hono'; +import { MemoryStorageAdapter } from '@nice/oidc-provider/storage'; + +const config = { + issuer: 'https://your-domain.com', + signingKey: 'your-signing-key', + storage: new MemoryStorageAdapter(), + + // 用户和客户端查找函数 + findUser: async (userId: string) => { + // 从数据库查找用户 + return await db.user.findUnique({ where: { id: userId } }); + }, + + findClient: async (clientId: string) => { + // 从数据库查找客户端 + return await db.client.findUnique({ where: { id: clientId } }); + }, + + // 认证配置 + authConfig: { + // 密码验证器 + passwordValidator: async (username: string, password: string) => { + const user = await db.user.findUnique({ where: { username } }); + if (user && await bcrypt.compare(password, user.hashedPassword)) { + return user.id; + } + return null; + }, + + // 会话配置 + sessionTTL: 24 * 60 * 60, // 24小时 + rememberMeMaxAge: 30 * 24 * 60 * 60, // 30天 + + // 页面配置 + pageConfig: { + title: '用户登录', + brandName: '我的应用', + logoUrl: '/logo.png' + } + } +}; + +// 创建 OIDC Provider Hono 应用 +const oidcApp = createOIDCProvider(config); +``` + +### 3. 集成到 Hono 应用 + +```typescript +import { Hono } from 'hono'; + +const app = new Hono(); + +// 挂载 OIDC Provider +app.route('/oidc', oidcApp); + +export default app; +``` + +## API 端点 + +创建后的 OIDC Provider 将提供以下标准端点: + +- `POST /login` - 用户登录 +- `GET /logout` - 用户登出 +- `POST /logout` - 用户登出(POST 方式) +- `GET /.well-known/openid-configuration` - OIDC 发现文档 +- `GET /.well-known/jwks.json` - JSON Web Key Set +- `GET /auth` - 授权端点 +- `POST /token` - 令牌端点 +- `GET /userinfo` - 用户信息端点 +- `POST /revoke` - 令牌撤销端点 +- `POST /introspect` - 令牌内省端点 + +## 配置选项 + +### OIDCProviderConfig + +| 字段 | 类型 | 必需 | 描述 | +|------|------|------|------| +| `issuer` | string | ✓ | 发行者标识符 | +| `signingKey` | string | ✓ | JWT 签名密钥 | +| `storage` | StorageAdapter | ✓ | 存储适配器 | +| `findUser` | function | ✓ | 用户查找函数 | +| `findClient` | function | ✓ | 客户端查找函数 | +| `authConfig` | AuthConfig | - | 认证配置 | +| `tokenTTL` | TokenTTLConfig | - | 令牌过期时间配置 | + +### AuthConfig + +| 字段 | 类型 | 必需 | 描述 | +|------|------|------|------| +| `passwordValidator` | function | - | 密码验证函数 | +| `sessionTTL` | number | - | 会话过期时间(秒) | +| `rememberMeMaxAge` | number | - | 记住我最长时间(秒) | +| `pageConfig` | PageConfig | - | 登录页面配置 | + +### PageConfig + +| 字段 | 类型 | 描述 | +|------|------|------| +| `title` | string | 登录页面标题 | +| `brandName` | string | 品牌名称 | +| `logoUrl` | string | Logo URL | + +## 存储适配器 + +项目提供了多种存储适配器: + +- `MemoryStorageAdapter` - 内存存储(适用于开发和测试) +- `RedisStorageAdapter` - Redis 存储 +- `DatabaseStorageAdapter` - 数据库存储 + +### 自定义存储适配器 + +```typescript +import { StorageAdapter } from '@nice/oidc-provider/storage'; + +class CustomStorageAdapter implements StorageAdapter { + // 实现所需的方法 +} +``` + +## 安全考虑 + +1. **签名密钥安全**:确保 `signingKey` 足够复杂且妥善保管 +2. **HTTPS**:生产环境必须使用 HTTPS +3. **客户端验证**:实现严格的客户端验证逻辑 +4. **密码策略**:在 `passwordValidator` 中实现适当的密码策略 + +## 许可证 + +MIT \ No newline at end of file diff --git a/packages/oidc-provider/src/auth/auth-manager.ts b/packages/oidc-provider/src/auth/auth-manager.ts deleted file mode 100644 index 201c97c..0000000 --- a/packages/oidc-provider/src/auth/auth-manager.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { Context } from 'hono'; -import type { AuthorizationRequest, OIDCProviderConfig } from '../types'; -import { PasswordAuthStrategy, type AuthenticationResult, type PasswordAuthConfig } from './strategies/password-auth-strategy'; - -/** - * 密码验证器类型 - */ -export type PasswordValidator = (username: string, password: string) => Promise; - -/** - * 认证管理器配置 - */ -export interface AuthManagerConfig { - /** 会话TTL(秒) */ - sessionTTL?: number; - /** 记住我最大存活时间(秒) */ - rememberMeMaxAge?: number; - /** 页面配置 */ - pageConfig?: { - title?: string; - brandName?: string; - logoUrl?: string; - }; -} - -/** - * 认证管理器 - * 专门处理密码认证 - */ -export class AuthManager { - private readonly passwordAuth: PasswordAuthStrategy; - - constructor( - oidcConfig: OIDCProviderConfig, - passwordValidator: PasswordValidator, - config: AuthManagerConfig = {} - ) { - const passwordConfig: PasswordAuthConfig = { - sessionTTL: config.sessionTTL || 24 * 60 * 60, // 默认24小时 - rememberMeMaxAge: config.rememberMeMaxAge || 30 * 24 * 60 * 60, // 默认30天 - pageConfig: config.pageConfig || {} - }; - - this.passwordAuth = new PasswordAuthStrategy( - oidcConfig, - passwordValidator, - passwordConfig - ); - } - - /** - * 检查当前用户是否已认证 - */ - async getCurrentUser(c: Context): Promise { - return await this.passwordAuth.getCurrentUser(c); - } - - /** - * 处理认证要求 - */ - async handleAuthenticationRequired( - c: Context, - authRequest: AuthorizationRequest - ): Promise { - return await this.passwordAuth.handleAuthenticationRequired(c, authRequest); - } - - /** - * 处理认证请求 - */ - async authenticate(c: Context): Promise { - return await this.passwordAuth.authenticate(c); - } - - /** - * 处理登出请求 - */ - async logout(c: Context): Promise { - return await this.passwordAuth.logout(c); - } - - /** - * 处理完整的登录流程 - */ - async handleLogin(c: Context, authRequest: AuthorizationRequest): Promise { - try { - // 执行认证 - const authResult = await this.passwordAuth.authenticate(c); - - if (authResult.success) { - // 认证成功,处理后续操作 - return await this.handleAuthenticationSuccess(c, authResult); - } else { - // 认证失败,返回错误页面 - return await this.handleAuthenticationFailure(c, authResult, authRequest); - } - } catch (error) { - console.error('登录流程处理失败:', error); - - // 返回通用错误页面 - return await this.handleAuthenticationFailure( - c, - { success: false, error: '服务器内部错误' }, - authRequest - ); - } - } - - /** - * 检查认证状态并处理 - */ - async checkAuthenticationStatus( - c: Context, - authRequest: AuthorizationRequest - ): Promise<{ authenticated: boolean; userId?: string; response?: Response }> { - try { - const userId = await this.getCurrentUser(c); - - if (userId) { - return { - authenticated: true, - userId - }; - } else { - const response = await this.handleAuthenticationRequired(c, authRequest); - return { - authenticated: false, - response - }; - } - } catch (error) { - console.error('检查认证状态失败:', error); - throw error; - } - } - - /** - * 处理认证成功 - */ - private async handleAuthenticationSuccess( - c: Context, - result: AuthenticationResult - ): Promise { - return await this.passwordAuth.handleAuthenticationSuccess(c, result); - } - - /** - * 处理认证失败 - */ - private async handleAuthenticationFailure( - c: Context, - result: AuthenticationResult, - authRequest: AuthorizationRequest - ): Promise { - return await this.passwordAuth.handleAuthenticationFailure(c, result, authRequest); - } -} \ No newline at end of file diff --git a/packages/oidc-provider/src/auth/index.ts b/packages/oidc-provider/src/auth/index.ts index 4220fce..691f9ba 100644 --- a/packages/oidc-provider/src/auth/index.ts +++ b/packages/oidc-provider/src/auth/index.ts @@ -1,13 +1,13 @@ -// 核心管理器 -export { AuthManager, type AuthManagerConfig, type PasswordValidator } from './auth-manager'; - // 密码认证 -export { PasswordAuthStrategy, type AuthenticationResult, type PasswordAuthConfig } from './strategies/password-auth-strategy'; +export { PasswordAuth, type AuthenticationResult, type PasswordAuthConfig } from './password-auth'; + +// 类型定义(从 types 模块重新导出) +export type { PasswordValidator } from '../types'; // 工具类 export { CookieUtils, type CookieConfig } from './utils/cookie-utils'; export { HtmlTemplates, type PageConfig } from './utils/html-templates'; -// 原有组件(向后兼容) +// 存储管理器 export { TokenManager } from './token-manager'; export { SessionManager } from './session-manager'; \ No newline at end of file diff --git a/packages/oidc-provider/src/auth/strategies/password-auth-strategy.ts b/packages/oidc-provider/src/auth/password-auth.ts similarity index 74% rename from packages/oidc-provider/src/auth/strategies/password-auth-strategy.ts rename to packages/oidc-provider/src/auth/password-auth.ts index 036bf53..d0374f6 100644 --- a/packages/oidc-provider/src/auth/strategies/password-auth-strategy.ts +++ b/packages/oidc-provider/src/auth/password-auth.ts @@ -1,8 +1,8 @@ import type { Context } from 'hono'; -import type { AuthorizationRequest, LoginCredentials, PasswordValidator, OIDCProviderConfig } from '../../types'; -import { SessionManager } from '../session-manager'; -import { CookieUtils } from '../utils/cookie-utils'; -import { HtmlTemplates, type PageConfig } from '../utils/html-templates'; +import { SessionManager } from './session-manager'; +import { CookieUtils } from './utils/cookie-utils'; +import { HtmlTemplates, type PageConfig } from './utils/html-templates'; +import { OIDCProviderConfig, PasswordValidator, AuthorizationRequest, LoginCredentials } from '../types'; /** * 认证结果接口 @@ -31,14 +31,18 @@ export interface PasswordAuthConfig { } /** - * 密码认证服务 + * 密码认证策略 * 实现基于用户名和密码的传统认证方式 */ -export class PasswordAuthStrategy { +export class PasswordAuth { readonly name = 'password'; private readonly sessionManager: SessionManager; - private readonly config: PasswordAuthConfig; + private readonly config: { + sessionTTL: number; + rememberMeMaxAge: number; + pageConfig?: PageConfig; + }; constructor( private readonly oidcConfig: OIDCProviderConfig, @@ -46,8 +50,8 @@ export class PasswordAuthStrategy { config: PasswordAuthConfig = {} ) { this.config = { - sessionTTL: 24 * 60 * 60, // 默认24小时 - rememberMeMaxAge: 30 * 24 * 60 * 60, // 默认30天 + sessionTTL: 24 * 60 * 60, // 24小时 + rememberMeMaxAge: 30 * 24 * 60 * 60, // 30天 ...config }; this.sessionManager = new SessionManager(oidcConfig.storage, this.config.sessionTTL); @@ -58,9 +62,7 @@ export class PasswordAuthStrategy { */ async getCurrentUser(c: Context): Promise { const sessionId = CookieUtils.getSessionIdFromCookie(c); - if (!sessionId) { - return null; - } + if (!sessionId) return null; try { const session = await this.sessionManager.getSession(sessionId); @@ -76,7 +78,6 @@ export class PasswordAuthStrategy { */ async handleAuthenticationRequired(c: Context, authRequest: AuthorizationRequest): Promise { const loginHtml = HtmlTemplates.generateLoginPage(authRequest, this.config.pageConfig); - c.header('Content-Type', 'text/html; charset=utf-8'); return c.body(loginHtml); } @@ -86,7 +87,6 @@ export class PasswordAuthStrategy { */ async authenticate(c: Context): Promise { try { - // 解析表单数据 const formData = await c.req.formData(); const credentials = this.parseCredentials(formData); const authParams = this.extractAuthParams(formData); @@ -94,41 +94,27 @@ export class PasswordAuthStrategy { // 验证输入 const validationError = this.validateCredentials(credentials); if (validationError) { - return { - success: false, - error: validationError - }; + return { success: false, error: validationError }; } // 验证用户密码 const userId = await this.passwordValidator(credentials.username, credentials.password); if (!userId) { - return { - success: false, - error: '用户名或密码错误' - }; + return { success: false, error: '用户名或密码错误' }; } // 检查用户是否存在 const user = await this.oidcConfig.findUser(userId); if (!user) { - return { - success: false, - error: '用户不存在' - }; + return { success: false, error: '用户不存在' }; } // 创建会话 - const session = await this.sessionManager.createSession( - userId, - authParams.client_id, - authParams - ); + const session = await this.sessionManager.createSession(userId, authParams.client_id, authParams); - // 设置Cookie const maxAge = credentials.remember_me - ? this.config.rememberMeMaxAge! - : this.config.sessionTTL!; + ? this.config.rememberMeMaxAge + : this.config.sessionTTL; return { success: true, @@ -140,13 +126,9 @@ export class PasswordAuthStrategy { rememberMe: credentials.remember_me } }; - } catch (error) { console.error('认证处理失败:', error); - return { - success: false, - error: '服务器内部错误,请稍后重试' - }; + return { success: false, error: '服务器内部错误,请稍后重试' }; } } @@ -164,10 +146,8 @@ export class PasswordAuthStrategy { } } - // 清除Cookie CookieUtils.clearSessionCookie(c); - // 处理重定向 const postLogoutRedirectUri = c.req.query('post_logout_redirect_uri'); const state = c.req.query('state'); @@ -185,20 +165,14 @@ export class PasswordAuthStrategy { /** * 处理认证成功后的操作 */ - async handleAuthenticationSuccess( - c: Context, - result: AuthenticationResult - ): Promise { + async handleAuthenticationSuccess(c: Context, result: AuthenticationResult): Promise { if (!result.success || !result.metadata) { throw new Error('认证结果无效'); } const { sessionId, maxAge, authParams } = result.metadata; - // 设置会话Cookie CookieUtils.setSessionCookie(c, sessionId, maxAge); - - // 重定向到授权端点 const authUrl = this.buildAuthorizationUrl(authParams); return c.redirect(authUrl); } @@ -211,12 +185,7 @@ export class PasswordAuthStrategy { result: AuthenticationResult, authRequest: AuthorizationRequest ): Promise { - const errorHtml = HtmlTemplates.generateLoginPage( - authRequest, - this.config.pageConfig, - result.error - ); - + const errorHtml = HtmlTemplates.generateLoginPage(authRequest, this.config.pageConfig, result.error); c.header('Content-Type', 'text/html; charset=utf-8'); return c.body(errorHtml); } @@ -236,22 +205,10 @@ export class PasswordAuthStrategy { * 验证登录凭据 */ private validateCredentials(credentials: LoginCredentials): string | null { - if (!credentials.username.trim()) { - return '请输入用户名'; - } - - if (!credentials.password) { - return '请输入密码'; - } - - if (credentials.username.length > 100) { - return '用户名过长'; - } - - if (credentials.password.length > 200) { - return '密码过长'; - } - + if (!credentials.username.trim()) return '请输入用户名'; + if (!credentials.password) return '请输入密码'; + if (credentials.username.length > 100) return '用户名过长'; + if (credentials.password.length > 200) return '密码过长'; return null; } @@ -259,9 +216,7 @@ export class PasswordAuthStrategy { * 构建登出后重定向URL */ private buildPostLogoutRedirectUrl(redirectUri: string, state?: string): string { - if (!state) { - return redirectUri; - } + if (!state) return redirectUri; const url = new URL(redirectUri); url.searchParams.set('state', state); @@ -271,7 +226,7 @@ export class PasswordAuthStrategy { /** * 构建授权URL */ - protected buildAuthorizationUrl(authParams: AuthorizationRequest): string { + private buildAuthorizationUrl(authParams: AuthorizationRequest): string { const params = new URLSearchParams(); params.set('response_type', authParams.response_type); params.set('client_id', authParams.client_id); @@ -289,7 +244,7 @@ export class PasswordAuthStrategy { /** * 从表单数据提取授权参数 */ - protected extractAuthParams(formData: FormData): AuthorizationRequest { + private extractAuthParams(formData: FormData): AuthorizationRequest { return { response_type: formData.get('response_type')?.toString() || '', client_id: formData.get('client_id')?.toString() || '', diff --git a/packages/oidc-provider/src/auth/session-manager.ts b/packages/oidc-provider/src/auth/session-manager.ts index 0baedfb..000bb24 100644 --- a/packages/oidc-provider/src/auth/session-manager.ts +++ b/packages/oidc-provider/src/auth/session-manager.ts @@ -1,5 +1,5 @@ -import type { StorageAdapter } from '../storage'; import type { AuthorizationRequest } from '../types'; +import type { StorageAdapter } from '../storage'; /** * 会话数据接口 @@ -19,7 +19,7 @@ interface SessionData { export class SessionManager { constructor(private storage: StorageAdapter, private sessionTTL: number = 3600) { } - private getSessionKey(sessionId: string): string { + private getKey(sessionId: string): string { return `session:${sessionId}`; } @@ -59,17 +59,17 @@ export class SessionManager { } async storeSession(sessionId: string, data: any): Promise { - const key = this.getSessionKey(sessionId); + const key = this.getKey(sessionId); await this.storage.set(key, data, this.sessionTTL); } async getSession(sessionId: string): Promise { - const key = this.getSessionKey(sessionId); + const key = this.getKey(sessionId); return await this.storage.get(key); } async deleteSession(sessionId: string): Promise { - const key = this.getSessionKey(sessionId); + const key = this.getKey(sessionId); await this.storage.delete(key); } diff --git a/packages/oidc-provider/src/auth/token-manager.ts b/packages/oidc-provider/src/auth/token-manager.ts index 0e18d26..11c7827 100644 --- a/packages/oidc-provider/src/auth/token-manager.ts +++ b/packages/oidc-provider/src/auth/token-manager.ts @@ -13,7 +13,14 @@ import type { StorageAdapter } from '../storage'; export class TokenManager { constructor(private storage: StorageAdapter) { } - // 生成存储键 + /** + * 计算TTL(秒) + */ + private calculateTTL(expiresAt: Date): number { + return Math.max(Math.floor((expiresAt.getTime() - Date.now()) / 1000), 1); + } + + // 生成令牌存储键 private getTokenKey(type: string, token: string): string { return `${type}:${token}`; } @@ -25,27 +32,13 @@ export class TokenManager { // 授权码管理 async storeAuthorizationCode(authCode: AuthorizationCode): Promise { const key = this.getTokenKey('auth_code', authCode.code); - const ttl = Math.floor((authCode.expires_at.getTime() - Date.now()) / 1000); - - const data = { - ...authCode, - expires_at: authCode.expires_at.toISOString(), - created_at: authCode.created_at.toISOString(), - }; - - await this.storage.set(key, data, Math.max(ttl, 1)); + const ttl = this.calculateTTL(authCode.expires_at); + await this.storage.set(key, authCode, ttl); } async getAuthorizationCode(code: string): Promise { const key = this.getTokenKey('auth_code', code); - const data = await this.storage.get(key); - if (!data) return null; - - return { - ...data, - expires_at: new Date(data.expires_at), - created_at: new Date(data.created_at), - }; + return await this.storage.get(key); } async deleteAuthorizationCode(code: string): Promise { @@ -57,33 +50,20 @@ export class TokenManager { async storeAccessToken(token: AccessToken): Promise { const key = this.getTokenKey('access_token', token.token); const userClientKey = this.getUserClientKey('access_tokens', token.user_id, token.client_id); - const ttl = Math.floor((token.expires_at.getTime() - Date.now()) / 1000); - - const data = { - ...token, - expires_at: token.expires_at.toISOString(), - created_at: token.created_at.toISOString(), - }; + const ttl = this.calculateTTL(token.expires_at); // 存储令牌数据 - await this.storage.set(key, data, Math.max(ttl, 1)); + await this.storage.set(key, token, ttl); // 存储用户-客户端索引 const existingTokens = await this.storage.get(userClientKey) || []; existingTokens.push(token.token); - await this.storage.set(userClientKey, existingTokens, Math.max(ttl, 1)); + await this.storage.set(userClientKey, existingTokens, ttl); } async getAccessToken(token: string): Promise { const key = this.getTokenKey('access_token', token); - const data = await this.storage.get(key); - if (!data) return null; - - return { - ...data, - expires_at: new Date(data.expires_at), - created_at: new Date(data.created_at), - }; + return await this.storage.get(key); } async deleteAccessToken(token: string): Promise { @@ -123,33 +103,20 @@ export class TokenManager { async storeRefreshToken(token: RefreshToken): Promise { const key = this.getTokenKey('refresh_token', token.token); const userClientKey = this.getUserClientKey('refresh_tokens', token.user_id, token.client_id); - const ttl = Math.floor((token.expires_at.getTime() - Date.now()) / 1000); - - const data = { - ...token, - expires_at: token.expires_at.toISOString(), - created_at: token.created_at.toISOString(), - }; + const ttl = this.calculateTTL(token.expires_at); // 存储令牌数据 - await this.storage.set(key, data, Math.max(ttl, 1)); + await this.storage.set(key, token, ttl); // 存储用户-客户端索引 const existingTokens = await this.storage.get(userClientKey) || []; existingTokens.push(token.token); - await this.storage.set(userClientKey, existingTokens, Math.max(ttl, 1)); + await this.storage.set(userClientKey, existingTokens, ttl); } async getRefreshToken(token: string): Promise { const key = this.getTokenKey('refresh_token', token); - const data = await this.storage.get(key); - if (!data) return null; - - return { - ...data, - expires_at: new Date(data.expires_at), - created_at: new Date(data.created_at), - }; + return await this.storage.get(key); } async deleteRefreshToken(token: string): Promise { @@ -175,27 +142,13 @@ export class TokenManager { // ID令牌管理 async storeIDToken(token: IDToken): Promise { const key = this.getTokenKey('id_token', token.token); - const ttl = Math.floor((token.expires_at.getTime() - Date.now()) / 1000); - - const data = { - ...token, - expires_at: token.expires_at.toISOString(), - created_at: token.created_at.toISOString(), - }; - - await this.storage.set(key, data, Math.max(ttl, 1)); + const ttl = this.calculateTTL(token.expires_at); + await this.storage.set(key, token, ttl); } async getIDToken(token: string): Promise { const key = this.getTokenKey('id_token', token); - const data = await this.storage.get(key); - if (!data) return null; - - return { - ...data, - expires_at: new Date(data.expires_at), - created_at: new Date(data.created_at), - }; + return await this.storage.get(key); } async deleteIDToken(token: string): Promise { diff --git a/packages/oidc-provider/src/errors/error-factory.ts b/packages/oidc-provider/src/errors/error-factory.ts new file mode 100644 index 0000000..de1ce65 --- /dev/null +++ b/packages/oidc-provider/src/errors/error-factory.ts @@ -0,0 +1,109 @@ +import type { OIDCError } from '../types'; + +/** + * OIDC错误处理工厂类 + * 用于创建标准化的OIDC错误响应 + */ +export class OIDCErrorFactory { + /** + * 创建授权错误(带可选的重定向URI和state) + */ + static createAuthError(error: string, description: string, state?: string) { + return { + success: false as const, + error: { error, error_description: description, state }, + redirectUri: undefined as string | undefined, + }; + } + + /** + * 创建令牌错误 + */ + static createTokenError(error: string, description: string) { + return { + success: false as const, + error: { error, error_description: description }, + }; + } + + /** + * 创建简单错误(用于一般API响应) + */ + static createSimpleError(error: string, description: string): OIDCError { + return { error, error_description: description }; + } + + /** + * 服务器错误 + */ + static serverError(state?: string) { + return this.createAuthError('server_error', 'Internal server error', state); + } + + /** + * 无效令牌错误 + */ + static invalidToken(description = 'Invalid token') { + return this.createSimpleError('invalid_token', description); + } + + /** + * 无效请求错误 + */ + static invalidRequest(description: string) { + return this.createSimpleError('invalid_request', description); + } + + /** + * 无效客户端错误 + */ + static invalidClient(description: string) { + return this.createTokenError('invalid_client', description); + } + + /** + * 无效授权错误 + */ + static invalidGrant(description: string) { + return this.createTokenError('invalid_grant', description); + } + + /** + * 不支持的授权类型错误 + */ + static unsupportedGrantType(description = 'Grant type not supported') { + return this.createTokenError('unsupported_grant_type', description); + } + + /** + * 无效作用域错误 + */ + static invalidScope(description: string) { + return this.createTokenError('invalid_scope', description); + } + + /** + * 需要登录错误 + */ + static loginRequired(description = 'User authentication is required', state?: string) { + return this.createAuthError('login_required', description, state); + } + + /** + * PKCE相关错误 + */ + static pkceError(description: string, state?: string) { + return this.createAuthError('invalid_request', description, state); + } + + /** + * 构建错误响应的URL参数 + */ + static buildErrorResponse(error: OIDCError): URLSearchParams { + const params = new URLSearchParams(); + Object.entries(error).forEach(([key, value]) => { + if (value != null) params.set(key, String(value)); + }); + return params; + } +} \ No newline at end of file diff --git a/packages/oidc-provider/src/errors/index.ts b/packages/oidc-provider/src/errors/index.ts new file mode 100644 index 0000000..4a596c9 --- /dev/null +++ b/packages/oidc-provider/src/errors/index.ts @@ -0,0 +1 @@ +export { OIDCErrorFactory } from './error-factory'; \ No newline at end of file diff --git a/packages/oidc-provider/src/index.ts b/packages/oidc-provider/src/index.ts index db6c9c7..0c80dda 100644 --- a/packages/oidc-provider/src/index.ts +++ b/packages/oidc-provider/src/index.ts @@ -1,7 +1,7 @@ -// 核心类 +// 重新导出核心组件 export { OIDCProvider } from './provider'; -// 类型定义 +// 重新导出类型定义 export type { OIDCProviderConfig, OIDCClient, @@ -20,41 +20,29 @@ export type { PasswordValidator, } from './types'; -// 存储适配器接口和示例实现 -export type { StorageAdapter } from './storage'; -export { RedisStorageAdapter } from './storage'; +// 重新导出存储适配器 +export type { StorageAdapter } from './storage/adapter'; +export { RedisStorageAdapter } from './storage/redis-adapter'; -// Hono中间件 -export { - createOIDCProvider, - oidcProvider, - getOIDCProvider -} from './middleware/hono'; - -// 导出中间件配置类型 -export type { - OIDCHonoOptions -} from './middleware/hono'; - -// 工具类 +// 重新导出JWT工具 export { JWTUtils } from './utils/jwt'; -export { PKCEUtils } from './utils/pkce'; -export { ValidationUtils } from './utils/validation'; -// 认证模块 +// 重新导出验证工具 +export { ValidationUtils } from './utils/validation'; +export { PKCEUtils } from './utils/pkce'; + +// 重新导出认证相关 export { - AuthManager, - PasswordAuthStrategy, + PasswordAuth, + type AuthenticationResult, + type PasswordAuthConfig, + TokenManager, + SessionManager, CookieUtils, HtmlTemplates, - SessionManager, - TokenManager + type PageConfig, + type CookieConfig, } from './auth'; -export type { - AuthManagerConfig, - PasswordAuthConfig, - AuthenticationResult, - CookieConfig, - PageConfig -} from './auth'; \ No newline at end of file +// 重新导出中间件 +export { createOIDCProvider, oidcProvider, getOIDCProvider } from './middleware/hono'; \ No newline at end of file diff --git a/packages/oidc-provider/src/middleware/hono.ts b/packages/oidc-provider/src/middleware/hono.ts index d4b2293..583ecd4 100644 --- a/packages/oidc-provider/src/middleware/hono.ts +++ b/packages/oidc-provider/src/middleware/hono.ts @@ -1,313 +1,62 @@ import { Hono } from 'hono'; import type { Context, Next } from 'hono'; import { OIDCProvider } from '../provider'; -import type { OIDCProviderConfig, AuthorizationRequest, TokenRequest } from '../types'; -import { AuthManager, type PasswordValidator } from '../auth'; - -/** - * OIDC Provider配置选项 - */ -export interface OIDCHonoOptions { - /** OIDC Provider配置 */ - config: OIDCProviderConfig; - /** 密码验证器 - 用于验证用户名和密码 */ - passwordValidator: PasswordValidator; - /** 认证配置选项 */ - authConfig?: { - /** 会话TTL(秒) */ - sessionTTL?: number; - /** 登录页面标题 */ - loginPageTitle?: string; - /** 品牌名称 */ - brandName?: string; - /** 品牌Logo URL */ - logoUrl?: string; - }; -} +import type { OIDCProviderConfig } from '../types'; /** * 创建OIDC Provider Hono应用 */ -export function createOIDCProvider(options: OIDCHonoOptions): Hono { - const { config, passwordValidator, authConfig = {} } = options; - - // 创建认证管理器 - const authManager = new AuthManager( - config, - passwordValidator, - { - sessionTTL: authConfig.sessionTTL, - pageConfig: { - title: authConfig.loginPageTitle, - brandName: authConfig.brandName, - logoUrl: authConfig.logoUrl - } - } - ); - +export function createOIDCProvider(config: OIDCProviderConfig): Hono { const app = new Hono(); const provider = new OIDCProvider(config); // 登录端点 app.post('/login', async (c: Context) => { - // 从表单中提取授权参数 - const formData = await c.req.formData(); - const authRequest = { - response_type: formData.get('response_type')?.toString() || '', - client_id: formData.get('client_id')?.toString() || '', - redirect_uri: formData.get('redirect_uri')?.toString() || '', - scope: formData.get('scope')?.toString() || '', - state: formData.get('state')?.toString(), - nonce: formData.get('nonce')?.toString(), - code_challenge: formData.get('code_challenge')?.toString(), - code_challenge_method: formData.get('code_challenge_method')?.toString() as 'plain' | 'S256' | undefined, - }; - - return await authManager.handleLogin(c, authRequest); + return await provider.handleLogin(c); }); // 登出端点 app.get('/logout', async (c: Context) => { - return await authManager.logout(c); + return await provider.handleLogout(c); }); app.post('/logout', async (c: Context) => { - return await authManager.logout(c); + return await provider.handleLogout(c); }); - // 发现文档端点 - app.get('/.well-known/openid-configuration', async (c: Context) => { - const discovery = provider.getDiscoveryDocument(); - return c.json(discovery); + // 发现文档端点 - 直接调用核心方法 + app.get('/.well-known/openid-configuration', (c: Context) => { + return c.json(provider.getDiscoveryDocument()); }); - // JWKS端点 + // JWKS端点 - 直接调用核心方法 app.get('/.well-known/jwks.json', async (c: Context) => { - const jwks = await provider.getJWKS(); - return c.json(jwks); + return c.json(await provider.getJWKS()); }); - // 授权端点 - 使用认证管理器 + // 授权端点 app.get('/auth', async (c: Context) => { - const query = c.req.query(); - - const authRequest: AuthorizationRequest = { - response_type: query.response_type || '', - client_id: query.client_id || '', - redirect_uri: query.redirect_uri || '', - scope: query.scope || '', - state: query.state, - nonce: query.nonce, - code_challenge: query.code_challenge, - code_challenge_method: query.code_challenge_method as 'plain' | 'S256' | undefined, - prompt: query.prompt, - max_age: query.max_age ? parseInt(query.max_age) : undefined, - id_token_hint: query.id_token_hint, - login_hint: query.login_hint, - acr_values: query.acr_values, - }; - - // 检查用户认证状态 - const userId = await authManager.getCurrentUser(c); - if (!userId) { - // 用户未认证,显示登录页面 - return await authManager.handleAuthenticationRequired(c, authRequest); - } - - // 用户已认证,处理授权请求 - const result = await provider.handleAuthorizationRequest(authRequest, userId); - - if (!result.success) { - const error = result.error; - const errorParams = new URLSearchParams({ - error: error.error, - ...(error.error_description && { error_description: error.error_description }), - ...(error.error_uri && { error_uri: error.error_uri }), - ...(error.state && { state: error.state }), - }); - - const redirectUri = result.redirectUri || query.redirect_uri; - if (redirectUri) { - return c.redirect(`${redirectUri}?${errorParams.toString()}`); - } else { - c.status(400); - return c.json({ - error: error.error, - error_description: error.error_description, - }); - } - } - - // 成功生成授权码,重定向回客户端 - const params = new URLSearchParams({ - code: result.code, - ...(query.state && { state: query.state }), - }); - - return c.redirect(`${result.redirectUri}?${params.toString()}`); + return await provider.handleAuthorization(c); }); // 令牌端点 app.post('/token', async (c: Context) => { - const body = await c.req.formData(); - // 将可选字段的类型处理为可选的而不是undefined - const clientId = body.get('client_id')?.toString(); - const tokenRequest: TokenRequest = { - grant_type: body.get('grant_type')?.toString() || '', - client_id: clientId || '', - }; - // 可选参数,只有存在时才添加 - const code = body.get('code')?.toString(); - if (code) tokenRequest.code = code; - - const redirectUri = body.get('redirect_uri')?.toString(); - if (redirectUri) tokenRequest.redirect_uri = redirectUri; - - const clientSecret = body.get('client_secret')?.toString(); - if (clientSecret) tokenRequest.client_secret = clientSecret; - - const refreshToken = body.get('refresh_token')?.toString(); - if (refreshToken) tokenRequest.refresh_token = refreshToken; - - const codeVerifier = body.get('code_verifier')?.toString(); - if (codeVerifier) tokenRequest.code_verifier = codeVerifier; - - const scope = body.get('scope')?.toString(); - if (scope) tokenRequest.scope = scope; - - // 客户端认证 - const authHeader = c.req.header('Authorization'); - if (authHeader?.startsWith('Basic ')) { - const decoded = atob(authHeader.substring(6)); - const [headerClientId, headerClientSecret] = decoded.split(':'); - if (headerClientId) { - tokenRequest.client_id = headerClientId; - } - if (headerClientSecret) { - tokenRequest.client_secret = headerClientSecret; - } - } - - // 请求令牌 - const result = await provider.handleTokenRequest(tokenRequest); - - if (!result.success) { - c.status(400); - return c.json({ - error: result.error.error, - error_description: result.error.error_description, - }); - } - - return c.json(result.response); + return await provider.handleToken(c); }); // 用户信息端点 app.get('/userinfo', async (c: Context) => { - const authHeader = c.req.header('Authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - c.status(401); - c.header('WWW-Authenticate', 'Bearer'); - return c.json({ - error: 'invalid_token', - error_description: '无效的访问令牌', - }); - } - - const accessToken = authHeader.substring(7); - const result = await provider.getUserInfo(accessToken); - - if (!result.success) { - c.status(401); - c.header('WWW-Authenticate', `Bearer error="${result.error.error}"`); - return c.json({ - error: result.error.error, - error_description: result.error.error_description, - }); - } - - return c.json(result.user); + return await provider.handleUserInfo(c); }); // 令牌撤销端点 app.post('/revoke', async (c: Context) => { - const body = await c.req.formData(); - - const token = body.get('token')?.toString() || ''; - const tokenTypeHint = body.get('token_type_hint')?.toString(); - const clientId = body.get('client_id')?.toString(); - const clientSecret = body.get('client_secret')?.toString(); - - if (!token) { - c.status(400); - return c.json({ - error: 'invalid_request', - error_description: '缺少token参数', - }); - } - - // 客户端认证 - let authClientId = clientId; - let authClientSecret = clientSecret; - - const authHeader = c.req.header('Authorization'); - if (authHeader?.startsWith('Basic ')) { - const decoded = atob(authHeader.substring(6)); - const [id, secret] = decoded.split(':'); - authClientId = id; - authClientSecret = secret; - } - - // 撤销令牌 - const result = await provider.revokeToken(token, tokenTypeHint); - - if (!result.success && result.error) { - c.status(400); - return c.json({ - error: result.error.error, - error_description: result.error.error_description, - }); - } - - // 撤销成功 - c.status(200); - return c.body(null); + return await provider.handleRevoke(c); }); // 令牌内省端点 app.post('/introspect', async (c: Context) => { - const body = await c.req.formData(); - - const token = body.get('token')?.toString() || ''; - const tokenTypeHint = body.get('token_type_hint')?.toString(); - const clientId = body.get('client_id')?.toString(); - const clientSecret = body.get('client_secret')?.toString(); - - if (!token) { - c.status(400); - return c.json({ - error: 'invalid_request', - error_description: '缺少token参数', - }); - } - - // 客户端认证 - let authClientId = clientId; - let authClientSecret = clientSecret; - - const authHeader = c.req.header('Authorization'); - if (authHeader?.startsWith('Basic ')) { - const decoded = atob(authHeader.substring(6)); - const [id, secret] = decoded.split(':'); - authClientId = id; - authClientSecret = secret; - } - - // 内省令牌 - const result = await provider.introspectToken(token); - - // 返回内省结果 - return c.json(result); + return await provider.handleIntrospect(c); }); // 返回应用实例 diff --git a/packages/oidc-provider/src/provider.ts b/packages/oidc-provider/src/provider.ts index dfc99cf..f0a81ab 100644 --- a/packages/oidc-provider/src/provider.ts +++ b/packages/oidc-provider/src/provider.ts @@ -1,4 +1,5 @@ import { nanoid } from 'nanoid'; +import type { Context } from 'hono'; import type { OIDCProviderConfig, OIDCClient, @@ -12,18 +13,21 @@ import type { TokenResponse, OIDCError, DiscoveryDocument, + PasswordValidator, } from './types'; import type { StorageAdapter } from './storage/adapter'; import { TokenManager } from './auth/token-manager'; import { JWTUtils } from './utils/jwt'; import { PKCEUtils } from './utils/pkce'; import { ValidationUtils } from './utils/validation'; +import { PasswordAuth } from './auth'; +import { OIDCErrorFactory } from './errors'; /** * OIDC Provider核心类 */ export class OIDCProvider { - private config: OIDCProviderConfig & { + private readonly config: OIDCProviderConfig & { tokenTTL: { accessToken: number; refreshToken: number; @@ -38,26 +42,32 @@ export class OIDCProvider { requirePKCE: boolean; rotateRefreshTokens: boolean; }; - private storage: StorageAdapter; - private tokenManager: TokenManager; - private jwtUtils: JWTUtils; - private findUser: (userId: string) => Promise; - private findClient: (clientId: string) => Promise; + private readonly storage: StorageAdapter; + private readonly tokenManager: TokenManager; + private readonly jwtUtils: JWTUtils; + private readonly findUser: (userId: string) => Promise; + private readonly findClient: (clientId: string) => Promise; + private passwordAuth: PasswordAuth; constructor(config: OIDCProviderConfig) { - // 设置默认配置 - const defaultTokenTTL = { - accessToken: 3600, // 1小时 - refreshToken: 30 * 24 * 3600, // 30天 - authorizationCode: 600, // 10分钟 - idToken: 3600, // 1小时 - }; + this.config = this.normalizeConfig(config); + this.storage = config.storage; + this.tokenManager = new TokenManager(config.storage); + this.findUser = config.findUser; + this.findClient = config.findClient; + this.jwtUtils = new JWTUtils(config.signingKey, config.signingAlgorithm); + this.passwordAuth = new PasswordAuth(this.config, config.authConfig.passwordValidator, config.authConfig); + } - this.config = { + private normalizeConfig(config: OIDCProviderConfig) { + return { ...config, tokenTTL: { - ...defaultTokenTTL, - ...config.tokenTTL, + accessToken: 3600, + refreshToken: 30 * 24 * 3600, + authorizationCode: 600, + idToken: 3600, + ...(config.tokenTTL || {}), }, responseTypes: config.responseTypes || ['code', 'id_token', 'token', 'code id_token', 'code token', 'id_token token', 'code id_token token'], grantTypes: config.grantTypes || ['authorization_code', 'refresh_token', 'implicit'], @@ -72,20 +82,83 @@ export class OIDCProvider { requirePKCE: config.requirePKCE ?? false, rotateRefreshTokens: config.rotateRefreshTokens ?? true, }; - - this.storage = config.storage; - this.tokenManager = new TokenManager(config.storage); - this.findUser = config.findUser; - this.findClient = config.findClient; - this.jwtUtils = new JWTUtils(config.signingKey, config.signingAlgorithm); } - /** - * 获取发现文档 - */ + // 统一的令牌查找和验证 + private async findTokenData(token: string): Promise<{ + accessToken?: AccessToken | null; + refreshToken?: RefreshToken | null; + tokenData?: AccessToken | RefreshToken | null; + isExpired: boolean; + }> { + const [accessToken, refreshToken] = await Promise.all([ + this.tokenManager.getAccessToken(token), + this.tokenManager.getRefreshToken(token) + ]); + + const tokenData = accessToken || refreshToken; + const isExpired = tokenData ? tokenData.expires_at < new Date() : false; + + if (isExpired && tokenData) { + await Promise.all([ + accessToken ? this.tokenManager.deleteAccessToken(token) : Promise.resolve(), + refreshToken ? this.tokenManager.deleteRefreshToken(token) : Promise.resolve(), + ]); + } + + return { accessToken, refreshToken, tokenData, isExpired }; + } + + private parseAuthorizationRequest(query: Record): AuthorizationRequest { + return { + response_type: query.response_type || '', + client_id: query.client_id || '', + redirect_uri: query.redirect_uri || '', + scope: query.scope || '', + state: query.state, + nonce: query.nonce, + code_challenge: query.code_challenge, + code_challenge_method: query.code_challenge_method as 'plain' | 'S256' | undefined, + prompt: query.prompt, + max_age: query.max_age ? parseInt(query.max_age, 10) : undefined, + id_token_hint: query.id_token_hint, + login_hint: query.login_hint, + acr_values: query.acr_values, + }; + } + + private parseTokenRequest(body: FormData, authHeader?: string): TokenRequest { + const tokenRequest: any = { + grant_type: body.get('grant_type')?.toString() || '', + client_id: body.get('client_id')?.toString() || '' + }; + + ['code', 'redirect_uri', 'client_secret', 'refresh_token', 'code_verifier', 'scope'] + .forEach(field => { + const value = body.get(field)?.toString(); + if (value) tokenRequest[field] = value; + }); + + // 处理Basic认证 + if (authHeader?.startsWith('Basic ')) { + try { + const [id, secret] = atob(authHeader.substring(6)).split(':'); + if (id) tokenRequest.client_id = id; + if (secret) tokenRequest.client_secret = secret; + } catch { + // 忽略无效的Basic认证头 + } + } + + return tokenRequest; + } + + configurePasswordAuth(passwordValidator: PasswordValidator, authConfig: OIDCProviderConfig['authConfig']): void { + this.passwordAuth = new PasswordAuth(this.config, passwordValidator, authConfig); + } + getDiscoveryDocument(): DiscoveryDocument { const baseUrl = this.config.issuer; - return { issuer: this.config.issuer, authorization_endpoint: `${baseUrl}/auth`, @@ -112,428 +185,231 @@ export class OIDCProvider { }; } - /** - * 处理授权请求 - */ async handleAuthorizationRequest( request: Partial, userId?: string ): Promise<{ success: true; code: string; redirectUri: string } | { success: false; error: OIDCError; redirectUri?: string }> { try { - // 获取客户端信息 - if (!request.client_id) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Missing client_id parameter', - state: request.state, - }, - }; - } + const validationResult = await this.validateAuthorizationRequest(request, userId); + if (!validationResult.success) return validationResult; - const client = await this.findClient(request.client_id); - if (!client) { - return { - success: false, - error: { - error: 'invalid_client', - error_description: 'Invalid client_id', - state: request.state, - }, - }; - } - - // 验证请求 - const validation = ValidationUtils.validateAuthorizationRequest( - request, - client, - this.config.scopes, - this.config.responseTypes - ); - - if (!validation.valid) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: validation.errors.join(', '), - state: request.state, - }, - redirectUri: request.redirect_uri, - }; - } - - // 检查是否需要用户认证 - if (!userId) { - return { - success: false, - error: { - error: 'login_required', - error_description: 'User authentication is required', - state: request.state, - }, - redirectUri: request.redirect_uri, - }; - } - - // 强制PKCE检查 - if (this.config.requirePKCE && client.client_type === 'public' && !request.code_challenge) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'PKCE is required for public clients', - state: request.state, - }, - redirectUri: request.redirect_uri, - }; - } - - // 验证PKCE(如果启用且提供了代码挑战) - if (this.config.enablePKCE && request.code_challenge) { - if (!request.code_challenge_method) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Missing code_challenge_method', - state: request.state, - }, - redirectUri: request.redirect_uri, - }; - } - - if (!PKCEUtils.isSupportedMethod(request.code_challenge_method)) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Unsupported code_challenge_method', - state: request.state, - }, - redirectUri: request.redirect_uri, - }; - } - - if (!PKCEUtils.isValidCodeChallenge(request.code_challenge, request.code_challenge_method)) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Invalid code_challenge', - state: request.state, - }, - redirectUri: request.redirect_uri, - }; - } - } - - // 生成授权码 const code = nanoid(32); const now = new Date(); - const expiresAt = new Date(now.getTime() + this.config.tokenTTL.authorizationCode * 1000); const authCode: AuthorizationCode = { code, - client_id: request.client_id, - user_id: userId, + client_id: request.client_id!, + user_id: userId!, redirect_uri: request.redirect_uri!, scope: request.scope!, code_challenge: request.code_challenge, code_challenge_method: request.code_challenge_method, nonce: request.nonce, state: request.state, - expires_at: expiresAt, + expires_at: new Date(now.getTime() + this.config.tokenTTL.authorizationCode * 1000), created_at: now, }; await this.tokenManager.storeAuthorizationCode(authCode); - - return { - success: true, - code, - redirectUri: request.redirect_uri!, - }; + return { success: true, code, redirectUri: request.redirect_uri! }; } catch (error) { - return { - success: false, - error: { - error: 'server_error', - error_description: 'Internal server error', - state: request.state, - }, - redirectUri: request.redirect_uri, - }; + return { ...OIDCErrorFactory.serverError(request.state), redirectUri: request.redirect_uri }; } } - /** - * 处理令牌请求 - */ + private async validateAuthorizationRequest( + request: Partial, + userId?: string + ): Promise<{ success: true; client: OIDCClient } | { success: false; error: OIDCError; redirectUri?: string }> { + if (!request.client_id) { + return { + success: false, + error: { error: 'invalid_request', error_description: 'Missing client_id parameter', state: request.state }, + redirectUri: request.redirect_uri, + }; + } + + const client = await this.findClient(request.client_id); + if (!client) { + return { + success: false, + error: { error: 'invalid_client', error_description: 'Invalid client_id', state: request.state }, + redirectUri: request.redirect_uri, + }; + } + + const validation = ValidationUtils.validateAuthorizationRequest( + request, client, this.config.scopes, this.config.responseTypes + ); + if (!validation.valid) { + return { + success: false, + error: { error: 'invalid_request', error_description: validation.errors.join(', '), state: request.state }, + redirectUri: request.redirect_uri, + }; + } + + if (!userId) { + return { + success: false, + error: { error: 'login_required', error_description: 'User authentication is required', state: request.state }, + redirectUri: request.redirect_uri, + }; + } + + const pkceError = this.validatePKCE(request, client); + if (pkceError) return { success: false, error: pkceError, redirectUri: request.redirect_uri }; + + return { success: true, client }; + } + + private validatePKCE(request: Partial, client: OIDCClient): OIDCError | null { + if (this.config.requirePKCE && client.client_type === 'public' && !request.code_challenge) { + return { error: 'invalid_request', error_description: 'PKCE is required for public clients', state: request.state }; + } + + if (this.config.enablePKCE && request.code_challenge) { + if (!request.code_challenge_method) { + return { error: 'invalid_request', error_description: 'Missing code_challenge_method', state: request.state }; + } + if (!PKCEUtils.isSupportedMethod(request.code_challenge_method)) { + return { error: 'invalid_request', error_description: 'Unsupported code_challenge_method', state: request.state }; + } + if (!PKCEUtils.isValidCodeChallenge(request.code_challenge, request.code_challenge_method)) { + return { error: 'invalid_request', error_description: 'Invalid code_challenge', state: request.state }; + } + } + return null; + } + async handleTokenRequest( request: Partial ): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> { try { - // 获取客户端信息 - if (!request.client_id) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Missing client_id parameter', - }, - }; - } + const validationResult = await this.validateTokenRequest(request); + if (!validationResult.success) return validationResult; - const client = await this.findClient(request.client_id); - if (!client) { - return { - success: false, - error: { - error: 'invalid_client', - error_description: 'Invalid client_id', - }, - }; - } - - // 验证客户端认证(如果需要) - if (client.client_type === 'confidential') { - if (!request.client_secret) { - return { - success: false, - error: { - error: 'invalid_client', - error_description: 'Client authentication required', - }, - }; - } - - if (request.client_secret !== client.client_secret) { - return { - success: false, - error: { - error: 'invalid_client', - error_description: 'Invalid client credentials', - }, - }; - } - } - - // 验证请求 - const validation = ValidationUtils.validateTokenRequest( - request, - client, - this.config.grantTypes - ); - - if (!validation.valid) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: validation.errors.join(', '), - }, - }; - } + const { client } = validationResult; if (request.grant_type === 'authorization_code') { return await this.handleAuthorizationCodeGrant(request, client); } else if (request.grant_type === 'refresh_token') { return await this.handleRefreshTokenGrant(request, client); } else { - return { - success: false, - error: { - error: 'unsupported_grant_type', - error_description: 'Grant type not supported', - }, - }; + return OIDCErrorFactory.unsupportedGrantType(); } } catch (error) { - return { - success: false, - error: { - error: 'server_error', - error_description: 'Internal server error', - }, - }; + return OIDCErrorFactory.createTokenError('server_error', 'Internal server error'); } } - /** - * 处理授权码授权类型 - */ + private async validateTokenRequest( + request: Partial + ): Promise<{ success: true; client: OIDCClient } | { success: false; error: OIDCError }> { + if (!request.client_id) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing client_id parameter'); + + const client = await this.findClient(request.client_id); + if (!client) return OIDCErrorFactory.invalidClient('Invalid client_id'); + + // 客户端认证 + if (client.client_type === 'confidential') { + if (!request.client_secret) return OIDCErrorFactory.invalidClient('Client authentication required'); + if (request.client_secret !== client.client_secret) return OIDCErrorFactory.invalidClient('Invalid client credentials'); + } + + const validation = ValidationUtils.validateTokenRequest(request, client, this.config.grantTypes); + if (!validation.valid) return OIDCErrorFactory.createTokenError('invalid_request', validation.errors.join(', ')); + + return { success: true, client }; + } + private async handleAuthorizationCodeGrant( request: Partial, client: OIDCClient ): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> { - if (!request.code) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Missing authorization code', - }, - }; - } + if (!request.code) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing authorization code'); - // 获取授权码 const authCode = await this.tokenManager.getAuthorizationCode(request.code); - if (!authCode) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Invalid authorization code', - }, - }; - } + if (!authCode) return OIDCErrorFactory.invalidGrant('Invalid authorization code'); - // 检查授权码是否过期 if (authCode.expires_at < new Date()) { await this.tokenManager.deleteAuthorizationCode(request.code); - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Authorization code expired', - }, - }; + return OIDCErrorFactory.invalidGrant('Authorization code expired'); } - // 验证客户端ID - if (authCode.client_id !== request.client_id) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Authorization code was not issued to this client', - }, - }; - } + if (authCode.client_id !== request.client_id) return OIDCErrorFactory.invalidGrant('Authorization code was not issued to this client'); + if (authCode.redirect_uri !== request.redirect_uri) return OIDCErrorFactory.invalidGrant('Redirect URI mismatch'); - // 验证重定向URI - if (authCode.redirect_uri !== request.redirect_uri) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Redirect URI mismatch', - }, - }; - } - - // 验证PKCE(如果使用) + // PKCE验证 if (authCode.code_challenge) { - if (!request.code_verifier) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Missing code_verifier', - }, - }; - } - - const isValidPKCE = PKCEUtils.verifyCodeChallenge( - request.code_verifier, - authCode.code_challenge, - authCode.code_challenge_method || 'plain' - ); - - if (!isValidPKCE) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Invalid code_verifier', - }, - }; + if (!request.code_verifier) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing code_verifier'); + if (!PKCEUtils.verifyCodeChallenge(request.code_verifier, authCode.code_challenge, authCode.code_challenge_method || 'plain')) { + return OIDCErrorFactory.invalidGrant('Invalid code_verifier'); } } - // 获取用户信息 const user = await this.findUser(authCode.user_id); - if (!user) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'User not found', - }, - }; - } + if (!user) return OIDCErrorFactory.invalidGrant('User not found'); - // 生成令牌 const now = new Date(); - const accessTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000); - const refreshTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.refreshToken * 1000); - const idTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.idToken * 1000); - - // 生成访问令牌 const accessTokenJWT = await this.jwtUtils.generateAccessToken({ issuer: this.config.issuer, subject: user.sub, - audience: request.client_id!, - clientId: request.client_id!, + audience: client.client_id, + clientId: client.client_id, scope: authCode.scope, expiresIn: this.config.tokenTTL.accessToken, }); - const accessToken: AccessToken = { - token: accessTokenJWT, - client_id: request.client_id!, - user_id: user.sub, - scope: authCode.scope, - expires_at: accessTokenExpiry, - created_at: now, - }; - - // 生成刷新令牌 const refreshTokenValue = nanoid(64); - const refreshToken: RefreshToken = { - token: refreshTokenValue, - client_id: request.client_id!, - user_id: user.sub, - scope: authCode.scope, - expires_at: refreshTokenExpiry, - created_at: now, - }; - // 生成ID令牌(如果请求了openid作用域) let idTokenJWT: string | undefined; if (authCode.scope.includes('openid')) { const requestedClaims = this.getRequestedClaims(authCode.scope); - idTokenJWT = await this.jwtUtils.generateIDToken({ issuer: this.config.issuer, subject: user.sub, - audience: request.client_id!, + audience: client.client_id, user, authTime: Math.floor(now.getTime() / 1000), nonce: authCode.nonce, expiresIn: this.config.tokenTTL.idToken, requestedClaims, }); - - const idToken: IDToken = { - token: idTokenJWT, - client_id: request.client_id!, - user_id: user.sub, - nonce: authCode.nonce, - expires_at: idTokenExpiry, - created_at: now, - }; - - await this.tokenManager.storeIDToken(idToken); } // 存储令牌 - await this.tokenManager.storeAccessToken(accessToken); - await this.tokenManager.storeRefreshToken(refreshToken); + const accessTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000); + const refreshTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.refreshToken * 1000); + const idTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.idToken * 1000); + + await Promise.all([ + this.tokenManager.storeAccessToken({ + token: accessTokenJWT, + client_id: client.client_id, + user_id: user.sub, + scope: authCode.scope, + expires_at: accessTokenExpiry, + created_at: now, + }), + this.tokenManager.storeRefreshToken({ + token: refreshTokenValue, + client_id: client.client_id, + user_id: user.sub, + scope: authCode.scope, + expires_at: refreshTokenExpiry, + created_at: now, + }), + ...(idTokenJWT ? [this.tokenManager.storeIDToken({ + token: idTokenJWT, + client_id: client.client_id, + user_id: user.sub, + nonce: undefined, + expires_at: idTokenExpiry, + created_at: now, + })] : []) + ]); - // 删除已使用的授权码 await this.tokenManager.deleteAuthorizationCode(request.code); const response: TokenResponse = { @@ -544,103 +420,41 @@ export class OIDCProvider { scope: authCode.scope, }; - if (idTokenJWT) { - response.id_token = idTokenJWT; - } - - return { - success: true, - response, - }; + if (idTokenJWT) response.id_token = idTokenJWT; + return { success: true, response }; } - /** - * 处理刷新令牌授权类型 - 修复以符合OIDC规范 - */ private async handleRefreshTokenGrant( request: Partial, client: OIDCClient ): Promise<{ success: true; response: TokenResponse } | { success: false; error: OIDCError }> { - if (!request.refresh_token) { - return { - success: false, - error: { - error: 'invalid_request', - error_description: 'Missing refresh_token', - }, - }; - } + if (!request.refresh_token) return OIDCErrorFactory.createTokenError('invalid_request', 'Missing refresh_token'); - // 获取刷新令牌 const refreshToken = await this.tokenManager.getRefreshToken(request.refresh_token); - if (!refreshToken) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Invalid refresh_token', - }, - }; - } + if (!refreshToken) return OIDCErrorFactory.invalidGrant('Invalid refresh_token'); - // 检查刷新令牌是否过期 if (refreshToken.expires_at < new Date()) { await this.tokenManager.deleteRefreshToken(request.refresh_token); - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Refresh token expired', - }, - }; + return OIDCErrorFactory.invalidGrant('Refresh token expired'); } - // 验证客户端ID - if (refreshToken.client_id !== request.client_id) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'Refresh token was not issued to this client', - }, - }; - } + if (refreshToken.client_id !== request.client_id) return OIDCErrorFactory.invalidGrant('Refresh token was not issued to this client'); - // 获取用户信息 const user = await this.findUser(refreshToken.user_id); - if (!user) { - return { - success: false, - error: { - error: 'invalid_grant', - error_description: 'User not found', - }, - }; - } + if (!user) return OIDCErrorFactory.invalidGrant('User not found'); - // 确定作用域(使用请求的作用域或原始作用域) + // 处理作用域 let scope = refreshToken.scope; if (request.scope) { const requestedScopes = request.scope.split(' '); const originalScopes = refreshToken.scope.split(' '); - - // 请求的作用域不能超过原始作用域 if (!requestedScopes.every(s => originalScopes.includes(s))) { - return { - success: false, - error: { - error: 'invalid_scope', - error_description: 'Requested scope exceeds original scope', - }, - }; + return OIDCErrorFactory.invalidScope('Requested scope exceeds original scope'); } scope = request.scope; } - // 生成新的访问令牌 const now = new Date(); - const accessTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000); - const accessTokenJWT = await this.jwtUtils.generateAccessToken({ issuer: this.config.issuer, subject: user.sub, @@ -650,36 +464,14 @@ export class OIDCProvider { expiresIn: this.config.tokenTTL.accessToken, }); - const accessToken: AccessToken = { + await this.tokenManager.storeAccessToken({ token: accessTokenJWT, client_id: request.client_id!, user_id: user.sub, scope, - expires_at: accessTokenExpiry, + expires_at: new Date(now.getTime() + this.config.tokenTTL.accessToken * 1000), created_at: now, - }; - - await this.tokenManager.storeAccessToken(accessToken); - - // 可选:生成新的刷新令牌(刷新令牌轮换) - let newRefreshToken: string | undefined; - if (this.config.rotateRefreshTokens !== false) { // 默认启用刷新令牌轮换 - const refreshTokenExpiry = new Date(now.getTime() + this.config.tokenTTL.refreshToken * 1000); - newRefreshToken = nanoid(64); - - const newRefreshTokenRecord: RefreshToken = { - token: newRefreshToken, - client_id: request.client_id!, - user_id: user.sub, - scope, - expires_at: refreshTokenExpiry, - created_at: now, - }; - - await this.tokenManager.storeRefreshToken(newRefreshTokenRecord); - // 删除旧的刷新令牌 - await this.tokenManager.deleteRefreshToken(request.refresh_token); - } + }); const response: TokenResponse = { access_token: accessTokenJWT, @@ -688,245 +480,246 @@ export class OIDCProvider { scope, }; - if (newRefreshToken) { + // 刷新令牌轮换 + if (this.config.rotateRefreshTokens) { + const newRefreshToken = nanoid(64); + await this.tokenManager.storeRefreshToken({ + token: newRefreshToken, + client_id: request.client_id!, + user_id: user.sub, + scope, + expires_at: new Date(now.getTime() + this.config.tokenTTL.refreshToken * 1000), + created_at: now, + }); + await this.tokenManager.deleteRefreshToken(request.refresh_token); response.refresh_token = newRefreshToken; } - return { - success: true, - response, - }; + return { success: true, response }; } - /** - * 获取用户信息 - */ async getUserInfo(accessToken: string): Promise<{ success: true; user: Partial } | { success: false; error: OIDCError }> { try { - // 验证访问令牌 const payload = await this.jwtUtils.verifyToken(accessToken); - - // 检查令牌类型 if ((payload as any).token_type !== 'access_token') { - return { - success: false, - error: { - error: 'invalid_token', - error_description: 'Invalid token type', - }, - }; + return { success: false, error: OIDCErrorFactory.invalidToken('Invalid token type') }; } - // 获取令牌数据 const tokenData = await this.tokenManager.getAccessToken(accessToken); - if (!tokenData) { - return { - success: false, - error: { - error: 'invalid_token', - error_description: 'Token not found', - }, - }; - } + if (!tokenData) return { success: false, error: OIDCErrorFactory.invalidToken('Token not found') }; - // 检查令牌是否过期 if (tokenData.expires_at < new Date()) { await this.tokenManager.deleteAccessToken(accessToken); - return { - success: false, - error: { - error: 'invalid_token', - error_description: 'Token expired', - }, - }; + return { success: false, error: OIDCErrorFactory.invalidToken('Token expired') }; } - // 获取用户信息 const user = await this.findUser(tokenData.user_id); - if (!user) { - return { - success: false, - error: { - error: 'invalid_token', - error_description: 'User not found', - }, - }; - } + if (!user) return { success: false, error: OIDCErrorFactory.invalidToken('User not found') }; - // 获取请求的声明 const requestedClaims = this.getRequestedClaims(tokenData.scope); const filteredUser = this.filterUserClaims(user, requestedClaims); - return { - success: true, - user: filteredUser, - }; + return { success: true, user: filteredUser }; } catch (error) { - return { - success: false, - error: { - error: 'invalid_token', - error_description: 'Invalid token', - }, - }; + return { success: false, error: OIDCErrorFactory.invalidToken() }; } } - /** - * 根据作用域获取请求的声明 - */ private getRequestedClaims(scope: string): string[] { const scopes = scope.split(' '); - const claims: string[] = ['sub']; // 总是包含sub + const claims = ['sub']; - if (scopes.includes('profile')) { - claims.push( - 'name', 'family_name', 'given_name', 'middle_name', 'nickname', - 'preferred_username', 'profile', 'picture', 'website', 'gender', - 'birthdate', 'zoneinfo', 'locale', 'updated_at' - ); - } + const scopeClaimsMap = { + profile: ['name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at'], + email: ['email', 'email_verified'], + phone: ['phone_number', 'phone_number_verified'], + address: ['address'], + }; - if (scopes.includes('email')) { - claims.push('email', 'email_verified'); - } - - if (scopes.includes('phone')) { - claims.push('phone_number', 'phone_number_verified'); - } - - if (scopes.includes('address')) { - claims.push('address'); - } + scopes.forEach(scope => { + if (scope in scopeClaimsMap) { + claims.push(...scopeClaimsMap[scope as keyof typeof scopeClaimsMap]); + } + }); return claims; } - /** - * 根据请求的声明过滤用户信息 - */ private filterUserClaims(user: OIDCUser, requestedClaims: string[]): Partial { - const filtered: Partial = {}; - - for (const claim of requestedClaims) { - if (claim in user && user[claim as keyof OIDCUser] !== undefined) { - (filtered as any)[claim] = user[claim as keyof OIDCUser]; - } - } - - return filtered; + return Object.fromEntries( + requestedClaims + .filter(claim => claim in user && user[claim as keyof OIDCUser] !== undefined) + .map(claim => [claim, user[claim as keyof OIDCUser]]) + ) as Partial; } - /** - * 撤销令牌 - */ - async revokeToken(token: string, tokenTypeHint?: string): Promise<{ success: boolean; error?: OIDCError }> { + async revokeToken(token: string): Promise<{ success: boolean; error?: OIDCError }> { try { - // 尝试作为访问令牌撤销 - const accessToken = await this.tokenManager.getAccessToken(token); + const { accessToken, refreshToken } = await this.findTokenData(token); + if (accessToken) { await this.tokenManager.deleteAccessToken(token); - return { success: true }; + } else if (refreshToken) { + await Promise.all([ + this.tokenManager.deleteRefreshToken(token), + this.tokenManager.deleteAccessTokensByUserAndClient(refreshToken.user_id, refreshToken.client_id) + ]); } - // 尝试作为刷新令牌撤销 - const refreshToken = await this.tokenManager.getRefreshToken(token); - if (refreshToken) { - await this.tokenManager.deleteRefreshToken(token); - // 同时撤销相关的访问令牌 - await this.tokenManager.deleteAccessTokensByUserAndClient(refreshToken.user_id, refreshToken.client_id); - return { success: true }; - } - - // 令牌不存在,根据RFC 7009,这应该返回成功 return { success: true }; } catch (error) { - return { - success: false, - error: { - error: 'server_error', - error_description: 'Internal server error', - }, - }; + return { success: false, error: OIDCErrorFactory.createSimpleError('server_error', 'Internal server error') }; } } - /** - * 内省令牌 - */ - async introspectToken(token: string): Promise<{ - active: boolean; - scope?: string; - client_id?: string; - username?: string; - token_type?: string; - exp?: number; - iat?: number; - sub?: string; - aud?: string; - iss?: string; - }> { + async introspectToken(token: string) { try { - // 尝试作为访问令牌内省 - const accessToken = await this.tokenManager.getAccessToken(token); - if (accessToken) { - const isExpired = accessToken.expires_at < new Date(); - if (isExpired) { - await this.tokenManager.deleteAccessToken(token); - return { active: false }; - } + const { accessToken, refreshToken, tokenData, isExpired } = await this.findTokenData(token); - const user = await this.findUser(accessToken.user_id); + if (!tokenData || isExpired) return { active: false }; - return { - active: true, - scope: accessToken.scope, - client_id: accessToken.client_id, - username: user?.username, - token_type: 'Bearer', - exp: Math.floor(accessToken.expires_at.getTime() / 1000), - iat: Math.floor(accessToken.created_at.getTime() / 1000), - sub: accessToken.user_id, - aud: accessToken.client_id, - iss: this.config.issuer, - }; - } + const user = await this.findUser(tokenData.user_id); - // 尝试作为刷新令牌内省 - const refreshToken = await this.tokenManager.getRefreshToken(token); - if (refreshToken) { - const isExpired = refreshToken.expires_at < new Date(); - if (isExpired) { - await this.tokenManager.deleteRefreshToken(token); - return { active: false }; - } - - const user = await this.findUser(refreshToken.user_id); - - return { - active: true, - scope: refreshToken.scope, - client_id: refreshToken.client_id, - username: user?.username, - token_type: 'refresh_token', - exp: Math.floor(refreshToken.expires_at.getTime() / 1000), - iat: Math.floor(refreshToken.created_at.getTime() / 1000), - sub: refreshToken.user_id, - aud: refreshToken.client_id, - iss: this.config.issuer, - }; - } - - return { active: false }; + return { + active: true, + scope: tokenData.scope, + client_id: tokenData.client_id, + username: user?.username, + token_type: accessToken ? 'Bearer' : 'refresh_token', + exp: Math.floor(tokenData.expires_at.getTime() / 1000), + iat: Math.floor(tokenData.created_at.getTime() / 1000), + sub: tokenData.user_id, + aud: tokenData.client_id, + iss: this.config.issuer, + }; } catch (error) { return { active: false }; } } - /** - * 获取JWKS - */ async getJWKS(): Promise<{ keys: any[] }> { return await this.jwtUtils.generateJWKS(); } -} \ No newline at end of file + + // HTTP处理方法 + async handleLogin(c: Context): Promise { + const formData = await c.req.formData(); + const authRequest = Object.fromEntries( + ['response_type', 'client_id', 'redirect_uri', 'scope', 'state', 'nonce', 'code_challenge', 'code_challenge_method'] + .map(field => [field, formData.get(field)?.toString() || '']) + ) as any; + + try { + const authResult = await this.passwordAuth.authenticate(c); + return authResult.success + ? await this.passwordAuth.handleAuthenticationSuccess(c, authResult) + : await this.passwordAuth.handleAuthenticationFailure(c, authResult, authRequest); + } catch (error) { + console.error('登录流程处理失败:', error); + return await this.passwordAuth.handleAuthenticationFailure( + c, { success: false, error: '服务器内部错误' }, authRequest + ); + } + } + + async handleLogout(c: Context): Promise { + return await this.passwordAuth.logout(c); + } + + async handleAuthorization(c: Context): Promise { + const authRequest = this.parseAuthorizationRequest(c.req.query()); + const userId = await this.passwordAuth.getCurrentUser(c); + + if (!userId) { + return await this.passwordAuth.handleAuthenticationRequired(c, authRequest); + } + + const result = await this.handleAuthorizationRequest(authRequest, userId); + + if (!result.success) { + const errorParams = OIDCErrorFactory.buildErrorResponse(result.error); + const redirectUri = result.redirectUri || authRequest.redirect_uri; + + return redirectUri + ? c.redirect(`${redirectUri}?${errorParams.toString()}`) + : c.json({ + error: result.error.error, + error_description: result.error.error_description || 'Unknown error' + }, 400); + } + + const params = new URLSearchParams(); + params.set('code', result.code); + if (authRequest.state) params.set('state', authRequest.state); + return c.redirect(`${result.redirectUri}?${params.toString()}`); + } + + async handleToken(c: Context): Promise { + const body = await c.req.formData(); + const tokenRequest = this.parseTokenRequest(body, c.req.header('Authorization')); + const result = await this.handleTokenRequest(tokenRequest); + + return result.success + ? c.json(result.response) + : c.json({ + error: result.error.error, + error_description: result.error.error_description || 'Unknown error' + }, 400); + } + + async handleUserInfo(c: Context): Promise { + const authHeader = c.req.header('Authorization'); + if (!authHeader?.startsWith('Bearer ')) { + c.header('WWW-Authenticate', 'Bearer'); + return c.json(OIDCErrorFactory.invalidToken('无效的访问令牌'), 401); + } + + const result = await this.getUserInfo(authHeader.substring(7)); + if (!result.success) { + c.header('WWW-Authenticate', `Bearer error="${result.error.error}"`); + return c.json(result.error, 401); + } + + return c.json(result.user); + } + + async handleRevoke(c: Context): Promise { + const body = await c.req.formData(); + const token = body.get('token')?.toString(); + + if (!token) { + const error = OIDCErrorFactory.invalidRequest('缺少token参数'); + return c.json({ + error: error.error, + error_description: error.error_description || 'Invalid request' + }, 400); + } + + const result = await this.revokeToken(token); + if (!result.success && result.error) { + const error = result.error as OIDCError; + return c.json({ + error: error.error, + error_description: error.error_description || 'Unknown error' + }, 400); + } + + return c.body(null, 200); + } + + async handleIntrospect(c: Context): Promise { + const body = await c.req.formData(); + const token = body.get('token')?.toString(); + + if (!token) { + const error = OIDCErrorFactory.invalidRequest('缺少token参数'); + return c.json({ + error: error.error, + error_description: error.error_description || 'Invalid request' + }, 400); + } + + const result = await this.introspectToken(token); + return c.json(result); + } +} diff --git a/packages/oidc-provider/src/types/index.ts b/packages/oidc-provider/src/types/index.ts index 7432a09..54c70cf 100644 --- a/packages/oidc-provider/src/types/index.ts +++ b/packages/oidc-provider/src/types/index.ts @@ -13,6 +13,24 @@ export interface OIDCProviderConfig { findUser: (userId: string) => Promise; /** 获取客户端的回调函数 */ findClient: (clientId: string) => Promise; + /** 认证配置选项 - 必须配置 */ + authConfig: { + /** 密码验证器 - 用于验证用户名和密码,必须配置 */ + passwordValidator: PasswordValidator; + /** 会话TTL(秒) */ + sessionTTL?: number; + /** 页面配置 */ + pageConfig?: { + /** 登录页面标题 */ + title?: string; + /** 品牌名称 */ + brandName?: string; + /** 品牌Logo URL */ + logoUrl?: string; + }; + /** 记住我功能的最大时长(秒) */ + rememberMeMaxAge?: number; + }; /** 令牌过期时间配置 */ tokenTTL?: { accessToken?: number; // 默认 3600 秒 diff --git a/packages/tus/package.json b/packages/tus/package.json index c0d332c..aa8de5c 100644 --- a/packages/tus/package.json +++ b/packages/tus/package.json @@ -1,10 +1,10 @@ { "name": "@repo/tus", "version": "1.0.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", "private": true, + "exports": { + ".": "./src/index.ts" + }, "scripts": { "build": "tsup", "dev": "tsup --watch", diff --git a/packages/tus/tsup.config.ts b/packages/tus/tsup.config.ts deleted file mode 100644 index 1eacf7a..0000000 --- a/packages/tus/tsup.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/index.ts'], - format: ['cjs', 'esm'], - splitting: false, - sourcemap: true, - clean: false, - dts: true -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b55278a..e26a1ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,9 +47,15 @@ importers: '@repo/oidc-provider': specifier: workspace:* version: link:../../packages/oidc-provider + '@repo/tus': + specifier: workspace:* + version: link:../../packages/tus '@trpc/server': specifier: 11.1.2 version: 11.1.2(typescript@5.8.3) + dayjs: + specifier: ^1.11.12 + version: 1.11.13 hono: specifier: ^4.7.10 version: 4.7.10 @@ -65,9 +71,6 @@ importers: nanoid: specifier: ^5.1.5 version: 5.1.5 - nanoid-cjs: - specifier: ^0.0.7 - version: 0.0.7 node-cron: specifier: ^4.0.7 version: 4.0.7 @@ -77,6 +80,9 @@ importers: superjson: specifier: ^2.2.2 version: 2.2.2 + transliteration: + specifier: ^2.3.5 + version: 2.3.5 valibot: specifier: ^1.1.0 version: 1.1.0(typescript@5.8.3) @@ -98,7 +104,7 @@ importers: version: 7.1.1 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) apps/web: dependencies: @@ -1241,72 +1247,85 @@ packages: resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.1.0': resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.1.0': resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.1.0': resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.1.0': resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.1.0': resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.1.0': resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.2': resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.2': resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.2': resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.2': resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.2': resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.2': resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.2': resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} @@ -1394,24 +1413,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@15.3.2': resolution: {integrity: sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@15.3.2': resolution: {integrity: sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@15.3.2': resolution: {integrity: sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@15.3.2': resolution: {integrity: sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==} @@ -1851,56 +1874,67 @@ packages: resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.41.0': resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.41.0': resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.41.0': resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.41.0': resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.41.0': resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.41.0': resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.41.0': resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.41.0': resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.41.0': resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.41.0': resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.41.0': resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==} @@ -2237,24 +2271,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.11.29': resolution: {integrity: sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.11.29': resolution: {integrity: sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.11.29': resolution: {integrity: sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.11.29': resolution: {integrity: sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==} @@ -2333,24 +2371,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.7': resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.7': resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.7': resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.7': resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} @@ -2856,9 +2898,6 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -3023,9 +3062,6 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - combine-errors@3.0.3: - resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -3139,9 +3175,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - custom-error-instance@2.1.1: - resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} - data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -3291,10 +3324,6 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} - engines: {node: '>=12'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3522,6 +3551,10 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + fast-xml-parser@4.5.3: resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} hasBin: true @@ -4082,9 +4115,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-base64@3.7.7: - resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4185,24 +4215,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -4235,24 +4269,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash._baseiteratee@4.7.0: - resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} - - lodash._basetostring@4.12.0: - resolution: {integrity: sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==} - - lodash._baseuniq@4.6.0: - resolution: {integrity: sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==} - - lodash._createset@4.0.3: - resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==} - - lodash._root@3.0.1: - resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==} - - lodash._stringtopath@4.8.0: - resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -4275,9 +4291,6 @@ packages: lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - lodash.uniqby@4.5.0: - resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -4434,9 +4447,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid-cjs@0.0.7: - resolution: {integrity: sha512-z72crZ0JcTb5s40Pm9Vk99qfEw9Oe1qyVjK/kpelCKyZDH8YTX4HejSfp54PMJT8F5rmsiBpG6wfVAGAhLEFhA==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4768,9 +4778,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - proxy-agent@6.5.0: resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} @@ -4790,9 +4797,6 @@ packages: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4889,9 +4893,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4916,10 +4917,6 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5460,10 +5457,6 @@ packages: resolution: {integrity: sha512-iHuaNcq5GZZnr3XDZNuu2LSyCzAOPwDuo5Qt+q64DfsTP1i3T2bKfxJhni2ZQxsvAoxRbuUK5QetJki4qc5aYA==} hasBin: true - tus-js-client@4.3.1: - resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==} - engines: {node: '>=18'} - tw-animate-css@1.3.0: resolution: {integrity: sha512-jrJ0XenzS9KVuDThJDvnhalbl4IYiMQ/XvpA0a2FL8KmlK+6CSMviO7ROY/I7z1NnUs5NnDhlM6fXmF40xPxzw==} @@ -5571,9 +5564,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -8411,8 +8401,6 @@ snapshots: buffer-crc32@0.2.13: {} - buffer-from@1.1.2: {} - buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -8598,11 +8586,6 @@ snapshots: color-string: 1.9.1 optional: true - combine-errors@3.0.3: - dependencies: - custom-error-instance: 2.1.1 - lodash.uniqby: 4.5.0 - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -8716,8 +8699,6 @@ snapshots: csstype@3.1.3: {} - custom-error-instance@2.1.1: {} - data-uri-to-buffer@6.0.2: {} data-view-buffer@1.0.2: @@ -8859,8 +8840,6 @@ snapshots: dotenv@16.4.5: {} - dotenv@16.5.0: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9273,6 +9252,10 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-xml-parser@4.4.1: + dependencies: + strnum: 1.1.2 + fast-xml-parser@4.5.3: dependencies: strnum: 1.1.2 @@ -9883,8 +9866,6 @@ snapshots: joycon@3.1.1: {} - js-base64@3.7.7: {} - js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -10022,25 +10003,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash._baseiteratee@4.7.0: - dependencies: - lodash._stringtopath: 4.8.0 - - lodash._basetostring@4.12.0: {} - - lodash._baseuniq@4.6.0: - dependencies: - lodash._createset: 4.0.3 - lodash._root: 3.0.1 - - lodash._createset@4.0.3: {} - - lodash._root@3.0.1: {} - - lodash._stringtopath@4.8.0: - dependencies: - lodash._basetostring: 4.12.0 - lodash.camelcase@4.3.0: {} lodash.defaults@4.2.0: {} @@ -10055,11 +10017,6 @@ snapshots: lodash.throttle@4.1.1: {} - lodash.uniqby@4.5.0: - dependencies: - lodash._baseiteratee: 4.7.0 - lodash._baseuniq: 4.6.0 - lodash@4.17.21: {} log-symbols@3.0.0: @@ -10207,10 +10164,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid-cjs@0.0.7: - dependencies: - nanoid: 5.1.5 - nanoid@3.3.11: {} nanoid@5.1.5: {} @@ -10562,12 +10515,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - proper-lockfile@4.1.2: - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - proxy-agent@6.5.0: dependencies: agent-base: 7.1.3 @@ -10596,8 +10543,6 @@ snapshots: split-on-first: 1.1.0 strict-uri-encode: 2.0.0 - querystringify@2.2.0: {} - queue-microtask@1.2.3: {} quick-lru@7.0.1: {} @@ -10701,8 +10646,6 @@ snapshots: require-directory@2.1.1: {} - requires-port@1.0.0: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -10726,8 +10669,6 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry@0.12.0: {} - reusify@1.1.0: {} rimraf@3.0.2: @@ -11358,16 +11299,6 @@ snapshots: turbo-windows-64: 2.5.3 turbo-windows-arm64: 2.5.3 - tus-js-client@4.3.1: - dependencies: - buffer-from: 1.1.2 - combine-errors: 3.0.3 - is-stream: 2.0.1 - js-base64: 3.7.7 - lodash.throttle: 4.1.1 - proper-lockfile: 4.1.2 - url-parse: 1.5.10 - tw-animate-css@1.3.0: {} type-check@0.4.0: @@ -11478,11 +11409,6 @@ snapshots: dependencies: punycode: 2.3.1 - url-parse@1.5.10: - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - use-callback-ref@1.3.3(@types/react@19.1.5)(react@19.1.0): dependencies: react: 19.1.0 @@ -11561,7 +11487,7 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - vitest@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.4 '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)) @@ -11585,6 +11511,7 @@ snapshots: vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.12 '@types/node': 22.15.21 transitivePeerDependencies: - jiti