# @bloomneo/appkit - Authentication Module 🔐

[![npm version](https://img.shields.io/npm/v/@bloomneo/appkit.svg)](https://www.npmjs.com/package/@bloomneo/appkit)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

> Ultra-simple authentication with JWT tokens, bcrypt passwords, and role-based permissions. Express-only middleware with clear separation between user authentication and API access.

**Two token types** for different authentication needs: Login tokens for users, API tokens for external services. Built-in role hierarchy and permission inheritance. Production-ready security.

> **See also:** [AGENTS.md](../../AGENTS.md) (agent rules) · [llms.txt](../../llms.txt) (full API reference) · [examples/auth.ts](../../examples/auth.ts) · cookbook: [api-key-service.ts](../../cookbook/api-key-service.ts), [auth-protected-crud.ts](../../cookbook/auth-protected-crud.ts)

## 🚀 Why Choose This?

- **⚡ Simple API** - Just `authClass.get()`, everything else is automatic
- **🔒 Two Token Types** - Login tokens for users, API tokens for services
- **🎯 Clear Separation** - No confusion between user auth and API auth
- **👥 Smart Role Hierarchy** - Built-in role.level inheritance (user.basic → admin.system)
- **🔧 Zero Configuration** - Smart defaults for everything
- **🛡️ Null-Safe Access** - Safe user extraction with `auth.getUser(req)`
- **🤖 AI-Ready** - Optimized for LLM code generation with clear method names

## 📦 Installation

```bash
npm install @bloomneo/appkit
```

## 🏃‍♂️ Quick Start (30 seconds)

**⚠️ AUTH_SECRET Required**: You must generate a secure secret for JWT tokens.

```bash
# Generate and set your JWT secret (required for startup)
echo "BLOOM_AUTH_SECRET=your-super-secure-jwt-secret-key-2024-minimum-32-chars" > .env
```

```typescript
import { authClass } from '@bloomneo/appkit/auth';

const auth = authClass.get();

// User authentication (login tokens)
const loginToken = auth.generateLoginToken({ 
  userId: 123, 
  role: 'user', 
  level: 'basic' 
});

// API authentication (API tokens)
// IMPORTANT: role.level must exist in the configured role hierarchy.
// The default hierarchy ships with user.basic → admin.system. To use a
// custom service role like 'service.webhook', register it via the
// BLOOM_AUTH_ROLES env var first (see "Custom Role Examples" below).
const apiToken = auth.generateApiToken({
  keyId: 'webhook_service',
  role: 'admin',
  level: 'system'
});

// Express middleware protection
app.get('/user/profile', auth.requireLoginToken(), handler);
app.post('/api/webhook', auth.requireApiToken(), handler);
app.get('/admin', auth.requireLoginToken(), auth.requireUserRoles(['admin.tenant']), handler);
```

## 🎯 Two Token Types - Crystal Clear

### **Login Tokens (User Authentication)**
For humans logging into your app (mobile/web):

```typescript
// Generate login token
const loginToken = auth.generateLoginToken({
  userId: 123,
  role: 'user',
  level: 'basic'
}, '7d'); // Short-medium expiry

// Protect user routes
app.get('/profile', auth.requireLoginToken(), handler);
app.get('/admin', 
  auth.requireLoginToken(), 
  auth.requireUserRoles(['admin.tenant']), 
  handler
);
```

### **API Tokens (External Access)**
For third-party services, webhooks, and integrations:

```typescript
// Generate API token
// Uses admin.system from the default role hierarchy. For narrower service
// scopes, register custom roles via BLOOM_AUTH_ROLES (see Custom Roles below).
const apiToken = auth.generateApiToken({
  keyId: 'webhook_payment_service',
  role: 'admin',
  level: 'system'
}, '1y'); // Long expiry

// Protect API routes (no user roles/permissions)
app.post('/webhook/payment', auth.requireApiToken(), handler);
app.get('/api/public-data', auth.requireApiToken(), handler);
```

## 🏗️ Role-Level-Permission Architecture

> 🧩 **Two forms of the same identity — don't mix them up.**
> - **Token payloads** use two separate fields: `{ role: 'admin', level: 'tenant' }`
> - **Middleware, `hasRole`, and the hierarchy map** use the dotted form: `'admin.tenant'`
>
> `role.level` is literally `` `${role}.${level}` ``. Every entry in the hierarchy is keyed by the dotted form. If you pass `role: 'admin.tenant'` to `generateLoginToken`, the token will fail validation — split it into the two fields.

**Built-in Role Hierarchy (9 levels):**

```typescript
'user.basic'        // Level 1 - Basic user
'user.pro'         // Level 2 - Premium user  
'user.max'         // Level 3 - Max user
'moderator.review' // Level 4 - Can review content
'moderator.approve'// Level 5 - Can approve content
'moderator.manage' // Level 6 - Can manage content
'admin.tenant'     // Level 7 - Tenant admin
'admin.org'        // Level 8 - Organization admin
'admin.system'     // Level 9 - System admin
```

**Permission System:**
- **Actions**: `view`, `create`, `edit`, `delete`, `manage`
- **Scopes**: `own`, `tenant`, `org`, `system`
- **Format**: `action:scope` (e.g., `manage:tenant`)

**Inheritance Examples:**
```typescript
// ✅ These return TRUE (higher includes lower)
auth.hasRole('admin.org', 'admin.tenant'); // org > tenant
auth.hasRole('admin.system', 'user.basic'); // system > basic

// ❌ These return FALSE (lower cannot access higher)
auth.hasRole('user.basic', 'admin.tenant'); // basic < tenant
```

## 🧮 Matrix mode — two-axis roles (opt-in)

A single ladder conflates two orthogonal things: how far a role **reaches**
and what it may **do**. On one list `moderator.system` outranks `admin.tenant`
and therefore inherits delete — which is wrong, and is why apps end up
sprinkling ad-hoc `role === 'admin'` checks to compensate.

Set both axes, low → high, and inheritance becomes the **product** of two
chains instead of a single line:

```bash
BLOOM_AUTH_SCOPES="client,tenant,org,system"   # reach
BLOOM_AUTH_TIERS="user,moderator,admin"        # capability
```

A role satisfies a requirement only when **both** are high enough:

```
scope(user) >= scope(required)  AND  tier(user) >= tier(required)
```

| Caller | Gate requires | Granted? | Why |
|---|---|:--:|---|
| `admin.system` | `admin.tenant` | ✅ | higher on both axes |
| `moderator.system` | `moderator.tenant` | ✅ | same tier, greater reach |
| `moderator.system` | `admin.tenant` | ❌ | **platform moderator can't delete firm data** |
| `admin.tenant` | `admin.client` | ✅ | firm admin manages its clients |
| `moderator.tenant` | `admin.client` | ❌ | firm moderator can't client-admin |
| `admin.tenant` | `admin.system` | ❌ | firm admin is not platform |

Some roles are deliberately **incomparable** — that's the feature. The two
invariants apps keep re-implementing ("moderators never delete", "lower scope
can't reach higher") now fall out of the model: gate delete at `admin.<scope>`
and inheritance does the rest.

Any pair from the cross-product is valid — no per-pair registration:

```ts
auth.generateLoginToken({ userId, role: 'admin', level: 'tenant', tenantId: 'firm-1' });
```

New helpers (matrix mode only):

```ts
auth.roleParts('admin.tenant')   // { tier, scope, tierRank, scopeRank } — null in linear mode
auth.requireTier('admin')        // capability, any reach
auth.requireScope('tenant')      // reach, any capability
```

**Backward compatible.** Leave both unset and appkit stays in linear mode with
the 9-level ladder — nothing changes. Setting only one axis throws rather than
guessing the other, because a half-configured lattice would silently give you a
different authorization model than you asked for.

### Capability vs data scope — keep them separate

Two different questions, two different mechanisms:

- **"May they do this?"** → the role (tier × scope), via `requireUserRoles()`.
- **"On whose data?"** → `tenantId` / `clientId`, carried in the token.

`admin.tenant` grants firm-admin *capability*; the token's `tenantId` binds it
to *one* firm. Conflating the two is how you end up with a firm admin who can
read another firm.

```ts
const invoices = await db.invoice.findMany({
  where: { ...auth.scopedWhere(req), status: 'open' },
});
```

`scopedWhere()` returns `{}` for a platform account (null bindings), so it is
always safe to spread. Carrying the binding in the token also removes a
per-request lookup, and lets an offline or mobile client know its own reach
without a round trip.

## 🧭 Which case is your app? (Decision tree)

The 9-level default hierarchy is **scaffolding, not a requirement**. Most apps
only use 3 roles. Pick your shape, ignore the rest.

**The 3 core roles every app needs:**

| Role | Purpose | Example |
|---|---|---|
| `user.basic` | Regular end-user | The person using the product |
| `moderator.manage` | Content / community lead | Reviews flagged content, manages users |
| `admin.system` | You / your team | Owns the platform |

Everything else (`user.pro`, `user.max`, `moderator.review`, `moderator.approve`,
`admin.tenant`, `admin.org`) is **optional** and only needed if your app has
**pricing tiers** or **admin-level scoping** (multi-tenancy / multi-org). Skip
them on day one — add only when product reality demands it.

---

### Case 1 — Admin + users (the ~50% case)

> **Examples:** todo apps, single-team SaaS, internal tools, blogs with comments,
> a course platform, a personal dashboard.

**Roles you actually use:** `user.basic`, `moderator.manage`, `admin.system`
**Roles you ignore:** everything else (5 roles)

```typescript
// Sign up a regular user
const token = auth.generateLoginToken({
  userId: user.id,
  role: 'user',
  level: 'basic',
});

// Protect a user route
app.get('/dashboard', auth.requireLoginToken(), handler);

// Protect a moderator route (admin.system also passes — higher includes lower)
app.post('/flag/:id/review',
  auth.requireLoginToken(),
  auth.requireUserRoles(['moderator.manage']),
  handler,
);

// Protect an admin-only route
app.delete('/users/:id',
  auth.requireLoginToken(),
  auth.requireUserRoles(['admin.system']),
  handler,
);
```

**You don't need:** custom permissions, multi-tenancy, `BLOOM_DB_TENANT`,
`admin.tenant`, `admin.org`, pricing tiers.

**Optional (add later):** `user.pro` / `user.max` if you launch paid plans.

---

### Case 2 — Admin + orgs + users (the ~30% case)

> **Examples:** a B2B SaaS where each customer is a company with multiple
> employees (Slack, Linear, Notion, GitHub teams).

**Roles you actually use:** same 3 core roles. Org membership is a **database
concern**, not an auth concern — store `org_id` on the user row, not in the
JWT role.

**Roles you ignore:** `user.pro`, `user.max`, `moderator.review`,
`moderator.approve`, `admin.tenant`, `admin.org`.

```typescript
// Sign up a user that belongs to an org
const token = auth.generateLoginToken({
  userId: user.id,
  role: 'user',
  level: 'basic',
});

// Routes look identical to Case 1. Org isolation happens at the data layer:
app.get('/projects', auth.requireLoginToken(), async (req, res) => {
  const u = auth.getUser(req)!;
  // Query is scoped by org_id from your database, not from the JWT
  const rows = await db.project.findMany({ where: { org_id: u.orgId } });
  res.json(rows);
});
```

**Optional (add later):**
- `admin.org` if you want a per-org admin role distinct from `admin.system`
- `user.pro` / `user.max` for pricing tiers
- `BLOOM_DB_TENANT=auto` (in the database module) to **auto-filter** queries
  by `org_id` instead of writing it in every `where` clause

---

### Case 3 — Admin + orgs + tenants (the ~20% case)

> **Examples:** a multi-tenant platform where each org owns multiple isolated
> tenants (a white-label SaaS provider, a CRM that hosts brands per workspace,
> Shopify-style multi-store).

**Roles you actually use:** still the same 3 core roles. The extra layer
(tenant) is **also a database concern** — store `tenant_id` and `org_id` on
the user row.

**Roles you ignore:** `user.pro`, `user.max`, `moderator.review`,
`moderator.approve`.

```typescript
const token = auth.generateLoginToken({
  userId: user.id,
  role: 'user',
  level: 'basic',
});

// Tenant isolation also happens at the data layer.
// With BLOOM_DB_TENANT=auto, the database module injects WHERE tenant_id=?
// automatically — you don't write it.
app.get('/orders', auth.requireLoginToken(), async (req, res) => {
  const rows = await db.order.findMany(); // auto-scoped to req.tenant_id
  res.json(rows);
});
```

**Optional (add later):**
- `admin.tenant` for a per-tenant admin role
- `admin.org` for a per-org admin role
- `user.pro` / `user.max` for pricing tiers

---

### Cheat sheet

| Question | Answer |
|---|---|
| "Which roles do I need on day one?" | `user.basic`, `moderator.manage`, `admin.system` — always these 3 |
| "When do I add `user.pro` / `user.max`?" | When you launch paid tiers and need different feature gates |
| "When do I add `admin.tenant` / `admin.org`?" | When you have customers managing their own tenants/orgs and you don't want to give them `admin.system` |
| "Where does multi-tenancy live?" | The **database** module (`BLOOM_DB_TENANT=auto`), not auth roles |
| "Can I just delete the roles I don't use?" | No — they're harmless. They only cost something if you reference them |
| "Why does the moderator role exist in Case 1?" | Almost every app eventually needs *someone* who isn't you to deal with reports and bad actors. Add the role on day one, even if you're the only person using it for the first 6 months |

## 🛡️ Express Middleware Patterns

### **User Authentication Flow**
```typescript
// Step 1: Authenticate user
app.get('/user/dashboard', auth.requireLoginToken(), (req, res) => {
  const user = auth.getUser(req); // Safe access, never null here
  res.json({ userId: user.userId, role: user.role });
});

// Step 2: Require specific roles (user needs ANY of these)
app.get('/admin/panel', 
  auth.requireLoginToken(),
  auth.requireUserRoles(['admin.tenant', 'admin.org']),
  handler
);

// Step 3: Require specific permissions (user needs ALL of these)
app.post('/admin/users', 
  auth.requireLoginToken(),
  auth.requireUserPermissions(['manage:users', 'edit:tenant']),
  handler
);
```

### **API Access Flow**
```typescript
// Simple API protection (no roles/permissions)
app.post('/api/webhook', auth.requireApiToken(), (req, res) => {
  const token = auth.getUser(req); // Gets API token info
  console.log('API call from:', token.keyId);
  res.json({ status: 'received' });
});
```

## 🤖 LLM Quick Reference - Copy These Patterns

### **Token Generation (Copy Exactly)**

```typescript
// ✅ CORRECT - Login tokens for users
const loginToken = auth.generateLoginToken({
  userId: 123,           // Required: user identifier
  role: 'user',         // Required: role name
  level: 'basic',       // Required: level within role
  permissions: ['manage:own']  // Optional: custom permissions
}, '7d');

// ✅ CORRECT - API tokens for services
// role.level must exist in the configured role hierarchy. The default
// hierarchy ships with user.basic → admin.system. To use a custom service
// role like 'service.webhook', register it via BLOOM_AUTH_ROLES first
// (see "Custom Role Examples" below).
const apiToken = auth.generateApiToken({
  keyId: 'webhook_service',  // Required: service identifier
  role: 'admin',             // Required: role name (from configured hierarchy)
  level: 'system',           // Required: level within role
  permissions: ['webhook:receive']  // Optional: custom permissions
}, '1y');

// ❌ WRONG - Don't mix these up
auth.generateLoginToken({ keyId: 'test' }); // keyId is for API tokens
auth.generateApiToken({ userId: 123 });    // userId is for login tokens
```

#### Payload pass-through — attach your own fields

The payload object accepts any extra fields beyond the required ones. They're
signed into the JWT and are available on `req.user` after `requireLoginToken()`
verifies the token. Useful for stashing identifying data that doesn't belong
in your user table (e.g. `email`, `orgId`, `tenantId`) so `/me`-style handlers
can respond without a database round-trip.

```typescript
// Attach extra fields at token creation — they survive verification intact
const token = auth.generateLoginToken({
  userId: user.id,
  role: 'user',
  level: 'basic',
  // extras — any JSON-serializable key/value is preserved
  email: user.email,
  orgId: user.orgId,
});

// After requireLoginToken(), the whole payload is on req.user
app.get('/me', auth.requireLoginToken(), (req, res) => {
  const u = auth.getUser(req);    // { userId, role, level, email, orgId, iat, exp }
  res.json({ id: u.userId, email: u.email });  // no DB lookup needed
});
```

Constraints: keys are preserved verbatim but JWTs are **not** encrypted — don't
put secrets, password hashes, or PII you wouldn't want a client to see in there.
Keep payloads small (<1 KB is a good ceiling) so they fit in `Authorization`
headers comfortably.

### **Middleware Patterns (Copy These)**

```typescript
// ✅ CORRECT - User routes with roles
app.get('/admin/users', 
  auth.requireLoginToken(),                    // Authenticate user
  auth.requireUserRoles(['admin.tenant']),     // Check user role
  handler
);

// ✅ CORRECT - API routes (no roles)
app.post('/webhook/data', 
  auth.requireApiToken(),  // Authenticate API token only
  handler
);

// ❌ WRONG - Don't use user roles with API tokens
app.post('/webhook', 
  auth.requireApiToken(),
  auth.requireUserRoles(['admin']),  // ERROR: API tokens don't have user roles
  handler
);
```

## ⚠️ Common LLM Mistakes - Avoid These

### **Token Type Confusion**

```typescript
// ❌ Using wrong token type for wrong purpose
const userToken = auth.generateApiToken({ userId: 123 }); // Wrong: use generateLoginToken
const apiToken = auth.generateLoginToken({ keyId: 'api' }); // Wrong: use generateApiToken

// ✅ Use correct token type for purpose
const userToken = auth.generateLoginToken({ userId: 123, role: 'user', level: 'basic' });
const apiToken = auth.generateApiToken({ keyId: 'api_key', role: 'admin', level: 'system' });
```

### **Middleware Errors**

```typescript
// ❌ Trying to use user roles with API tokens
app.post('/api/data', 
  auth.requireApiToken(),
  auth.requireUserRoles(['admin']), // ERROR: API tokens don't have user roles
  handler
);

// ✅ Keep API routes simple
app.post('/api/data', auth.requireApiToken(), handler);

// ✅ Use user roles only with login tokens
app.get('/admin', 
  auth.requireLoginToken(),
  auth.requireUserRoles(['admin.tenant']),
  handler
);
```

### **Role Array Format**

```typescript
// ❌ Wrong parameter types
auth.requireUserRoles('admin.tenant'); // String - should be array
auth.requireUserRoles(['admin', 'tenant']); // Wrong format - should be role.level

// ✅ Correct array format
auth.requireUserRoles(['admin.tenant']);
auth.requireUserRoles(['admin.tenant', 'admin.org']); // Multiple roles (OR logic)
auth.requireUserPermissions(['manage:users', 'edit:tenant']); // Multiple permissions (AND logic)
```

### **Permissions: Replacement, not Additive**

The `permissions` array on a JWT payload **replaces** the role's default
permissions — it does NOT add to them. This matches AWS IAM, Casbin, OPA,
and Auth0 RBAC: explicit permissions are the truth, defaults are the fallback.

```typescript
// ✅ NO explicit permissions → role defaults apply
const u1 = auth.generateLoginToken({ userId: 1, role: 'admin', level: 'tenant' });
// → user has all of admin.tenant's default permissions (manage:tenant, etc.)

// ✅ Explicit permissions → defaults are IGNORED, only the explicit set applies
const u2 = auth.generateLoginToken({
  userId: 2,
  role: 'admin',
  level: 'tenant',
  permissions: ['view:own'],   // ← user can ONLY view:own, despite being admin.tenant
});
// → auth.hasPermission(u2, 'manage:tenant') === false
// → auth.hasPermission(u2, 'view:own')      === true

// ✅ Empty array = ZERO permissions (explicit downgrade)
const u3 = auth.generateLoginToken({
  userId: 3,
  role: 'admin',
  level: 'tenant',
  permissions: [],  // ← user has no permissions despite admin role
});
// → auth.hasPermission(u3, 'view:own') === false

// ✅ Action inheritance still works WITHIN the explicit set
const u4 = auth.generateLoginToken({
  userId: 4,
  role: 'admin',
  level: 'tenant',
  permissions: ['manage:tenant'],
});
// → auth.hasPermission(u4, 'edit:tenant') === true   (manage inherits all sub-actions)
// → auth.hasPermission(u4, 'view:tenant') === true
// → auth.hasPermission(u4, 'manage:org')  === false  (different scope)

// ❌ Common mistake: assuming permissions are ADDITIVE
// Old (pre-1.5.2) behavior was buggy and additive. If you've seen examples
// or wrote code assuming `permissions: ['edit:own']` would extend the role
// defaults, that's no longer how it works. Pass an explicit array only when
// you want to OVERRIDE the role defaults.
```

**Mental model:** `permissions` is the user's complete capability list when
present. To use the role's defaults, omit the field entirely.

## 🚨 Error Handling Patterns

### **Token Operations**

```typescript
try {
  const loginToken = auth.generateLoginToken({ userId, role, level });
  return { token: loginToken };
} catch (error) {
  // Invalid role.level, missing fields, etc.
  return res.status(500).json({ error: 'Token creation failed' });
}

import { TokenError } from '@bloomneo/appkit/auth';

try {
  const payload = auth.verifyToken(token);
  // Use payload...
} catch (error) {
  // verifyToken throws a typed TokenError with a stable `code` discriminant.
  // Do NOT string-match on error.message — messages are prefixed
  // ("[@bloomneo/appkit/auth] ...") and may be reworded.
  if (error instanceof TokenError && error.code === 'expired') {
    return res.status(401).json({ error: 'Session expired' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}
```

### **Middleware Error Handling**

```typescript
// Errors are handled automatically by middleware
app.get('/admin', 
  auth.requireLoginToken(),        // 401 if no/invalid token
  auth.requireUserRoles(['admin.tenant']), // 403 if insufficient role
  (req, res) => {
    // This only runs if all auth succeeds
    const user = auth.getUser(req); // Safe - never null here
    res.json({ message: 'Welcome admin!' });
  }
);
```

## 🚀 Production Deployment Checklist

### **Environment Setup**

```bash
# ✅ Required - Strong secret (32+ characters)
BLOOM_AUTH_SECRET=your-cryptographically-secure-secret-key-here

# ✅ Recommended - Shorter expiry for security
BLOOM_AUTH_EXPIRES_IN=2h

# ✅ Performance - Higher rounds for better security
BLOOM_AUTH_BCRYPT_ROUNDS=12

# ✅ Optional - Custom role hierarchy (overrides the default user.basic → admin.system)
# These names (user.premium, admin.super) are EXAMPLES of custom roles you can
# define for your app. The default hierarchy uses user.basic, user.pro, user.max,
# moderator.review, moderator.approve, moderator.manage, admin.tenant, admin.org,
# admin.system. Set this var only if you need different role names.
BLOOM_AUTH_ROLES=user.basic:1,user.premium:2,admin.super:10

# ✅ Optional - Custom permissions (must reference roles defined above)
BLOOM_AUTH_PERMISSIONS=user.premium:manage:own,admin.super:manage:system
```

### **Security Validation**

```typescript
// App startup validation
try {
  const auth = authClass.get();
  console.log('✅ Auth initialized successfully');
} catch (error) {
  console.error('❌ Auth setup failed:', error.message);
  process.exit(1);
}
```

## 📖 Essential Usage Patterns

### **Complete Authentication Flow**

```typescript
// Registration
app.post('/register', async (req, res) => {
  const { email, password } = req.body;
  
  // Hash password
  const hashedPassword = await auth.hashPassword(password);
  
  // Save user to database
  const user = await User.create({ email, password: hashedPassword });
  
  // Generate login token
  const token = auth.generateLoginToken({
    userId: user.id,
    role: 'user',
    level: 'basic'
  });
  
  res.json({ token, user: { id: user.id, email } });
});

// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await User.findOne({ email });
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });
  
  const isValid = await auth.comparePassword(password, user.password);
  if (!isValid) return res.status(401).json({ error: 'Invalid credentials' });
  
  const token = auth.generateLoginToken({
    userId: user.id,
    role: user.role,
    level: user.level
  });
  
  res.json({ token });
});
```

### **API Token Management**

```typescript
// Create API token for external service
app.post('/admin/api-tokens', 
  auth.requireLoginToken(),
  auth.requireUserRoles(['admin.tenant']),
  async (req, res) => {
    const { name, permissions } = req.body;
    
    const apiToken = auth.generateApiToken({
      keyId: `api_${Date.now()}`,
      role: 'admin',     // role.level must exist in the configured hierarchy
      level: 'system',   // (default ships user.basic → admin.system)
      permissions
    }, '1y');
    
    // Store token info in database (store hash, not plain token)
    const hashedToken = await auth.hashPassword(apiToken);
    await ApiToken.create({ name, token: hashedToken });
    
    // Return token once (client should save it)
    res.json({ apiToken });
  }
);
```

## 🌍 Environment Variables

```bash
# Required
BLOOM_AUTH_SECRET=your-super-secure-jwt-secret-key-2024-minimum-32-chars

# Optional
BLOOM_AUTH_BCRYPT_ROUNDS=12        # Default: 10
BLOOM_AUTH_EXPIRES_IN=1h           # Default: 7d

# Custom role hierarchy (optional)
BLOOM_AUTH_ROLES=user.basic:1,user.pro:2,admin.system:9

# Custom permissions (optional)
BLOOM_AUTH_PERMISSIONS=user.basic:view:own,admin.tenant:manage:tenant
```

## 📖 API Reference

### **Core Function**

```typescript
const auth = authClass.get(); // One function, all methods
```

### **Token Generation**

```typescript
auth.generateLoginToken({ userId, role, level, permissions }, expiresIn); // Create login JWT
auth.generateApiToken({ keyId, role, level, permissions }, expiresIn);    // Create API JWT
auth.verifyToken(token); // Verify any JWT token
```

### **Password Security**

```typescript
auth.hashPassword(password, rounds); // Hash password with bcrypt
auth.comparePassword(password, hash); // Verify password
```

### **User Access**

```typescript
auth.getUser(req); // Safe user extraction (returns null if not authenticated)
```

### **Authorization**

```typescript
auth.hasRole(userRole, requiredRole); // Check role hierarchy
auth.hasPermission(user, permission); // Check permission
```

### **Express Middleware**

```typescript
auth.requireLoginToken(options); // Login token authentication
auth.requireApiToken(options);   // API token authentication
auth.requireUserRoles(roles);    // User role authorization (array of strings)
auth.requireUserPermissions(permissions); // User permission authorization (array of strings)
```

### **Utility Methods**

```typescript
authClass.getRoles(); // Get role hierarchy
authClass.getPermissions(); // Get permission config
authClass.getAllRoles(); // Get all roles sorted by level
authClass.isValidRole(roleLevel); // Validate role format
authClass.reset(newConfig); // Reset instance (testing only)
```

## 🔧 Custom Role Examples

### **E-commerce Platform**

```bash
BLOOM_AUTH_ROLES=customer.basic:1,customer.premium:2,vendor.starter:3,vendor.pro:4,staff.support:5,admin.store:6

BLOOM_AUTH_PERMISSIONS=customer.basic:view:own,customer.premium:manage:own,vendor.starter:manage:products,admin.store:manage:store
```

### **Healthcare System**

```bash
BLOOM_AUTH_ROLES=patient.basic:1,nurse.junior:2,nurse.senior:3,doctor.resident:4,doctor.attending:5,admin.clinic:6

BLOOM_AUTH_PERMISSIONS=patient.basic:view:own,nurse.junior:view:patient,doctor.resident:manage:patient,admin.clinic:manage:clinic
```

## 🧪 Testing

```typescript
// Reset for clean testing
const auth = authClass.reset({
  jwt: { secret: 'test-secret-32-characters-long-for-security' },
  roles: {
    'test.user': { level: 1, inherits: [] },
    'test.admin': { level: 2, inherits: ['test.user'] },
  },
});

// Test login token
const loginToken = auth.generateLoginToken({
  userId: 123,
  role: 'test',
  level: 'user'
});

// Test API token  
const apiToken = auth.generateApiToken({
  keyId: 'test_api',
  role: 'test', 
  level: 'admin'
});

// Test middleware
const req = { headers: { authorization: `Bearer ${loginToken}` } };
const middleware = auth.requireLoginToken();
// Test with mock req/res objects
```

## 📈 Performance

- **JWT Operations**: ~1ms per token
- **Password Hashing**: ~100ms (10 rounds)
- **Permission Checking**: ~0.1ms per check
- **Memory Usage**: <1MB overhead
- **Environment Parsing**: Once per app startup

## 🔍 TypeScript Support

```typescript
import type {
  JwtPayload,
  LoginTokenPayload,
  ApiTokenPayload,
  AuthConfig,
  RoleHierarchy,
  ExpressRequest,
  ExpressResponse,
  ExpressMiddleware,
} from '@bloomneo/appkit/auth';

// All methods are fully typed
const user: JwtPayload | null = auth.getUser(req);
const middleware: ExpressMiddleware = auth.requireUserRoles(['admin.tenant']);
```

## ❓ FAQ

**Q: Can I use both login and API tokens in the same app?**
A: Yes! Use login tokens for user authentication and API tokens for external services.

**Q: Can API tokens have user roles?**
A: No, API tokens represent services, not users. Use `requireUserRoles()` only with login tokens.

**Q: How do I handle token expiration?**
A: The middleware automatically returns 401 with "Token has expired" message. Handle this in your frontend.

**Q: Can I customize the role hierarchy?**
A: Yes, use environment variables or pass custom config to `authClass.get()`.

**Q: What's the difference between roles and permissions?**
A: Roles are hierarchical (admin.org > admin.tenant), permissions are specific actions (edit:tenant).

## Agent-Dev Friendliness Score

**Score: 89/100 — 🟢 Exemplary** *(no cap applies)*
*Scored 2026-04-14 by Claude · Rubric [`AGENT_DEV_SCORING_ALGORITHM.md`](../../docs/AGENT_DEV_SCORING_ALGORITHM.md) v1.1*

> ✅ **Cap cleared**: Previous round was capped at 50 because cookbook files
> referenced hallucinated `auth.requireLogin()` / `auth.requireRole()`.
> Pre-v1 audit completed 2026-04-14 rewrote all cookbook files, renamed
> `auth.user()` → `auth.getUser()` and `auth.can()` → `auth.hasPermission()`
> (no aliases kept), and re-ran `tsx examples/auth.ts` clean. `dist/` rebuilt
> today. Every doc file grep'd is free of the old names. **Delta vs previous:
> +39 points (50 → 89), uncapped +5.4 (83.6 → 89.0).**

| # | Dimension | Score | Notes |
|---|---|---:|---|
| 1 | API correctness | **10** | 12 public methods; all verified by `auth.test.ts` (drift check asserts both PUBLIC_METHODS and HALLUCINATED_METHODS). Zero hallucinated refs in README / AGENTS.md / llms.txt / examples / cookbook as of 2026-04-14. |
| 2 | Doc consistency | **9** | `getUser` / `hasPermission` / `requireLoginToken` / `requireUserRoles` used identically across every doc surface. Minor variance remains in `BLOOM_AUTH_ROLES` env-var phrasing. |
| 3 | Runtime verification | **10** | `auth.test.ts` covers all 12 methods + 8 permission-resolution cases + drift assertions; imports from `./index.js` (real entry). |
| 4 | Type safety | **7** | `role`/`level` still typed as plain `string` not literal unions; `JwtPayload` has `[key: string]: any` index signature. `any` in `.d.ts` is justified for Express request/response shapes. |
| 5 | Discoverability | **9** | README hero has copy-pasteable canonical import in first 40 lines. Pointers to AGENTS.md / llms.txt / examples / cookbook prominent. |
| 6 | Example completeness | **10** ⬆ | **Was 10.** Fresh `examples/auth.ts` (2026-04-14) runtime-verified (`tsx examples/auth.ts` passes). Covers all 12 primitives in ~99 LOC. |
| 7 | Composability | **9** ⬆ | **Was 3.** 5 cookbook recipes now clean. `cookbook/api-key-service.ts` typechecks and composes auth + security + database + error + logger — canonical "issue + revoke API key" wiring. |
| 8 | Educational errors | **9** | Every `throw new Error` names `[@bloomneo/appkit/auth]`, the offending input, and appends `DOCS_URL#anchor`. `Invalid role.level` error lists the default hierarchy inline. |
| 9 | Convention enforcement | **9** | One canonical way per task. Bare-noun aliases (`user`, `can`) removed — not kept as compat shims. NAMING.md violations would now surface as drift test failures. |
| 10 | Drift prevention | **6** ⬆ | **Was 5.** `auth.test.ts` drift check is a real runtime gate (`npm test` fails if public surface moves). No `.github/workflows/` CI yaml or `scripts/check-auth-drift.mjs` yet — still manual invocation. |
| 11 | Reading order | **10** | "Which case is your app?" decision tree maps the 9-level hierarchy to 3 real-world app shapes. README → AGENTS.md → examples → source path is unbroken. |
| **12** | **Simplicity** | **7** | 12 methods (>8 ideal). 5 concepts still required (tokens, role.level, perms, middleware chaining, `req.user` vs `req.token`). Decision tree softens the perceived surface but doesn't shrink it. |
| **13** | **Clarity** | **9** ⬆ | **Was 8.** `getUser` / `hasPermission` now read as sentences. `hasRole(userRoleLevel, requiredRoleLevel)` argument names are explicit. `requireUserRoles` vs `requireUserPermissions` distinguishes the OR vs AND semantics. |
| **14** | **Unambiguity** | **9** ⬆ | **Was 8.** Renames resolved the `user(req)` null-overload critique. `hasPermission` REPLACES (not ADDS to) role defaults — matches AWS IAM / Casbin / OPA. Remaining minor gaps: `hasRole(a,b)` arg-order discovery without docs, OR-vs-AND asymmetry between the two middleware factories. |
| **15** | **Learning curve** | **9** | Decision tree + README hero give a 30-second working snippet. Errors link to exact anchor. A dev knowing Express + JWT is productive in ≤5 min. |

### Weighted (v1.1)

```
(10×.12)+(9×.08)+(10×.09)+(7×.06)+(9×.06)+(10×.08)+(9×.06)+(9×.05)+(9×.05)+(6×.04)+(10×.03)
+(7×.09)+(9×.09)+(9×.05)+(9×.05)
= 1.20+0.72+0.90+0.42+0.54+0.80+0.54+0.45+0.45+0.24+0.30+0.63+0.81+0.45+0.45
= 8.90 → 89.0/100
No anti-pattern cap applies (cookbook/examples re-verified 2026-04-14).
```

### Round-by-round score history

| Round | Date | Uncapped | Capped | Key change |
|---|---|---:|---:|---|
| v1.0 initial | 2026-04-11 | 79.5 | 50 | First scoring against 11 dimensions |
| v1.1 (4 new dims) | 2026-04-11 | 79.5 | 50 | +D12 Simplicity, D13 Clarity, D14 Unambiguity, D15 Learning curve |
| v1.1 + can() fix | 2026-04-11 | 82.3 | 50 | `auth.hasPermission()` REPLACES defaults (not additive). D14 4 → 8. |
| v1.1 + decision tree | 2026-04-11 | 83.6 | 50 | Added "Which case is your app?". D11 9→10, D15 7→9. |
| **v1.1 pre-v1 audit** | **2026-04-14** | **89.0** | **89.0** | **Cap cleared. `user()`→`getUser()`, `can()`→`hasPermission()` renames shipped; cookbook and examples re-verified; drift test asserts both presence and absence. D6 10, D7 3→9, D10 5→6, D13 8→9, D14 8→9. Net +39 vs previous capped, +5.4 vs previous uncapped.** |

### Gaps to reach 🟢 90+

1. **D10 Drift prevention → 9**: add `.github/workflows/drift.yml` (or husky pre-commit) that runs the drift test on every PR; add `scripts/check-auth-drift.mjs` that greps `AGENTS.md` / `llms.txt` / cookbook / examples for `auth.<method>(` and intersects with the runtime surface. +0.12.
2. **D4 Type safety → 9**: export `DefaultRoleLevel` as a literal union (`'user.basic' | 'user.pro' | ... | 'admin.system'`) and retype `role` / `level` parameters. +0.12.
3. **D12 Simplicity → 9**: fold `requireUserRoles` + `requireUserPermissions` into one `require({ roles?, permissions? })` factory, or accept this is an API-design ceiling. +0.18.
4. **D14 Unambiguity → 10**: rename `hasRole(userRoleLevel, requiredRoleLevel)` → `userHasRole(userRole, requiredRole)` (or flip to method on a user object); align OR vs AND by naming `requireAnyUserRole` / `requireAllUserPermissions`. +0.05.
5. **D13 Clarity → 10**: same rename pass as #4.

**Realistic ceiling:** ~93/100 (with D10 + D4 + D14 fixes). D12 Simplicity remains the structural limit without redesign.

---

## 📄 License

MIT © [Bloomneo](https://github.com/bloomneo)

---

<p align="center">
  Built with ❤️ in India by the <a href="https://github.com/orgs/bloomneo/people">Bloomneo Team</a>
</p>