This commit is contained in:
ditiqi 2025-05-28 20:04:05 +08:00
commit 862d3bef72
16 changed files with 875 additions and 1384 deletions

View File

@ -10,7 +10,10 @@
"@hono/zod-validator": "^0.5.0",
"@repo/db": "workspace:*",
"@repo/oidc-provider": "workspace:*",
"@repo/tus": "workspace:*",
"@repo/storage": "workspace:*",
"@trpc/server": "11.1.2",
"dayjs": "^1.11.12",
"hono": "^4.7.10",
"ioredis": "5.4.1",
"jose": "^6.0.11",
@ -19,9 +22,9 @@
"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",
"@repo/storage": "workspace:*"
"zod": "^3.25.23"
},
"devDependencies": {
"@types/bun": "latest",

View File

@ -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

View File

@ -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<string | null>;
/**
*
*/
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<string | null> {
return await this.passwordAuth.getCurrentUser(c);
}
/**
*
*/
async handleAuthenticationRequired(
c: Context,
authRequest: AuthorizationRequest
): Promise<Response> {
return await this.passwordAuth.handleAuthenticationRequired(c, authRequest);
}
/**
*
*/
async authenticate(c: Context): Promise<AuthenticationResult> {
return await this.passwordAuth.authenticate(c);
}
/**
*
*/
async logout(c: Context): Promise<Response> {
return await this.passwordAuth.logout(c);
}
/**
*
*/
async handleLogin(c: Context, authRequest: AuthorizationRequest): Promise<Response> {
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<Response> {
return await this.passwordAuth.handleAuthenticationSuccess(c, result);
}
/**
*
*/
private async handleAuthenticationFailure(
c: Context,
result: AuthenticationResult,
authRequest: AuthorizationRequest
): Promise<Response> {
return await this.passwordAuth.handleAuthenticationFailure(c, result, authRequest);
}
}

View File

@ -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';

View File

@ -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<string | null> {
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<Response> {
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<AuthenticationResult> {
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<Response> {
async handleAuthenticationSuccess(c: Context, result: AuthenticationResult): Promise<Response> {
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<Response> {
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() || '',

View File

@ -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<void> {
const key = this.getSessionKey(sessionId);
const key = this.getKey(sessionId);
await this.storage.set(key, data, this.sessionTTL);
}
async getSession(sessionId: string): Promise<any> {
const key = this.getSessionKey(sessionId);
const key = this.getKey(sessionId);
return await this.storage.get(key);
}
async deleteSession(sessionId: string): Promise<void> {
const key = this.getSessionKey(sessionId);
const key = this.getKey(sessionId);
await this.storage.delete(key);
}

View File

@ -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<void> {
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<AuthorizationCode | null> {
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<AuthorizationCode>(key);
}
async deleteAuthorizationCode(code: string): Promise<void> {
@ -57,33 +50,20 @@ export class TokenManager {
async storeAccessToken(token: AccessToken): Promise<void> {
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<string[]>(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<AccessToken | null> {
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<AccessToken>(key);
}
async deleteAccessToken(token: string): Promise<void> {
@ -123,33 +103,20 @@ export class TokenManager {
async storeRefreshToken(token: RefreshToken): Promise<void> {
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<string[]>(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<RefreshToken | null> {
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<RefreshToken>(key);
}
async deleteRefreshToken(token: string): Promise<void> {
@ -175,27 +142,13 @@ export class TokenManager {
// ID令牌管理
async storeIDToken(token: IDToken): Promise<void> {
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<IDToken | null> {
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<IDToken>(key);
}
async deleteIDToken(token: string): Promise<void> {

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export { OIDCErrorFactory } from './error-factory';

View File

@ -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';
// 重新导出中间件
export { createOIDCProvider, oidcProvider, getOIDCProvider } from './middleware/hono';

View File

@ -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);
});
// 返回应用实例

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,24 @@ export interface OIDCProviderConfig {
findUser: (userId: string) => Promise<OIDCUser | null>;
/** 获取客户端的回调函数 */
findClient: (clientId: string) => Promise<OIDCClient | null>;
/** 认证配置选项 - 必须配置 */
authConfig: {
/** 密码验证器 - 用于验证用户名和密码,必须配置 */
passwordValidator: PasswordValidator;
/** 会话TTL */
sessionTTL?: number;
/** 页面配置 */
pageConfig?: {
/** 登录页面标题 */
title?: string;
/** 品牌名称 */
brandName?: string;
/** 品牌Logo URL */
logoUrl?: string;
};
/** 记住我功能的最大时长(秒) */
rememberMeMaxAge?: number;
};
/** 令牌过期时间配置 */
tokenTTL?: {
accessToken?: number; // 默认 3600 秒

View File

@ -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",

View File

@ -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
});

View File

@ -47,12 +47,15 @@ importers:
'@repo/oidc-provider':
specifier: workspace:*
version: link:../../packages/oidc-provider
'@repo/storage':
'@repo/tus':
specifier: workspace:*
version: link:../../packages/storage
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
@ -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)
@ -1281,72 +1287,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==}
@ -1434,24 +1453,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==}
@ -1891,56 +1914,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==}
@ -2277,24 +2311,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==}
@ -2373,24 +2411,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==}
@ -2896,9 +2938,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==}
@ -3063,9 +3102,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'}
@ -3179,9 +3215,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'}
@ -4122,9 +4155,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==}
@ -4225,24 +4255,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==}
@ -4275,24 +4309,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==}
@ -4315,9 +4331,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==}
@ -4805,9 +4818,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'}
@ -4827,9 +4837,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==}
@ -4926,9 +4933,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'}
@ -4953,10 +4957,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'}
@ -5497,10 +5497,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==}
@ -5608,9 +5604,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'}
@ -8448,8 +8441,6 @@ snapshots:
buffer-crc32@0.2.13: {}
buffer-from@1.1.2: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@ -8635,11 +8626,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
@ -8753,8 +8739,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:
@ -9922,8 +9906,6 @@ snapshots:
joycon@3.1.1: {}
js-base64@3.7.7: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@ -10061,25 +10043,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: {}
@ -10094,11 +10057,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:
@ -10597,12 +10555,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
@ -10631,8 +10583,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: {}
@ -10736,8 +10686,6 @@ snapshots:
require-directory@2.1.1: {}
requires-port@1.0.0: {}
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
@ -10761,8 +10709,6 @@ snapshots:
onetime: 5.1.2
signal-exit: 3.0.7
retry@0.12.0: {}
reusify@1.1.0: {}
rimraf@3.0.2:
@ -11393,16 +11339,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:
@ -11513,11 +11449,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