| name | afd-auth |
| description | Provider-agnostic authentication adapter for AFD applications. Covers AuthAdapter interface, discriminated union session states, middleware for auth-gated commands, AFD command wrappers, multi-tab session sync, React hooks, and built-in adapters (Mock, Convex, BetterAuth). Triggers: auth, authentication, sign-in, sign-out, session, auth adapter, auth middleware, auth commands, convex auth, better-auth.
|
AFD Auth Patterns
@lushly-dev/afd-auth provides provider-agnostic authentication for AFD applications.
Core Design
Three session states as a discriminated union — TypeScript enforces status check before accessing user:
type AuthSessionState =
| { status: 'unauthenticated'; session: null; user: null }
| { status: 'loading'; session: null; user: null }
| { status: 'authenticated'; session: Session; user: User };
const state = adapter.getSession();
if (state.status === 'authenticated') {
console.log(state.user.email);
}
No token on Session — tokens are internal to adapter implementations.
No refreshing state — deferred to future versions.
AuthAdapter Interface
interface AuthAdapter {
signIn(options: SignInOptions): Promise<void>;
signOut(): Promise<void>;
getSession(): AuthSessionState;
onAuthStateChange(callback: (state: AuthSessionState) => void): { unsubscribe: () => void };
}
SignInOptions is a discriminated union on method:
type SignInOptions =
| { method: 'credentials'; email: string; password?: string }
| { method: 'oauth'; provider: Provider; scopes?: string[]; redirectTo?: string };
Auth Middleware
Gates commands behind authentication using CommandMiddleware from @lushly-dev/afd-core:
import { createAuthMiddleware } from '@lushly-dev/afd-auth';
const middleware = createAuthMiddleware(adapter, {
exclude: ['auth-sign-in', 'auth-session-get'],
});
AFD Commands
createAuthCommands(adapter) returns three CommandDefinition[]:
| Command | mutation | destructive | expose |
|---|
auth-sign-in | true | false | palette/agent/cli (NOT mcp) |
auth-sign-out | true | true | palette/agent/cli (NOT mcp) |
auth-session-get | false | false | palette/agent/cli/mcp |
Requires @lushly-dev/afd-server + zod as peer dependencies.
Error Handling
import { AuthAdapterError } from '@lushly-dev/afd-auth';
AuthAdapterError.invalidCredentials()
AuthAdapterError.tokenExpired()
AuthAdapterError.providerError(name)
AuthAdapterError.networkError()
AuthAdapterError.refreshFailed()
Built-in Adapters
MockAuthAdapter (Testing)
import { MockAuthAdapter } from '@lushly-dev/afd-auth';
const adapter = new MockAuthAdapter({ delay: 50 });
adapter._setUser({ id: 'u1', email: 'test@example.com' });
adapter._setLoading();
adapter._reset();
adapter._triggerError('INVALID_CREDENTIALS');
adapter._getListenerCount();
useConvexAuthAdapter (React Hook)
import { useConvexAuthAdapter } from '@lushly-dev/afd-auth';
const adapter = useConvexAuthAdapter({
useAuthActions: () => useAuthActions(),
useConvexAuth: () => useConvexAuth(),
meQuery: () => useQuery(api.users.me),
});
Synthetic expiresAt (24h) — Convex manages tokens internally.
BetterAuthAdapter
import { BetterAuthAdapter } from '@lushly-dev/afd-auth';
const adapter = new BetterAuthAdapter({ client: authClient });
adapter.dispose();
React Hooks
Sub-path import — no React dependency on main entrypoint:
import { createAuthHooks } from '@lushly-dev/afd-auth/react';
const { useAuth, useSession, useUser } = createAuthHooks(adapter);
Session Sync (Multi-Tab)
import { SessionSync } from '@lushly-dev/afd-auth';
const sync = new SessionSync({
channelName: 'afd-auth-session',
lockTimeoutMs: 10_000,
debounceMs: 100,
visibilityRefreshMs: 300_000,
});
sync.notifySessionChanged(data);
sync.onSessionChanged(callback);
sync.acquireRefreshLock();
sync.releaseRefreshLock();
sync.dispose();
BroadcastChannel primary, localStorage storage event fallback. SSR-safe.
Package Structure
packages/auth/src/
├── index.ts # Main export (zero React dependency)
├── types.ts # AuthAdapter, AuthSessionState, Session, User
├── errors.ts # AuthAdapterError, AuthErrorCode
├── middleware.ts # createAuthMiddleware()
├── commands.ts # createAuthCommands()
├── session-sync.ts # SessionSync class
├── react.ts # Sub-path: createAuthHooks()
└── adapters/
├── mock.ts # MockAuthAdapter
├── convex.ts # useConvexAuthAdapter()
└── better-auth.ts # BetterAuthAdapter