(null);
+ const [loading, setLoading] = useState(true);
+
+ // Check for existing auth on mount
+ useEffect(() => {
+ checkAuth();
+ }, []);
+
+ const checkAuth = async () => {
+ try {
+ const token = localStorage.getItem('accessToken');
+ if (!token) {
+ setLoading(false);
+ return;
+ }
+
+ const response = await fetch('/api/auth/me', {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setUser(data.user);
+ } else {
+ // Token might be expired, try to refresh
+ await refreshToken();
+ }
+ } catch (error) {
+ console.error('Auth check failed:', error);
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const login = async (email: string, password: string) => {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, password }),
+ });
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Login failed');
+ }
+
+ const data = await response.json();
+ localStorage.setItem('accessToken', data.tokens.accessToken);
+ localStorage.setItem('refreshToken', data.tokens.refreshToken);
+ setUser(data.user);
+ };
+
+ const logout = () => {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('refreshToken');
+ setUser(null);
+ };
+
+ const refreshToken = async () => {
+ try {
+ const refreshTokenValue = localStorage.getItem('refreshToken');
+ if (!refreshTokenValue) {
+ throw new Error('No refresh token available');
+ }
+
+ const response = await fetch('/api/auth/refresh', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ refreshToken: refreshTokenValue }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Token refresh failed');
+ }
+
+ const data = await response.json();
+ localStorage.setItem('accessToken', data.tokens.accessToken);
+ localStorage.setItem('refreshToken', data.tokens.refreshToken);
+
+ // Re-check auth to get updated user info
+ await checkAuth();
+ } catch (error) {
+ console.error('Token refresh failed:', error);
+ logout();
+ throw error;
+ }
+ };
+
+ const value = {
+ user,
+ loading,
+ login,
+ logout,
+ refreshToken,
+ };
+
+ return {children};
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
+
+// Helper hook for auth state
+export function useUser() {
+ const { user } = useAuth();
+ return user;
+}
+
+// Helper hook to check if user is authenticated
+export function useIsAuthenticated() {
+ const { user, loading } = useAuth();
+ return { isAuthenticated: !!user, loading };
+}
\ No newline at end of file
diff --git a/apps/web/components/auth/auth-guard.tsx b/apps/web/components/auth/auth-guard.tsx
new file mode 100644
index 00000000..313b3f16
--- /dev/null
+++ b/apps/web/components/auth/auth-guard.tsx
@@ -0,0 +1,74 @@
+/**
+ * Auth guard component to protect routes
+ */
+
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { useAuth } from './auth-context';
+import { Loader2 } from 'lucide-react';
+
+interface AuthGuardProps {
+ children: React.ReactNode;
+ redirectTo?: string;
+ requireEmailVerification?: boolean;
+}
+
+export function AuthGuard({
+ children,
+ redirectTo = '/login',
+ requireEmailVerification = false
+}: AuthGuardProps) {
+ const { user, loading } = useAuth();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (!loading) {
+ if (!user) {
+ router.push(redirectTo);
+ return;
+ }
+
+ if (requireEmailVerification && !user.isEmailVerified) {
+ router.push('/verify-email');
+ return;
+ }
+ }
+ }, [user, loading, router, redirectTo, requireEmailVerification]);
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return null; // Will redirect
+ }
+
+ if (requireEmailVerification && !user.isEmailVerified) {
+ return null; // Will redirect to email verification
+ }
+
+ return <>{children}>;
+}
+
+// Wrapper for pages that require authentication
+export function withAuth(
+ Component: React.ComponentType
,
+ options?: {
+ redirectTo?: string;
+ requireEmailVerification?: boolean;
+ }
+) {
+ return function AuthenticatedComponent(props: P) {
+ return (
+
+
+
+ );
+ };
+}
\ No newline at end of file
diff --git a/apps/web/components/auth/index.ts b/apps/web/components/auth/index.ts
new file mode 100644
index 00000000..a5a9d783
--- /dev/null
+++ b/apps/web/components/auth/index.ts
@@ -0,0 +1,4 @@
+export { LoginForm } from './login-form';
+export { RegisterForm } from './register-form';
+export { AuthProvider, useAuth, useUser, useIsAuthenticated } from './auth-context';
+export { AuthGuard, withAuth } from './auth-guard';
\ No newline at end of file
diff --git a/apps/web/components/auth/login-form.tsx b/apps/web/components/auth/login-form.tsx
new file mode 100644
index 00000000..bb37cd52
--- /dev/null
+++ b/apps/web/components/auth/login-form.tsx
@@ -0,0 +1,114 @@
+/**
+ * Login form component
+ */
+
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Loader2 } from 'lucide-react';
+
+interface LoginFormProps {
+ onSuccess?: (user: any, tokens: any) => void;
+ redirectTo?: string;
+}
+
+export function LoginForm({ onSuccess, redirectTo = '/projects' }: LoginFormProps) {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const router = useRouter();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+
+ try {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, password }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Login failed');
+ }
+
+ // Store tokens in localStorage (in a real app, consider httpOnly cookies)
+ localStorage.setItem('accessToken', data.tokens.accessToken);
+ localStorage.setItem('refreshToken', data.tokens.refreshToken);
+
+ if (onSuccess) {
+ onSuccess(data.user, data.tokens);
+ } else {
+ router.push(redirectTo);
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Login failed');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Sign In
+
+ Enter your email and password to access your account
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/components/auth/register-form.tsx b/apps/web/components/auth/register-form.tsx
new file mode 100644
index 00000000..11ba4174
--- /dev/null
+++ b/apps/web/components/auth/register-form.tsx
@@ -0,0 +1,175 @@
+/**
+ * Registration form component
+ */
+
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Loader2 } from 'lucide-react';
+
+interface RegisterFormProps {
+ onSuccess?: (user: any) => void;
+ redirectTo?: string;
+}
+
+export function RegisterForm({ onSuccess, redirectTo = '/login' }: RegisterFormProps) {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [name, setName] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [success, setSuccess] = useState(false);
+ const router = useRouter();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setLoading(true);
+ setError('');
+
+ if (password !== confirmPassword) {
+ setError('Passwords do not match');
+ setLoading(false);
+ return;
+ }
+
+ if (password.length < 8) {
+ setError('Password must be at least 8 characters long');
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/auth/register', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, password, name: name || undefined }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Registration failed');
+ }
+
+ setSuccess(true);
+
+ if (onSuccess) {
+ onSuccess(data.user);
+ } else {
+ // Redirect to login page after successful registration
+ setTimeout(() => {
+ router.push(redirectTo);
+ }, 2000);
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Registration failed');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (success) {
+ return (
+
+
+ Registration Successful!
+
+ Please check your email for a verification link.
+
+
+
+
+
+ We've sent a verification email to {email}. Please click the link in the email to verify your account.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Create Account
+
+ Enter your information to create a new account
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/components/provider/app-providers.tsx b/apps/web/components/provider/app-providers.tsx
index d1448297..d0f1993f 100644
--- a/apps/web/components/provider/app-providers.tsx
+++ b/apps/web/components/provider/app-providers.tsx
@@ -3,13 +3,16 @@
import { ThemeProvider } from '@/components/provider/theme-provider';
import { AppLayout } from '@/components/layout/app-layout';
import { StoreProvider } from '@/components/provider/store-provider';
+import { AuthProvider } from '@/components/auth';
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
diff --git a/apps/web/lib/auth-middleware.ts b/apps/web/lib/auth-middleware.ts
new file mode 100644
index 00000000..ed417dfd
--- /dev/null
+++ b/apps/web/lib/auth-middleware.ts
@@ -0,0 +1,92 @@
+/**
+ * Authentication middleware for API routes
+ */
+
+import { NextRequest } from 'next/server';
+import type { SessionUser } from '@codervisor/devlog-core';
+
+export interface AuthenticatedRequest extends NextRequest {
+ user: SessionUser;
+}
+
+/**
+ * Middleware to verify JWT tokens and extract user information
+ */
+export async function withAuth(
+ handler: (req: AuthenticatedRequest, ...args: T[]) => Promise,
+) {
+ return async (req: NextRequest, ...args: T[]) => {
+ try {
+ const authHeader = req.headers.get('authorization');
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return new Response(JSON.stringify({ error: 'Missing or invalid authorization header' }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
+
+ // Import AuthService dynamically to avoid initialization issues
+ const { AuthService } = await import('@codervisor/devlog-core/auth');
+ const authService = AuthService.getInstance();
+
+ const user = await authService.verifyToken(token);
+
+ // Attach user to request
+ const authenticatedReq = req as AuthenticatedRequest;
+ authenticatedReq.user = user;
+
+ return await handler(authenticatedReq, ...args);
+ } catch (error) {
+ return new Response(JSON.stringify({ error: 'Invalid or expired token' }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+ };
+}
+
+/**
+ * Optional authentication middleware - allows both authenticated and unauthenticated requests
+ */
+export async function withOptionalAuth(
+ handler: (req: NextRequest & { user?: SessionUser }, ...args: T[]) => Promise,
+) {
+ return async (req: NextRequest, ...args: T[]) => {
+ try {
+ const authHeader = req.headers.get('authorization');
+ if (authHeader && authHeader.startsWith('Bearer ')) {
+ const token = authHeader.substring(7);
+
+ const { AuthService } = await import('@codervisor/devlog-core/auth');
+ const authService = AuthService.getInstance();
+
+ try {
+ const user = await authService.verifyToken(token);
+ (req as any).user = user;
+ } catch {
+ // Ignore token verification errors for optional auth
+ }
+ }
+
+ return await handler(req as NextRequest & { user?: SessionUser }, ...args);
+ } catch (error) {
+ return await handler(req as NextRequest & { user?: SessionUser }, ...args);
+ }
+ };
+}
+
+/**
+ * Extract user from authenticated request
+ */
+export function getUser(req: AuthenticatedRequest): SessionUser {
+ return req.user;
+}
+
+/**
+ * Check if user is authenticated
+ */
+export function isAuthenticated(req: NextRequest & { user?: SessionUser }): req is AuthenticatedRequest {
+ return !!(req as any).user;
+}
\ No newline at end of file
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index 582e992a..0479d971 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -14,6 +14,9 @@ const nextConfig = {
'mysql2',
'better-sqlite3',
'reflect-metadata',
+ // Keep authentication dependencies server-side only
+ 'bcrypt',
+ 'jsonwebtoken',
],
},
webpack: (config, { isServer }) => {
@@ -26,12 +29,25 @@ const nextConfig = {
/Module not found.*typeorm.*react-native/,
/Module not found.*typeorm.*mysql/,
/Module not found.*typeorm.*hana/,
+ // Bcrypt and authentication related warnings
+ /Module not found: Can't resolve 'mock-aws-s3'/,
+ /Module not found: Can't resolve 'aws-sdk'/,
+ /Module not found: Can't resolve 'nock'/,
];
// Handle the workspace packages properly
if (isServer) {
// Ensure these packages are treated as externals for server-side
config.externals = config.externals || [];
+ config.externals.push(
+ 'bcrypt',
+ 'jsonwebtoken',
+ '@mapbox/node-pre-gyp',
+ 'node-pre-gyp',
+ 'mock-aws-s3',
+ 'aws-sdk',
+ 'nock'
+ );
}
// Fix Monaco Editor issues for client-side
@@ -56,6 +72,14 @@ const nextConfig = {
mysql: false,
'better-sqlite3': false,
'reflect-metadata': false,
+ // Exclude authentication modules from client bundle
+ 'bcrypt': false,
+ 'jsonwebtoken': false,
+ '@mapbox/node-pre-gyp': false,
+ 'node-pre-gyp': false,
+ 'mock-aws-s3': false,
+ 'aws-sdk': false,
+ 'nock': false,
// Exclude problematic TypeORM drivers
'react-native-sqlite-storage': false,
'@sap/hana-client': false,
diff --git a/packages/core/package.json b/packages/core/package.json
index 63e3fd49..2fd814fe 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -17,6 +17,10 @@
"./server": {
"types": "./build/server.d.ts",
"import": "./build/server.js"
+ },
+ "./auth": {
+ "types": "./build/auth.d.ts",
+ "import": "./build/auth.js"
}
},
"files": [
@@ -58,9 +62,11 @@
},
"license": "Apache-2.0",
"dependencies": {
+ "bcrypt": "^5.1.1",
"better-sqlite3": "^11.0.0",
"cheerio": "1.1.2",
"dotenv": "16.5.0",
+ "jsonwebtoken": "^9.0.2",
"mysql2": "^3.11.0",
"pg": "^8.12.0",
"reflect-metadata": "0.2.2",
@@ -68,7 +74,9 @@
"zod": "^3.22.4"
},
"devDependencies": {
+ "@types/bcrypt": "^5.0.2",
"@types/better-sqlite3": "^7.6.0",
+ "@types/jsonwebtoken": "^9.0.7",
"@types/node": "^20.0.0",
"@types/pg": "^8.11.0",
"@vitest/ui": "^2.1.9",
diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts
new file mode 100644
index 00000000..61b23563
--- /dev/null
+++ b/packages/core/src/auth.ts
@@ -0,0 +1,5 @@
+// Authentication-specific server exports
+// These include bcrypt and JWT dependencies that should only be imported on the server
+export { AuthService } from './services/auth-service.js';
+export * from './entities/user.entity.js';
+export * from './types/auth.js';
\ No newline at end of file
diff --git a/packages/core/src/entities/index.ts b/packages/core/src/entities/index.ts
index f447c573..133e4977 100644
--- a/packages/core/src/entities/index.ts
+++ b/packages/core/src/entities/index.ts
@@ -5,4 +5,5 @@ export * from './project.entity.js';
export * from './chat-session.entity.js';
export * from './chat-message.entity.js';
export * from './chat-devlog-link.entity.js';
+export * from './user.entity.js';
export * from './decorators.js';
diff --git a/packages/core/src/entities/user.entity.ts b/packages/core/src/entities/user.entity.ts
new file mode 100644
index 00000000..14198962
--- /dev/null
+++ b/packages/core/src/entities/user.entity.ts
@@ -0,0 +1,262 @@
+/**
+ * User Entity for authentication and user management
+ */
+
+import 'reflect-metadata';
+import { Column, CreateDateColumn, Entity, OneToMany, ManyToOne, JoinColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+import type { User } from '../types/index.js';
+import { getTimestampType, TimestampColumn } from './decorators.js';
+
+@Entity('devlog_users')
+export class UserEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column({ type: 'varchar', length: 255, unique: true })
+ email!: string;
+
+ @Column({ type: 'varchar', length: 255, nullable: true })
+ name?: string;
+
+ @Column({ type: 'varchar', length: 255, nullable: true })
+ avatarUrl?: string;
+
+ @Column({ type: 'varchar', length: 255 })
+ passwordHash!: string;
+
+ @Column({ type: 'boolean', default: false })
+ isEmailVerified!: boolean;
+
+ @CreateDateColumn({
+ type: getTimestampType(),
+ name: 'created_at',
+ })
+ createdAt!: Date;
+
+ @UpdateDateColumn({
+ type: getTimestampType(),
+ name: 'updated_at',
+ })
+ updatedAt!: Date;
+
+ @TimestampColumn({ name: 'last_login_at', nullable: true })
+ lastLoginAt?: Date;
+
+ @OneToMany(() => UserProviderEntity, provider => provider.user)
+ providers?: UserProviderEntity[];
+
+ /**
+ * Convert entity to User type (without password hash)
+ */
+ toUser(): User {
+ return {
+ id: this.id,
+ email: this.email,
+ name: this.name,
+ avatarUrl: this.avatarUrl,
+ isEmailVerified: this.isEmailVerified,
+ createdAt: this.createdAt.toISOString(),
+ updatedAt: this.updatedAt.toISOString(),
+ lastLoginAt: this.lastLoginAt?.toISOString(),
+ };
+ }
+
+ /**
+ * Create entity from user registration data
+ */
+ static fromRegistration(
+ registration: { email: string; name?: string; passwordHash: string },
+ ): UserEntity {
+ const entity = new UserEntity();
+ entity.email = registration.email;
+ entity.name = registration.name;
+ entity.passwordHash = registration.passwordHash;
+ entity.isEmailVerified = false;
+ return entity;
+ }
+
+ /**
+ * Update entity with partial user data
+ */
+ updateFromUserData(updates: Partial): void {
+ if (updates.name !== undefined) this.name = updates.name;
+ if (updates.avatarUrl !== undefined) this.avatarUrl = updates.avatarUrl;
+ if (updates.isEmailVerified !== undefined) this.isEmailVerified = updates.isEmailVerified;
+ this.updatedAt = new Date();
+ }
+
+ /**
+ * Update last login timestamp
+ */
+ updateLastLogin(): void {
+ this.lastLoginAt = new Date();
+ }
+}
+
+@Entity('devlog_user_providers')
+export class UserProviderEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column({ type: 'int' })
+ userId!: number;
+
+ @Column({ type: 'varchar', length: 50 })
+ provider!: string; // 'github' | 'google' | 'wechat'
+
+ @Column({ type: 'varchar', length: 255 })
+ providerId!: string;
+
+ @CreateDateColumn({
+ type: getTimestampType(),
+ name: 'created_at',
+ })
+ createdAt!: Date;
+
+ @ManyToOne(() => UserEntity, user => user.providers)
+ @JoinColumn({ name: 'user_id' })
+ user!: UserEntity;
+
+ /**
+ * Convert entity to UserProvider type
+ */
+ toUserProvider(): import('../types/index.js').UserProvider {
+ return {
+ id: this.id,
+ userId: this.userId,
+ provider: this.provider as import('../types/index.js').SSOProvider,
+ providerId: this.providerId,
+ createdAt: this.createdAt.toISOString(),
+ };
+ }
+
+ /**
+ * Create entity from SSO user info
+ */
+ static fromSSOInfo(
+ userId: number,
+ ssoInfo: import('../types/index.js').SSOUserInfo,
+ ): UserProviderEntity {
+ const entity = new UserProviderEntity();
+ entity.userId = userId;
+ entity.provider = ssoInfo.provider;
+ entity.providerId = ssoInfo.providerId;
+ return entity;
+ }
+}
+
+@Entity('devlog_email_verification_tokens')
+export class EmailVerificationTokenEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column({ type: 'int' })
+ userId!: number;
+
+ @Column({ type: 'varchar', length: 255, unique: true })
+ token!: string;
+
+ @TimestampColumn({ name: 'expires_at' })
+ expiresAt!: Date;
+
+ @CreateDateColumn({
+ type: getTimestampType(),
+ name: 'created_at',
+ })
+ createdAt!: Date;
+
+ /**
+ * Convert entity to EmailVerificationToken type
+ */
+ toEmailVerificationToken(): import('../types/index.js').EmailVerificationToken {
+ return {
+ id: this.id,
+ userId: this.userId,
+ token: this.token,
+ expiresAt: this.expiresAt.toISOString(),
+ createdAt: this.createdAt.toISOString(),
+ };
+ }
+
+ /**
+ * Create entity from token data
+ */
+ static createToken(userId: number, token: string, expiresAt: Date): EmailVerificationTokenEntity {
+ const entity = new EmailVerificationTokenEntity();
+ entity.userId = userId;
+ entity.token = token;
+ entity.expiresAt = expiresAt;
+ return entity;
+ }
+
+ /**
+ * Check if token is expired
+ */
+ isExpired(): boolean {
+ return new Date() > this.expiresAt;
+ }
+}
+
+@Entity('devlog_password_reset_tokens')
+export class PasswordResetTokenEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Column({ type: 'int' })
+ userId!: number;
+
+ @Column({ type: 'varchar', length: 255, unique: true })
+ token!: string;
+
+ @TimestampColumn({ name: 'expires_at' })
+ expiresAt!: Date;
+
+ @CreateDateColumn({
+ type: getTimestampType(),
+ name: 'created_at',
+ })
+ createdAt!: Date;
+
+ @Column({ type: 'boolean', default: false })
+ used!: boolean;
+
+ /**
+ * Convert entity to PasswordResetToken type
+ */
+ toPasswordResetToken(): import('../types/index.js').PasswordResetToken {
+ return {
+ id: this.id,
+ userId: this.userId,
+ token: this.token,
+ expiresAt: this.expiresAt.toISOString(),
+ createdAt: this.createdAt.toISOString(),
+ used: this.used,
+ };
+ }
+
+ /**
+ * Create entity from token data
+ */
+ static createToken(userId: number, token: string, expiresAt: Date): PasswordResetTokenEntity {
+ const entity = new PasswordResetTokenEntity();
+ entity.userId = userId;
+ entity.token = token;
+ entity.expiresAt = expiresAt;
+ entity.used = false;
+ return entity;
+ }
+
+ /**
+ * Check if token is expired or used
+ */
+ isValid(): boolean {
+ return !this.used && new Date() <= this.expiresAt;
+ }
+
+ /**
+ * Mark token as used
+ */
+ markAsUsed(): void {
+ this.used = true;
+ }
+}
\ No newline at end of file
diff --git a/packages/core/src/services/auth-service.ts b/packages/core/src/services/auth-service.ts
new file mode 100644
index 00000000..ba167af8
--- /dev/null
+++ b/packages/core/src/services/auth-service.ts
@@ -0,0 +1,548 @@
+/**
+ * Authentication Service
+ * Manages user authentication, registration, and session handling
+ */
+
+import 'reflect-metadata';
+import { DataSource, Repository } from 'typeorm';
+import * as bcrypt from 'bcrypt';
+import * as jwt from 'jsonwebtoken';
+import * as crypto from 'crypto';
+import {
+ UserEntity,
+ UserProviderEntity,
+ EmailVerificationTokenEntity,
+ PasswordResetTokenEntity,
+} from '../entities/user.entity.js';
+import type {
+ User,
+ UserRegistration,
+ UserLogin,
+ AuthResponse,
+ AuthToken,
+ SessionUser,
+ JWTPayload,
+ SSOUserInfo,
+ EmailVerificationToken,
+ PasswordResetToken,
+} from '../types/index.js';
+import { createDataSource } from '../utils/typeorm-config.js';
+
+interface AuthServiceInstance {
+ service: AuthService;
+ createdAt: number;
+}
+
+export class AuthService {
+ private static instances: Map = new Map();
+ private static readonly TTL_MS = 5 * 60 * 1000; // 5 minutes TTL
+ private database: DataSource;
+ private userRepository: Repository;
+ private providerRepository: Repository;
+ private emailTokenRepository: Repository;
+ private passwordResetRepository: Repository;
+ private initPromise: Promise | null = null;
+
+ // Configuration
+ private readonly JWT_SECRET: string;
+ private readonly JWT_EXPIRES_IN = '15m'; // Access token expiry
+ private readonly JWT_REFRESH_EXPIRES_IN = '7d'; // Refresh token expiry
+ private readonly BCRYPT_ROUNDS = 12;
+ private readonly EMAIL_TOKEN_EXPIRES_HOURS = 24;
+ private readonly PASSWORD_RESET_EXPIRES_HOURS = 1;
+
+ private constructor() {
+ this.database = createDataSource({
+ entities: [
+ UserEntity,
+ UserProviderEntity,
+ EmailVerificationTokenEntity,
+ PasswordResetTokenEntity,
+ ],
+ });
+ this.userRepository = this.database.getRepository(UserEntity);
+ this.providerRepository = this.database.getRepository(UserProviderEntity);
+ this.emailTokenRepository = this.database.getRepository(EmailVerificationTokenEntity);
+ this.passwordResetRepository = this.database.getRepository(PasswordResetTokenEntity);
+
+ // Get JWT secret from environment
+ this.JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-key';
+ if (this.JWT_SECRET === 'dev-secret-key' && process.env.NODE_ENV === 'production') {
+ throw new Error('JWT_SECRET must be set in production environment');
+ }
+ }
+
+ /**
+ * Get singleton instance with TTL
+ */
+ static getInstance(): AuthService {
+ const instanceKey = 'default';
+ const now = Date.now();
+ const existingInstance = AuthService.instances.get(instanceKey);
+
+ if (!existingInstance || now - existingInstance.createdAt > AuthService.TTL_MS) {
+ const newService = new AuthService();
+ AuthService.instances.set(instanceKey, {
+ service: newService,
+ createdAt: now,
+ });
+ return newService;
+ }
+
+ return existingInstance.service;
+ }
+
+ /**
+ * Initialize the database connection if not already initialized
+ */
+ async ensureInitialized(): Promise {
+ if (this.initPromise) {
+ return this.initPromise;
+ }
+
+ this.initPromise = this._initialize();
+ return this.initPromise;
+ }
+
+ /**
+ * Internal initialization method
+ */
+ private async _initialize(): Promise {
+ if (!this.database.isInitialized) {
+ await this.database.initialize();
+ }
+ }
+
+ /**
+ * Dispose of the service and close database connection
+ */
+ async dispose(): Promise {
+ if (this.database.isInitialized) {
+ await this.database.destroy();
+ }
+ this.initPromise = null;
+ }
+
+ /**
+ * Register a new user with email and password
+ */
+ async register(registration: UserRegistration): Promise<{ user: User; emailToken?: string }> {
+ await this.ensureInitialized();
+
+ // Check if user already exists
+ const existingUser = await this.userRepository.findOne({
+ where: { email: registration.email },
+ });
+
+ if (existingUser) {
+ throw new Error('User with this email already exists');
+ }
+
+ // Hash password
+ const passwordHash = await bcrypt.hash(registration.password, this.BCRYPT_ROUNDS);
+
+ // Create user entity
+ const userEntity = UserEntity.fromRegistration({
+ email: registration.email,
+ name: registration.name,
+ passwordHash,
+ });
+
+ // Save user
+ const savedUser = await this.userRepository.save(userEntity);
+
+ // Generate email verification token
+ const emailToken = await this.generateEmailVerificationToken(savedUser.id);
+
+ return {
+ user: savedUser.toUser(),
+ emailToken: emailToken.token,
+ };
+ }
+
+ /**
+ * Login with email and password
+ */
+ async login(login: UserLogin): Promise {
+ await this.ensureInitialized();
+
+ // Find user by email
+ const userEntity = await this.userRepository.findOne({
+ where: { email: login.email },
+ });
+
+ if (!userEntity) {
+ throw new Error('Invalid email or password');
+ }
+
+ // Verify password
+ const isPasswordValid = await bcrypt.compare(login.password, userEntity.passwordHash);
+ if (!isPasswordValid) {
+ throw new Error('Invalid email or password');
+ }
+
+ // Update last login
+ userEntity.updateLastLogin();
+ await this.userRepository.save(userEntity);
+
+ // Generate tokens
+ const tokens = await this.generateTokens(userEntity);
+
+ return {
+ user: userEntity.toUser(),
+ tokens,
+ };
+ }
+
+ /**
+ * Verify email with token
+ */
+ async verifyEmail(token: string): Promise {
+ await this.ensureInitialized();
+
+ const tokenEntity = await this.emailTokenRepository.findOne({
+ where: { token },
+ });
+
+ if (!tokenEntity || tokenEntity.isExpired()) {
+ throw new Error('Invalid or expired verification token');
+ }
+
+ // Find and update user
+ const userEntity = await this.userRepository.findOne({
+ where: { id: tokenEntity.userId },
+ });
+
+ if (!userEntity) {
+ throw new Error('User not found');
+ }
+
+ userEntity.isEmailVerified = true;
+ await this.userRepository.save(userEntity);
+
+ // Delete used token
+ await this.emailTokenRepository.remove(tokenEntity);
+
+ return userEntity.toUser();
+ }
+
+ /**
+ * Generate new access and refresh tokens
+ */
+ async generateTokens(user: UserEntity): Promise {
+ const now = Math.floor(Date.now() / 1000);
+
+ // Access token payload
+ const accessPayload: JWTPayload = {
+ userId: user.id,
+ email: user.email,
+ type: 'access',
+ iat: now,
+ exp: now + 15 * 60, // 15 minutes
+ };
+
+ // Refresh token payload
+ const refreshPayload: JWTPayload = {
+ userId: user.id,
+ email: user.email,
+ type: 'refresh',
+ iat: now,
+ exp: now + 7 * 24 * 60 * 60, // 7 days
+ };
+
+ const accessToken = jwt.sign(accessPayload, this.JWT_SECRET);
+ const refreshToken = jwt.sign(refreshPayload, this.JWT_SECRET);
+
+ return {
+ accessToken,
+ refreshToken,
+ expiresAt: new Date(accessPayload.exp * 1000).toISOString(),
+ };
+ }
+
+ /**
+ * Verify and decode JWT token
+ */
+ async verifyToken(token: string): Promise {
+ try {
+ const payload = jwt.verify(token, this.JWT_SECRET) as JWTPayload;
+
+ if (payload.type !== 'access') {
+ throw new Error('Invalid token type');
+ }
+
+ // Get current user data
+ const user = await this.getUserById(payload.userId);
+ if (!user) {
+ throw new Error('User not found');
+ }
+
+ return {
+ id: user.id,
+ email: user.email,
+ name: user.name,
+ avatarUrl: user.avatarUrl,
+ isEmailVerified: user.isEmailVerified,
+ };
+ } catch (error) {
+ throw new Error('Invalid or expired token');
+ }
+ }
+
+ /**
+ * Refresh access token using refresh token
+ */
+ async refreshToken(refreshToken: string): Promise {
+ try {
+ const payload = jwt.verify(refreshToken, this.JWT_SECRET) as JWTPayload;
+
+ if (payload.type !== 'refresh') {
+ throw new Error('Invalid token type');
+ }
+
+ // Get user and generate new tokens
+ const userEntity = await this.userRepository.findOne({
+ where: { id: payload.userId },
+ });
+
+ if (!userEntity) {
+ throw new Error('User not found');
+ }
+
+ return this.generateTokens(userEntity);
+ } catch (error) {
+ throw new Error('Invalid or expired refresh token');
+ }
+ }
+
+ /**
+ * Get user by ID
+ */
+ async getUserById(id: number): Promise {
+ await this.ensureInitialized();
+
+ const userEntity = await this.userRepository.findOne({
+ where: { id },
+ });
+
+ return userEntity ? userEntity.toUser() : null;
+ }
+
+ /**
+ * Get user by email
+ */
+ async getUserByEmail(email: string): Promise {
+ await this.ensureInitialized();
+
+ const userEntity = await this.userRepository.findOne({
+ where: { email },
+ });
+
+ return userEntity ? userEntity.toUser() : null;
+ }
+
+ /**
+ * Generate email verification token
+ */
+ async generateEmailVerificationToken(userId: number): Promise {
+ await this.ensureInitialized();
+
+ const token = crypto.randomBytes(32).toString('hex');
+ const expiresAt = new Date();
+ expiresAt.setHours(expiresAt.getHours() + this.EMAIL_TOKEN_EXPIRES_HOURS);
+
+ const tokenEntity = EmailVerificationTokenEntity.createToken(userId, token, expiresAt);
+ const savedToken = await this.emailTokenRepository.save(tokenEntity);
+
+ return savedToken.toEmailVerificationToken();
+ }
+
+ /**
+ * Generate password reset token
+ */
+ async generatePasswordResetToken(email: string): Promise {
+ await this.ensureInitialized();
+
+ const user = await this.userRepository.findOne({
+ where: { email },
+ });
+
+ if (!user) {
+ // Don't reveal if email exists or not
+ return null;
+ }
+
+ const token = crypto.randomBytes(32).toString('hex');
+ const expiresAt = new Date();
+ expiresAt.setHours(expiresAt.getHours() + this.PASSWORD_RESET_EXPIRES_HOURS);
+
+ const tokenEntity = PasswordResetTokenEntity.createToken(user.id, token, expiresAt);
+ const savedToken = await this.passwordResetRepository.save(tokenEntity);
+
+ return savedToken.toPasswordResetToken();
+ }
+
+ /**
+ * Reset password using token
+ */
+ async resetPassword(token: string, newPassword: string): Promise {
+ await this.ensureInitialized();
+
+ const tokenEntity = await this.passwordResetRepository.findOne({
+ where: { token },
+ });
+
+ if (!tokenEntity || !tokenEntity.isValid()) {
+ throw new Error('Invalid or expired reset token');
+ }
+
+ // Find user and update password
+ const userEntity = await this.userRepository.findOne({
+ where: { id: tokenEntity.userId },
+ });
+
+ if (!userEntity) {
+ throw new Error('User not found');
+ }
+
+ // Hash new password
+ const passwordHash = await bcrypt.hash(newPassword, this.BCRYPT_ROUNDS);
+ userEntity.passwordHash = passwordHash;
+ await this.userRepository.save(userEntity);
+
+ // Mark token as used
+ tokenEntity.markAsUsed();
+ await this.passwordResetRepository.save(tokenEntity);
+
+ return userEntity.toUser();
+ }
+
+ /**
+ * Handle SSO login/registration
+ */
+ async handleSSOLogin(ssoInfo: SSOUserInfo): Promise {
+ await this.ensureInitialized();
+
+ // Check if user already exists with this provider
+ let userEntity = await this.findUserByProvider(ssoInfo.provider, ssoInfo.providerId);
+
+ if (!userEntity) {
+ // Check if user exists with this email
+ userEntity = await this.userRepository.findOne({
+ where: { email: ssoInfo.email },
+ });
+
+ if (userEntity) {
+ // Link SSO provider to existing user
+ await this.linkSSOProvider(userEntity.id, ssoInfo);
+ } else {
+ // Create new user
+ userEntity = await this.createUserFromSSO(ssoInfo);
+ }
+ }
+
+ // Update last login
+ userEntity.updateLastLogin();
+ await this.userRepository.save(userEntity);
+
+ // Generate tokens
+ const tokens = await this.generateTokens(userEntity);
+
+ return {
+ user: userEntity.toUser(),
+ tokens,
+ };
+ }
+
+ /**
+ * Find user by SSO provider
+ */
+ private async findUserByProvider(provider: string, providerId: string): Promise {
+ const providerEntity = await this.providerRepository.findOne({
+ where: { provider, providerId },
+ relations: ['user'],
+ });
+
+ return providerEntity?.user || null;
+ }
+
+ /**
+ * Link SSO provider to existing user
+ */
+ private async linkSSOProvider(userId: number, ssoInfo: SSOUserInfo): Promise {
+ const providerEntity = UserProviderEntity.fromSSOInfo(userId, ssoInfo);
+ await this.providerRepository.save(providerEntity);
+ }
+
+ /**
+ * Create new user from SSO information
+ */
+ private async createUserFromSSO(ssoInfo: SSOUserInfo): Promise {
+ // Create user with random password (since they'll use SSO)
+ const randomPassword = crypto.randomBytes(32).toString('hex');
+ const passwordHash = await bcrypt.hash(randomPassword, this.BCRYPT_ROUNDS);
+
+ const userEntity = UserEntity.fromRegistration({
+ email: ssoInfo.email,
+ name: ssoInfo.name,
+ passwordHash,
+ });
+
+ // SSO users are automatically email verified
+ userEntity.isEmailVerified = true;
+ userEntity.avatarUrl = ssoInfo.avatarUrl;
+
+ const savedUser = await this.userRepository.save(userEntity);
+
+ // Link SSO provider
+ await this.linkSSOProvider(savedUser.id, ssoInfo);
+
+ return savedUser;
+ }
+
+ /**
+ * Update user profile
+ */
+ async updateUser(userId: number, updates: Partial): Promise {
+ await this.ensureInitialized();
+
+ const userEntity = await this.userRepository.findOne({
+ where: { id: userId },
+ });
+
+ if (!userEntity) {
+ throw new Error('User not found');
+ }
+
+ userEntity.updateFromUserData(updates);
+ const savedUser = await this.userRepository.save(userEntity);
+
+ return savedUser.toUser();
+ }
+
+ /**
+ * Change user password
+ */
+ async changePassword(userId: number, currentPassword: string, newPassword: string): Promise {
+ await this.ensureInitialized();
+
+ const userEntity = await this.userRepository.findOne({
+ where: { id: userId },
+ });
+
+ if (!userEntity) {
+ throw new Error('User not found');
+ }
+
+ // Verify current password
+ const isCurrentPasswordValid = await bcrypt.compare(currentPassword, userEntity.passwordHash);
+ if (!isCurrentPasswordValid) {
+ throw new Error('Current password is incorrect');
+ }
+
+ // Hash and save new password
+ const passwordHash = await bcrypt.hash(newPassword, this.BCRYPT_ROUNDS);
+ userEntity.passwordHash = passwordHash;
+ await this.userRepository.save(userEntity);
+ }
+}
\ No newline at end of file
diff --git a/packages/core/src/services/index.ts b/packages/core/src/services/index.ts
index bef1b075..5b881178 100644
--- a/packages/core/src/services/index.ts
+++ b/packages/core/src/services/index.ts
@@ -1,3 +1,4 @@
export { DevlogService } from './devlog-service.js';
export { ProjectService } from './project-service.js';
+// export { AuthService } from './auth-service.js'; // Moved to auth.ts export
// export { IntegrationService } from './integration-service.js';
diff --git a/packages/core/src/types/auth.ts b/packages/core/src/types/auth.ts
new file mode 100644
index 00000000..bdabf397
--- /dev/null
+++ b/packages/core/src/types/auth.ts
@@ -0,0 +1,138 @@
+/**
+ * Authentication and user management types
+ */
+
+export interface User {
+ id: number;
+ email: string;
+ name?: string;
+ avatarUrl?: string;
+ isEmailVerified: boolean;
+ createdAt: string;
+ updatedAt: string;
+ lastLoginAt?: string;
+}
+
+export interface UserRegistration {
+ email: string;
+ password: string;
+ name?: string;
+}
+
+export interface UserLogin {
+ email: string;
+ password: string;
+}
+
+export interface AuthToken {
+ accessToken: string;
+ refreshToken: string;
+ expiresAt: string;
+}
+
+export interface AuthResponse {
+ user: User;
+ tokens: AuthToken;
+}
+
+export interface PasswordReset {
+ email: string;
+}
+
+export interface PasswordResetConfirm {
+ token: string;
+ newPassword: string;
+}
+
+export interface EmailVerification {
+ token: string;
+}
+
+// SSO Provider types
+export type SSOProvider = 'github' | 'google' | 'wechat';
+
+export interface SSOConfig {
+ github?: GitHubSSOConfig;
+ google?: GoogleSSOConfig;
+ wechat?: WeChatSSOConfig;
+}
+
+export interface GitHubSSOConfig {
+ clientId: string;
+ clientSecret: string;
+ redirectUri: string;
+}
+
+export interface GoogleSSOConfig {
+ clientId: string;
+ clientSecret: string;
+ redirectUri: string;
+}
+
+export interface WeChatSSOConfig {
+ appId: string;
+ appSecret: string;
+ redirectUri: string;
+}
+
+export interface SSOUserInfo {
+ provider: SSOProvider;
+ providerId: string;
+ email: string;
+ name?: string;
+ avatarUrl?: string;
+}
+
+export interface UserProvider {
+ id: number;
+ userId: number;
+ provider: SSOProvider;
+ providerId: string;
+ createdAt: string;
+}
+
+// Session and JWT types
+export interface JWTPayload {
+ userId: number;
+ email: string;
+ type: 'access' | 'refresh';
+ iat: number;
+ exp: number;
+}
+
+export interface SessionUser {
+ id: number;
+ email: string;
+ name?: string;
+ avatarUrl?: string;
+ isEmailVerified: boolean;
+}
+
+// Email verification types
+export interface EmailVerificationToken {
+ id: number;
+ userId: number;
+ token: string;
+ expiresAt: string;
+ createdAt: string;
+}
+
+export interface PasswordResetToken {
+ id: number;
+ userId: number;
+ token: string;
+ expiresAt: string;
+ createdAt: string;
+ used: boolean;
+}
+
+// Auth error types
+export interface AuthError {
+ code: string;
+ message: string;
+ details?: Record;
+}
+
+export interface AuthValidationError extends AuthError {
+ field: string;
+}
\ No newline at end of file
diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts
index a812abf0..8d062cdd 100644
--- a/packages/core/src/types/index.ts
+++ b/packages/core/src/types/index.ts
@@ -25,3 +25,6 @@ export * from './event.js';
// Change tracking and field history types
export * from './change-tracking.js';
+
+// Authentication and user management types
+export * from './auth.js';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6c174a1a..f2100525 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -330,6 +330,9 @@ importers:
packages/core:
dependencies:
+ bcrypt:
+ specifier: ^5.1.1
+ version: 5.1.1
better-sqlite3:
specifier: ^11.0.0
version: 11.10.0
@@ -339,6 +342,9 @@ importers:
dotenv:
specifier: 16.5.0
version: 16.5.0
+ jsonwebtoken:
+ specifier: ^9.0.2
+ version: 9.0.2
mysql2:
specifier: ^3.11.0
version: 3.14.1
@@ -355,9 +361,15 @@ importers:
specifier: ^3.22.4
version: 3.25.67
devDependencies:
+ '@types/bcrypt':
+ specifier: ^5.0.2
+ version: 5.0.2
'@types/better-sqlite3':
specifier: ^7.6.0
version: 7.6.13
+ '@types/jsonwebtoken':
+ specifier: ^9.0.7
+ version: 9.0.10
'@types/node':
specifier: ^20.0.0
version: 20.19.1
@@ -796,6 +808,10 @@ packages:
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
+ '@mapbox/node-pre-gyp@1.0.11':
+ resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
+ hasBin: true
+
'@modelcontextprotocol/sdk@1.13.0':
resolution: {integrity: sha512-P5FZsXU0kY881F6Hbk9GhsYx02/KgWK1DYf7/tyE/1lcFKhDYPQR9iYjhQXJn+Sg6hQleMo3DB7h7+p4wgp2Lw==}
engines: {node: '>=18'}
@@ -1466,6 +1482,9 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
+ '@types/bcrypt@5.0.2':
+ resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
+
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
@@ -1511,6 +1530,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+ '@types/jsonwebtoken@9.0.10':
+ resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
+
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -1614,6 +1636,9 @@ packages:
'@vitest/utils@2.1.9':
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
+ abbrev@1.1.1:
+ resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
+
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@@ -1627,6 +1652,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1665,6 +1694,14 @@ packages:
resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
engines: {node: '>= 6.0.0'}
+ aproba@2.1.0:
+ resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==}
+
+ are-we-there-yet@2.0.0:
+ resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
+ engines: {node: '>=10'}
+ deprecated: This package is no longer supported.
+
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -1706,6 +1743,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+ bcrypt@5.1.1:
+ resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==}
+ engines: {node: '>= 10.0.0'}
+
better-sqlite3@11.10.0:
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
@@ -1741,6 +1782,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
+ buffer-equal-constant-time@1.0.1:
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
+
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -1826,6 +1870,10 @@ packages:
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+ chownr@2.0.0:
+ resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
+ engines: {node: '>=10'}
+
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -1872,6 +1920,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ color-support@1.1.3:
+ resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
+ hasBin: true
+
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@@ -1905,6 +1957,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ console-control-strings@1.1.0:
+ resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
+
content-disposition@1.0.0:
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
engines: {node: '>= 0.6'}
@@ -2037,6 +2092,9 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ delegates@1.0.0:
+ resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
+
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
@@ -2092,6 +2150,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ ecdsa-sig-formatter@1.0.11:
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -2295,6 +2356,13 @@ packages:
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+ fs-minipass@2.1.0:
+ resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
+ engines: {node: '>= 8'}
+
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2303,6 +2371,11 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ gauge@3.0.2:
+ resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
+ engines: {node: '>=10'}
+ deprecated: This package is no longer supported.
+
generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
@@ -2344,6 +2417,10 @@ packages:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
+ glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Glob versions prior to v9 are no longer supported
+
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -2370,6 +2447,9 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
+ has-unicode@2.0.1:
+ resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -2433,6 +2513,10 @@ packages:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
+ https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
@@ -2448,6 +2532,10 @@ packages:
ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -2578,6 +2666,16 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ jsonwebtoken@9.0.2:
+ resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
+ engines: {node: '>=12', npm: '>=6'}
+
+ jwa@1.4.2:
+ resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
+
+ jws@3.2.2:
+ resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
+
lightningcss-darwin-arm64@1.30.1:
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
@@ -2661,12 +2759,30 @@ packages:
lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
+ lodash.includes@4.3.0:
+ resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
+
+ lodash.isboolean@3.0.3:
+ resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
+
+ lodash.isinteger@4.0.4:
+ resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
+
+ lodash.isnumber@3.0.3:
+ resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
+
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
+ lodash.isstring@4.0.1:
+ resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
+
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ lodash.once@4.1.1:
+ resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
+
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -2716,6 +2832,10 @@ packages:
magicast@0.3.5:
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+ make-dir@3.1.0:
+ resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
+ engines: {node: '>=8'}
+
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
@@ -2906,13 +3026,30 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ minipass@3.3.6:
+ resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
+ engines: {node: '>=8'}
+
+ minipass@5.0.0:
+ resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
+ engines: {node: '>=8'}
+
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
+ minizlib@2.1.2:
+ resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
+ engines: {node: '>= 8'}
+
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+ mkdirp@1.0.4:
+ resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
+ engines: {node: '>=10'}
+ hasBin: true
+
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -2975,6 +3112,9 @@ packages:
resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==}
engines: {node: '>=10'}
+ node-addon-api@5.1.0:
+ resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
+
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -2992,6 +3132,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ nopt@5.0.0:
+ resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
+ engines: {node: '>=6'}
+ hasBin: true
+
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -3000,6 +3145,10 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
+ npmlog@5.0.1:
+ resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
+ deprecated: This package is no longer supported.
+
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -3052,6 +3201,10 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -3420,6 +3573,11 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+ rimraf@3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
+ hasBin: true
+
rimraf@5.0.10:
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
hasBin: true
@@ -3448,6 +3606,10 @@ packages:
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
@@ -3464,6 +3626,9 @@ packages:
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
engines: {node: '>= 18'}
+ set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -3507,6 +3672,9 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -3681,6 +3849,10 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
+ tar@6.2.1:
+ resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
+ engines: {node: '>=10'}
+
terser@5.43.1:
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
engines: {node: '>=10'}
@@ -4071,6 +4243,9 @@ packages:
engines: {node: '>=8'}
hasBin: true
+ wide-align@1.1.5:
+ resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -4106,6 +4281,9 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
+ yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
@@ -4374,6 +4552,21 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
+ '@mapbox/node-pre-gyp@1.0.11':
+ dependencies:
+ detect-libc: 2.0.4
+ https-proxy-agent: 5.0.1
+ make-dir: 3.1.0
+ node-fetch: 2.7.0
+ nopt: 5.0.0
+ npmlog: 5.0.1
+ rimraf: 3.0.2
+ semver: 7.7.2
+ tar: 6.2.1
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
'@modelcontextprotocol/sdk@1.13.0':
dependencies:
ajv: 6.12.6
@@ -5010,6 +5203,10 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.17
+ '@types/bcrypt@5.0.2':
+ dependencies:
+ '@types/node': 20.19.1
+
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 20.19.1
@@ -5056,6 +5253,11 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/jsonwebtoken@9.0.10':
+ dependencies:
+ '@types/ms': 2.1.0
+ '@types/node': 20.19.1
+
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -5189,6 +5391,8 @@ snapshots:
loupe: 3.1.4
tinyrainbow: 1.2.0
+ abbrev@1.1.1: {}
+
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@@ -5201,6 +5405,12 @@ snapshots:
acorn@8.15.0:
optional: true
+ agent-base@6.0.2:
+ dependencies:
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -5233,6 +5443,13 @@ snapshots:
app-root-path@3.1.0: {}
+ aproba@2.1.0: {}
+
+ are-we-there-yet@2.0.0:
+ dependencies:
+ delegates: 1.0.0
+ readable-stream: 3.6.2
+
arg@5.0.2: {}
aria-hidden@1.2.6:
@@ -5273,6 +5490,14 @@ snapshots:
base64-js@1.5.1: {}
+ bcrypt@5.1.1:
+ dependencies:
+ '@mapbox/node-pre-gyp': 1.0.11
+ node-addon-api: 5.1.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
better-sqlite3@11.10.0:
dependencies:
bindings: 1.5.0
@@ -5326,6 +5551,8 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.3)
+ buffer-equal-constant-time@1.0.1: {}
+
buffer-from@1.1.2:
optional: true
@@ -5432,6 +5659,8 @@ snapshots:
chownr@1.1.4: {}
+ chownr@2.0.0: {}
+
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -5483,6 +5712,8 @@ snapshots:
color-name@1.1.4: {}
+ color-support@1.1.3: {}
+
colorette@2.0.20: {}
combined-stream@1.0.8:
@@ -5512,6 +5743,8 @@ snapshots:
tree-kill: 1.2.2
yargs: 17.7.2
+ console-control-strings@1.1.0: {}
+
content-disposition@1.0.0:
dependencies:
safe-buffer: 5.2.1
@@ -5623,6 +5856,8 @@ snapshots:
delayed-stream@1.0.0: {}
+ delegates@1.0.0: {}
+
denque@2.1.0: {}
depd@2.0.0: {}
@@ -5674,6 +5909,10 @@ snapshots:
eastasianwidth@0.2.0: {}
+ ecdsa-sig-formatter@1.0.11:
+ dependencies:
+ safe-buffer: 5.2.1
+
ee-first@1.1.1: {}
electron-to-chromium@1.5.208: {}
@@ -5910,11 +6149,29 @@ snapshots:
fs-constants@1.0.0: {}
+ fs-minipass@2.1.0:
+ dependencies:
+ minipass: 3.3.6
+
+ fs.realpath@1.0.0: {}
+
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
+ gauge@3.0.2:
+ dependencies:
+ aproba: 2.1.0
+ color-support: 1.1.3
+ console-control-strings: 1.1.0
+ has-unicode: 2.0.1
+ object-assign: 4.1.1
+ signal-exit: 3.0.7
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wide-align: 1.1.5
+
generate-function@2.3.1:
dependencies:
is-property: 1.0.2
@@ -5966,6 +6223,15 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
+ glob@7.2.3:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@@ -5984,6 +6250,8 @@ snapshots:
dependencies:
has-symbols: 1.1.0
+ has-unicode@2.0.1: {}
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -6114,6 +6382,13 @@ snapshots:
statuses: 2.0.1
toidentifier: 1.0.1
+ https-proxy-agent@5.0.1:
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.4.1
+ transitivePeerDependencies:
+ - supports-color
+
husky@9.1.7: {}
iconv-lite@0.6.3:
@@ -6124,6 +6399,11 @@ snapshots:
ignore-by-default@1.0.1: {}
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
inherits@2.0.4: {}
ini@1.3.8: {}
@@ -6226,6 +6506,30 @@ snapshots:
json-schema-traverse@0.4.1: {}
+ jsonwebtoken@9.0.2:
+ dependencies:
+ jws: 3.2.2
+ lodash.includes: 4.3.0
+ lodash.isboolean: 3.0.3
+ lodash.isinteger: 4.0.4
+ lodash.isnumber: 3.0.3
+ lodash.isplainobject: 4.0.6
+ lodash.isstring: 4.0.1
+ lodash.once: 4.1.1
+ ms: 2.1.3
+ semver: 7.7.2
+
+ jwa@1.4.2:
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.1
+
+ jws@3.2.2:
+ dependencies:
+ jwa: 1.4.2
+ safe-buffer: 5.2.1
+
lightningcss-darwin-arm64@1.30.1:
optional: true
@@ -6302,10 +6606,22 @@ snapshots:
lodash.castarray@4.4.0: {}
+ lodash.includes@4.3.0: {}
+
+ lodash.isboolean@3.0.3: {}
+
+ lodash.isinteger@4.0.4: {}
+
+ lodash.isnumber@3.0.3: {}
+
lodash.isplainobject@4.0.6: {}
+ lodash.isstring@4.0.1: {}
+
lodash.merge@4.6.2: {}
+ lodash.once@4.1.1: {}
+
lodash@4.17.21: {}
log-symbols@6.0.0:
@@ -6357,6 +6673,10 @@ snapshots:
'@babel/types': 7.28.1
source-map-js: 1.2.1
+ make-dir@3.1.0:
+ dependencies:
+ semver: 6.3.1
+
make-dir@4.0.0:
dependencies:
semver: 7.7.2
@@ -6746,10 +7066,23 @@ snapshots:
minimist@1.2.8: {}
+ minipass@3.3.6:
+ dependencies:
+ yallist: 4.0.0
+
+ minipass@5.0.0: {}
+
minipass@7.1.2: {}
+ minizlib@2.1.2:
+ dependencies:
+ minipass: 3.3.6
+ yallist: 4.0.0
+
mkdirp-classic@0.5.3: {}
+ mkdirp@1.0.4: {}
+
mrmime@2.0.1: {}
ms@2.1.3: {}
@@ -6818,6 +7151,8 @@ snapshots:
dependencies:
semver: 7.7.2
+ node-addon-api@5.1.0: {}
+
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
@@ -6837,10 +7172,21 @@ snapshots:
touch: 3.1.1
undefsafe: 2.0.5
+ nopt@5.0.0:
+ dependencies:
+ abbrev: 1.1.1
+
normalize-path@3.0.0: {}
normalize-range@0.1.2: {}
+ npmlog@5.0.1:
+ dependencies:
+ are-we-there-yet: 2.0.0
+ console-control-strings: 1.1.0
+ gauge: 3.0.2
+ set-blocking: 2.0.0
+
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
@@ -6904,6 +7250,8 @@ snapshots:
parseurl@1.3.3: {}
+ path-is-absolute@1.0.1: {}
+
path-key@3.1.1: {}
path-parse@1.0.7: {}
@@ -7324,6 +7672,10 @@ snapshots:
rfdc@1.4.1: {}
+ rimraf@3.0.2:
+ dependencies:
+ glob: 7.2.3
+
rimraf@5.0.10:
dependencies:
glob: 10.4.5
@@ -7380,6 +7732,8 @@ snapshots:
dependencies:
loose-envify: 1.4.0
+ semver@6.3.1: {}
+
semver@7.7.2: {}
send@1.2.0:
@@ -7409,6 +7763,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ set-blocking@2.0.0: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -7464,6 +7820,8 @@ snapshots:
siginfo@2.0.0: {}
+ signal-exit@3.0.7: {}
+
signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
@@ -7654,6 +8012,15 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
+ tar@6.2.1:
+ dependencies:
+ chownr: 2.0.0
+ fs-minipass: 2.1.0
+ minipass: 5.0.0
+ minizlib: 2.1.2
+ mkdirp: 1.0.4
+ yallist: 4.0.0
+
terser@5.43.1:
dependencies:
'@jridgewell/source-map': 0.3.10
@@ -8020,6 +8387,10 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
+ wide-align@1.1.5:
+ dependencies:
+ string-width: 4.2.3
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -8046,6 +8417,8 @@ snapshots:
y18n@5.0.8: {}
+ yallist@4.0.0: {}
+
yaml@2.8.0: {}
yargs-parser@21.1.1: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index efb95e02..7bbf236b 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -3,5 +3,6 @@ packages:
- apps/*
onlyBuiltDependencies:
+ - bcrypt
- better-sqlite3
- esbuild