managarten/packages/shared-auth
Till JS c31ce4448f fix(packages): modal keydown handlers, $derived.by usage, UserData fields
Eight more package-level type errors that all came from the same
small handful of patterns.

Modal escape-key handlers calling click-style functions
  Four modals (AuthGateModal, GuestWelcomeModal, ConfirmationPopover,
  ShareModal) had `onkeydown={(e) => { if (e.key === 'Escape')
  handleBackdropClick(); }}` — but handleBackdropClick took a MouseEvent
  parameter, so the no-arg call failed with "Expected 1 arguments,
  got 0". Fix: route the keyboard escape path through the right
  no-arg helper (`onClose` / `handleClose` / `handleContinueAsGuest`)
  or pass the keyboard event through with a cast for the popover
  trigger that genuinely shares its handler with the click path.

WallpaperModal $derived
  `currentLayout` and `currentBackground` were declared with
  `$derived(() => {...})` — passing a function expression. The
  variant that takes a thunk is `$derived.by(...)`; plain `$derived`
  expects a single value expression. Result: the variables held the
  arrow function itself, the call sites had to invoke them as
  `currentLayout()`, and TS rejected the function value where Layout
  was expected. Switch to `$derived.by`, drop the call-site parens.

TagList.svelte
  Generic param was named `Tag` in the handler signature
  (`tag: Tag`) but the imported type was aliased as `TagType`. Tag
  was undefined → "Cannot find name 'Tag'". Renamed to TagType.

TagStrip.svelte
  `dropAccepts?: string[]` is too wide for `passiveDropZone`'s
  `accepts: DragType[]`. Narrowed the prop type to `DragType[]`
  and added the missing import.

shared-auth/types: UserData.{name,image}?
  Two more optional fields for the public user shape. Both come
  from the JWT user_metadata claim when the user has filled in
  their profile during onboarding. Without these the
  ProfileStep.svelte onboarding component couldn't read
  `authStore.user?.name` / `?.image` without `as any`. Added
  alongside `twoFactorEnabled` from the previous shared-auth
  commit; same Optional rationale (guest tokens omit the claim).

Net: -10 type errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:24:05 +02:00
..
src fix(packages): modal keydown handlers, $derived.by usage, UserData fields 2026-04-09 20:24:05 +02:00
package.json chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
README.md chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
tsconfig.json style: auto-format codebase with Prettier 2025-11-27 18:33:16 +01:00
vitest.config.js test(auth): add 68 unit tests for auth-ui, shared-auth, and shared-branding 2026-03-31 16:35:16 +02:00

@mana/shared-auth

Shared authentication utilities for Mana apps. This package provides a configurable authentication service that can be used across React Native (Expo) and web apps.

Features

  • Configurable Auth Service: Create auth services with custom base URLs and endpoints
  • Token Manager: Handle token refresh, queueing, and state management
  • JWT Utilities: Decode tokens, check expiration, extract user data
  • Fetch Interceptor: Automatically attach tokens and handle 401 responses
  • Platform Adapters: Pluggable storage, device, and network adapters

Installation

pnpm add @mana/shared-auth

Quick Start

Web (SvelteKit, React, etc.)

import { initializeWebAuth } from '@mana/shared-auth';

const { authService, tokenManager } = initializeWebAuth({
	baseUrl: 'https://api.example.com',
});

// Sign in
const result = await authService.signIn('user@example.com', 'password');
if (result.success) {
	console.log('Signed in!');
}

// Get current user
const user = await authService.getUserFromToken();
console.log(user?.email);

// Sign out
await authService.signOut();

React Native (Expo)

import {
	createAuthService,
	createTokenManager,
	setStorageAdapter,
	setDeviceAdapter,
	setNetworkAdapter,
	setupFetchInterceptor,
} from '@mana/shared-auth';
import * as SecureStore from 'expo-secure-store';

// Create storage adapter for Expo
const expoStorageAdapter = {
	async getItem<T = string>(key: string): Promise<T | null> {
		const value = await SecureStore.getItemAsync(key);
		if (!value) return null;
		try {
			return JSON.parse(value) as T;
		} catch {
			return value as T;
		}
	},
	async setItem(key: string, value: string): Promise<void> {
		await SecureStore.setItemAsync(key, value);
	},
	async removeItem(key: string): Promise<void> {
		await SecureStore.deleteItemAsync(key);
	},
};

// Set up adapters
setStorageAdapter(expoStorageAdapter);
setDeviceAdapter(yourDeviceAdapter);
setNetworkAdapter(yourNetworkAdapter);

// Create services
const authService = createAuthService({
	baseUrl: process.env.EXPO_PUBLIC_API_URL,
});
const tokenManager = createTokenManager(authService);

// Set up fetch interceptor
setupFetchInterceptor(authService, tokenManager);

API Reference

createAuthService(config)

Creates an authentication service instance.

const authService = createAuthService({
	baseUrl: 'https://api.example.com',
	storageKeys: {
		APP_TOKEN: '@auth/appToken',
		REFRESH_TOKEN: '@auth/refreshToken',
		USER_EMAIL: '@auth/userEmail',
	},
	endpoints: {
		signIn: '/auth/signin',
		signUp: '/auth/signup',
		// ... other endpoints
	},
});

createTokenManager(authService, config?)

Creates a token manager for handling token refresh and state.

const tokenManager = createTokenManager(authService, {
	maxQueueSize: 50,
	queueTimeoutMs: 30000,
	maxRefreshAttempts: 3,
	refreshCooldownMs: 5000,
});

// Subscribe to state changes
const unsubscribe = tokenManager.subscribe((state, token) => {
	console.log('Token state:', state);
});

// Get valid token (refreshes if needed)
const token = await tokenManager.getValidToken();

JWT Utilities

import {
	decodeToken,
	isTokenValidLocally,
	getUserFromToken,
	isB2BUser,
	getB2BInfo,
} from '@mana/shared-auth';

const payload = decodeToken(token);
const isValid = isTokenValidLocally(token);
const user = getUserFromToken(token);
const isB2B = isB2BUser(token);

Adapters

The package uses adapters for platform-specific functionality:

  • StorageAdapter: For storing tokens securely
  • DeviceAdapter: For getting device information
  • NetworkAdapter: For checking network connectivity
import { setStorageAdapter, setDeviceAdapter, setNetworkAdapter } from '@mana/shared-auth';

setStorageAdapter(myStorageAdapter);
setDeviceAdapter(myDeviceAdapter);
setNetworkAdapter(myNetworkAdapter);

Migration from Existing Auth

To migrate from existing auth implementations:

  1. Install the package
  2. Set up the adapters for your platform
  3. Replace direct authService calls with the shared service
  4. Update token manager usage

Before

// memoro/apps/mobile/features/auth/services/authService.ts
import { authService } from './authService';
await authService.signIn(email, password);

After

// Use the shared auth service
import { authService } from '@/services/auth'; // Your configured instance
await authService.signIn(email, password);

Token States

The token manager tracks these states:

  • IDLE: Initial state
  • VALID: Token is valid
  • REFRESHING: Token refresh in progress
  • EXPIRED: Token has expired
  • EXPIRED_OFFLINE: Token expired while offline (preserves auth)

Contributing

  1. Make changes to the source files in src/
  2. Run pnpm run type-check to validate TypeScript
  3. Run pnpm run build to compile