Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions docs/AUTH_STATE_MACHINE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# AUTH BOOTSTRAP STATE MACHINE

**Status:** IN PROGRESS
**Date:** 2026-07-26

---

## Problem

19 commits adjusted auth timeout (8s→4s→fast-fail→optimistic). The code is:
- Timeouts stacked on each other
- No clear state transitions
- Hard to reason about

---

## Solution: State Machine

```typescript
// src/lib/authMachine.ts

type AuthState =
| 'idle'
| 'booting'
| 'authenticating'
| 'authenticated'
| 'anonymous'
| 'offline'
| 'error';

interface AuthContext {
state: AuthState;
user: User | null;
error: Error | null;
lastChecked: Date | null;
}

type AuthEvent =
| { type: 'CHECK' }
| { type: 'CHECK_SUCCESS'; user: User }
| { type: 'CHECK_FAILED'; error: Error }
| { type: 'GO_OFFLINE' }
| { type: 'GO_ONLINE' }
| { type: 'RESET' };

// State transitions
const authMachine: StateMachine<AuthState, AuthEvent, AuthContext> = {
initial: 'idle',

states: {
idle: {
on: { CHECK: 'booting' }
},

booting: {
on: {
CHECK_SUCCESS: 'authenticated',
CHECK_FAILED: 'error',
GO_OFFLINE: 'offline',
TIMEOUT: 'anonymous'
}
},

authenticated: {
on: {
CHECK: 'booting',
GO_OFFLINE: 'offline'
}
},

anonymous: {
on: {
CHECK: 'booting'
}
},

offline: {
on: {
GO_ONLINE: 'booting'
}
},

error: {
on: {
RESET: 'idle',
CHECK: 'booting'
}
}
}
};
```

---

## Hook Implementation

```typescript
// src/hooks/useAuthState.ts

export function useAuthState() {
const [context, dispatch] = useReducer(authReducer, { state: 'idle' });

const check = useCallback(async () => {
dispatch({ type: 'CHECK' });

try {
const { data: { user } } = await supabase.auth.getUser();

if (user) {
dispatch({ type: 'CHECK_SUCCESS', user });
} else {
dispatch({ type: 'CHECK_FAILED', error: new Error('No user') });
}
} catch (error) {
dispatch({ type: 'CHECK_FAILED', error });
}
}, []);

// ... rest of implementation
}
```

---

## State Diagram

```
┌───────┐
│ idle │
└───┬───┘
│ CHECK
┌─────────┐
───►│ booting │
└────┬────┘
┌────┼────┬──────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌─────┐ ┌──────────┐
│authen-│ │error│ │ anonymous│
│ticated│ └─────┘ └──────────┘
└────────┘
```

---

*Document Status: IN PROGRESS*
147 changes: 147 additions & 0 deletions docs/BRANDED_TYPES_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# BRANDED TYPES IMPLEMENTATION

**Status:** IN PROGRESS
**Date:** 2026-07-26

---

## Problem

27+ commits added `isValidUUID()` guards to prevent JID being used as UUID. This is treating symptoms, not the cause.

```typescript
// Current: Guards everywhere
if (!isValidUUID(id)) return; // JID check
const result = await query(id); // Still works with JID

// Problem: Any new file forgets this check
```

---

## Solution: Branded Types

### Implementation

```typescript
// src/types/branded.ts

// Branded type for JID (WhatsApp ID)
type JID = string & { readonly __brand: 'JID' };

// Branded type for UUID (PostgreSQL)
type Uuid = string & { readonly __brand: 'Uuid' };

// Constructor functions (only way to create branded values)
function asJID(value: string): JID {
return value as JID;
}

function asUuid(value: string): Uuid {
// Validate UUID format
if (!isValidUUID(value)) {
throw new Error(`Invalid UUID: ${value}`);
}
return value as Uuid;
}

// Type guards
function isJID(value: string): value is JID {
return value.includes('@');
}

function isUuid(value: string): value is Uuid {
return isValidUUID(value);
}
```

---

## Usage

### Before

```typescript
// prone to errors
async function getContact(id: string) {
return supabase.from('contacts').select().eq('id', id);
}
```

### After

```typescript
// type-safe
async function getContact(id: Uuid) {
return supabase.from('contacts').select().eq('id', id);
}

// Compile error: Argument of type 'JID' is not assignable to parameter of type 'Uuid'
getContact(contactJid);
```

---

## Migration Plan

### Phase 1: Define Types

```typescript
// src/types/branded.ts
export type Jid = string & { readonly __brand: 'Jid' };
export type Uuid = string & { readonly __brand: 'Uuid' };
```

### Phase 2: Create Conversion Functions

```typescript
// src/types/branded.ts
export function toUuid(value: string): Uuid {
if (!isValidUUID(value)) {
throw new Error(`Invalid UUID: ${value.substring(0, 20)}...`);
}
return value as Uuid;
}

export function toJid(value: string): Jid {
return value as Jid;
}
```

### Phase 3: Update Function Signatures

```typescript
// Before
async function getMessage(id: string): Promise<Message>

// After
async function getMessage(id: Uuid): Promise<Message>
```

### Phase 4: Remove Guards

After all functions use branded types, remove `isValidUUID()` guards.

---

## Expected Impact

| Metric | Before | After |
|--------|--------|-------|
| `isValidUUID()` calls | 60+ | 0 |
| Type errors for JID-as-UUID | Runtime | Compile-time |
| Bug class recurrence | High | None |

---

## Files to Update

Priority order:
1. Type definitions
2. Repository functions
3. Hook parameters
4. API routes

---

*Document Status: IN PROGRESS*
Loading
Loading