refactor(auth): absorb shared-auth-stores into shared-auth-ui

Merge the auth store factories (createManaAuthStore, createAuthStore) from
@manacore/shared-auth-stores into @manacore/shared-auth-ui, reducing
from 3 auth packages to 2.

- Copy store files into shared-auth-ui/src/stores/
- Re-export store factories and types from shared-auth-ui
- Update imports in manacore/web and arcade/web
- Remove shared-auth-stores from active package.json dependencies

Result: @manacore/shared-auth (core, platform-agnostic) +
        @manacore/shared-auth-ui (Svelte components + stores)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 21:43:42 +02:00
parent baca70155a
commit 03434c2802
8 changed files with 612 additions and 4 deletions

View file

@ -33,6 +33,18 @@ export {
} from './utils/guestNudge';
export { parseUserAgent, getDeviceType, formatUserAgent } from './utils/userAgent';
// Auth Stores (absorbed from @manacore/shared-auth-stores)
export { createManaAuthStore } from './stores/createManaAuthStore.svelte';
export type { ManaAuthStoreConfig, ManaAuthStore } from './stores/createManaAuthStore.svelte';
export { createAuthStore } from './stores/createAuthStore.svelte';
export type {
BaseUser,
AuthServiceAdapter,
AuthStoreState,
AuthStoreActions,
AuthStore,
} from './stores/store-types';
// Types
export type {
AuthUIConfig,

View file

@ -0,0 +1,186 @@
/**
* Svelte 5 Auth Store Factory
*
* Creates a reactive auth store using Svelte 5 runes.
* Generic over user type to support app-specific user models.
*
* @example
* ```ts
* // In your app's auth store file
* import { createAuthStore } from '@manacore/shared-auth-stores';
* import { authService } from '$lib/auth';
* import type { AppUser } from '$lib/types';
*
* export const authStore = createAuthStore<AppUser>(authService);
* ```
*/
import type { AuthServiceAdapter, AuthResult, BaseUser } from './types';
/**
* Create a Svelte 5 runes-based auth store
*
* @param authService - Auth service adapter implementing the AuthServiceAdapter interface
* @returns Reactive auth store with state and actions
*/
export function createAuthStore<TUser extends BaseUser>(authService: AuthServiceAdapter<TUser>) {
// Reactive state using Svelte 5 runes
let user = $state<TUser | null>(null);
let loading = $state(true);
let error = $state<string | null>(null);
return {
// Reactive getters
get user() {
return user;
},
get loading() {
return loading;
},
get error() {
return error;
},
get isAuthenticated() {
return !!user;
},
/**
* Initialize auth state from stored tokens/session
*/
async initialize() {
loading = true;
error = null;
try {
const isAuth = await authService.isAuthenticated();
if (isAuth) {
user = await authService.getUserFromToken();
} else {
user = null;
}
} catch (e) {
console.error('Failed to initialize auth:', e);
error = e instanceof Error ? e.message : 'Failed to initialize authentication';
user = null;
} finally {
loading = false;
}
},
/**
* Set user manually (useful for SSR hydration)
*/
setUser(newUser: TUser | null) {
user = newUser;
error = null;
},
/**
* Sign in with email and password
*/
async signIn(email: string, password: string): Promise<AuthResult<TUser>> {
loading = true;
error = null;
try {
const result = await authService.signIn(email, password);
if (result.success) {
user = await authService.getUserFromToken();
} else {
error = result.error || 'Sign in failed';
}
return result;
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Sign in failed';
error = errorMessage;
return { success: false, error: errorMessage };
} finally {
loading = false;
}
},
/**
* Sign up with email and password
*/
async signUp(email: string, password: string): Promise<AuthResult<TUser>> {
loading = true;
error = null;
try {
const result = await authService.signUp(email, password);
if (result.success && !result.needsVerification) {
user = await authService.getUserFromToken();
} else if (!result.success) {
error = result.error || 'Sign up failed';
}
return result;
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Sign up failed';
error = errorMessage;
return { success: false, error: errorMessage };
} finally {
loading = false;
}
},
/**
* Send password reset email
*/
async forgotPassword(email: string): Promise<{ success: boolean; error?: string }> {
loading = true;
error = null;
try {
const result = await authService.forgotPassword(email);
if (!result.success) {
error = result.error || 'Password reset failed';
}
return result;
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Password reset failed';
error = errorMessage;
return { success: false, error: errorMessage };
} finally {
loading = false;
}
},
/**
* Sign out user
*/
async signOut() {
loading = true;
error = null;
try {
await authService.signOut();
user = null;
} catch (e) {
console.error('Sign out failed:', e);
error = e instanceof Error ? e.message : 'Sign out failed';
} finally {
loading = false;
}
},
/**
* Check authentication status
*/
async checkAuth(): Promise<boolean> {
try {
const isAuth = await authService.isAuthenticated();
if (!isAuth) {
user = null;
return false;
}
return true;
} catch (e) {
console.error('Auth check failed:', e);
user = null;
return false;
}
},
/**
* Clear error state
*/
clearError() {
error = null;
},
};
}

View file

@ -0,0 +1,320 @@
/**
* Mana Auth Store Factory
*
* Creates a complete auth store using @manacore/shared-auth.
* Replaces the ~350 lines of duplicated auth.svelte.ts in each app
* with a single factory call.
*
* @example
* ```ts
* // apps/todo/apps/web/src/lib/stores/auth.svelte.ts
* import { createManaAuthStore } from '@manacore/shared-auth-stores';
*
* export const authStore = createManaAuthStore({
* devBackendPort: 3031,
* });
* ```
*
* @example With post-login callback
* ```ts
* import { createManaAuthStore } from '@manacore/shared-auth-stores';
* import { apiClient } from '$lib/api/client';
*
* export const authStore = createManaAuthStore({
* devBackendPort: 3030,
* onAuthenticated: async (authService) => {
* const token = await authService.getAppToken();
* if (token) apiClient.setAccessToken(token);
* },
* });
* ```
*/
import { browser } from '$app/environment';
import { initializeWebAuth, type UserData, type AuthServiceInterface } from '@manacore/shared-auth';
export interface ManaAuthStoreConfig {
/** Dev backend port (e.g. 3031 for todo). Only used in development. */
devBackendPort?: number;
/** Dev auth port. Defaults to 3001. */
devAuthPort?: number;
/** Callback after successful authentication (sign in, SSO, 2FA). */
onAuthenticated?: (authService: AuthServiceInterface) => void | Promise<void>;
/** Callback after sign out. */
onSignOut?: () => void | Promise<void>;
}
export function createManaAuthStore(config: ManaAuthStoreConfig = {}) {
const devAuthUrl = `http://localhost:${config.devAuthPort ?? 3001}`;
const devBackendUrl = config.devBackendPort ? `http://localhost:${config.devBackendPort}` : '';
// URL resolution (runtime, not build-time)
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
const injected = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
if (injected) return injected;
return import.meta.env.DEV ? devAuthUrl : '';
}
return process.env.PUBLIC_MANA_CORE_AUTH_URL || devAuthUrl;
}
function getBackendUrl(): string {
if (browser && typeof window !== 'undefined') {
const injected = (window as unknown as { __PUBLIC_BACKEND_URL__?: string })
.__PUBLIC_BACKEND_URL__;
if (injected) return injected;
return import.meta.env.DEV ? devBackendUrl : '';
}
return process.env.PUBLIC_BACKEND_URL || devBackendUrl;
}
// Lazy init (SSR-safe)
let _authService: AuthServiceInterface | null = null;
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
function getAuthService() {
if (!browser) return null;
if (!_authService) {
const backendUrl = getBackendUrl();
const auth = initializeWebAuth({
baseUrl: getAuthUrl(),
...(backendUrl ? { backendUrl } : {}),
});
_authService = auth.authService;
_tokenManager = auth.tokenManager;
}
return _authService;
}
function getTokenManager() {
if (!browser) return null;
getAuthService();
return _tokenManager;
}
async function handleAuthenticated() {
if (config.onAuthenticated) {
const svc = getAuthService();
if (svc) await config.onAuthenticated(svc);
}
}
// Reactive state
let user = $state<UserData | null>(null);
let loading = $state(true);
let initialized = $state(false);
return {
// Getters
get user() {
return user;
},
get loading() {
return loading;
},
get isAuthenticated() {
return !!user;
},
get initialized() {
return initialized;
},
async initialize() {
if (initialized) return;
const authService = getAuthService();
if (!authService) {
initialized = true;
loading = false;
return;
}
loading = true;
try {
let authenticated = await authService.isAuthenticated();
if (!authenticated) {
const ssoResult = await authService.trySSO();
if (ssoResult.success) authenticated = true;
}
if (authenticated) {
user = await authService.getUserFromToken();
await handleAuthenticated();
}
initialized = true;
} catch (error) {
console.error('Failed to initialize auth:', error);
user = null;
} finally {
loading = false;
}
},
// 2FA
async verifyTwoFactor(code: string, trustDevice?: boolean) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
const result = await authService.verifyTwoFactor(code, trustDevice);
if (result.success) {
user = await authService.getUserFromToken();
await handleAuthenticated();
}
return result;
},
async verifyBackupCode(code: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
const result = await authService.verifyBackupCode(code);
if (result.success) {
user = await authService.getUserFromToken();
await handleAuthenticated();
}
return result;
},
// Magic Link
async sendMagicLink(email: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
return authService.sendMagicLink(email);
},
// Passkeys
isPasskeyAvailable(): boolean {
const authService = getAuthService();
if (!authService) return false;
return authService.isPasskeyAvailable();
},
async signInWithPasskey() {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
try {
const result = await authService.signInWithPasskey();
if (!result.success)
return { success: false, error: result.error || 'Passkey authentication failed' };
user = await authService.getUserFromToken();
await handleAuthenticated();
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
},
// Sign In
async signIn(email: string, password: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
try {
const result = await authService.signIn(email, password);
if (!result.success) return { success: false, error: result.error || 'Login failed' };
user = await authService.getUserFromToken();
await handleAuthenticated();
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
},
// Sign Up
async signUp(email: string, password: string) {
const authService = getAuthService();
if (!authService)
return { success: false, error: 'Auth not available on server', needsVerification: false };
try {
const sourceAppUrl = browser ? window.location.origin : undefined;
const result = await authService.signUp(email, password, sourceAppUrl);
if (!result.success)
return {
success: false,
error: result.error || 'Signup failed',
needsVerification: false,
};
if (result.needsVerification) return { success: true, needsVerification: true };
const signInResult = await this.signIn(email, password);
return { ...signInResult, needsVerification: false };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
needsVerification: false,
};
}
},
// Sign Out
async signOut() {
const authService = getAuthService();
if (!authService) {
user = null;
return;
}
try {
await authService.signOut();
user = null;
if (config.onSignOut) await config.onSignOut();
} catch (error) {
console.error('Sign out error:', error);
user = null;
}
},
// Password Reset
async resetPassword(email: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
try {
const redirectTo = browser ? window.location.origin : undefined;
const result = await authService.forgotPassword(email, redirectTo);
if (!result.success)
return { success: false, error: result.error || 'Password reset failed' };
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
},
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success)
return { success: false, error: result.error || 'Failed to reset password' };
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
},
// Token access
async getAccessToken() {
const authService = getAuthService();
if (!authService) return null;
return await authService.getAppToken();
},
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) return null;
return await tokenManager.getValidToken();
},
// Email verification
async resendVerificationEmail(email: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
try {
const sourceAppUrl = browser ? window.location.origin : undefined;
const result = await authService.resendVerificationEmail(email, sourceAppUrl);
if (!result.success)
return { success: false, error: result.error || 'Failed to send verification email' };
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
},
};
}
export type ManaAuthStore = ReturnType<typeof createManaAuthStore>;

View file

@ -0,0 +1,92 @@
/**
* Shared Auth Store Types
* Generic types for creating auth stores with custom user types
*/
/**
* Base user interface that all app-specific user types must extend
*/
export interface BaseUser {
id: string;
email: string;
}
/**
* Auth operation result with typed user
*/
export interface AuthResult<TUser extends BaseUser = BaseUser> {
success: boolean;
error?: string;
needsVerification?: boolean;
user?: TUser;
}
/**
* Auth service interface that auth stores expect
* Apps implement this to integrate with their auth backend
*/
export interface AuthServiceAdapter<TUser extends BaseUser = BaseUser> {
/** Check if user is authenticated */
isAuthenticated(): Promise<boolean>;
/** Get user data from stored token/session */
getUserFromToken(): Promise<TUser | null>;
/** Sign in with email and password */
signIn(email: string, password: string): Promise<AuthResult<TUser>>;
/** Sign up with email and password */
signUp(email: string, password: string): Promise<AuthResult<TUser>>;
/** Send password reset email */
forgotPassword(email: string): Promise<{ success: boolean; error?: string }>;
/** Sign out user */
signOut(): Promise<void>;
}
/**
* Auth store state interface
*/
export interface AuthStoreState<TUser extends BaseUser = BaseUser> {
user: TUser | null;
loading: boolean;
error: string | null;
isAuthenticated: boolean;
}
/**
* Auth store actions interface
*/
export interface AuthStoreActions<TUser extends BaseUser = BaseUser> {
/** Initialize auth state from stored tokens */
initialize(): Promise<void>;
/** Set user manually */
setUser(user: TUser | null): void;
/** Sign in with email and password */
signIn(email: string, password: string): Promise<AuthResult<TUser>>;
/** Sign up with email and password */
signUp(email: string, password: string): Promise<AuthResult<TUser>>;
/** Send password reset email */
forgotPassword(email: string): Promise<{ success: boolean; error?: string }>;
/** Sign out user */
signOut(): Promise<void>;
/** Check authentication status */
checkAuth(): Promise<boolean>;
/** Clear error state */
clearError(): void;
}
/**
* Complete auth store interface
*/
export interface AuthStore<TUser extends BaseUser = BaseUser>
extends AuthStoreState<TUser>,
AuthStoreActions<TUser> {}