@spfn/auth 0.1.0-alpha.88 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +1385 -1199
  2. package/dist/config.d.ts +405 -0
  3. package/dist/config.js +240 -0
  4. package/dist/config.js.map +1 -0
  5. package/dist/dto-81uR9gzF.d.ts +630 -0
  6. package/dist/errors.d.ts +196 -0
  7. package/dist/errors.js +173 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/index.d.ts +273 -14
  10. package/dist/index.js +511 -6665
  11. package/dist/index.js.map +1 -1
  12. package/dist/nextjs/api.js +345 -0
  13. package/dist/nextjs/api.js.map +1 -0
  14. package/dist/{adapters/nextjs → nextjs}/server.d.ts +47 -65
  15. package/dist/nextjs/server.js +179 -0
  16. package/dist/nextjs/server.js.map +1 -0
  17. package/dist/server.d.ts +4328 -529
  18. package/dist/server.js +7841 -1247
  19. package/dist/server.js.map +1 -1
  20. package/migrations/{0000_skinny_christian_walker.sql → 0000_mysterious_colossus.sql} +53 -23
  21. package/migrations/meta/0000_snapshot.json +281 -46
  22. package/migrations/meta/_journal.json +2 -2
  23. package/package.json +31 -31
  24. package/dist/adapters/nextjs/api.d.ts +0 -446
  25. package/dist/adapters/nextjs/api.js +0 -3279
  26. package/dist/adapters/nextjs/api.js.map +0 -1
  27. package/dist/adapters/nextjs/server.js +0 -3645
  28. package/dist/adapters/nextjs/server.js.map +0 -1
  29. package/dist/lib/api/auth-codes-verify.d.ts +0 -37
  30. package/dist/lib/api/auth-codes-verify.js +0 -2949
  31. package/dist/lib/api/auth-codes-verify.js.map +0 -1
  32. package/dist/lib/api/auth-codes.d.ts +0 -37
  33. package/dist/lib/api/auth-codes.js +0 -2949
  34. package/dist/lib/api/auth-codes.js.map +0 -1
  35. package/dist/lib/api/auth-exists.d.ts +0 -38
  36. package/dist/lib/api/auth-exists.js +0 -2949
  37. package/dist/lib/api/auth-exists.js.map +0 -1
  38. package/dist/lib/api/auth-invitations-accept.d.ts +0 -38
  39. package/dist/lib/api/auth-invitations-accept.js +0 -2883
  40. package/dist/lib/api/auth-invitations-accept.js.map +0 -1
  41. package/dist/lib/api/auth-invitations-cancel.d.ts +0 -37
  42. package/dist/lib/api/auth-invitations-cancel.js +0 -2883
  43. package/dist/lib/api/auth-invitations-cancel.js.map +0 -1
  44. package/dist/lib/api/auth-invitations-delete.d.ts +0 -36
  45. package/dist/lib/api/auth-invitations-delete.js +0 -2883
  46. package/dist/lib/api/auth-invitations-delete.js.map +0 -1
  47. package/dist/lib/api/auth-invitations-resend.d.ts +0 -37
  48. package/dist/lib/api/auth-invitations-resend.js +0 -2883
  49. package/dist/lib/api/auth-invitations-resend.js.map +0 -1
  50. package/dist/lib/api/auth-invitations.d.ts +0 -109
  51. package/dist/lib/api/auth-invitations.js +0 -2887
  52. package/dist/lib/api/auth-invitations.js.map +0 -1
  53. package/dist/lib/api/auth-keys-rotate.d.ts +0 -37
  54. package/dist/lib/api/auth-keys-rotate.js +0 -2949
  55. package/dist/lib/api/auth-keys-rotate.js.map +0 -1
  56. package/dist/lib/api/auth-login.d.ts +0 -39
  57. package/dist/lib/api/auth-login.js +0 -2949
  58. package/dist/lib/api/auth-login.js.map +0 -1
  59. package/dist/lib/api/auth-logout.d.ts +0 -36
  60. package/dist/lib/api/auth-logout.js +0 -2949
  61. package/dist/lib/api/auth-logout.js.map +0 -1
  62. package/dist/lib/api/auth-me.d.ts +0 -50
  63. package/dist/lib/api/auth-me.js +0 -2949
  64. package/dist/lib/api/auth-me.js.map +0 -1
  65. package/dist/lib/api/auth-password.d.ts +0 -36
  66. package/dist/lib/api/auth-password.js +0 -2949
  67. package/dist/lib/api/auth-password.js.map +0 -1
  68. package/dist/lib/api/auth-register.d.ts +0 -38
  69. package/dist/lib/api/auth-register.js +0 -2949
  70. package/dist/lib/api/auth-register.js.map +0 -1
  71. package/dist/lib/api/index.d.ts +0 -356
  72. package/dist/lib/api/index.js +0 -3261
  73. package/dist/lib/api/index.js.map +0 -1
  74. package/dist/lib/config.d.ts +0 -70
  75. package/dist/lib/config.js +0 -64
  76. package/dist/lib/config.js.map +0 -1
  77. package/dist/lib/contracts/auth.d.ts +0 -302
  78. package/dist/lib/contracts/auth.js +0 -2951
  79. package/dist/lib/contracts/auth.js.map +0 -1
  80. package/dist/lib/contracts/index.d.ts +0 -3
  81. package/dist/lib/contracts/index.js +0 -3190
  82. package/dist/lib/contracts/index.js.map +0 -1
  83. package/dist/lib/contracts/invitation.d.ts +0 -243
  84. package/dist/lib/contracts/invitation.js +0 -2883
  85. package/dist/lib/contracts/invitation.js.map +0 -1
  86. package/dist/lib/crypto.d.ts +0 -76
  87. package/dist/lib/crypto.js +0 -127
  88. package/dist/lib/crypto.js.map +0 -1
  89. package/dist/lib/index.d.ts +0 -4
  90. package/dist/lib/index.js +0 -313
  91. package/dist/lib/index.js.map +0 -1
  92. package/dist/lib/session.d.ts +0 -68
  93. package/dist/lib/session.js +0 -126
  94. package/dist/lib/session.js.map +0 -1
  95. package/dist/lib/types/api.d.ts +0 -45
  96. package/dist/lib/types/api.js +0 -1
  97. package/dist/lib/types/api.js.map +0 -1
  98. package/dist/lib/types/index.d.ts +0 -3
  99. package/dist/lib/types/index.js +0 -2647
  100. package/dist/lib/types/index.js.map +0 -1
  101. package/dist/lib/types/schemas.d.ts +0 -45
  102. package/dist/lib/types/schemas.js +0 -2647
  103. package/dist/lib/types/schemas.js.map +0 -1
  104. package/dist/lib.js +0 -1
  105. package/dist/lib.js.map +0 -1
  106. package/dist/plugin.d.ts +0 -12
  107. package/dist/plugin.js +0 -9083
  108. package/dist/plugin.js.map +0 -1
  109. package/dist/server/entities/index.d.ts +0 -11
  110. package/dist/server/entities/index.js +0 -395
  111. package/dist/server/entities/index.js.map +0 -1
  112. package/dist/server/entities/invitations.d.ts +0 -241
  113. package/dist/server/entities/invitations.js +0 -184
  114. package/dist/server/entities/invitations.js.map +0 -1
  115. package/dist/server/entities/permissions.d.ts +0 -196
  116. package/dist/server/entities/permissions.js +0 -49
  117. package/dist/server/entities/permissions.js.map +0 -1
  118. package/dist/server/entities/role-permissions.d.ts +0 -107
  119. package/dist/server/entities/role-permissions.js +0 -115
  120. package/dist/server/entities/role-permissions.js.map +0 -1
  121. package/dist/server/entities/roles.d.ts +0 -196
  122. package/dist/server/entities/roles.js +0 -50
  123. package/dist/server/entities/roles.js.map +0 -1
  124. package/dist/server/entities/schema.d.ts +0 -14
  125. package/dist/server/entities/schema.js +0 -7
  126. package/dist/server/entities/schema.js.map +0 -1
  127. package/dist/server/entities/user-permissions.d.ts +0 -163
  128. package/dist/server/entities/user-permissions.js +0 -193
  129. package/dist/server/entities/user-permissions.js.map +0 -1
  130. package/dist/server/entities/user-public-keys.d.ts +0 -227
  131. package/dist/server/entities/user-public-keys.js +0 -156
  132. package/dist/server/entities/user-public-keys.js.map +0 -1
  133. package/dist/server/entities/user-social-accounts.d.ts +0 -189
  134. package/dist/server/entities/user-social-accounts.js +0 -149
  135. package/dist/server/entities/user-social-accounts.js.map +0 -1
  136. package/dist/server/entities/users.d.ts +0 -235
  137. package/dist/server/entities/users.js +0 -117
  138. package/dist/server/entities/users.js.map +0 -1
  139. package/dist/server/entities/verification-codes.d.ts +0 -191
  140. package/dist/server/entities/verification-codes.js +0 -49
  141. package/dist/server/entities/verification-codes.js.map +0 -1
  142. package/dist/server/routes/auth/index.d.ts +0 -10
  143. package/dist/server/routes/auth/index.js +0 -4460
  144. package/dist/server/routes/auth/index.js.map +0 -1
  145. package/dist/server/routes/index.d.ts +0 -6
  146. package/dist/server/routes/index.js +0 -6584
  147. package/dist/server/routes/index.js.map +0 -1
  148. package/dist/server/routes/invitations/index.d.ts +0 -10
  149. package/dist/server/routes/invitations/index.js +0 -4395
  150. package/dist/server/routes/invitations/index.js.map +0 -1
  151. /package/dist/{lib.d.ts → nextjs/api.d.ts} +0 -0
package/README.md CHANGED
@@ -1,791 +1,860 @@
1
- # @spfn/auth
1
+ # @spfn/auth - Technical Documentation
2
2
 
3
- ![Coverage](https://img.shields.io/badge/coverage-85%2B%25-green)
4
- ![Tests](https://img.shields.io/badge/tests-226%20passed-brightgreen)
3
+ **Version:** 0.1.0-alpha.88
4
+ **Status:** Alpha - Internal Development
5
5
 
6
- Authentication, authorization, and comprehensive RBAC module for SPFN.
6
+ > **Note:** This is a technical documentation for developers working on the @spfn/auth package.
7
+ > For user-facing documentation, see [SPFN Documentation](https://spfn.dev/docs).
7
8
 
8
- ## Features
9
-
10
- - **Asymmetric JWT Authentication** - Client-signed tokens with ES256/RS256
11
- - **User Management** - Email/phone-based identity with bcrypt password hashing
12
- - **Multi-Factor Authentication** - 6-digit OTP via email/SMS
13
- - **Session Management** - Public key rotation and revocation (90-day expiry)
14
- - **Role-Based Access Control (RBAC)** - superadmin, admin, user roles
15
- - **Account Status Management** - active, inactive, suspended states
16
- - **Verification Flow** - Temporary tokens (15min) for secure operations
17
- - **Type-Safe API Contracts** - Built with Typebox validation
18
-
19
- ## Architecture
20
-
21
- ### Asymmetric JWT Authentication
22
-
23
- This package uses **client-signed JWT tokens** for enhanced security compared to traditional symmetric JWT:
24
-
25
- ```
26
- ┌─────────────┐ ┌─────────────┐
27
- │ Client │ │ Server │
28
- │ │ │ │
29
- │ 1. Generate│ │ │
30
- │ keypair │ │ │
31
- │ (ES256) │ │ │
32
- │ │ │ │
33
- │ 2. Register│──────────────────────────>│ 3. Store │
34
- │ publicKey │ publicKey│
35
- │ + fingerprint │ (verify │
36
- │ │ fingerprint)
37
- │ │ │ │
38
- │ 4. Sign JWT│ │ │
39
- │ with │ │ │
40
- │ privateKey │ │
41
- │ │ │ │
42
- │ 5. Request │──────────────────────────>│ 6. Verify │
43
- │ + JWT │ Authorization: Bearer │ signature│
44
- │ + keyId │ X-Key-Id: uuid │ with │
45
- │ │ │ publicKey│
46
- │ │ │ │
47
- │ │<──────────────────────────│ 7. Success │
48
- │ │ { success: true } │ │
49
- └─────────────┘ └─────────────┘
50
- ```
51
-
52
- **Key Benefits:**
53
- - Server never knows the private key
54
- - No shared secrets (unlike HMAC)
55
- - Each client has unique key pair
56
- - Easy key rotation without global impact
57
- - Automatic 90-day key expiry
58
-
59
- **Supported Algorithms:**
60
- - **ES256** (ECDSA P-256) - Recommended, ~91 bytes, compact and fast
61
- - **RS256** (RSA 2048) - Fallback, ~294 bytes, wider compatibility
62
-
63
- ## Installation
64
-
65
- ```bash
66
- pnpm add @spfn/auth
67
- ```
68
-
69
- ## Quick Start
9
+ ---
70
10
 
71
- ### 1. Client-Side Key Generation
11
+ ## Table of Contents
12
+
13
+ - [Overview](#overview)
14
+ - [Installation](#installation)
15
+ - [Architecture](#architecture)
16
+ - [Package Structure](#package-structure)
17
+ - [Module Exports](#module-exports)
18
+ - [Email & SMS Services](#email--sms-services)
19
+ - [Email Templates](#email-templates)
20
+ - [Server-Side API](#server-side-api)
21
+ - [Database Schema](#database-schema)
22
+ - [RBAC System](#rbac-system)
23
+ - [Next.js Adapter](#nextjs-adapter)
24
+ - [Testing](#testing)
25
+ - [Development Workflow](#development-workflow)
26
+ - [Known Issues](#known-issues)
27
+ - [Roadmap](#roadmap)
72
28
 
73
- ```typescript
74
- import { generateKeyPair } from '@spfn/auth/client';
29
+ ---
75
30
 
76
- // Generate ES256 key pair (recommended)
77
- const keyPair = generateKeyPair('ES256');
31
+ ## Overview
78
32
 
79
- console.log(keyPair);
80
- // {
81
- // privateKey: 'MIG...', // Base64 DER (store securely!)
82
- // publicKey: 'MFkw...', // Base64 DER (send to server)
83
- // keyId: '550e8400-...', // UUID v4
84
- // fingerprint: 'a1b2c3...', // SHA-256 (64 hex chars)
85
- // algorithm: 'ES256'
86
- // }
33
+ `@spfn/auth` is an authentication and authorization package for the SPFN framework, providing:
87
34
 
88
- // Store privateKey securely in localStorage/sessionStorage
89
- localStorage.setItem('auth.privateKey', keyPair.privateKey);
90
- localStorage.setItem('auth.keyId', keyPair.keyId);
91
- ```
35
+ - **Asymmetric JWT Authentication** - Client-signed tokens using ES256/RS256
36
+ - **User Management** - Email/phone-based identity with bcrypt hashing
37
+ - **Multi-Factor Authentication** - OTP verification via email/SMS
38
+ - **Session Management** - Public key rotation with 90-day expiry
39
+ - **Role-Based Access Control** - Flexible RBAC with runtime role/permission management
40
+ - **Next.js Integration** - Session helpers and server-side guards
92
41
 
93
- ### 2. User Registration
42
+ ### Design Principles
94
43
 
95
- ```typescript
96
- import { authRegister } from '@spfn/auth/api';
44
+ 1. **Security First** - Asymmetric cryptography, no shared secrets
45
+ 2. **Type Safety** - Full TypeScript support with Typebox validation
46
+ 3. **Framework Integration** - Seamless SPFN plugin architecture
47
+ 4. **Extensibility** - Service layer for custom authentication flows
48
+ 5. **Developer Experience** - Clear separation of concerns, reusable components
97
49
 
98
- // Step 1: Send verification code
99
- await authSendCode({
100
- target: 'user@example.com',
101
- targetType: 'email',
102
- purpose: 'registration'
103
- });
50
+ ---
104
51
 
105
- // Step 2: Verify code and get temporary token
106
- const { verificationToken } = await authVerifyCode({
107
- target: 'user@example.com',
108
- targetType: 'email',
109
- code: '123456',
110
- purpose: 'registration'
111
- });
52
+ ## Installation
112
53
 
113
- // Step 3: Register with verification token
114
- const result = await authRegister({
115
- email: 'user@example.com',
116
- password: 'securePassword123',
117
- verificationToken,
118
- publicKey: keyPair.publicKey,
119
- keyId: keyPair.keyId,
120
- fingerprint: keyPair.fingerprint,
121
- algorithm: 'ES256'
122
- });
54
+ ### 1. Install Package
123
55
 
124
- console.log(result);
125
- // { userId: '42', email: 'user@example.com' }
56
+ ```bash
57
+ pnpm add @spfn/auth
126
58
  ```
127
59
 
128
- ### 3. User Login
129
-
130
- ```typescript
131
- import { authLogin } from '@spfn/auth/api';
132
-
133
- // Generate new key pair for this session
134
- const newKeyPair = generateKeyPair('ES256');
135
-
136
- const result = await authLogin({
137
- email: 'user@example.com',
138
- password: 'securePassword123',
139
- publicKey: newKeyPair.publicKey,
140
- keyId: newKeyPair.keyId,
141
- fingerprint: newKeyPair.fingerprint,
142
- oldKeyId: localStorage.getItem('auth.keyId'), // Revoke old key
143
- algorithm: 'ES256'
144
- });
60
+ ### 2. Configure Server
145
61
 
146
- // Store new credentials
147
- localStorage.setItem('auth.privateKey', newKeyPair.privateKey);
148
- localStorage.setItem('auth.keyId', newKeyPair.keyId);
149
- localStorage.setItem('auth.userId', result.userId);
150
- ```
151
-
152
- ### 4. Making Authenticated Requests
62
+ #### Add Lifecycle to `server.config.ts`
153
63
 
154
64
  ```typescript
155
- import { generateClientToken } from '@spfn/auth/client';
156
-
157
- // Sign JWT with your private key
158
- const privateKey = localStorage.getItem('auth.privateKey');
159
- const keyId = localStorage.getItem('auth.keyId');
160
- const userId = localStorage.getItem('auth.userId');
161
-
162
- const token = generateClientToken(
163
- { userId, keyId, timestamp: Date.now() },
164
- privateKey,
165
- 'ES256',
166
- { expiresIn: '15m', issuer: 'spfn-client' }
167
- );
65
+ import { defineServerConfig } from '@spfn/core/server';
66
+ import { createAuthLifecycle } from '@spfn/auth/server';
67
+ import { appRouter } from './router';
168
68
 
169
- // Send request with Authorization header
170
- const response = await fetch('/_auth/logout', {
171
- method: 'POST',
172
- headers: {
173
- 'Authorization': `Bearer ${token}`,
174
- 'X-Key-Id': keyId,
175
- 'Content-Type': 'application/json'
176
- },
177
- body: JSON.stringify({})
178
- });
69
+ export default defineServerConfig()
70
+ .port(8790)
71
+ .host('0.0.0.0')
72
+ .routes(appRouter)
73
+ .lifecycle(createAuthLifecycle()) // Add auth lifecycle
74
+ .build();
179
75
  ```
180
76
 
181
- ### 5. Server-Side Middleware
77
+ #### Register Router in `router.ts`
182
78
 
183
79
  ```typescript
184
- import { createApp } from '@spfn/core/route';
185
- import { authenticate } from '@spfn/auth/server';
186
- import { getAuth, getUser } from '@spfn/auth/server';
187
-
188
- const app = createApp();
189
-
190
- // Apply authentication middleware
191
- app.bind(myProtectedRoute, [authenticate], async (c) => {
192
- // Get authenticated user
193
- const { user, userId, keyId } = getAuth(c);
194
-
195
- // Or just get user directly
196
- const user = getUser(c);
80
+ import { defineRouter } from '@spfn/core/route';
81
+ import { authRouter } from '@spfn/auth/server';
197
82
 
198
- console.log(user.email, user.role, user.status);
83
+ export const appRouter = defineRouter({
84
+ // Auth routes (fixed namespace)
85
+ auth: authRouter,
199
86
 
200
- return c.success({ message: 'Authenticated!' });
87
+ // ... your other routes
201
88
  });
202
89
  ```
203
90
 
204
- ---
205
-
206
- ## Next.js Integration
207
-
208
- The `@spfn/auth/nextjs` adapter provides seamless authentication integration for Next.js applications with automatic JWT injection and session management.
209
-
210
- ### Features
211
-
212
- - **Automatic JWT Generation** - Generates JWT from HttpOnly cookie sessions
213
- - **Server-Side Interceptor** - Auto-inject JWT in server components and API routes
214
- - **Client-Side Proxy** - Auto-inject JWT for browser requests via `/api/actions`
215
- - **High-Level Auth API** - Simple wrappers with automatic key generation
216
- - **Session Helpers** - Server-side session management utilities
91
+ ### 3. Configure Client (Next.js)
217
92
 
218
- ### Installation
93
+ #### Register Router Metadata and Errors in `api-client.ts`
219
94
 
220
95
  ```typescript
221
- import { authApi } from '@spfn/auth/nextjs';
96
+ import { createApi } from '@spfn/core/nextjs';
97
+ import type { AppRouter } from '@/server/router';
98
+ import { appMetadata as authAppMetadata } from "@spfn/auth";
99
+ import { authErrorRegistry } from "@spfn/auth/errors";
100
+ import { appMetadata } from '@/server/router.metadata';
101
+ import { errorRegistry } from "@spfn/core/errors";
102
+
103
+ export const api = createApi<AppRouter>({
104
+ metadata: { ...appMetadata, ...authAppMetadata },
105
+ errorRegistry: errorRegistry.concat(authErrorRegistry),
106
+ });
222
107
  ```
223
108
 
224
- ### 1. High-Level Auth API
225
-
226
- The `authApi` object provides simplified authentication functions with automatic key generation and session management:
109
+ ### 4. Environment Variables
227
110
 
228
- ```typescript
229
- import { authApi } from '@spfn/auth/nextjs';
111
+ ```bash
112
+ # Required
113
+ SPFN_AUTH_JWT_SECRET=your-secret-key
114
+ SPFN_AUTH_VERIFICATION_TOKEN_SECRET=your-verification-secret
115
+ DATABASE_URL=postgresql://...
230
116
 
231
- // Register (automatic key generation + session)
232
- const result = await authApi.register({
233
- email: 'user@example.com',
234
- password: 'SecurePass123!',
235
- verificationToken: '...' // from verification code
236
- });
237
- // → Automatically generates keypair, stores in session cookie
117
+ # Next.js (required)
118
+ SPFN_AUTH_SESSION_SECRET=your-32-char-secret
238
119
 
239
- // ✅ Login (automatic key generation + rotation)
240
- const result = await authApi.login({
241
- email: 'user@example.com',
242
- password: 'SecurePass123!'
243
- });
244
- // → Automatically generates new keypair, rotates old key
120
+ # Optional
121
+ SPFN_AUTH_JWT_EXPIRES_IN=7d
122
+ SPFN_AUTH_BCRYPT_SALT_ROUNDS=10
123
+ SPFN_AUTH_SESSION_TTL=7d
245
124
 
246
- // Logout (revokes current key)
247
- await authApi.logout();
125
+ # AWS SES (Email)
126
+ SPFN_AUTH_AWS_REGION=ap-northeast-2
127
+ SPFN_AUTH_AWS_SES_ACCESS_KEY_ID=AKIA...
128
+ SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY=...
129
+ SPFN_AUTH_AWS_SES_FROM_EMAIL=noreply@yourdomain.com
248
130
 
249
- // Rotate Key (before 90-day expiry)
250
- await authApi.rotateKey();
131
+ # AWS SNS (SMS)
132
+ SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID=AKIA...
133
+ SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY=...
134
+ SPFN_AUTH_AWS_SNS_SENDER_ID=MyApp
251
135
  ```
252
136
 
253
- **No manual key generation needed!** The `authApi` handles:
254
- - ES256 keypair generation
255
- - Public key registration with server
256
- - Private key storage in encrypted HttpOnly cookies
257
- - Automatic key rotation on login
258
-
259
- ### 2. Server-Side JWT Injection
260
-
261
- For server components and API routes, use the `createAuthInterceptor`:
137
+ ### 5. Run Migrations
262
138
 
263
- ```typescript
264
- // app/api/protected/route.ts
265
- import { UniversalClient } from '@spfn/core/client';
266
- import { createAuthInterceptor } from '@spfn/auth/nextjs';
267
-
268
- // Create client with auth interceptor
269
- const client = new UniversalClient({
270
- baseURL: process.env.SPFN_API_URL!,
271
- requestInterceptor: createAuthInterceptor()
272
- });
139
+ ```bash
140
+ # Generate migrations (if needed)
141
+ pnpm spfn db generate
273
142
 
274
- export async function GET() {
275
- // JWT is automatically injected from session cookie
276
- const data = await client.call(someContract);
277
- return Response.json(data);
278
- }
143
+ # Run migrations
144
+ pnpm spfn db migrate
279
145
  ```
280
146
 
281
- **How it works:**
282
- 1. Reads `session` HttpOnly cookie
283
- 2. Unseals session to get `privateKey`, `keyId`, `userId`
284
- 3. Generates JWT signed with `privateKey`
285
- 4. Adds `Authorization: Bearer <jwt>` header automatically
286
-
287
- ### 3. Client-Side Proxy Setup
288
-
289
- For browser requests, set up the Next.js API Route proxy:
147
+ ---
290
148
 
291
- ```typescript
292
- // app/api/actions/[...path]/route.ts
293
- export {
294
- GET,
295
- POST,
296
- PUT,
297
- DELETE,
298
- PATCH
299
- } from '@spfn/auth/nextjs/proxy';
300
- ```
149
+ ## Architecture
301
150
 
302
- Then use `UniversalClient` from browser:
151
+ ### High-Level Overview
152
+
153
+ ```
154
+ ┌─────────────────────────────────────────────────────────────┐
155
+ │ @spfn/auth Package │
156
+ ├─────────────────────────────────────────────────────────────┤
157
+ │ │
158
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
159
+ │ │ Server │ │ Next.js │ │ Client │ │
160
+ │ │ (server.ts) │ │ (nextjs/*) │ │ (client.ts) │ │
161
+ │ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
162
+ │ │ │ │ │
163
+ │ ┌───────▼───────────────────▼───────────────────▼───────┐ │
164
+ │ │ Common Types & Entities │ │
165
+ │ │ (index.ts) │ │
166
+ │ └────────────────────────────────────────────────────────┘ │
167
+ │ │
168
+ └─────────────────────────────────────────────────────────────┘
169
+ ```
170
+
171
+ ### Module Separation
172
+
173
+ The package is split into three distinct entry points to ensure proper code separation:
174
+
175
+ 1. **Common Module** (`@spfn/auth`)
176
+ - Database entities (users, roles, permissions)
177
+ - TypeScript types and interfaces
178
+ - RBAC type definitions
179
+ - Can be imported anywhere (server/client)
180
+
181
+ 2. **Server Module** (`@spfn/auth/server`)
182
+ - Server-only code (marked with Node.js APIs)
183
+ - Routes, services, repositories
184
+ - Middleware, helpers (JWT, password)
185
+ - RBAC initialization
186
+ - **Never** import in client-side code
187
+
188
+ 3. **Client Module** (`@spfn/auth/client`)
189
+ - Client-only code (React hooks, components)
190
+ - Currently in development (placeholders only)
191
+ - **Never** import in server-side code
192
+
193
+ 4. **Next.js Adapter** (`@spfn/auth/nextjs/*`)
194
+ - Next.js-specific integrations
195
+ - `@spfn/auth/nextjs/api` - Interceptors for API routes
196
+ - `@spfn/auth/nextjs/server` - Server Components guards & session helpers
197
+
198
+ ### Asymmetric JWT Flow
199
+
200
+ ```
201
+ ┌──────────┐ ┌──────────┐
202
+ │ Client │ │ Server │
203
+ └────┬─────┘ └────┬─────┘
204
+ │ │
205
+ │ 1. Generate ES256 keypair │
206
+ │ (privateKey stored locally) │
207
+ │ │
208
+ │ 2. POST /_auth/register │
209
+ │ { email, password, publicKey, keyId } │
210
+ ├──────────────────────────────────────────────>│
211
+ │ │
212
+ │ 3. Store publicKey │
213
+ │ (user_public_keys)
214
+ │ │
215
+ │ 4. Sign JWT with privateKey │
216
+ │ payload: { userId, keyId } │
217
+ │ │
218
+ │ 5. Request with Authorization header │
219
+ │ Authorization: Bearer <jwt> │
220
+ ├──────────────────────────────────────────────>│
221
+ │ │
222
+ │ 6. Decode JWT → keyId │
223
+ │ Fetch publicKey │
224
+ │ Verify signature │
225
+ │ │
226
+ │ 7. Success │
227
+ │<──────────────────────────────────────────────┤
228
+ │ │
229
+ ```
230
+
231
+ **Key Points:**
232
+ - Server **never** knows the private key
233
+ - Each client has a unique keypair
234
+ - JWT verification uses stored public key
235
+ - No shared secrets (unlike HMAC-based JWT)
303
236
 
304
- ```typescript
305
- 'use client';
306
- import { UniversalClient } from '@spfn/core/client';
237
+ ---
307
238
 
308
- const client = new UniversalClient({
309
- baseURL: '/api/actions' // Proxy endpoint
310
- });
239
+ ## Package Structure
311
240
 
312
- // Browser → /api/actions/user/profile → SPFN API (with JWT injected)
313
- const user = await client.call(getUserContract);
314
241
  ```
242
+ packages/auth/
243
+ ├── dist/ # Compiled output (tsup)
244
+ │ ├── index.js # Common exports
245
+ │ ├── index.d.ts
246
+ │ ├── server.js # Server exports
247
+ │ ├── server.d.ts
248
+ │ ├── client.js # Client exports (minimal)
249
+ │ ├── client.d.ts
250
+ │ ├── config/ # Configuration module
251
+ │ ├── errors/ # Error classes
252
+ │ ├── nextjs/ # Next.js adapter
253
+ │ └── server/ # Server implementation
254
+
255
+ ├── migrations/ # Drizzle database migrations
256
+ │ └── *.sql
257
+
258
+ ├── src/
259
+ │ ├── index.ts # Common entry point
260
+ │ ├── server.ts # Server entry point
261
+ │ ├── client.ts # Client entry point
262
+ │ │
263
+ │ ├── config/ # Configuration system
264
+ │ │ ├── index.ts
265
+ │ │ ├── schema.ts # Env var schema
266
+ │ │ └── types.ts
267
+ │ │
268
+ │ ├── errors/ # Error definitions
269
+ │ │ ├── index.ts
270
+ │ │ └── auth-errors.ts
271
+ │ │
272
+ │ ├── lib/ # Shared code
273
+ │ │ └── contracts/ # Typebox schemas
274
+ │ │
275
+ │ ├── server/ # Server-side implementation
276
+ │ │ ├── entities/ # Drizzle ORM entities
277
+ │ │ ├── services/ # Business logic layer
278
+ │ │ ├── repositories/ # Database access layer
279
+ │ │ ├── routes/ # HTTP route handlers
280
+ │ │ ├── middleware/ # Auth middleware
281
+ │ │ ├── helpers/ # JWT, password, context
282
+ │ │ ├── rbac/ # RBAC types and builtins
283
+ │ │ ├── lib/ # Server utilities
284
+ │ │ ├── lifecycle.ts # SPFN lifecycle hooks
285
+ │ │ ├── setup.ts # Initialization
286
+ │ │ ├── logger.ts # Logging
287
+ │ │ └── types.ts # Server types
288
+ │ │
289
+ │ ├── nextjs/ # Next.js adapter
290
+ │ │ ├── api.ts # Interceptor exports
291
+ │ │ ├── server.ts # Server Components guards
292
+ │ │ ├── session-helpers.ts# Session management
293
+ │ │ ├── interceptors/ # Request interceptors
294
+ │ │ └── guards/ # Auth guards
295
+ │ │
296
+ │ └── client/ # Client-side (WIP)
297
+ │ ├── hooks/ # React hooks (TODO)
298
+ │ ├── store/ # Zustand store (TODO)
299
+ │ └── components/ # UI components (TODO)
300
+
301
+ ├── package.json # Package configuration + SPFN plugin config
302
+ ├── tsup.config.ts # Build configuration
303
+ ├── drizzle.config.ts # Database migration config
304
+ └── README.md # This file
305
+ ```
306
+
307
+ ### Layer Responsibilities
308
+
309
+ #### 1. **Routes Layer** (`src/server/routes/`)
310
+ - Thin HTTP handlers
311
+ - Request validation (Typebox)
312
+ - Delegates to services
313
+ - Returns responses
314
+
315
+ #### 2. **Services Layer** (`src/server/services/`)
316
+ - Business logic
317
+ - Transaction management
318
+ - Reusable functions
319
+ - Can be used outside of routes
320
+
321
+ #### 3. **Repositories Layer** (`src/server/repositories/`)
322
+ - Database access only
323
+ - CRUD operations
324
+ - No business logic
325
+ - Drizzle ORM queries
326
+
327
+ #### 4. **Helpers Layer** (`src/server/helpers/`)
328
+ - Utility functions (JWT, password hashing)
329
+ - Context accessors (getAuth, getUser)
330
+ - Stateless operations
315
331
 
316
- **How it works:**
317
- 1. Browser makes request to `/api/actions/*`
318
- 2. Proxy reads `session` cookie (server-side only)
319
- 3. Generates JWT from session
320
- 4. Forwards request to SPFN API with `Authorization` header
321
- 5. Returns response to browser
332
+ ---
322
333
 
323
- ### 4. Session Helpers
334
+ ## Module Exports
324
335
 
325
- Direct session management utilities:
336
+ ### Common Module (`@spfn/auth`)
326
337
 
338
+ **Entities:**
327
339
  ```typescript
328
340
  import {
329
- saveSession,
330
- getSession,
331
- clearSession
332
- } from '@spfn/auth/nextjs';
333
-
334
- // Save session data (encrypted HttpOnly cookie)
335
- await saveSession({
336
- userId: '123',
337
- privateKey: '...',
338
- keyId: 'uuid',
339
- algorithm: 'ES256'
340
- }, 60 * 60 * 24 * 7); // 7 days
341
-
342
- // Get current session
343
- const session = await getSession();
344
- console.log(session?.userId);
345
-
346
- // Clear session
347
- await clearSession();
348
- ```
349
-
350
- ### 5. JWT Helper (Manual Usage)
351
-
352
- Generate JWT from session manually if needed:
353
-
341
+ users,
342
+ userPublicKeys,
343
+ verificationCodes,
344
+ roles,
345
+ permissions,
346
+ rolePermissions,
347
+ userPermissions,
348
+ userInvitations,
349
+ userSocialAccounts,
350
+ userProfiles
351
+ } from '@spfn/auth';
352
+ ```
353
+
354
+ **Types:**
354
355
  ```typescript
355
- import { generateJWTFromSession } from '@spfn/auth/nextjs';
356
-
357
- const jwt = await generateJWTFromSession();
358
- // → Returns signed JWT or null if no session
356
+ import type {
357
+ User,
358
+ UserPublicKey,
359
+ VerificationCode,
360
+ Role,
361
+ Permission,
362
+ // ... etc
363
+ } from '@spfn/auth';
359
364
  ```
360
365
 
361
- ### Updated Middleware (No X-Key-Id Required)
362
-
363
- The authenticate middleware now extracts `keyId` from the JWT payload, so you **no longer need** to send the `X-Key-Id` header:
364
-
366
+ **RBAC:**
365
367
  ```typescript
366
- // ❌ Old way (deprecated)
367
- fetch('/api/protected', {
368
- headers: {
369
- 'Authorization': `Bearer ${jwt}`,
370
- 'X-Key-Id': keyId // ← No longer needed!
371
- }
372
- });
368
+ import {
369
+ BUILTIN_ROLES,
370
+ BUILTIN_PERMISSIONS,
371
+ BUILTIN_ROLE_PERMISSIONS
372
+ } from '@spfn/auth';
373
373
 
374
- // New way
375
- fetch('/api/protected', {
376
- headers: {
377
- 'Authorization': `Bearer ${jwt}` // keyId extracted from JWT
378
- }
379
- });
374
+ import type {
375
+ RoleConfig,
376
+ PermissionConfig,
377
+ InitializeAuthOptions,
378
+ BuiltinRoleName,
379
+ BuiltinPermissionName
380
+ } from '@spfn/auth';
380
381
  ```
381
382
 
382
- The middleware flow:
383
- 1. Decode JWT to extract `keyId` (without verification)
384
- 2. Fetch public key from database using `keyId`
385
- 3. Verify JWT signature with public key
386
- 4. Validate user and attach to context
383
+ ---
387
384
 
388
- ### Complete Next.js Example
385
+ ### Server Module (`@spfn/auth/server`)
389
386
 
387
+ **Router:**
390
388
  ```typescript
391
- // app/auth/login/route.ts
392
- import { authApi } from '@spfn/auth/nextjs';
393
-
394
- export async function POST(request: Request) {
395
- const { email, password } = await request.json();
396
-
397
- try {
398
- const result = await authApi.login({ email, password });
399
- return Response.json({ success: true, userId: result.userId });
400
- } catch (error) {
401
- return Response.json({ success: false, error: error.message }, { status: 401 });
402
- }
403
- }
404
-
405
- // app/dashboard/page.tsx (Server Component)
406
- import { UniversalClient } from '@spfn/core/client';
407
- import { createAuthInterceptor } from '@spfn/auth/nextjs';
408
-
409
- const client = new UniversalClient({
410
- baseURL: process.env.SPFN_API_URL!,
411
- requestInterceptor: createAuthInterceptor()
412
- });
413
-
414
- export default async function Dashboard() {
415
- // JWT automatically injected
416
- const user = await client.call(getUserContract);
389
+ import { authRouter } from '@spfn/auth/server';
417
390
 
418
- return <div>Welcome {user.email}</div>;
419
- }
420
-
421
- // app/profile/page.tsx (Client Component)
422
- 'use client';
423
- import { UniversalClient } from '@spfn/core/client';
424
-
425
- const client = new UniversalClient({
426
- baseURL: '/api/actions'
391
+ // Explicit registration in your app router
392
+ export const appRouter = defineRouter({
393
+ auth: authRouter, // Mounts at /_auth/*
427
394
  });
428
-
429
- export default function Profile() {
430
- const [user, setUser] = useState(null);
431
-
432
- useEffect(() => {
433
- // Browser → Proxy → SPFN API (JWT auto-injected)
434
- client.call(getUserContract).then(setUser);
435
- }, []);
436
-
437
- return <div>{user?.email}</div>;
438
- }
439
- ```
440
-
441
- ### Environment Variables
442
-
443
- ```bash
444
- # Required for session encryption
445
- SPFN_AUTH_SESSION_SECRET=your-32-char-secret-key
446
-
447
- # SPFN API URL (server-side)
448
- SPFN_API_URL=http://localhost:8790
449
-
450
- # Public API URL (optional, for client-side)
451
- NEXT_PUBLIC_API_URL=http://localhost:8790
452
395
  ```
453
396
 
454
- ---
455
-
456
- ## Service Layer (Reusable Business Logic)
457
-
458
- The `@spfn/auth` package provides **service functions** that encapsulate all business logic, making it easy to create custom authentication flows while reusing the same secure logic.
459
-
460
- ### Why Service Layer?
461
-
462
- Instead of being locked into predefined API routes, you can:
463
- - **Create custom authentication flows** that match your app's UX
464
- - **Add custom logic** before/after authentication operations
465
- - **Integrate with external systems** (CRM, analytics, Slack notifications)
466
- - **Build complex workflows** combining multiple auth operations
467
- - **Maintain consistency** by reusing the same secure business logic
468
-
469
- ### Available Services
470
-
471
- #### Authentication Services
472
-
397
+ **Services:**
473
398
  ```typescript
474
399
  import {
400
+ // Auth
475
401
  checkAccountExistsService,
476
402
  registerService,
477
403
  loginService,
478
404
  logoutService,
479
405
  changePasswordService,
406
+
407
+ // Verification
408
+ sendVerificationCodeService,
409
+ verifyCodeService,
410
+
411
+ // Key Management
412
+ registerPublicKeyService,
413
+ rotateKeyService,
414
+ revokeKeyService,
415
+
416
+ // User
417
+ getUserByIdService,
418
+ getUserByEmailService,
419
+ getUserByPhoneService,
420
+ updateUserService,
421
+ updateLastLoginService,
422
+
423
+ // RBAC
424
+ initializeAuth,
425
+
426
+ // Permission
427
+ getUserPermissions,
428
+ hasPermission,
429
+ hasAnyPermission,
430
+ hasAllPermissions,
431
+ hasRole,
432
+ hasAnyRole,
433
+
434
+ // Role
435
+ createRole,
436
+ updateRole,
437
+ deleteRole,
438
+ addPermissionToRole,
439
+ removePermissionFromRole,
440
+ setRolePermissions,
441
+ getAllRoles,
442
+ getRoleByName,
443
+ getRolePermissions,
444
+
445
+ // Invitation
446
+ createInvitation,
447
+ getInvitationByToken,
448
+ getInvitationWithDetails,
449
+ validateInvitation,
450
+ acceptInvitation,
451
+ listInvitations,
452
+ cancelInvitation,
453
+ deleteInvitation,
454
+ expireOldInvitations,
455
+ resendInvitation,
456
+
457
+ // Session
458
+ getAuthSessionService,
459
+ getUserProfileService,
460
+
461
+ // Email
462
+ sendEmail,
463
+ registerEmailProvider,
464
+
465
+ // SMS
466
+ sendSMS,
467
+ registerSMSProvider,
468
+
469
+ // Email Templates
470
+ registerEmailTemplates,
471
+ getVerificationCodeTemplate,
472
+ getWelcomeTemplate,
473
+ getPasswordResetTemplate,
474
+ getInvitationTemplate,
480
475
  } from '@spfn/auth/server';
481
476
  ```
482
477
 
483
- #### Verification Services
484
-
478
+ **Repositories:**
485
479
  ```typescript
486
480
  import {
487
- sendVerificationCodeService,
488
- verifyCodeService,
481
+ usersRepository,
482
+ keysRepository,
483
+ rolesRepository,
484
+ permissionsRepository,
485
+ verificationCodesRepository,
486
+ invitationsRepository,
487
+ rolePermissionsRepository,
488
+ userPermissionsRepository,
489
+ userProfilesRepository,
489
490
  } from '@spfn/auth/server';
490
491
  ```
491
492
 
492
- #### Key Management Services
493
-
493
+ **Middleware:**
494
494
  ```typescript
495
495
  import {
496
- registerPublicKeyService,
497
- rotateKeyService,
498
- revokeKeyService,
496
+ authenticate,
497
+ requirePermissions,
498
+ requireRole,
499
499
  } from '@spfn/auth/server';
500
- ```
501
500
 
502
- #### User Services
501
+ // Usage
502
+ app.bind(
503
+ myContract,
504
+ [authenticate, requirePermissions('user:delete')],
505
+ async (c) => {
506
+ // Handler
507
+ }
508
+ );
509
+ ```
503
510
 
511
+ **Helpers:**
504
512
  ```typescript
505
513
  import {
506
- getUserByIdService,
507
- getUserByEmailService,
508
- getUserByPhoneService,
509
- updateLastLoginService,
510
- updateUserService,
514
+ // Context
515
+ getAuth,
516
+ getUser,
517
+ getUserId,
518
+ getKeyId,
519
+
520
+ // JWT
521
+ generateToken, // Legacy server-signed (deprecated)
522
+ verifyToken, // Legacy server-signed (deprecated)
523
+ verifyClientToken, // Client-signed asymmetric JWT
524
+ decodeToken, // Decode without verification (debugging)
525
+ verifyKeyFingerprint,
526
+
527
+ // Password
528
+ hashPassword,
529
+ verifyPassword,
511
530
  } from '@spfn/auth/server';
512
531
  ```
513
532
 
533
+ **Lifecycle:**
534
+ ```typescript
535
+ import { createAuthLifecycle } from '@spfn/auth/server';
536
+
537
+ // SPFN plugin lifecycle hooks
538
+ const lifecycle = createAuthLifecycle();
539
+ ```
540
+
514
541
  ---
515
542
 
516
- ### Example 1: Custom Login with Slack Notification
543
+ ### Client Module (`@spfn/auth/client`)
517
544
 
518
- ```typescript
519
- import { createApp } from '@spfn/core/route';
520
- import { loginService } from '@spfn/auth/server';
545
+ > **Status:** Work in Progress - Placeholders only
521
546
 
522
- const app = createApp();
547
+ ```typescript
548
+ // Currently empty exports
549
+ import {} from '@spfn/auth/client';
550
+ ```
523
551
 
524
- app.post('/custom-login', async (c) => {
525
- const body = await c.req.json();
552
+ **Planned:**
553
+ - React hooks (useAuth, useSession)
554
+ - Zustand store
555
+ - UI components (LoginForm, etc.)
526
556
 
527
- // Log login attempt
528
- console.log(`Login attempt: ${body.email}`);
557
+ ---
529
558
 
530
- try {
531
- // Reuse auth service
532
- const result = await loginService({
533
- email: body.email,
534
- password: body.password,
535
- publicKey: body.publicKey,
536
- keyId: body.keyId,
537
- fingerprint: body.fingerprint,
538
- algorithm: body.algorithm,
539
- });
559
+ ### Configuration Module (`@spfn/auth/config`)
540
560
 
541
- // Send Slack notification
542
- await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
543
- method: 'POST',
544
- body: JSON.stringify({
545
- text: `✅ User ${result.email} logged in successfully!`,
546
- }),
547
- });
561
+ ```typescript
562
+ import { env, envSchema } from '@spfn/auth/config';
548
563
 
549
- // Track analytics
550
- await trackEvent('user_login', {
551
- userId: result.userId,
552
- email: result.email,
553
- });
564
+ // Access environment variables (validated at startup)
565
+ console.log(env.SPFN_AUTH_JWT_SECRET);
566
+ console.log(env.SPFN_AUTH_JWT_EXPIRES_IN);
567
+ console.log(env.SPFN_AUTH_BCRYPT_SALT_ROUNDS);
554
568
 
555
- return c.json(result);
556
- } catch (error) {
557
- // Custom error handling
558
- await trackEvent('login_failed', { email: body.email });
559
- throw error;
560
- }
561
- });
569
+ // envSchema can be used for custom validation
562
570
  ```
563
571
 
564
572
  ---
565
573
 
566
- ### Example 2: Custom Registration with CRM Integration
574
+ ### Errors Module (`@spfn/auth/errors`)
567
575
 
568
576
  ```typescript
569
577
  import {
570
- verifyCodeService,
571
- registerService,
572
- } from '@spfn/auth/server';
573
-
574
- app.post('/custom-register', async (c) => {
575
- const body = await c.req.json();
578
+ // Auth namespace (contains all error classes)
579
+ AuthError,
580
+
581
+ // Individual error classes
582
+ InvalidCredentialsError,
583
+ InvalidTokenError,
584
+ TokenExpiredError,
585
+ KeyExpiredError,
586
+ AccountDisabledError,
587
+ AccountAlreadyExistsError,
588
+ InvalidVerificationCodeError,
589
+ InvalidVerificationTokenError,
590
+ InvalidKeyFingerprintError,
591
+ VerificationTokenPurposeMismatchError,
592
+ VerificationTokenTargetMismatchError,
593
+ InsufficientPermissionsError,
594
+ InsufficientRoleError,
595
+
596
+ // Error registry for client-side error handling
597
+ authErrorRegistry,
598
+ } from '@spfn/auth/errors';
599
+ ```
576
600
 
577
- // Step 1: Verify OTP code
578
- const { verificationToken } = await verifyCodeService({
579
- target: body.email,
580
- targetType: 'email',
581
- code: body.otp,
582
- purpose: 'registration',
583
- });
601
+ ---
584
602
 
585
- // Step 2: Register user
586
- const user = await registerService({
587
- email: body.email,
588
- password: body.password,
589
- verificationToken,
590
- publicKey: body.publicKey,
591
- keyId: body.keyId,
592
- fingerprint: body.fingerprint,
593
- algorithm: 'ES256',
594
- });
603
+ ### Next.js Adapter (`@spfn/auth/nextjs/*`)
595
604
 
596
- // Step 3: Add to CRM
597
- await fetch('https://api.your-crm.com/contacts', {
598
- method: 'POST',
599
- headers: { 'Authorization': `Bearer ${process.env.CRM_API_KEY}` },
600
- body: JSON.stringify({
601
- email: user.email,
602
- userId: user.userId,
603
- source: 'registration',
604
- createdAt: new Date().toISOString(),
605
- }),
606
- });
605
+ #### `@spfn/auth/nextjs/api`
607
606
 
608
- // Step 4: Send welcome email
609
- await sendWelcomeEmail(user.email);
607
+ ```typescript
608
+ import {
609
+ authInterceptors,
610
+ loginRegisterInterceptor,
611
+ generalAuthInterceptor,
612
+ keyRotationInterceptor,
613
+ } from '@spfn/auth/nextjs/api';
610
614
 
611
- return c.json({
612
- success: true,
613
- userId: user.userId,
614
- message: 'Registration complete! Check your email for next steps.',
615
- });
616
- });
615
+ // Auto-registers interceptors on import
616
+ import '@spfn/auth/nextjs/api';
617
617
  ```
618
618
 
619
- ---
620
-
621
- ### Example 3: Complex Multi-Step Flow
619
+ #### `@spfn/auth/nextjs/server`
622
620
 
623
621
  ```typescript
624
622
  import {
625
- checkAccountExistsService,
626
- sendVerificationCodeService,
627
- verifyCodeService,
628
- registerService,
629
- } from '@spfn/auth/server';
623
+ // Guards (Server Components)
624
+ RequireAuth,
625
+ RequireRole,
626
+ RequirePermission,
627
+
628
+ // Auth Utils
629
+ getUserRole,
630
+ getUserPermissions,
631
+ hasAnyRole,
632
+ hasAnyPermission,
633
+
634
+ // Session Helpers
635
+ saveSession,
636
+ getSession,
637
+ clearSession,
638
+
639
+ // Types
640
+ type SessionData,
641
+ type PublicSession,
642
+ type SaveSessionOptions,
643
+ } from '@spfn/auth/nextjs/server';
644
+ ```
645
+
646
+ **Session Helpers Usage:**
647
+ ```typescript
648
+ // Save session (Server Actions / Route Handlers)
649
+ await saveSession({
650
+ userId: '123',
651
+ privateKey: '...',
652
+ keyId: 'uuid',
653
+ algorithm: 'ES256',
654
+ });
630
655
 
631
- app.post('/signup-wizard', async (c) => {
632
- const { step, email, code, password, publicKey, keyId, fingerprint } = await c.req.json();
656
+ // Get session (read-only, safe in Server Components)
657
+ const session = await getSession();
633
658
 
634
- if (step === 1) {
635
- // Check if account already exists
636
- const { exists } = await checkAccountExistsService({ email });
659
+ // Clear session
660
+ await clearSession();
661
+ ```
637
662
 
638
- if (exists) {
639
- return c.json({ error: 'Account already exists', suggestLogin: true }, 409);
640
- }
663
+ **Guard Usage:**
664
+ ```typescript
665
+ // app/dashboard/page.tsx
666
+ import { RequireAuth } from '@spfn/auth/nextjs/server';
641
667
 
642
- // Send verification code
643
- const result = await sendVerificationCodeService({
644
- target: email,
645
- targetType: 'email',
646
- purpose: 'registration',
647
- });
668
+ export default async function DashboardPage()
669
+ {
670
+ return (
671
+ <RequireAuth redirectTo="/login">
672
+ <div>Protected content</div>
673
+ </RequireAuth>
674
+ );
675
+ }
676
+ ```
648
677
 
649
- return c.json({ step: 2, expiresAt: result.expiresAt });
650
- }
678
+ ---
651
679
 
652
- if (step === 2) {
653
- // Verify code
654
- const { verificationToken } = await verifyCodeService({
655
- target: email,
656
- targetType: 'email',
657
- code,
658
- purpose: 'registration',
659
- });
680
+ ## Email & SMS Services
660
681
 
661
- // Store token temporarily
662
- return c.json({ step: 3, verificationToken });
663
- }
682
+ ### Email Service
664
683
 
665
- if (step === 3) {
666
- // Complete registration
667
- const user = await registerService({
668
- email,
669
- password,
670
- verificationToken: body.verificationToken,
671
- publicKey,
672
- keyId,
673
- fingerprint,
674
- algorithm: 'ES256',
675
- });
684
+ The email service uses AWS SES by default, with fallback to console logging in development.
676
685
 
677
- return c.json({ success: true, userId: user.userId });
678
- }
686
+ **Send Email:**
687
+ ```typescript
688
+ import { sendEmail } from '@spfn/auth/server';
689
+
690
+ await sendEmail({
691
+ to: 'user@example.com',
692
+ subject: 'Welcome!',
693
+ text: 'Plain text content',
694
+ html: '<h1>HTML content</h1>',
695
+ purpose: 'welcome', // for logging
696
+ });
697
+ ```
679
698
 
680
- return c.json({ error: 'Invalid step' }, 400);
699
+ **Custom Email Provider:**
700
+ ```typescript
701
+ import { registerEmailProvider } from '@spfn/auth/server';
702
+
703
+ // Register SendGrid provider
704
+ registerEmailProvider({
705
+ name: 'sendgrid',
706
+ sendEmail: async ({ to, subject, text, html }) => {
707
+ // Your SendGrid implementation
708
+ return { success: true, messageId: '...' };
709
+ },
681
710
  });
682
711
  ```
683
712
 
684
713
  ---
685
714
 
686
- ### Example 4: Check User Without Creating Route
715
+ ### SMS Service
716
+
717
+ The SMS service uses AWS SNS by default.
687
718
 
719
+ **Send SMS:**
688
720
  ```typescript
689
- import { getUserByEmailService } from '@spfn/auth/server';
721
+ import { sendSMS } from '@spfn/auth/server';
690
722
 
691
- // Use in any server code
692
- async function sendNotificationToAdmin(email: string) {
693
- const user = await getUserByEmailService(email);
723
+ await sendSMS({
724
+ phone: '+821012345678', // E.164 format
725
+ message: 'Your code is: 123456',
726
+ purpose: 'verification',
727
+ });
728
+ ```
694
729
 
695
- if (user && user.role === 'admin') {
696
- await sendEmail(user.email, 'Admin Notification', '...');
697
- }
698
- }
730
+ **Custom SMS Provider:**
731
+ ```typescript
732
+ import { registerSMSProvider } from '@spfn/auth/server';
733
+
734
+ // Register Twilio provider
735
+ registerSMSProvider({
736
+ name: 'twilio',
737
+ sendSMS: async ({ phone, message }) => {
738
+ // Your Twilio implementation
739
+ return { success: true, messageId: '...' };
740
+ },
741
+ });
699
742
  ```
700
743
 
701
744
  ---
702
745
 
703
- ### Service Function Signatures
746
+ ## Email Templates
747
+
748
+ ### Built-in Templates
704
749
 
705
- #### `loginService(params)`
750
+ | Template | Function | Purpose |
751
+ |----------|----------|---------|
752
+ | `verificationCode` | `getVerificationCodeTemplate` | Verification codes (registration, login, password reset) |
753
+ | `welcome` | `getWelcomeTemplate` | Welcome email after registration |
754
+ | `passwordReset` | `getPasswordResetTemplate` | Password reset link |
755
+ | `invitation` | `getInvitationTemplate` | User invitation |
706
756
 
757
+ **Usage:**
707
758
  ```typescript
708
- await loginService({
709
- email?: string; // One of email or phone required
710
- phone?: string;
711
- password: string;
712
- publicKey: string;
713
- keyId: string;
714
- fingerprint: string;
715
- oldKeyId?: string; // Optional: revoke old key
716
- algorithm?: 'ES256' | 'RS256';
759
+ import { getVerificationCodeTemplate, sendEmail } from '@spfn/auth/server';
760
+
761
+ const { subject, text, html } = getVerificationCodeTemplate({
762
+ code: '123456',
763
+ purpose: 'registration',
764
+ expiresInMinutes: 5,
765
+ appName: 'MyApp',
717
766
  });
718
767
 
719
- // Returns: { userId, email?, phone?, passwordChangeRequired }
768
+ await sendEmail({ to: 'user@example.com', subject, text, html });
720
769
  ```
721
770
 
722
- #### `registerService(params)`
723
-
724
- ```typescript
725
- await registerService({
726
- email?: string;
727
- phone?: string;
728
- verificationToken: string; // From verifyCodeService
729
- password: string;
730
- publicKey: string;
731
- keyId: string;
732
- fingerprint: string;
733
- algorithm?: 'ES256' | 'RS256';
734
- });
771
+ ---
735
772
 
736
- // Returns: { userId, email?, phone? }
737
- ```
773
+ ### Custom Templates
738
774
 
739
- #### `verifyCodeService(params)`
775
+ Register custom templates to override defaults with your brand design:
740
776
 
741
777
  ```typescript
742
- await verifyCodeService({
743
- target: string; // Email or phone
744
- targetType: 'email' | 'phone';
745
- code: string; // 6-digit code
746
- purpose: 'registration' | 'login' | 'password_reset';
778
+ import { registerEmailTemplates } from '@spfn/auth/server';
779
+
780
+ // Register at app initialization (e.g., server.config.ts)
781
+ registerEmailTemplates({
782
+ // Override verification code template
783
+ verificationCode: ({ code, purpose, expiresInMinutes, appName }) => ({
784
+ subject: `[${appName}] Your verification code`,
785
+ text: `Your code: ${code}\nExpires in ${expiresInMinutes} minutes.`,
786
+ html: `
787
+ <div style="font-family: Arial, sans-serif;">
788
+ <img src="https://myapp.com/logo.png" alt="Logo" />
789
+ <h1>Verification Code</h1>
790
+ <div style="font-size: 32px; font-weight: bold;">${code}</div>
791
+ <p>This code expires in ${expiresInMinutes} minutes.</p>
792
+ </div>
793
+ `,
794
+ }),
795
+
796
+ // Override invitation template
797
+ invitation: ({ inviteLink, inviterName, roleName, appName }) => ({
798
+ subject: `${inviterName} invited you to ${appName}`,
799
+ text: `Accept invitation: ${inviteLink}`,
800
+ html: `
801
+ <h1>You're Invited!</h1>
802
+ <p>${inviterName} invited you to join ${appName} as ${roleName}.</p>
803
+ <a href="${inviteLink}">Accept Invitation</a>
804
+ `,
805
+ }),
747
806
  });
748
-
749
- // Returns: { valid: true, verificationToken: string }
750
807
  ```
751
808
 
809
+ **Template Parameters:**
810
+
811
+ | Template | Parameters |
812
+ |----------|------------|
813
+ | `verificationCode` | `code`, `purpose`, `expiresInMinutes?`, `appName?` |
814
+ | `welcome` | `email`, `appName?` |
815
+ | `passwordReset` | `resetLink`, `expiresInMinutes?`, `appName?` |
816
+ | `invitation` | `inviteLink`, `inviterName?`, `roleName?`, `appName?` |
817
+
752
818
  ---
753
819
 
754
- ## API Reference
820
+ ## Server-Side API
755
821
 
756
- ### Public Endpoints (No Authentication Required)
822
+ ### Public Routes (No Authentication)
757
823
 
758
- #### `POST /_auth/codes`
759
- Send a 6-digit verification code to email or phone.
824
+ All routes are automatically registered at `/_auth/*` via SPFN plugin system.
825
+
826
+ #### `POST /_auth/exists`
827
+
828
+ Check if account exists.
760
829
 
761
830
  **Request:**
762
831
  ```typescript
763
832
  {
764
- target: string; // Email or phone number in E.164
765
- targetType: 'email' | 'phone';
766
- purpose: 'registration' | 'login' | 'password_reset';
833
+ email?: string;
834
+ phone?: string; // E.164 format
767
835
  }
768
836
  ```
769
837
 
770
838
  **Response:**
771
839
  ```typescript
772
840
  {
773
- success: boolean;
774
- expiresAt: string; // ISO 8601 timestamp
841
+ exists: boolean;
842
+ identifier: string;
843
+ identifierType: 'email' | 'phone';
775
844
  }
776
845
  ```
777
846
 
778
847
  ---
779
848
 
780
- #### `POST /_auth/codes/verify`
781
- Verify the 6-digit code and receive a temporary token (15min validity).
849
+ #### `POST /_auth/codes`
850
+
851
+ Send verification code.
782
852
 
783
853
  **Request:**
784
854
  ```typescript
785
855
  {
786
- target: string;
856
+ target: string; // Email or phone
787
857
  targetType: 'email' | 'phone';
788
- code: string; // 6 digits
789
858
  purpose: 'registration' | 'login' | 'password_reset';
790
859
  }
791
860
  ```
@@ -793,50 +862,53 @@ Verify the 6-digit code and receive a temporary token (15min validity).
793
862
  **Response:**
794
863
  ```typescript
795
864
  {
796
- valid: boolean;
797
- verificationToken?: string; // Use for registration/password reset
865
+ success: boolean;
866
+ expiresAt: string; // ISO 8601
798
867
  }
799
868
  ```
800
869
 
801
870
  ---
802
871
 
803
- #### `POST /_auth/exists`
804
- Check if an account with given email/phone already exists.
872
+ #### `POST /_auth/codes/verify`
873
+
874
+ Verify OTP code.
805
875
 
806
876
  **Request:**
807
877
  ```typescript
808
878
  {
809
- email?: string; // Email address
810
- phone?: string; // E.164 format (e.g., +821012345678)
879
+ target: string;
880
+ targetType: 'email' | 'phone';
881
+ code: string; // 6 digits
882
+ purpose: 'registration' | 'login' | 'password_reset';
811
883
  }
812
884
  ```
813
885
 
814
886
  **Response:**
815
887
  ```typescript
816
888
  {
817
- exists: boolean;
818
- identifier: string; // The checked value
819
- identifierType: 'email' | 'phone';
889
+ valid: boolean;
890
+ verificationToken?: string; // 15min JWT for registration
820
891
  }
821
892
  ```
822
893
 
823
894
  ---
824
895
 
825
896
  #### `POST /_auth/register`
826
- Register a new user account.
897
+
898
+ Register new user.
827
899
 
828
900
  **Request:**
829
901
  ```typescript
830
902
  {
831
- email?: string; // One of email or phone required
832
- phone?: string; // E.164 format
903
+ email?: string;
904
+ phone?: string;
833
905
  verificationToken: string; // From /codes/verify
834
- password: string; // Minimum 8 characters
835
- publicKey: string; // Base64 DER (SPKI format)
836
- keyId: string; // UUID v4
837
- fingerprint: string; // SHA-256 hex (64 chars)
906
+ password: string; // Min 8 chars
907
+ publicKey: string; // Base64 DER (SPKI)
908
+ keyId: string; // UUID v4
909
+ fingerprint: string; // SHA-256 hex (64 chars)
838
910
  algorithm: 'ES256' | 'RS256';
839
- keySize?: number; // Optional, for logging
911
+ keySize?: number;
840
912
  }
841
913
  ```
842
914
 
@@ -852,18 +924,19 @@ Register a new user account.
852
924
  ---
853
925
 
854
926
  #### `POST /_auth/login`
855
- Authenticate user and register new public key.
927
+
928
+ User login.
856
929
 
857
930
  **Request:**
858
931
  ```typescript
859
932
  {
860
- email?: string; // One of email or phone required
933
+ email?: string;
861
934
  phone?: string;
862
935
  password: string;
863
- publicKey: string; // New key for this session
864
- keyId: string; // UUID v4
865
- fingerprint: string; // SHA-256 hex
866
- oldKeyId?: string; // Previous key to revoke
936
+ publicKey: string; // New key for session
937
+ keyId: string;
938
+ fingerprint: string;
939
+ oldKeyId?: string; // Revoke previous key
867
940
  algorithm: 'ES256' | 'RS256';
868
941
  keySize?: number;
869
942
  }
@@ -875,16 +948,24 @@ Authenticate user and register new public key.
875
948
  userId: string;
876
949
  email?: string;
877
950
  phone?: string;
878
- passwordChangeRequired: boolean; // If true, must change password
951
+ passwordChangeRequired: boolean;
879
952
  }
880
953
  ```
881
954
 
882
955
  ---
883
956
 
884
- ### Authenticated Endpoints (Require JWT + X-Key-Id Headers)
957
+ ### Authenticated Routes (Require JWT)
958
+
959
+ **Authentication:**
960
+ - Header: `Authorization: Bearer <jwt>`
961
+ - JWT payload must contain: `{ userId, keyId }`
962
+ - Server extracts `keyId` from JWT, fetches public key, verifies signature
963
+
964
+ ---
885
965
 
886
966
  #### `POST /_auth/logout`
887
- Revoke current key and logout.
967
+
968
+ Logout and revoke current key.
888
969
 
889
970
  **Request:**
890
971
  ```typescript
@@ -901,14 +982,15 @@ Revoke current key and logout.
901
982
  ---
902
983
 
903
984
  #### `POST /_auth/keys/rotate`
904
- Replace current key with a new one (before 90-day expiry).
985
+
986
+ Rotate public key before expiry (90 days).
905
987
 
906
988
  **Request:**
907
989
  ```typescript
908
990
  {
909
- publicKey: string; // New public key
910
- keyId: string; // New UUID v4
911
- fingerprint: string; // New fingerprint
991
+ publicKey: string; // New public key
992
+ keyId: string; // New UUID
993
+ fingerprint: string;
912
994
  algorithm: 'ES256' | 'RS256';
913
995
  keySize?: number;
914
996
  }
@@ -918,20 +1000,21 @@ Replace current key with a new one (before 90-day expiry).
918
1000
  ```typescript
919
1001
  {
920
1002
  success: boolean;
921
- keyId: string; // New key ID
1003
+ keyId: string;
922
1004
  }
923
1005
  ```
924
1006
 
925
1007
  ---
926
1008
 
927
1009
  #### `PUT /_auth/password`
928
- Change user password (requires current password).
1010
+
1011
+ Change password.
929
1012
 
930
1013
  **Request:**
931
1014
  ```typescript
932
1015
  {
933
1016
  currentPassword: string;
934
- newPassword: string; // Minimum 8 characters
1017
+ newPassword: string; // Min 8 chars
935
1018
  }
936
1019
  ```
937
1020
 
@@ -946,228 +1029,272 @@ Change user password (requires current password).
946
1029
 
947
1030
  ## Database Schema
948
1031
 
949
- ### Table: `users`
1032
+ ### Core Tables
1033
+
1034
+ #### `users`
950
1035
 
951
1036
  Main user identity table.
952
1037
 
953
- | Column | Type | Description |
954
- |--------|------|-------------|
955
- | `id` | bigserial | Primary key |
956
- | `email` | text | Email address (unique, nullable) |
957
- | `phone` | text | Phone in E.164 format (unique, nullable) |
958
- | `passwordHash` | text | bcrypt hash ($2b$10$..., 60 chars) |
959
- | `passwordChangeRequired` | boolean | Force password change on next login |
960
- | `roleId` | bigint | Foreign key to roles.id |
961
- | `status` | enum | `active`, `inactive`, `suspended` |
962
- | `emailVerifiedAt` | timestamp | Email verification time |
963
- | `phoneVerifiedAt` | timestamp | Phone verification time |
964
- | `lastLoginAt` | timestamp | Last successful login |
965
- | `createdAt` | timestamp | Account creation time |
966
- | `updatedAt` | timestamp | Last update time |
1038
+ ```sql
1039
+ CREATE TABLE users (
1040
+ id BIGSERIAL PRIMARY KEY,
1041
+ email TEXT UNIQUE,
1042
+ phone TEXT UNIQUE,
1043
+ password_hash TEXT NOT NULL,
1044
+ password_change_required BOOLEAN DEFAULT false,
1045
+ role_id BIGINT REFERENCES roles(id) NOT NULL,
1046
+ status TEXT NOT NULL CHECK (status IN ('active', 'inactive', 'suspended')),
1047
+ email_verified_at TIMESTAMP,
1048
+ phone_verified_at TIMESTAMP,
1049
+ last_login_at TIMESTAMP,
1050
+ created_at TIMESTAMP DEFAULT NOW(),
1051
+ updated_at TIMESTAMP DEFAULT NOW(),
1052
+
1053
+ CONSTRAINT users_identifier_check CHECK (
1054
+ (email IS NOT NULL) OR (phone IS NOT NULL)
1055
+ )
1056
+ );
1057
+ ```
967
1058
 
968
- **Constraints:**
969
- - At least one of `email` OR `phone` must be provided
970
- - Email and phone are unique when not null
971
- - `roleId` references roles.id (NOT NULL)
1059
+ **Key Points:**
1060
+ - At least one of `email` OR `phone` required
1061
+ - `passwordHash` is bcrypt ($2b$10$..., 60 chars)
1062
+ - `roleId` references roles table (NOT NULL)
972
1063
 
973
1064
  ---
974
1065
 
975
- ### Table: `user_public_keys`
1066
+ #### `user_public_keys`
976
1067
 
977
1068
  Stores client public keys for JWT verification.
978
1069
 
979
- | Column | Type | Description |
980
- |--------|------|-------------|
981
- | `id` | bigserial | Primary key |
982
- | `userId` | bigint | Foreign key to users.id |
983
- | `keyId` | text | Client-generated UUID (unique) |
984
- | `publicKey` | text | Base64 DER encoded (SPKI) |
985
- | `algorithm` | enum | `ES256`, `RS256` |
986
- | `fingerprint` | text | SHA-256 hex (64 chars) |
987
- | `isActive` | boolean | Key status (true = active) |
988
- | `createdAt` | timestamp | Key creation time |
989
- | `lastUsedAt` | timestamp | Last authentication time |
990
- | `expiresAt` | timestamp | Expiry time (90 days default) |
991
- | `revokedAt` | timestamp | Revocation time |
992
- | `revokedReason` | text | Revocation reason |
1070
+ ```sql
1071
+ CREATE TABLE user_public_keys (
1072
+ id BIGSERIAL PRIMARY KEY,
1073
+ user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
1074
+ key_id TEXT UNIQUE NOT NULL,
1075
+ public_key TEXT NOT NULL,
1076
+ algorithm TEXT NOT NULL CHECK (algorithm IN ('ES256', 'RS256')),
1077
+ fingerprint TEXT NOT NULL,
1078
+ is_active BOOLEAN DEFAULT true,
1079
+ created_at TIMESTAMP DEFAULT NOW(),
1080
+ last_used_at TIMESTAMP,
1081
+ expires_at TIMESTAMP NOT NULL,
1082
+ revoked_at TIMESTAMP,
1083
+ revoked_reason TEXT
1084
+ );
1085
+
1086
+ CREATE INDEX idx_user_public_keys_user_id ON user_public_keys(user_id);
1087
+ CREATE INDEX idx_user_public_keys_key_id ON user_public_keys(key_id);
1088
+ CREATE INDEX idx_user_public_keys_is_active ON user_public_keys(is_active);
1089
+ ```
993
1090
 
994
- **Indexes:**
995
- - `userId`, `keyId`, `isActive`, `fingerprint`
1091
+ **Key Points:**
1092
+ - `keyId` is client-generated UUID v4
1093
+ - `fingerprint` is SHA-256(publicKey) for verification
1094
+ - `expiresAt` defaults to 90 days from creation
1095
+ - `isActive` determines if key can be used
996
1096
 
997
1097
  ---
998
1098
 
999
- ### Table: `verification_codes`
1099
+ #### `verification_codes`
1000
1100
 
1001
- Stores OTP codes for email/SMS verification.
1101
+ OTP codes for email/SMS verification.
1002
1102
 
1003
- | Column | Type | Description |
1004
- |--------|------|-------------|
1005
- | `id` | bigserial | Primary key |
1006
- | `target` | text | Email or phone number |
1007
- | `targetType` | enum | `email`, `phone` |
1008
- | `code` | text | 6-digit code |
1009
- | `purpose` | enum | `registration`, `login`, `password_reset`, etc. |
1010
- | `expiresAt` | timestamp | Code expiry (5-10 minutes) |
1011
- | `usedAt` | timestamp | Time code was used |
1012
- | `createdAt` | timestamp | Code creation time |
1103
+ ```sql
1104
+ CREATE TABLE verification_codes (
1105
+ id BIGSERIAL PRIMARY KEY,
1106
+ target TEXT NOT NULL,
1107
+ target_type TEXT NOT NULL CHECK (target_type IN ('email', 'phone')),
1108
+ code TEXT NOT NULL,
1109
+ purpose TEXT NOT NULL CHECK (purpose IN ('registration', 'login', 'password_reset')),
1110
+ expires_at TIMESTAMP NOT NULL,
1111
+ used_at TIMESTAMP,
1112
+ created_at TIMESTAMP DEFAULT NOW()
1113
+ );
1013
1114
 
1014
- ---
1115
+ CREATE INDEX idx_verification_codes_target ON verification_codes(target);
1116
+ ```
1015
1117
 
1016
- ### Table: `user_social_accounts`
1118
+ **Key Points:**
1119
+ - 6-digit numeric code
1120
+ - Expires in 5-10 minutes (configurable)
1121
+ - Single-use (marked via `usedAt`)
1017
1122
 
1018
- OAuth provider accounts (future feature).
1123
+ ---
1124
+
1125
+ ### RBAC Tables
1126
+
1127
+ #### `roles`
1128
+
1129
+ ```sql
1130
+ CREATE TABLE roles (
1131
+ id BIGSERIAL PRIMARY KEY,
1132
+ name TEXT UNIQUE NOT NULL,
1133
+ display_name TEXT NOT NULL,
1134
+ description TEXT,
1135
+ is_builtin BOOLEAN DEFAULT false,
1136
+ is_system BOOLEAN DEFAULT false,
1137
+ is_active BOOLEAN DEFAULT true,
1138
+ priority INTEGER NOT NULL,
1139
+ created_at TIMESTAMP DEFAULT NOW(),
1140
+ updated_at TIMESTAMP DEFAULT NOW()
1141
+ );
1142
+ ```
1019
1143
 
1020
- | Column | Type | Description |
1021
- |--------|------|-------------|
1022
- | `id` | bigserial | Primary key |
1023
- | `userId` | bigint | Foreign key to users.id |
1024
- | `provider` | text | OAuth provider (google, github, etc.) |
1025
- | `providerId` | text | Provider's user ID |
1026
- | `accessToken` | text | OAuth access token |
1027
- | `refreshToken` | text | OAuth refresh token |
1028
- | `expiresAt` | timestamp | Token expiry |
1029
- | `createdAt` | timestamp | Account link time |
1030
-
1031
- ---
1032
-
1033
- ### Table: `roles`
1034
-
1035
- Role definitions for RBAC system.
1036
-
1037
- | Column | Type | Description |
1038
- |--------|------|-------------|
1039
- | `id` | bigserial | Primary key |
1040
- | `name` | text | Role name (unique, e.g., 'admin', 'user') |
1041
- | `displayName` | text | Human-readable name |
1042
- | `description` | text | Role description |
1043
- | `isBuiltin` | boolean | Cannot be deleted (user, admin, superadmin) |
1044
- | `isSystem` | boolean | System role (cannot be deleted) |
1045
- | `isActive` | boolean | Role status |
1046
- | `priority` | integer | Role hierarchy (higher = more privileged) |
1047
- | `createdAt` | timestamp | Creation time |
1048
- | `updatedAt` | timestamp | Last update time |
1049
-
1050
- **Built-in roles:**
1144
+ **Built-in Roles:**
1051
1145
  - `user` (priority 10) - Default role
1052
- - `admin` (priority 80) - Admin role
1053
- - `superadmin` (priority 100) - Super admin
1146
+ - `admin` (priority 80)
1147
+ - `superadmin` (priority 100)
1148
+
1149
+ ---
1150
+
1151
+ #### `permissions`
1152
+
1153
+ ```sql
1154
+ CREATE TABLE permissions (
1155
+ id BIGSERIAL PRIMARY KEY,
1156
+ name TEXT UNIQUE NOT NULL,
1157
+ display_name TEXT NOT NULL,
1158
+ description TEXT,
1159
+ category TEXT,
1160
+ is_builtin BOOLEAN DEFAULT false,
1161
+ is_system BOOLEAN DEFAULT false,
1162
+ is_active BOOLEAN DEFAULT true,
1163
+ created_at TIMESTAMP DEFAULT NOW(),
1164
+ updated_at TIMESTAMP DEFAULT NOW()
1165
+ );
1166
+ ```
1167
+
1168
+ **Built-in Permissions:**
1169
+ - `auth:self:manage`
1170
+ - `user:read`, `user:write`, `user:delete`
1171
+ - `rbac:role:manage`, `rbac:permission:manage`
1054
1172
 
1055
1173
  ---
1056
1174
 
1057
- ### Table: `permissions`
1175
+ #### `role_permissions`
1058
1176
 
1059
- Permission definitions for RBAC system.
1177
+ Many-to-many mapping between roles and permissions.
1060
1178
 
1061
- | Column | Type | Description |
1062
- |--------|------|-------------|
1063
- | `id` | bigserial | Primary key |
1064
- | `name` | text | Permission name (unique, e.g., 'user:delete') |
1065
- | `displayName` | text | Human-readable name |
1066
- | `description` | text | Permission description |
1067
- | `category` | text | Permission category (e.g., 'user', 'content') |
1068
- | `isBuiltin` | boolean | Built-in permission |
1069
- | `isSystem` | boolean | System permission |
1070
- | `isActive` | boolean | Permission status |
1071
- | `createdAt` | timestamp | Creation time |
1072
- | `updatedAt` | timestamp | Last update time |
1179
+ ```sql
1180
+ CREATE TABLE role_permissions (
1181
+ id BIGSERIAL PRIMARY KEY,
1182
+ role_id BIGINT REFERENCES roles(id) ON DELETE CASCADE,
1183
+ permission_id BIGINT REFERENCES permissions(id) ON DELETE CASCADE,
1184
+ created_at TIMESTAMP DEFAULT NOW(),
1185
+ updated_at TIMESTAMP DEFAULT NOW(),
1073
1186
 
1074
- **Built-in permissions:**
1075
- - `auth:self:manage` - Self auth management
1076
- - `user:read`, `user:write`, `user:delete` - User management
1077
- - `rbac:role:manage`, `rbac:permission:manage` - RBAC management
1187
+ UNIQUE(role_id, permission_id)
1188
+ );
1189
+ ```
1078
1190
 
1079
1191
  ---
1080
1192
 
1081
- ### Table: `role_permissions`
1193
+ #### `user_permissions`
1082
1194
 
1083
- Maps roles to permissions (many-to-many).
1195
+ User-specific permission overrides.
1084
1196
 
1085
- | Column | Type | Description |
1086
- |--------|------|-------------|
1087
- | `id` | bigserial | Primary key |
1088
- | `roleId` | bigint | Foreign key to roles.id |
1089
- | `permissionId` | bigint | Foreign key to permissions.id |
1090
- | `createdAt` | timestamp | Creation time |
1091
- | `updatedAt` | timestamp | Last update time |
1197
+ ```sql
1198
+ CREATE TABLE user_permissions (
1199
+ id BIGSERIAL PRIMARY KEY,
1200
+ user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
1201
+ permission_id BIGINT REFERENCES permissions(id) ON DELETE CASCADE,
1202
+ granted BOOLEAN NOT NULL,
1203
+ reason TEXT,
1204
+ expires_at TIMESTAMP,
1205
+ created_at TIMESTAMP DEFAULT NOW(),
1206
+ updated_at TIMESTAMP DEFAULT NOW(),
1207
+
1208
+ UNIQUE(user_id, permission_id)
1209
+ );
1210
+ ```
1092
1211
 
1093
- **Constraints:**
1094
- - `UNIQUE(roleId, permissionId)`
1095
- - `ON DELETE CASCADE` for both foreign keys
1212
+ **Use Cases:**
1213
+ - `granted: true` - Grant permission temporarily
1214
+ - `granted: false` - Revoke permission (even if role has it)
1215
+ - `expiresAt` - Temporary access with expiration
1096
1216
 
1097
1217
  ---
1098
1218
 
1099
- ### Table: `user_permissions`
1100
-
1101
- User-specific permission overrides.
1219
+ ### Supporting Tables
1102
1220
 
1103
- | Column | Type | Description |
1104
- |--------|------|-------------|
1105
- | `id` | bigserial | Primary key |
1106
- | `userId` | bigint | Foreign key to users.id |
1107
- | `permissionId` | bigint | Foreign key to permissions.id |
1108
- | `granted` | boolean | true = grant, false = revoke |
1109
- | `reason` | text | Reason for override |
1110
- | `expiresAt` | timestamp | Optional expiration time |
1111
- | `createdAt` | timestamp | Creation time |
1112
- | `updatedAt` | timestamp | Last update time |
1221
+ #### `invitations`
1113
1222
 
1114
- **Constraints:**
1115
- - `UNIQUE(userId, permissionId)`
1116
- - `ON DELETE CASCADE` for both foreign keys
1223
+ User invitation system.
1117
1224
 
1118
- **Use cases:**
1119
- - Temporary admin access (with `expiresAt`)
1120
- - Revoke specific permission (even if role has it)
1225
+ ```sql
1226
+ CREATE TABLE invitations (
1227
+ id BIGSERIAL PRIMARY KEY,
1228
+ email TEXT NOT NULL,
1229
+ token TEXT UNIQUE NOT NULL,
1230
+ role_id BIGINT REFERENCES roles(id),
1231
+ invited_by BIGINT REFERENCES users(id),
1232
+ status TEXT CHECK (status IN ('pending', 'accepted', 'cancelled', 'expired')),
1233
+ expires_at TIMESTAMP NOT NULL,
1234
+ accepted_at TIMESTAMP,
1235
+ created_at TIMESTAMP DEFAULT NOW()
1236
+ );
1237
+ ```
1121
1238
 
1122
1239
  ---
1123
1240
 
1124
- ## Role-Based Access Control (RBAC)
1125
-
1126
- The `@spfn/auth` package provides a flexible, extensible RBAC system that combines **code-defined system roles** with **runtime-created custom roles** and **granular permissions**.
1241
+ #### `user_profiles`
1242
+
1243
+ Extended user profile information.
1244
+
1245
+ ```sql
1246
+ CREATE TABLE user_profiles (
1247
+ id BIGSERIAL PRIMARY KEY,
1248
+ user_id BIGINT REFERENCES users(id) ON DELETE CASCADE UNIQUE,
1249
+ first_name TEXT,
1250
+ last_name TEXT,
1251
+ display_name TEXT,
1252
+ avatar_url TEXT,
1253
+ bio TEXT,
1254
+ created_at TIMESTAMP DEFAULT NOW(),
1255
+ updated_at TIMESTAMP DEFAULT NOW()
1256
+ );
1257
+ ```
1127
1258
 
1128
- ### Built-in Roles
1259
+ ---
1129
1260
 
1130
- These roles are automatically created and cannot be deleted:
1261
+ #### `user_social_accounts`
1131
1262
 
1132
- | Role | Priority | Built-in Permissions |
1133
- |------|----------|---------------------|
1134
- | `superadmin` | 100 | Full system access + RBAC management |
1135
- | `admin` | 80 | User management |
1136
- | `user` | 10 | Self auth management (default) |
1263
+ OAuth provider accounts (future feature).
1137
1264
 
1138
- ### Built-in Permissions
1265
+ ```sql
1266
+ CREATE TABLE user_social_accounts (
1267
+ id BIGSERIAL PRIMARY KEY,
1268
+ user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
1269
+ provider TEXT NOT NULL,
1270
+ provider_id TEXT NOT NULL,
1271
+ access_token TEXT,
1272
+ refresh_token TEXT,
1273
+ expires_at TIMESTAMP,
1274
+ created_at TIMESTAMP DEFAULT NOW(),
1275
+
1276
+ UNIQUE(provider, provider_id)
1277
+ );
1278
+ ```
1139
1279
 
1140
- Required permissions for auth package functionality:
1280
+ ---
1141
1281
 
1142
- - `auth:self:manage` - Change own password, rotate keys
1143
- - `user:read` - View user information
1144
- - `user:write` - Create and update users
1145
- - `user:delete` - Delete users
1146
- - `rbac:role:manage` - Create, update, delete roles
1147
- - `rbac:permission:manage` - Assign permissions
1282
+ ## RBAC System
1148
1283
 
1149
1284
  ### Initialization
1150
1285
 
1151
- #### Minimal Setup (Built-in Only)
1152
-
1153
1286
  ```typescript
1154
1287
  import { initializeAuth } from '@spfn/auth/server';
1155
1288
 
1156
- // Only built-in roles: user, admin, superadmin
1289
+ // Minimal setup (built-in roles only)
1157
1290
  await initializeAuth();
1158
- ```
1159
1291
 
1160
- #### With Presets
1161
-
1162
- ```typescript
1292
+ // With presets
1163
1293
  await initializeAuth({
1164
- usePresets: true, // Adds: moderator, editor, viewer + content permissions
1294
+ usePresets: true, // Adds moderator, editor, viewer roles
1165
1295
  });
1166
- ```
1167
-
1168
- #### Custom Roles & Permissions
1169
1296
 
1170
- ```typescript
1297
+ // Custom roles and permissions
1171
1298
  await initializeAuth({
1172
1299
  roles: [
1173
1300
  {
@@ -1175,11 +1302,6 @@ await initializeAuth({
1175
1302
  displayName: 'Content Creator',
1176
1303
  priority: 20,
1177
1304
  },
1178
- {
1179
- name: 'subscriber',
1180
- displayName: 'Subscriber',
1181
- priority: 15,
1182
- },
1183
1305
  ],
1184
1306
  permissions: [
1185
1307
  {
@@ -1187,34 +1309,35 @@ await initializeAuth({
1187
1309
  displayName: 'Create Posts',
1188
1310
  category: 'content',
1189
1311
  },
1190
- {
1191
- name: 'post:publish',
1192
- displayName: 'Publish Posts',
1193
- category: 'content',
1194
- },
1195
- {
1196
- name: 'video:upload',
1197
- displayName: 'Upload Videos',
1198
- category: 'media',
1199
- },
1200
1312
  ],
1201
1313
  rolePermissions: {
1202
- // Extend built-in admin role
1203
- admin: ['post:create', 'post:publish', 'video:upload'],
1204
-
1205
- // Custom role permissions
1206
- 'content-creator': ['post:create', 'post:publish', 'video:upload'],
1207
- subscriber: ['post:create'],
1314
+ 'content-creator': ['post:create'],
1208
1315
  },
1209
1316
  });
1210
1317
  ```
1211
1318
 
1212
- ### Permission Middleware
1319
+ ---
1320
+
1321
+ ### Built-in System
1322
+
1323
+ **Roles:**
1324
+ - `superadmin` (priority 100) - Full access
1325
+ - `admin` (priority 80) - User management
1326
+ - `user` (priority 10) - Self management
1327
+
1328
+ **Permissions:**
1329
+ - `auth:self:manage` - Change password, rotate keys
1330
+ - `user:read`, `user:write`, `user:delete`
1331
+ - `rbac:role:manage`, `rbac:permission:manage`
1332
+
1333
+ ---
1334
+
1335
+ ### Middleware Usage
1213
1336
 
1214
1337
  ```typescript
1215
1338
  import { authenticate, requirePermissions, requireRole } from '@spfn/auth/server';
1216
1339
 
1217
- // Require specific permission
1340
+ // Single permission
1218
1341
  app.bind(
1219
1342
  deleteUserContract,
1220
1343
  [authenticate, requirePermissions('user:delete')],
@@ -1223,7 +1346,7 @@ app.bind(
1223
1346
  }
1224
1347
  );
1225
1348
 
1226
- // Require multiple permissions (all)
1349
+ // Multiple permissions (all required)
1227
1350
  app.bind(
1228
1351
  publishPostContract,
1229
1352
  [authenticate, requirePermissions('post:write', 'post:publish')],
@@ -1232,545 +1355,608 @@ app.bind(
1232
1355
  }
1233
1356
  );
1234
1357
 
1235
- // Require role
1358
+ // Role-based
1236
1359
  app.bind(
1237
1360
  adminDashboardContract,
1238
1361
  [authenticate, requireRole('admin', 'superadmin')],
1239
1362
  async (c) => {
1240
- // Only admin or superadmin
1241
- }
1242
- );
1243
-
1244
- // Require any of these permissions
1245
- import { requireAnyPermission } from '@spfn/auth/server';
1246
-
1247
- app.bind(
1248
- viewContentContract,
1249
- [authenticate, requireAnyPermission('content:read', 'admin:access')],
1250
- async (c) => {
1251
- // Has either permission
1363
+ // Admin or superadmin only
1252
1364
  }
1253
1365
  );
1254
1366
  ```
1255
1367
 
1256
- ### Permission Checking in Code
1368
+ ---
1369
+
1370
+ ### Programmatic Checks
1257
1371
 
1258
1372
  ```typescript
1259
1373
  import { hasPermission, hasRole, getUserPermissions } from '@spfn/auth/server';
1260
1374
 
1261
- app.bind(createPostContract, [authenticate], async (c) => {
1262
- const { userId } = getAuth(c);
1263
-
1264
- // Check single permission
1265
- const canPublish = await hasPermission(userId, 'post:publish');
1266
-
1267
- // Check role
1268
- const isAdmin = await hasRole(userId, 'admin');
1375
+ const canPublish = await hasPermission(userId, 'post:publish');
1376
+ const isAdmin = await hasRole(userId, 'admin');
1377
+ const permissions = await getUserPermissions(userId);
1269
1378
 
1270
- // Get all permissions
1271
- const perms = await getUserPermissions(userId);
1272
-
1273
- // Conditional logic
1274
- const post = await createPost({
1275
- ...body,
1276
- status: canPublish ? 'published' : 'draft',
1277
- });
1278
-
1279
- return c.success(post);
1280
- });
1379
+ if (canPublish)
1380
+ {
1381
+ // Allow publish
1382
+ }
1281
1383
  ```
1282
1384
 
1385
+ ---
1386
+
1283
1387
  ### Runtime Role Management
1284
1388
 
1285
1389
  ```typescript
1286
1390
  import { createRole, addPermissionToRole } from '@spfn/auth/server';
1287
1391
 
1288
- // Create custom role at runtime
1392
+ // Create role
1289
1393
  const role = await createRole({
1290
1394
  name: 'moderator',
1291
- displayName: 'Community Moderator',
1292
- description: 'Manages community content',
1395
+ displayName: 'Moderator',
1293
1396
  priority: 40,
1294
- permissionIds: [1n, 2n, 3n], // Permission IDs
1397
+ permissionIds: [1n, 2n],
1295
1398
  });
1296
1399
 
1297
- // Add permission to role
1400
+ // Add permission
1298
1401
  await addPermissionToRole(role.id, 5n);
1299
1402
 
1300
- // Update role
1301
- await updateRole(role.id, {
1302
- displayName: 'Senior Moderator',
1303
- priority: 45,
1304
- });
1305
-
1306
- // Delete role (system roles protected)
1403
+ // Delete (system roles protected)
1307
1404
  await deleteRole(role.id);
1308
1405
  ```
1309
1406
 
1310
- ### Preset Roles & Permissions
1407
+ ---
1311
1408
 
1312
- Available presets (opt-in):
1409
+ ## Next.js Adapter
1313
1410
 
1314
- **Roles:**
1315
- - `moderator` (priority 50) - Content moderation
1316
- - `editor` (priority 30) - Content creation
1317
- - `viewer` (priority 5) - Read-only access
1411
+ ### Session Management
1318
1412
 
1319
- **Permissions:**
1320
- - `content:read`, `content:write`, `content:delete`, `content:publish`
1321
- - `comment:moderate`
1322
- - `system:config`
1323
- - `analytics:view`
1413
+ The Next.js adapter provides encrypted HttpOnly cookie-based sessions.
1414
+
1415
+ **Configuration:**
1416
+ ```bash
1417
+ # .env
1418
+ SPFN_AUTH_SESSION_SECRET=your-32-char-secret
1419
+ SPFN_AUTH_SESSION_TTL=7d # Optional, default 7d
1420
+ ```
1421
+
1422
+ **Session Data:**
1423
+ ```typescript
1424
+ interface SessionData {
1425
+ userId: string;
1426
+ privateKey: string; // Encrypted in cookie
1427
+ keyId: string;
1428
+ algorithm: 'ES256' | 'RS256';
1429
+ }
1430
+ ```
1324
1431
 
1325
- Use individually:
1432
+ ---
1433
+
1434
+ ### Server Component Guards
1326
1435
 
1327
1436
  ```typescript
1328
- import { PRESET_ROLES, PRESET_PERMISSIONS } from '@spfn/auth/server';
1437
+ // app/admin/page.tsx
1438
+ import { RequireAuth, RequireRole } from '@spfn/auth/nextjs/server';
1329
1439
 
1330
- await initializeAuth({
1331
- presetRoles: ['MODERATOR', 'EDITOR'],
1332
- presetPermissions: ['CONTENT_READ', 'CONTENT_WRITE', 'CONTENT_PUBLISH'],
1333
- rolePermissions: {
1334
- moderator: ['content:read', 'content:write', 'comment:moderate'],
1335
- editor: ['content:read', 'content:write', 'content:publish'],
1336
- },
1337
- });
1440
+ export default async function AdminPage()
1441
+ {
1442
+ return (
1443
+ <RequireAuth redirectTo="/login">
1444
+ <RequireRole roles={['admin', 'superadmin']} redirectTo="/forbidden">
1445
+ <div>Admin Dashboard</div>
1446
+ </RequireRole>
1447
+ </RequireAuth>
1448
+ );
1449
+ }
1338
1450
  ```
1339
1451
 
1340
- ### User-Specific Permissions
1452
+ ---
1341
1453
 
1342
- Grant or revoke permissions for individual users:
1454
+ ### Interceptors (API Routes)
1343
1455
 
1456
+ **Setup:**
1344
1457
  ```typescript
1345
- import { userPermissions } from '@spfn/auth';
1346
- import { getDatabase } from '@spfn/core/db';
1347
-
1348
- const db = getDatabase()!;
1349
-
1350
- // Grant temporary permission
1351
- await db.insert(userPermissions).values({
1352
- userId: 123n,
1353
- permissionId: 5n,
1354
- granted: true,
1355
- reason: 'Temporary admin access for migration',
1356
- expiresAt: new Date('2025-12-31'),
1357
- });
1458
+ // Simply import to auto-register
1459
+ import '@spfn/auth/nextjs/api';
1460
+ ```
1358
1461
 
1359
- // Revoke permission (even if role has it)
1360
- await db.insert(userPermissions).values({
1361
- userId: 456n,
1362
- permissionId: 3n,
1363
- granted: false,
1364
- reason: 'Security violation',
1365
- });
1462
+ **How It Works:**
1463
+ 1. Reads `session` HttpOnly cookie
1464
+ 2. Unseals session data
1465
+ 3. Generates JWT signed with `privateKey`
1466
+ 4. Injects `Authorization: Bearer <jwt>` header
1467
+
1468
+ **Target Routes:**
1469
+ - `/_auth/login`, `/_auth/register` - Login/register interceptor
1470
+ - `/_auth/keys/rotate` - Key rotation interceptor
1471
+ - All other authenticated routes - General auth interceptor
1472
+
1473
+ ---
1474
+
1475
+ ## Testing
1476
+
1477
+ ### Setup Test Environment
1478
+
1479
+ ```bash
1480
+ # Start test database
1481
+ pnpm docker:test:up
1482
+
1483
+ # Generate migrations
1484
+ pnpm db:generate
1485
+
1486
+ # Run migrations (via @spfn/core)
1487
+ cd ../../
1488
+ pnpm spfn db migrate
1366
1489
  ```
1367
1490
 
1368
- ### Account Status
1491
+ ---
1492
+
1493
+ ### Run Tests
1494
+
1495
+ ```bash
1496
+ # All tests
1497
+ pnpm test
1498
+
1499
+ # With coverage
1500
+ pnpm test:coverage
1501
+
1502
+ # Route tests only
1503
+ pnpm test:routes
1369
1504
 
1370
- | Status | Description | Login Allowed |
1371
- |--------|-------------|---------------|
1372
- | `active` | Normal operation | Yes |
1373
- | `inactive` | User deactivated account | No |
1374
- | `suspended` | Locked due to security/ToS violation | No |
1505
+ # Watch mode
1506
+ pnpm test --watch
1507
+ ```
1375
1508
 
1376
1509
  ---
1377
1510
 
1378
- ## Security
1511
+ ### Test Structure
1379
1512
 
1380
- ### Key Management Best Practices
1513
+ ```
1514
+ src/
1515
+ ├── __tests__/
1516
+ │ └── setup.ts # Global test setup
1517
+ └── server/
1518
+ ├── routes/
1519
+ │ └── auth/
1520
+ │ └── __tests__/
1521
+ │ ├── login.test.ts
1522
+ │ ├── register.test.ts
1523
+ │ └── ...
1524
+ └── services/
1525
+ └── __tests__/
1526
+ ├── auth.service.test.ts
1527
+ └── ...
1528
+ ```
1381
1529
 
1382
- 1. **Store Private Keys Securely**
1383
- - Use `sessionStorage` for session-only keys
1384
- - Use `localStorage` for persistent keys
1385
- - Never send private keys to server
1386
- - Never expose in logs or error messages
1530
+ ---
1387
1531
 
1388
- 2. **Rotate Keys Before Expiry**
1389
- - Keys expire after 90 days
1390
- - Rotate keys when `daysRemaining <= 7`
1391
- - Use `POST /_auth/keys/rotate` endpoint
1532
+ ### Writing Tests
1392
1533
 
1393
1534
  ```typescript
1394
- import { shouldRotateKey } from '@spfn/auth/client';
1535
+ import { describe, it, expect, beforeEach } from 'vitest';
1536
+ import { loginService } from '@/server/services';
1537
+
1538
+ describe('loginService', () =>
1539
+ {
1540
+ beforeEach(async () =>
1541
+ {
1542
+ // Setup test data
1543
+ });
1395
1544
 
1396
- const createdAt = new Date(localStorage.getItem('auth.keyCreatedAt'));
1397
- const { shouldRotate, daysRemaining } = shouldRotateKey(createdAt, 90);
1545
+ it('should login with valid credentials', async () =>
1546
+ {
1547
+ const result = await loginService({
1548
+ email: 'test@example.com',
1549
+ password: 'password123',
1550
+ publicKey: '...',
1551
+ keyId: '...',
1552
+ fingerprint: '...',
1553
+ algorithm: 'ES256',
1554
+ });
1398
1555
 
1399
- if (shouldRotate) {
1400
- console.warn(`Key expires in ${daysRemaining} days - rotate soon!`);
1401
- // Call rotation endpoint...
1402
- }
1556
+ expect(result.userId).toBeDefined();
1557
+ });
1558
+ });
1403
1559
  ```
1404
1560
 
1405
- 3. **Fingerprint Verification**
1406
- - Always send fingerprint with public key
1407
- - Server validates fingerprint = SHA-256(publicKey)
1408
- - Prevents key tampering during transmission
1561
+ ---
1409
1562
 
1410
- 4. **Token Expiry**
1411
- - JWT tokens expire after 15 minutes by default
1412
- - Use short expiry for sensitive operations
1413
- - Generate new token for each request or cache for <15min
1563
+ ### Test Database
1414
1564
 
1415
- 5. **Environment Variables**
1565
+ **docker-compose.test.yml:**
1566
+ ```yaml
1567
+ services:
1568
+ postgres-test:
1569
+ image: postgres:16-alpine
1570
+ environment:
1571
+ POSTGRES_DB: spfn_auth_test
1572
+ POSTGRES_USER: spfn
1573
+ POSTGRES_PASSWORD: spfn_dev_password
1574
+ ports:
1575
+ - "5433:5432"
1576
+ ```
1416
1577
 
1578
+ **Test env variables:**
1417
1579
  ```bash
1418
- # .env
1419
- SPFN_AUTH_JWT_SECRET=your-secret-key-change-in-production # For legacy tokens
1420
- SPFN_AUTH_JWT_EXPIRES_IN=7d # Token expiry
1580
+ DATABASE_URL=postgresql://spfn:spfn_dev_password@localhost:5433/spfn_auth_test
1421
1581
  ```
1422
1582
 
1423
1583
  ---
1424
1584
 
1425
- ## Setup
1585
+ ## Development Workflow
1586
+
1587
+ ### Initial Setup
1588
+
1589
+ ```bash
1590
+ # Install dependencies
1591
+ pnpm install
1592
+
1593
+ # Generate migrations
1594
+ pnpm db:generate
1426
1595
 
1427
- ### 1. Run Database Migrations
1596
+ # Build package
1597
+ pnpm build
1598
+ ```
1599
+
1600
+ ---
1601
+
1602
+ ### Development
1428
1603
 
1429
1604
  ```bash
1430
- npx spfn db migrate
1605
+ # Watch mode (auto-rebuild on changes)
1606
+ pnpm dev
1607
+
1608
+ # Type checking
1609
+ pnpm type-check
1610
+
1611
+ # Run tests
1612
+ pnpm test
1431
1613
  ```
1432
1614
 
1433
- This creates the auth schema with 8 tables:
1615
+ ---
1434
1616
 
1435
- **Core Tables:**
1436
- - `users` - User accounts and profiles
1437
- - `user_public_keys` - Client public keys for JWT
1438
- - `verification_codes` - OTP verification codes
1439
- - `user_social_accounts` - OAuth provider accounts
1617
+ ### Build Process
1440
1618
 
1441
- **RBAC Tables:**
1442
- - `roles` - System and custom roles
1443
- - `permissions` - System and custom permissions
1444
- - `role_permissions` - Role-permission mappings
1445
- - `user_permissions` - User-specific permission overrides
1619
+ The package uses `tsup` for building:
1446
1620
 
1447
- ### 2. Configure Environment Variables
1621
+ **tsup.config.ts:**
1622
+ ```typescript
1623
+ export default defineConfig({
1624
+ entry: {
1625
+ index: 'src/index.ts',
1626
+ server: 'src/server.ts',
1627
+ client: 'src/client.ts',
1628
+ // ... more entry points
1629
+ },
1630
+ format: ['esm'],
1631
+ dts: true,
1632
+ clean: true,
1633
+ sourcemap: true,
1634
+ });
1635
+ ```
1448
1636
 
1449
- #### Core Settings (Required)
1637
+ **Build outputs:**
1638
+ - `dist/index.js` + `dist/index.d.ts`
1639
+ - `dist/server.js` + `dist/server.d.ts`
1640
+ - `dist/client.js` + `dist/client.d.ts`
1641
+ - `dist/config/`, `dist/errors/`, `dist/nextjs/`
1450
1642
 
1451
- ```bash
1452
- # .env
1643
+ ---
1453
1644
 
1454
- # ========================================
1455
- # Core Authentication Settings (Required)
1456
- # ========================================
1645
+ ### Database Migrations
1457
1646
 
1458
- # JWT Token Settings
1459
- SPFN_AUTH_JWT_SECRET=your-secret-key-change-in-production # JWT signing secret (REQUIRED)
1460
- SPFN_AUTH_JWT_EXPIRES_IN=7d # JWT token expiry (default: 7d)
1647
+ ```bash
1648
+ # Generate new migration (after entity changes)
1649
+ pnpm db:generate
1461
1650
 
1462
- # Verification Token Settings
1463
- SPFN_AUTH_VERIFICATION_TOKEN_SECRET=separate-secret-key # Optional: separate secret for verification tokens
1464
- # If not set, uses SPFN_AUTH_JWT_SECRET
1651
+ # Apply migrations (via SPFN CLI)
1652
+ cd ../../
1653
+ pnpm spfn db migrate
1465
1654
 
1466
- # Password Hashing
1467
- SPFN_AUTH_BCRYPT_SALT_ROUNDS=10 # bcrypt salt rounds (default: 10)
1468
- # Higher = more secure but slower (10-12 recommended)
1655
+ # View database
1656
+ pnpm spfn db studio
1657
+ ```
1469
1658
 
1470
- # ========================================
1471
- # Client-Side Settings (Optional)
1472
- # ========================================
1659
+ **Migration files:** `migrations/*.sql`
1473
1660
 
1474
- # Session Management (for client-side session encryption)
1475
- SPFN_AUTH_SESSION_SECRET=session-encryption-key # Required if using client-side session features
1661
+ ---
1476
1662
 
1477
- # API URL Configuration (for client-side API calls)
1478
- SPFN_API_URL=http://localhost:8790 # SPFN API server URL
1479
- NEXT_PUBLIC_API_URL=http://localhost:8790 # Next.js public API URL (takes precedence)
1663
+ ### SPFN Plugin Integration
1480
1664
 
1481
- # Environment
1482
- NODE_ENV=production # production | development
1665
+ **package.json:**
1666
+ ```json
1667
+ {
1668
+ "spfn": {
1669
+ "schemas": ["./dist/server/entities/*.js"],
1670
+ "routes": {
1671
+ "basePath": "/_auth",
1672
+ "dir": "./dist/server/routes"
1673
+ },
1674
+ "migrations": {
1675
+ "dir": "./migrations"
1676
+ }
1677
+ }
1678
+ }
1483
1679
  ```
1484
1680
 
1485
- #### Admin Account Creation (Optional)
1681
+ **How it works:**
1682
+ 1. SPFN CLI discovers packages with `spfn` field
1683
+ 2. Auto-loads database schemas
1684
+ 3. Auto-registers routes at `basePath`
1685
+ 4. Includes migrations in `db migrate` command
1686
+
1687
+ ---
1688
+
1689
+ ### Code Style
1486
1690
 
1487
- See [Section 3: Create Initial Admin Accounts](#3-create-initial-admin-accounts-optional) below for details.
1691
+ Follow the project's code style (see `/Users/launchscreen/PROJECTS/SPFN/workspaces/.claude/rules.md`):
1488
1692
 
1489
- d---
1693
+ - **Brace placement:** Next line (Allman-style)
1694
+ - **Indentation:** 4 spaces
1695
+ - **Semicolons:** Always
1696
+ - **Type assertions:** Use `as`, not `<>`
1697
+
1698
+ **Example:**
1699
+ ```typescript
1700
+ export async function myFunction(): Promise<void>
1701
+ {
1702
+ if (condition)
1703
+ {
1704
+ await operation();
1705
+ }
1706
+ else
1707
+ {
1708
+ handleError();
1709
+ }
1710
+ }
1711
+ ```
1490
1712
 
1491
- ### Legacy Environment Variables (Backward Compatibility)
1713
+ ---
1492
1714
 
1493
- For backward compatibility, the package also supports legacy environment variable names without the `SPFN_AUTH_` prefix. The new prefixed versions take precedence:
1715
+ ### Environment Variables
1494
1716
 
1717
+ **Server-side:**
1495
1718
  ```bash
1496
- # Legacy (still supported, but deprecated)
1497
- JWT_SECRET=...
1498
- JWT_EXPIRES_IN=...
1499
- VERIFICATION_TOKEN_SECRET=...
1500
- BCRYPT_SALT_ROUNDS=...
1501
- SESSION_SECRET=...
1719
+ # Required
1720
+ SPFN_AUTH_JWT_SECRET=your-secret-key
1721
+ DATABASE_URL=postgresql://...
1502
1722
 
1503
- ADMIN_ACCOUNTS=...
1504
- ADMIN_EMAILS=...
1505
- ADMIN_PASSWORDS=...
1506
- ADMIN_ROLES=...
1507
- ADMIN_EMAIL=...
1508
- ADMIN_PASSWORD=...
1723
+ # Optional
1724
+ SPFN_AUTH_JWT_EXPIRES_IN=7d
1725
+ SPFN_AUTH_BCRYPT_SALT_ROUNDS=10
1726
+ SPFN_AUTH_VERIFICATION_TOKEN_SECRET=separate-secret
1509
1727
  ```
1510
1728
 
1511
- **Recommendation:** Use the new `SPFN_AUTH_*` prefixed variables to avoid conflicts with other packages.
1729
+ **Next.js adapter:**
1730
+ ```bash
1731
+ # Required
1732
+ SPFN_AUTH_SESSION_SECRET=your-32-char-secret
1733
+
1734
+ # Optional
1735
+ SPFN_AUTH_SESSION_TTL=7d
1736
+ SPFN_API_URL=http://localhost:8790
1737
+ ```
1512
1738
 
1513
- ### 3. Create Initial Admin Accounts (Optional)
1739
+ ---
1514
1740
 
1515
- You can automatically create admin accounts on server startup using environment variables. Three formats are supported:
1741
+ ### Debugging
1516
1742
 
1517
- #### Option 1: JSON Format (Most Flexible)
1743
+ **Enable logging:**
1744
+ ```typescript
1745
+ import { serverLogger } from '@/server/logger';
1518
1746
 
1519
- Allows full control over each account's configuration.
1747
+ serverLogger.info('Debug message', { context });
1748
+ serverLogger.error('Error occurred', error);
1749
+ ```
1520
1750
 
1751
+ **Inspect database:**
1521
1752
  ```bash
1522
- # .env
1523
- SPFN_AUTH_ADMIN_ACCOUNTS='[
1524
- {
1525
- "email": "super@example.com",
1526
- "password": "super-password",
1527
- "role": "superadmin",
1528
- "phone": "+821012345678",
1529
- "passwordChangeRequired": true
1530
- },
1531
- {
1532
- "email": "admin@example.com",
1533
- "password": "admin-password",
1534
- "role": "admin"
1535
- },
1536
- {
1537
- "email": "user@example.com",
1538
- "password": "user-password",
1539
- "role": "user",
1540
- "passwordChangeRequired": false
1541
- }
1542
- ]'
1753
+ pnpm spfn db studio
1543
1754
  ```
1544
1755
 
1545
- **JSON Fields:**
1546
- - `email` (required): Email address
1547
- - `password` (required): Initial password
1548
- - `role` (optional): `superadmin`, `admin`, or `user` (default: `user`)
1549
- - `phone` (optional): Phone number in E.164 format
1550
- - `passwordChangeRequired` (optional): Force password change on first login (default: `true`)
1756
+ **Check migrations:**
1757
+ ```bash
1758
+ ls migrations/
1759
+ ```
1551
1760
 
1552
1761
  ---
1553
1762
 
1554
- #### Option 2: Comma-Separated Format (Simple)
1763
+ ## Known Issues
1555
1764
 
1556
- Quick setup for multiple accounts with basic configuration.
1765
+ ### 1. Client Crypto Functions Missing
1557
1766
 
1558
- ```bash
1559
- # .env
1560
- SPFN_AUTH_ADMIN_EMAILS=super@example.com,admin@example.com,user@example.com
1561
- SPFN_AUTH_ADMIN_PASSWORDS=super-pass,admin-pass,user-pass
1562
- SPFN_AUTH_ADMIN_ROLES=superadmin,admin,user # Optional, defaults to 'user'
1563
- ```
1767
+ **Issue:** README documents `generateKeyPair` and `generateClientToken` in `@spfn/auth/client`, but they only exist in `@spfn/auth/server`.
1564
1768
 
1565
- **Requirements:**
1566
- - `SPFN_AUTH_ADMIN_EMAILS` and `SPFN_AUTH_ADMIN_PASSWORDS` must have the same number of items
1567
- - `SPFN_AUTH_ADMIN_ROLES` is optional (defaults to `user` for each account)
1568
- - All accounts will have `passwordChangeRequired: true`
1769
+ **Workaround:** Use server-side crypto functions or implement client-side crypto separately.
1770
+
1771
+ **Status:** Needs design decision - keep server-only or implement browser-compatible version.
1569
1772
 
1570
1773
  ---
1571
1774
 
1572
- #### Option 3: Single Account (Legacy)
1775
+ ### 2. Next.js Proxy Route Not Implemented
1573
1776
 
1574
- For backward compatibility, you can create a single superadmin account.
1777
+ **Issue:** Documentation mentions `@spfn/auth/nextjs/proxy` for client-side API proxying, but it doesn't exist.
1575
1778
 
1576
- ```bash
1577
- # .env
1578
- SPFN_AUTH_ADMIN_EMAIL=admin@example.com
1579
- SPFN_AUTH_ADMIN_PASSWORD=secure-password
1580
- ```
1779
+ **Status:** Feature planned but not implemented. Current alternative: use server-side `createAuthInterceptor`.
1780
+
1781
+ ---
1782
+
1783
+ ### 3. `lib/api` Client Functions Removed
1784
+
1785
+ **Issue:** Old `src/lib/api/` directory was deleted during refactoring.
1581
1786
 
1582
- This creates a single account with:
1583
- - `role: 'superadmin'`
1584
- - `passwordChangeRequired: true`
1787
+ **Status:** Intentional removal. Use services or HTTP routes directly.
1585
1788
 
1586
1789
  ---
1587
1790
 
1588
- #### Usage in Server Code
1791
+ ### 4. Test Coverage Below Target
1589
1792
 
1590
- Call `ensureAdminExists()` in your server startup code:
1793
+ **Current:** ~83%
1794
+ **Target:** 90%+
1591
1795
 
1592
- ```typescript
1593
- // src/server/index.ts or app initialization
1594
- import { ensureAdminExists } from '@spfn/auth/server';
1796
+ **Areas needing tests:**
1797
+ - Invitation service edge cases
1798
+ - RBAC permission checks
1799
+ - Key rotation scenarios
1800
+ - Session expiry handling
1595
1801
 
1596
- // Call during server startup
1597
- await ensureAdminExists();
1598
- ```
1802
+ ---
1599
1803
 
1600
- **Output Example:**
1601
- ```
1602
- [Auth] Creating 3 admin account(s)...
1603
- [Auth] ✅ Admin account created: super@example.com (superadmin)
1604
- [Auth] ✅ Admin account created: admin@example.com (admin)
1605
- [Auth] ⚠️ Account already exists: user@example.com (skipped)
1606
- [Auth] 📊 Summary: 2 created, 1 skipped, 0 failed
1607
- [Auth] ⚠️ Please change passwords on first login!
1608
- ```
1804
+ ## Roadmap
1805
+
1806
+ ### Short-term (Alpha Beta)
1609
1807
 
1610
- **Behavior:**
1611
- - Accounts are only created if they don't already exist
1612
- - All created accounts are auto-verified (`emailVerifiedAt` is set)
1613
- - By default, password change is required on first login
1614
- - If no environment variables are set, the function silently returns
1808
+ - [ ] **Client-side crypto** - Browser-compatible key generation
1809
+ - [ ] **Next.js proxy route** - Implement or remove from docs
1810
+ - [x] **High-level authApi** - Simplified Next.js auth functions (implemented in `@spfn/auth`)
1811
+ - [ ] **Test coverage** - Reach 90%+ coverage
1812
+ - [x] **Documentation** - Sync docs with actual code
1615
1813
 
1616
1814
  ---
1617
1815
 
1618
- ### 4. Import in Your SPFN Project
1816
+ ### Mid-term (Beta v1.0)
1619
1817
 
1620
- ```typescript
1621
- // Server-side only
1622
- import { authenticate, getAuth, getUser } from '@spfn/auth/server';
1623
- import { users, userPublicKeys } from '@spfn/auth'; // Entities
1818
+ - [ ] **React hooks** - useAuth, useSession, usePermissions
1819
+ - [ ] **UI components** - LoginForm, RegisterForm, AuthProvider
1820
+ - [ ] **OAuth integration** - Google, GitHub, etc.
1821
+ - [ ] **2FA support** - TOTP/authenticator apps
1822
+ - [ ] **Password reset flow** - Complete email-based reset
1823
+ - [ ] **Email change flow** - Verification for email updates
1824
+ - [ ] **Phone change flow** - SMS verification for phone updates
1825
+
1826
+ ---
1624
1827
 
1625
- // Client-side only
1626
- import { generateKeyPair, generateClientToken } from '@spfn/auth/client';
1828
+ ### Long-term (Post v1.0)
1627
1829
 
1628
- // Common (both sides)
1629
- import type { User, UserRole, UserStatus } from '@spfn/auth';
1630
- ```
1830
+ - [ ] **Admin UI** - User/role/permission management dashboard
1831
+ - [ ] **Audit logging** - Track auth events
1832
+ - [ ] **Rate limiting** - Built-in protection against brute force
1833
+ - [ ] **Multi-tenancy** - Organization/workspace support
1834
+ - [ ] **SSO integration** - SAML, OIDC
1835
+ - [ ] **Biometric auth** - WebAuthn/FIDO2 support
1631
1836
 
1632
1837
  ---
1633
1838
 
1634
- ## Testing
1839
+ ## Contributing
1635
1840
 
1636
- ### Run All Tests
1841
+ ### Before Contributing
1637
1842
 
1638
- ```bash
1639
- pnpm test
1640
- ```
1843
+ 1. Read this documentation thoroughly
1844
+ 2. Check existing issues/PRs
1845
+ 3. Understand the architecture
1846
+ 4. Follow code style guidelines
1641
1847
 
1642
- ### Run Tests with Coverage
1848
+ ---
1643
1849
 
1644
- ```bash
1645
- pnpm test:coverage
1646
- ```
1850
+ ### Pull Request Process
1647
1851
 
1648
- Current coverage: **83.01%** (25 tests passing)
1852
+ 1. **Create feature branch**
1853
+ ```bash
1854
+ git checkout -b feature/my-feature
1855
+ ```
1649
1856
 
1650
- ### Run Route Tests Only
1857
+ 2. **Make changes**
1858
+ - Follow code style
1859
+ - Add tests
1860
+ - Update docs if needed
1651
1861
 
1652
- ```bash
1653
- pnpm test:routes
1654
- ```
1862
+ 3. **Run checks**
1863
+ ```bash
1864
+ pnpm type-check
1865
+ pnpm test
1866
+ pnpm build
1867
+ ```
1655
1868
 
1656
- ### Start Test Database
1869
+ 4. **Commit with conventional commits**
1870
+ ```bash
1871
+ git commit -m "feat(auth): add password strength validation"
1872
+ ```
1657
1873
 
1658
- ```bash
1659
- pnpm docker:test:up
1660
- ```
1874
+ 5. **Push and create PR**
1875
+ ```bash
1876
+ git push origin feature/my-feature
1877
+ ```
1878
+
1879
+ ---
1661
1880
 
1662
- ### Stop Test Database
1881
+ ### Commit Message Format
1663
1882
 
1664
- ```bash
1665
- pnpm docker:test:down
1666
1883
  ```
1884
+ <type>(<scope>): <subject>
1667
1885
 
1668
- ---
1886
+ <body>
1669
1887
 
1670
- ## Package Structure
1888
+ <footer>
1889
+ ```
1890
+
1891
+ **Types:**
1892
+ - `feat` - New feature
1893
+ - `fix` - Bug fix
1894
+ - `refactor` - Code refactoring
1895
+ - `test` - Test changes
1896
+ - `docs` - Documentation
1897
+ - `chore` - Maintenance
1671
1898
 
1899
+ **Example:**
1672
1900
  ```
1673
- @spfn/auth/
1674
- ├── dist/
1675
- │ ├── index.js # Common exports (types, entities)
1676
- │ ├── server.js # Server-only exports (routes, middleware, helpers, services)
1677
- │ └── client.js # Client-only exports (crypto, hooks, store)
1678
- ├── migrations/ # Drizzle database migrations
1679
- └── src/
1680
- ├── index.ts # Common entry point
1681
- ├── server.ts # Server entry point
1682
- ├── client.ts # Client entry point
1683
- ├── lib/ # Shared code
1684
- │ ├── api/ # API client functions
1685
- │ ├── contracts/ # Type-safe API contracts
1686
- │ └── types/ # Shared TypeScript types
1687
- ├── server/ # Server-only code
1688
- │ ├── entities/ # Drizzle ORM entities
1689
- │ ├── services/ # 🆕 Business logic layer (reusable functions)
1690
- │ │ ├── auth.service.ts
1691
- │ │ ├── verification.service.ts
1692
- │ │ ├── key.service.ts
1693
- │ │ ├── user.service.ts
1694
- │ │ └── index.ts
1695
- │ ├── routes/ # API route handlers (thin layer calling services)
1696
- │ ├── middleware/ # Authentication middleware
1697
- │ ├── helpers/ # JWT, password, verification utils
1698
- │ └── repositories/ # Database access layer
1699
- └── client/ # Client-only code
1700
- ├── lib/ # Crypto helpers (key generation, JWT signing)
1701
- ├── hooks/ # React hooks (TODO)
1702
- ├── store/ # Zustand state management (TODO)
1703
- └── components/ # React components (TODO)
1704
- ```
1705
-
1706
- ---
1707
-
1708
- ## SPFN Framework Integration
1709
-
1710
- This package automatically integrates with SPFN via `package.json`:
1901
+ feat(rbac): add permission inheritance
1711
1902
 
1712
- ```json
1713
- {
1714
- "spfn": {
1715
- "prefix": "/_auth",
1716
- "schemas": ["./dist/server/entities/*.js"],
1717
- "routes": {
1718
- "basePath": "/auth",
1719
- "dir": "./dist/server/routes"
1720
- },
1721
- "migrations": {
1722
- "dir": "./migrations"
1723
- }
1724
- }
1725
- }
1903
+ Implement hierarchical permission inheritance where child roles
1904
+ automatically inherit parent role permissions.
1905
+
1906
+ Closes #123
1726
1907
  ```
1727
1908
 
1728
- Routes are automatically registered:
1729
- - `/_auth/codes` → Send verification code
1730
- - `/_auth/codes/verify` → Verify code
1731
- - `/_auth/exists` → Check account existence
1732
- - `/_auth/register` → Register user
1733
- - `/_auth/login` → Login
1734
- - `/_auth/logout` Logout (authenticated)
1735
- - `/_auth/keys/rotate` Rotate key (authenticated)
1736
- - `/_auth/password` Change password (authenticated)
1909
+ ---
1910
+
1911
+ ## Release Process
1912
+
1913
+ ### Version Naming
1914
+
1915
+ - `0.1.0-alpha.x` - Alpha releases (current)
1916
+ - `0.1.0-beta.x` - Beta releases
1917
+ - `1.0.0` - Stable release
1737
1918
 
1738
1919
  ---
1739
1920
 
1740
- ## Development Status
1921
+ ### Publishing
1741
1922
 
1742
- **Version:** 0.1.0-alpha.0 (Alpha)
1923
+ ```bash
1924
+ # Alpha release
1925
+ pnpm run publish:alpha
1743
1926
 
1744
- **Completed:**
1745
- - Asymmetric JWT authentication (ES256/RS256)
1746
- - User registration and login
1747
- - OTP verification flow (email/SMS)
1748
- - Session management with key rotation
1749
- - Password change functionality
1750
- - RBAC roles and account status
1751
- - Comprehensive test coverage (83%)
1927
+ # Beta release
1928
+ pnpm run publish:beta
1752
1929
 
1753
- **In Progress:**
1754
- - Client-side React hooks (useAuth, useSession)
1755
- - Client-side Zustand store
1756
- - React UI components (LoginForm, RegisterForm)
1930
+ # Production release
1931
+ pnpm run publish:latest
1932
+ ```
1757
1933
 
1758
- **Roadmap:**
1759
- - OAuth provider integration (Google, GitHub)
1760
- - Two-factor authentication (2FA)
1761
- - Password reset flow
1762
- - Email change flow
1763
- - Phone change flow
1764
- - Admin management APIs
1934
+ **Pre-publish checklist:**
1935
+ - [ ] All tests pass
1936
+ - [ ] Type checking passes
1937
+ - [ ] Build succeeds
1938
+ - [ ] CHANGELOG updated
1939
+ - [ ] Version bumped
1940
+ - [ ] Docs updated
1765
1941
 
1766
1942
  ---
1767
1943
 
1768
- ## Contributing
1944
+ ## Support
1769
1945
 
1770
- This is an internal SPFN package. Please follow the monorepo conventions when contributing.
1946
+ ### Internal Team
1947
+
1948
+ - **Issues:** GitHub Issues
1949
+ - **Discussions:** GitHub Discussions
1950
+ - **Slack:** #spfn-auth channel
1771
1951
 
1772
1952
  ---
1773
1953
 
1774
1954
  ## License
1775
1955
 
1776
- MIT
1956
+ MIT License - See LICENSE file for details.
1957
+
1958
+ ---
1959
+
1960
+ **Last Updated:** 2025-12-07
1961
+ **Document Version:** 2.2.0 (Technical Documentation)
1962
+ **Package Version:** 0.1.0-alpha.88