.crustagent/skills/user-session/SKILL.md
# ClawStack User Session Management Skill **Version:** 1.0 **Status:** Production Ready **Last Updated:** 2026-03-08 --- ## Overview This skill documents the complete user session lifecycle in ClawStack applications. It covers token generation, storage, verification, expiration handling, and cleanup—providing new developers with everything needed to implement session management correctly in any new ClawStack app. **Key Characteristics:** - **Token Format:** `api-` prefix + 32 base62 charact
npx skillsauth add acidgreenservers/clawchives .crustagent/skills/user-sessionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Version: 1.0 Status: Production Ready Last Updated: 2026-03-08
This skill documents the complete user session lifecycle in ClawStack applications. It covers token generation, storage, verification, expiration handling, and cleanup—providing new developers with everything needed to implement session management correctly in any new ClawStack app.
Key Characteristics:
api- prefix + 32 base62 characters (36 total)sessionStorage with cc_* key prefix┌─────────────────────────────────────────────────────────────────┐
│ ClawStack Session Lifecycle │
└─────────────────────────────────────────────────────────────────┘
[User Opens App]
│
▼
┌─────────────────┐
│ Check Browser │
│ sessionStorage │
└────────┬────────┘
│
┌─────┴──────────┐
│ │
[Token?] [No Token?]
│ │
▼ ▼
[Verify] [Show Login]
Token Exists Form
│
▼
GET /api/auth/verify
(Bearer token)
│
┌──┴──────────┐
│ │
[✓ Valid] [✗ Expired]
│ │
▼ ▼
Auth Clear Session
Success → Logout
│
▼
[During Session]
│
├─ User makes API calls
│ (with Bearer token)
│
├─ Token expires mid-session
│ (apiFetch intercepts 401)
│
└─ auth:expired event fires
→ useAuth listener clears session
→ User silently logged out
→ Redirect to login on next action
[Server-side]
│
├─ purgeExpiredTokens() on startup
│
└─ purgeExpiredTokens() every hour
→ Deletes all rows where expires_at <= now()
→ Prevents table bloat
┌──────────────┐
│ Loading │ (Initial state on app start)
└─────┬────────┘
│
├─→ [Invalid/No Token] ──→ ┌──────────────┐
│ │ Unauthenticated
│ │ (Show Login)
│ └──────────────┘
│
└─→ [Valid Token] ──────→ ┌──────────────┐
│ Authenticated
│ (Show App)
└──────────────┘
│
├─ User clicks Logout
│ → DELETE /api/auth/logout
│ → Clear sessionStorage
│ → Back to Unauthenticated
│
└─ Token expires mid-session
→ 401 from API
→ apiFetch fires auth:expired
→ useAuth listener clears session
→ Back to Unauthenticated
All session tokens follow a strict format:
api-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
│ │
│ └─ 32 base62 characters (256 bits of entropy)
│
└─── Prefix (immutable)
Breakdown:
api- (4 chars) — identifies token type0-9A-Za-z (62 chars)Source: /server/auth.js
// Base62 alphabet for token generation
const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
/**
* Generate a random base-62 string of specified length
* Used for both api- tokens (32 chars) and hu- keys (64 chars)
*/
export function generateBase62(length) {
const bytes = crypto.randomBytes(length)
return Array.from(bytes, (b) => BASE62[b % 62]).join('')
}
How it works:
crypto.randomBytes(32) — generates 32 cryptographically secure random bytesapi- is prepended by the callerModulo Bias Note:
Database Schema:
CREATE TABLE api_tokens (
token TEXT PRIMARY KEY, -- Full token (api-XXXXX...) stored plaintext
user_uuid TEXT NOT NULL, -- Foreign key to users.uuid
created_at TEXT NOT NULL, -- ISO 8601 timestamp
expires_at TEXT NOT NULL, -- ISO 8601 timestamp (now + 24h)
FOREIGN KEY (user_uuid) REFERENCES users(uuid)
);
Key Details:
token is stored plaintext (not hashed)
expires_at is ISO 8601 formatted: 2026-03-09T12:34:56.789Zdatetime() SQLite function for safe comparisonQuery Example (from requireAuth middleware):
const row = db
.prepare(
`SELECT user_uuid, username FROM api_tokens t
JOIN users u ON t.user_uuid = u.uuid
WHERE t.token = ? AND datetime(t.expires_at) > datetime('now')`
)
.get(token)
sessionStorage Keys:
| Key | Purpose | Example |
|-----|---------|---------|
| cc_api_token | Bearer token | api-Abc123... |
| cc_username | Cached username | alice_smith |
| cc_user_uuid | Cached user ID | 550e8400-e29b-41d4-a716-446655440000 |
| cc_key_type | Key derivation method | hu (human-memorable) |
Storage Location: Browser's sessionStorage
Example Storage (after login):
const SESSION_KEYS = {
token: 'cc_api_token',
username: 'cc_username',
uuid: 'cc_user_uuid',
keyType: 'cc_key_type',
} as const
// In login() function:
sessionStorage.setItem('cc_api_token', 'api-Abc123...')
sessionStorage.setItem('cc_username', 'alice_smith')
sessionStorage.setItem('cc_user_uuid', '550e8400-...')
sessionStorage.setItem('cc_key_type', 'hu')
During Registration (POST /api/auth/register):
const token = `api-${generateBase62(32)}`
const now = new Date().toISOString()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
db.prepare(
'INSERT INTO api_tokens (token, user_uuid, created_at, expires_at) VALUES (?, ?, ?, ?)'
).run(token, uuid, now, expiresAt)
res.status(201).json({ uuid, username, token })
During Login (POST /api/auth/token):
// 1. Validate key hash with timing-safe comparison
if (!timingSafeHashCompare(key_hash, user.key_hash)) {
return res.status(401).json({ error: 'Invalid username or key' })
}
// 2. Invalidate all old tokens (one token per user at a time)
db.prepare('DELETE FROM api_tokens WHERE user_uuid = ?').run(user.uuid)
// 3. Issue fresh token
const token = `api-${generateBase62(32)}`
const now = new Date().toISOString()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
db.prepare(
'INSERT INTO api_tokens (token, user_uuid, created_at, expires_at) VALUES (?, ?, ?, ?)'
).run(token, user.uuid, now, expiresAt)
res.status(200).json({ uuid: user.uuid, username, token })
When the user opens or refreshes the app, useAuth must restore their session:
[App Mounts]
│
▼
┌────────────────────────────────┐
│ useAuth Effect: verifyToken() │
│ (runs once on component mount) │
└────────────┬───────────────────┘
│
▼
┌─────────────────┐
│ Read from │
│ sessionStorage │
│ (cc_api_token) │
└────────┬────────┘
│
┌────┴─────────┐
│ │
[Token?] [No Token?]
│ │
▼ ▼
Continue [Exit: Not Auth]
│ setAuthState({
│ isAuthenticated: false,
│ isLoading: false,
│ username: null,
│ uuid: null
│ })
│
▼
GET /api/auth/verify
Header: Authorization: Bearer api-Abc123...
│
▼
┌──────────────────────┐
│ Server Validates: │
│ 1. Token exists │
│ 2. Not expired │
│ 3. User still exists │
└────────┬─────────────┘
│
┌────┴────────────┐
│ │
[✓ Valid] [✗ Invalid/Expired]
│ │
▼ ▼
Return: Return 401
{ uuid, username } │
│ ▼
▼ [Clear Session]
[Set Auth State] Object.values(SESSION_KEYS)
isAuthenticated: .forEach(k =>
true, sessionStorage.removeItem(k)
username: ..., )
uuid: ...
│
▼
[User Ready for App]
Source: /src/hooks/useAuth.tsx
// Verify token on component mount
useEffect(() => {
const verifyToken = async () => {
const token = sessionStorage.getItem(SESSION_KEYS.token)
// No token = not authenticated
if (!token) {
setAuthState({ isAuthenticated: false, isLoading: false, username: null, uuid: null })
return
}
try {
const response = await fetch('/api/auth/verify', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
// Server validated token
if (!response.ok) {
// Token expired or invalid — clear session
Object.values(SESSION_KEYS).forEach((k) => sessionStorage.removeItem(k))
setAuthState({ isAuthenticated: false, isLoading: false, username: null, uuid: null })
return
}
// Token is valid — restore session
const data = await response.json()
setAuthState({
isAuthenticated: true,
isLoading: false,
username: data.username,
uuid: data.uuid,
})
} catch {
// Network error — clear session to be safe
Object.values(SESSION_KEYS).forEach((k) => sessionStorage.removeItem(k))
setAuthState({ isAuthenticated: false, isLoading: false, username: null, uuid: null })
}
}
verifyToken()
}, []) // Runs once on mount
Key Points:
[] — runs only once on mountisLoading: true initially, then transitions to false after verification completesWhen a token expires during active use, the application must detect and handle it gracefully.
Without a 401 interceptor, users could:
apiFetch Wrapper: /src/lib/apiFetch.ts
/**
* API Fetch Wrapper
*
* Wraps the standard fetch() and intercepts 401 responses.
* When a 401 occurs, dispatches 'auth:expired' event so useAuth can auto-logout.
*
* Usage:
* const response = await apiFetch('/api/auth/token', { method: 'POST', body: ... })
*/
export async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
const response = await fetch(url, options)
// If token expired (401), dispatch event for useAuth to listen
if (response.status === 401) {
window.dispatchEvent(new Event('auth:expired'))
}
return response
}
Event Listener in useAuth:
// Listen for auth expiry (401 intercepted by apiFetch)
useEffect(() => {
const handleAuthExpired = () => {
// Immediately clear all session data
Object.values(SESSION_KEYS).forEach((k) => sessionStorage.removeItem(k))
setAuthState({ isAuthenticated: false, isLoading: false, username: null, uuid: null })
}
window.addEventListener('auth:expired', handleAuthExpired)
return () => window.removeEventListener('auth:expired', handleAuthExpired)
}, [])
Before (without interceptor):
// Component code (old way):
const response = await fetch('/api/user/profile', {
headers: { Authorization: `Bearer ${token}` }
})
// If token expired, response.ok is false, error message shown
// useAuth doesn't know about expiry — stale auth state
After (with apiFetch):
// Component code (new way):
import { apiFetch } from '@/lib/apiFetch'
const response = await apiFetch('/api/user/profile', {
headers: { Authorization: `Bearer ${token}` }
})
// If response is 401:
// 1. apiFetch dispatches 'auth:expired' event
// 2. useAuth listener immediately clears session
// 3. Component detects isAuthenticated = false
// 4. User redirected to login on next render
┌────────────────────────────────────┐
│ Component makes API call with │
│ apiFetch() instead of fetch() │
└─────────────┬──────────────────────┘
│
▼
┌──────────────────┐
│ apiFetch() │
│ calls fetch() │
└────────┬─────────┘
│
┌────┴────────────┐
│ │
[✓ 200-399] [✗ 401]
│ │
▼ ▼
Return response Dispatch event
directly 'auth:expired'
│ │
│ ▼
│ window.dispatchEvent(
│ new Event('auth:expired')
│ )
│ │
│ ▼
│ ┌──────────────────────┐
│ │ useAuth listener: │
│ │ handleAuthExpired() │
│ └─────────┬────────────┘
│ │
│ ▼
│ Clear all sessionStorage
│ setAuthState({
│ isAuthenticated: false,
│ username: null,
│ uuid: null
│ })
│ │
└──────────┬───────────┘
│
▼
Component detects
isAuthenticated = false
(via useAuth hook)
│
▼
Redirect to /login
(conditional render)
Alternative Approach (polling):
Event-Based Approach (chosen):
Logging out is a two-stage process: server-side revocation + client-side clearing.
[User clicks "Logout" button]
│
▼
┌──────────────────────────┐
│ Call useAuth.logout() │
└────────┬─────────────────┘
│
▼
┌────────────────────────────┐
│ Read token from │
│ sessionStorage │
│ (cc_api_token) │
└────────┬───────────────────┘
│
┌────┴─────────┐
│ │
[Token?] [No Token?]
│ │
▼ ▼
Continue Skip to cleanup
│ │
▼ ▼
POST /api/auth/logout
Header: Bearer token
│
▼
┌────────────────────┐
│ Server: │
│ 1. Verify token │
│ 2. DELETE from DB │
│ 3. Return 200 │
└────────┬───────────┘
│
┌────┴──────────────┐
│ │
[✓ Success] [✗ Failed]
│ │
├───────┬───────────┤
│ │ │
▼ ▼ ▼
└───→ [No matter what]
Clear all 4 sessionStorage keys
│
▼
setAuthState({
isAuthenticated: false,
username: null,
uuid: null
})
│
▼
[Redirect to /login]
Source: /src/hooks/useAuth.tsx
const logout = async () => {
const token = sessionStorage.getItem(SESSION_KEYS.token)
// Try to revoke on server (but don't fail locally if it does)
if (token) {
try {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
})
} catch {
// Server call failed, but we still clear client session
// User's local token is gone; server cleanup will happen at next hourly purge
}
}
// Always clear local session, regardless of server call result
Object.values(SESSION_KEYS).forEach((k) => sessionStorage.removeItem(k))
setAuthState({ isAuthenticated: false, isLoading: false, username: null, uuid: null })
}
Source: /server/routes/authRoutes.js
/**
* POST /api/auth/logout
* Invalidate the current session token
* Header: Authorization: Bearer <token>
* Returns: { ok: true }
*/
router.post('/logout', requireAuth, (req, res) => {
try {
const authHeader = req.headers.authorization
const token = authHeader.slice(7) // Remove "Bearer " prefix
const db = getDatabase()
db.prepare('DELETE FROM api_tokens WHERE token = ?').run(token)
res.json({ ok: true })
} catch (err) {
res.status(500).json({ error: 'Server error during logout' })
}
})
Key Details:
requireAuth middleware validates token before handler runsapi_tokens tableScenario: User loses network connection during logout
Timeline:
T=0: User clicks Logout
T=1: POST /api/auth/logout sent
T=2: Network drops (request lost)
T=3: Client clears sessionStorage anyway
T=4: User sees login form
T=5: Token still in server DB (alive until expiry or hourly purge)
Risk: Attacker steals old token, uses it before expiry
Mitigation: Token expires in 24h, hourly purge removes it sooner
Result: ACCEPTABLE — Logout is best-effort, but client always clears
The server automatically removes expired tokens to keep the database lean.
Problem: Without cleanup, api_tokens table grows indefinitely:
Solution: Two-tier purge strategy
expires_at <= now()Source: /server/database.js
/**
* Delete all expired API tokens from the database
* Called on server startup and periodically to prevent table bloat
* @returns {number} Number of rows deleted
*/
export function purgeExpiredTokens() {
const db = getDatabase()
try {
const result = db
.prepare(`DELETE FROM api_tokens WHERE datetime(expires_at) <= datetime('now')`)
.run()
if (result.changes > 0) {
console.log(`[Database] Purged ${result.changes} expired token(s)`)
}
return result.changes
} catch (err) {
console.error('[Database] Error purging expired tokens:', err)
return 0
}
}
Key Details:
datetime() function for timezone-safe comparisonSource: /server.js
import { initDatabase, purgeExpiredTokens } from './server/database.js'
// ... after initDatabase() ...
// Purge expired tokens on startup
purgeExpiredTokens()
// Purge expired tokens every hour
setInterval(purgeExpiredTokens, 60 * 60 * 1000)
Execution Timeline:
┌──────────────────────────────────────────────┐
│ Server Start │
├──────────────────────────────────────────────┤
│ 1. Initialize Express app │
│ 2. Initialize Database │
│ 3. Create tables if needed │
│ 4. Call purgeExpiredTokens() — cleanup old │
│ 5. Start setInterval() — cleanup every hour │
│ 6. Listen on port 6565 │
└──────────────────────────────────────────────┘
Then every hour:
┌──────────────────────────────────────────────┐
│ Hour 1: purgeExpiredTokens() runs │
│ Hour 2: purgeExpiredTokens() runs │
│ Hour 3: purgeExpiredTokens() runs │
│ ... continues until server stops │
└──────────────────────────────────────────────┘
Before Cleanup:
api_tokens table (after 1 week of 100 logins/day):
┌────────┬──────────────────────┬────────────┐
│ token │ user_uuid │ expires_at │
├────────┼──────────────────────┼────────────┤
│ api-X1 │ 550e8400-... (user A)│ 2026-03-09 │ ✗ Expired
│ api-X2 │ 550e8400-... (user A)│ 2026-03-09 │ ✗ Expired
│ api-X3 │ 550e8400-... (user A)│ 2026-03-09 │ ✗ Expired
│ ... │ ... │ ... │
│ api-Y1 │ 7a3b2c1d-... (user B)│ 2026-03-15 │ ✓ Valid
│ api-Y2 │ 7a3b2c1d-... (user B)│ 2026-03-15 │ ✓ Valid
└────────┴──────────────────────┴────────────┘
Rows: ~700 (stale entries accumulating)
After Cleanup (hourly):
api_tokens table (same scenario):
┌────────┬──────────────────────┬────────────┐
│ token │ user_uuid │ expires_at │
├────────┼──────────────────────┼────────────┤
│ api-Y1 │ 7a3b2c1d-... (user B)│ 2026-03-15 │ ✓ Valid
│ api-Y2 │ 7a3b2c1d-... (user B)│ 2026-03-15 │ ✓ Valid
└────────┴──────────────────────┴────────────┘
Rows: ~2 (only active tokens remain)
Query Performance:
Without cleanup:
SELECT ... FROM api_tokens WHERE user_uuid = ?
Scans: 700 rows → finds match quickly → O(n) worst case
With cleanup:
SELECT ... FROM api_tokens WHERE user_uuid = ?
Scans: 2 rows → instant → O(n) still, but negligible cost
Use this checklist when building a new ClawStack application:
[ ] Database Setup
/server/database.js (includes table schemas)api_tokens table is created with correct columns[ ] Authentication Routes
/server/routes/authRoutes.js (all auth endpoints)/server/auth.js (token generation, requireAuth middleware)app.use('/api/auth', authRoutes)[ ] Server-side Token Cleanup
purgeExpiredTokens in server.jspurgeExpiredTokens() after initDatabase() (startup cleanup)setInterval(purgeExpiredTokens, 60 * 60 * 1000) (hourly cleanup)[ ] API Wrapper
/src/lib/apiFetch.ts (401 interceptor)apiFetch in components that call /api/auth/*fetch() → apiFetch() in login, verify, logout flows[ ] Authentication Hook & Context
/src/hooks/useAuth.tsx (session management)<AuthProvider> at root (see example below)useAuth() in pages/components that need auth state[ ] Auth State Usage
isLoading to show spinner during initial verificationisAuthenticated to conditionally render login vs. applogin(username, uuid, token) after successful auth API calllogout() on logout button click[ ] Root Layout / App.tsx
import { AuthProvider } from '@/hooks/useAuth'
export default function App() {
return (
<AuthProvider>
<main>
<Routes>
{/* routes here */}
</Routes>
</main>
</AuthProvider>
)
}
[ ] Protected Routes
import { useAuth } from '@/hooks/useAuth'
function DashboardPage() {
const { isAuthenticated, isLoading } = useAuth()
if (isLoading) return <Spinner />
if (!isAuthenticated) return <Navigate to="/login" />
return <Dashboard />
}
[ ] Login Page
import { apiFetch } from '@/lib/apiFetch'
import { useAuth } from '@/hooks/useAuth'
function LoginPage() {
const { login } = useAuth()
const handleLogin = async (username, keyHash) => {
const res = await apiFetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, key_hash: keyHash })
})
const data = await res.json()
login(data.username, data.uuid, data.token)
navigate('/dashboard')
}
return <LoginForm onSubmit={handleLogin} />
}
[ ] Logout
function LogoutButton() {
const { logout } = useAuth()
const handleLogout = async () => {
await logout()
navigate('/login')
}
return <button onClick={handleLogout}>Logout</button>
}
[ ] Manual Token Expiry Test
db.prepare("UPDATE api_tokens SET expires_at = '2020-01-01' WHERE ...").run()POST /api/auth/verify 401[ ] Hourly Cleanup Test
SELECT COUNT(*) FROM api_tokensUPDATE api_tokens SET expires_at = '2020-01-01'[ ] Network Error Handling
[ ] Verify After Page Refresh
Complete file inventory for session management:
| File | Purpose | Copy? |
|------|---------|-------|
| /server/database.js | DB init, schema, purgeExpiredTokens() | ✓ Yes |
| /server/auth.js | generateBase62(), requireAuth, timingSafeHashCompare | ✓ Yes |
| /server/routes/authRoutes.js | register, token, verify, logout, lookup endpoints | ✓ Yes |
| /server.js | Express setup, purge scheduling | Adapt (modify your existing) |
| File | Purpose | Copy? |
|------|---------|-------|
| /src/lib/apiFetch.ts | 401 interceptor wrapper | ✓ Yes |
| /src/hooks/useAuth.tsx | Session context, verification, logout | ✓ Yes |
| src/features/auth/LoginPage.tsx | Example usage in login flow | Reference only |
| src/features/auth/components/LoginForm.tsx | Example form component | Reference only |
| File | Purpose |
|------|---------|
| .env.example | If using env vars for database path, token lifetime |
| package.json | Ensure better-sqlite3, express, cors are listed |
Here's how to integrate in a minimal new project:
my-clawstack-app/
├── server/
│ ├── auth.js ← Copy from ClawStack
│ ├── database.js ← Copy from ClawStack
│ ├── routes/
│ │ └── authRoutes.js ← Copy from ClawStack
│ └── server.js ← Adapt (add purge calls)
├── src/
│ ├── lib/
│ │ └── apiFetch.ts ← Copy from ClawStack
│ ├── hooks/
│ │ └── useAuth.tsx ← Copy from ClawStack
│ ├── features/
│ │ └── auth/
│ │ ├── LoginPage.tsx ← Create for your app
│ │ └── DashboardPage.tsx ← Create for your app
│ └── App.tsx ← Wrap in <AuthProvider>
└── package.json
1. Plaintext Token Storage (Server-side)
2. No Token Refresh Mechanism
3. Global 401 Handler Only in Auth API Calls
4. No Token Rotation During Session
Token Refresh Flow (Medium Effort)
// POST /api/auth/refresh — exchange old token for new one
// Allows long sessions without automatic logout at 24h
Token Hashing (Low Effort)
// Store tokens as SHA-256(token) in DB, compare hashes during verification
// Pros: Protects against DB leak
// Cons: Slower, requires hash comparison on every request
Sliding Expiry (Medium Effort)
// Extend token expiry each time verify() is called
// Users who are active never get logged out (unless 30 days inactive)
// Requires DB write on every API call (performance impact)
2FA Integration (High Effort)
// POST /api/auth/2fa/request — send OTP to email
// POST /api/auth/2fa/verify — validate OTP, issue token
// Requires email service, adds latency to login
HTTPS in Production:
// Enforce HTTPS in production
if (IS_PROD) {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`)
} else {
next()
}
})
}
Token in Headers (not URL/cookies):
Authorization: Bearer api-Abc123...?token=api-Abc123... (logged in server access logs)Secure; HttpOnly flagssessionStorage vs. localStorage:
✓ Using sessionStorage (correct)
✗ Don't use localStorage
Random Byte Generation:
// ✓ Correct
const bytes = crypto.randomBytes(32) // Cryptographically secure
// ✗ Wrong
const bytes = Math.random() // Predictable, NOT suitable for tokens
Base62 Alphabet:
// Base62 charset: 0-9A-Za-z
// Entropy per character: log₂(62) ≈ 5.95 bits
// Total: 32 chars × 5.95 ≈ 190 bits effective entropy
// Safe for tokens (256-bit session tokens are overkill, but harmless)
Timing-Safe Comparison:
// ✓ Correct (prevents timing attacks)
crypto.timingSafeEqual(provided, stored)
// ✗ Wrong (vulnerable to timing-based attacks)
provided === stored // Completes faster if first chars differ
Prepared Statements (Protection Against SQL Injection):
// ✓ Correct
db.prepare('SELECT * FROM users WHERE username = ?').get(username)
// ✗ Wrong (SQL injection possible)
db.exec(`SELECT * FROM users WHERE username = '${username}'`)
Foreign Key Constraints:
db.pragma('foreign_keys = ON') // Enabled in database.js
// Prevents orphaned tokens if user is deleted
Key Compromise Scenarios:
| Scenario | Likelihood | Mitigation | |----------|-----------|-----------| | Token stolen from network | Very Low (HTTPS) | Enforce HTTPS, never log tokens | | Token stolen from browser memory | Low | sessionStorage clears on close | | Token stolen from DevTools | Very Low (localStorage would be worse) | Use sessionStorage, not localStorage | | DB compromised (plaintext tokens) | Low (depends on infra) | Encrypt DB at rest, restrict file access | | 24-hour expiry too long | Medium | Implement token refresh (future work) |
Best Practices:
What NOT to log:
// ✗ BAD — token exposed
console.log(`User logged in with token: ${token}`)
console.log(`Authorization header: ${authHeader}`)
What IS safe to log:
// ✓ GOOD — token abstracted
console.log(`User authenticated: ${username}`)
console.log(`Token type: Bearer (length: ${token.length})`)
Generate a token (server-side):
import { generateBase62 } from './server/auth.js'
const token = `api-${generateBase62(32)}`
// Example output: api-aB7cD9eF2gH4iJ6kL8mN0oP1qR3sT5uV
Store in sessionStorage (client-side):
sessionStorage.setItem('cc_api_token', 'api-aB7cD9eF2gH4iJ6kL8mN0oP1qR3sT5uV')
sessionStorage.setItem('cc_username', 'alice')
sessionStorage.setItem('cc_user_uuid', '550e8400-e29b-41d4-a716-446655440000')
Retrieve and use (client-side):
const token = sessionStorage.getItem('cc_api_token')
const response = await apiFetch('/api/user/profile', {
headers: { 'Authorization': `Bearer ${token}` }
})
Verify on server:
// Automatic via requireAuth middleware
app.get('/api/protected', requireAuth, (req, res) => {
// req.user = { uuid, username }
res.json({ message: 'success', user: req.user })
})
Clear session (client-side):
const SESSION_KEYS = {
token: 'cc_api_token',
username: 'cc_username',
uuid: 'cc_user_uuid',
keyType: 'cc_key_type',
}
Object.values(SESSION_KEYS).forEach(k => sessionStorage.removeItem(k))
Token not stored?
// Check sessionStorage
console.log(sessionStorage.getItem('cc_api_token'))
// Check login response
const res = await apiFetch('/api/auth/token', {...})
const data = await res.json()
console.log(data.token) // Should exist and start with "api-"
Verify fails with 401?
// Check token format
const token = sessionStorage.getItem('cc_api_token')
console.log(token.startsWith('api-')) // Should be true
console.log(token.length) // Should be 36
// Check expiry in DB
SELECT expires_at FROM api_tokens WHERE token = '...'
-- Should be > NOW()
Auto-logout not working?
// Check if apiFetch is used (not raw fetch)
// Check if auth:expired event listener is registered
window.addEventListener('auth:expired', () => {
console.log('Auth expired event fired!')
})
// Manually trigger to test
window.dispatchEvent(new Event('auth:expired'))
Cleanup not running?
// Check server logs on startup
// Should see: "[Database] Purged X expired token(s)"
// Manually run cleanup
const { purgeExpiredTokens } = await import('./server/database.js')
const count = purgeExpiredTokens()
console.log(`Purged ${count} tokens`)
ClawStack's session system is simple, secure, and battle-tested:
✓ Token Generation: Cryptographically secure random (32 base62 chars) ✓ Storage: Browser sessionStorage + server DB (plaintext, ephemeral) ✓ Verification: On-page-load restore + mid-session 401 interception ✓ Expiry: Fixed 24-hour TTL + hourly server cleanup ✓ Logout: Two-stage (server revoke + client clear) ✓ Security: HTTPS-only, timing-safe comparison, prepared statements
For new developers: Copy the 8 core files, implement the checklist, test manually, and you're done. Session management is handled for you.
Document Version: 1.0 Last Reviewed: 2026-03-08 Maintained By: ClawStack Studios Engineering
development
# Feature Development Assistant ## Mission Statement You are an expert full-stack developer who builds complete features from concept to implementation using Desktop Commander's file management capabilities. Your role is to analyze existing codebases, design feature architecture, implement all necessary code, and integrate seamlessly with existing systems. ## Important: Multi-Chat Workflow **Feature development requires multiple chat sessions to avoid context limits and manage implementation c
development
The canonical ClawChives©™ agent integration skill. Full API reference for autonomous agents (Lobsters©™) to authenticate, manage bookmarks, folders, and integrate with the ClawChive bookmarking system.
development
# Truthpack Updater Skill ## When to Use Activate this skill whenever: - A new route is added to `server/routes/` - A new environment variable is introduced - The project structure or a feature cluster is modified - Security protocols or auth rules are updated ## Instructions 1. **Audit Phase**: Perform a full-codebase scan (`grep` or `list_dir`) to identify changes since the last truthpack sync. 2. **Atomic Updates**: Update the corresponding JSON file in `.crustagent/vibecheck/truthpack/`:
development
# Truthpack Lookup Skill ## When to Use Activate this skill BEFORE generating any code that: - Creates or modifies API routes - References environment variables - Touches authentication/authorization - Modifies API request/response shapes ## Instructions 1. Read `.crustagent/vibecheck/truthpack/routes.json` for verified API routes 2. Read `.crustagent/vibecheck/truthpack/env.json` for verified environment variables 3. Read `.crustagent/vibecheck/truthpack/auth.json` for verified auth rules 4.