@spfn/auth 0.1.0-alpha.88 → 0.2.0-beta.2
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.
- package/README.md +1385 -1199
- package/dist/config.d.ts +405 -0
- package/dist/config.js +240 -0
- package/dist/config.js.map +1 -0
- package/dist/dto-CLYtuAom.d.ts +630 -0
- package/dist/errors.d.ts +196 -0
- package/dist/errors.js +173 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +273 -14
- package/dist/index.js +511 -6665
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +345 -0
- package/dist/nextjs/api.js.map +1 -0
- package/dist/{adapters/nextjs → nextjs}/server.d.ts +47 -65
- package/dist/nextjs/server.js +179 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/server.d.ts +4328 -529
- package/dist/server.js +7841 -1247
- package/dist/server.js.map +1 -1
- package/migrations/{0000_skinny_christian_walker.sql → 0000_marvelous_justice.sql} +53 -23
- package/migrations/meta/0000_snapshot.json +281 -46
- package/migrations/meta/_journal.json +2 -2
- package/package.json +31 -31
- package/dist/adapters/nextjs/api.d.ts +0 -446
- package/dist/adapters/nextjs/api.js +0 -3279
- package/dist/adapters/nextjs/api.js.map +0 -1
- package/dist/adapters/nextjs/server.js +0 -3645
- package/dist/adapters/nextjs/server.js.map +0 -1
- package/dist/lib/api/auth-codes-verify.d.ts +0 -37
- package/dist/lib/api/auth-codes-verify.js +0 -2949
- package/dist/lib/api/auth-codes-verify.js.map +0 -1
- package/dist/lib/api/auth-codes.d.ts +0 -37
- package/dist/lib/api/auth-codes.js +0 -2949
- package/dist/lib/api/auth-codes.js.map +0 -1
- package/dist/lib/api/auth-exists.d.ts +0 -38
- package/dist/lib/api/auth-exists.js +0 -2949
- package/dist/lib/api/auth-exists.js.map +0 -1
- package/dist/lib/api/auth-invitations-accept.d.ts +0 -38
- package/dist/lib/api/auth-invitations-accept.js +0 -2883
- package/dist/lib/api/auth-invitations-accept.js.map +0 -1
- package/dist/lib/api/auth-invitations-cancel.d.ts +0 -37
- package/dist/lib/api/auth-invitations-cancel.js +0 -2883
- package/dist/lib/api/auth-invitations-cancel.js.map +0 -1
- package/dist/lib/api/auth-invitations-delete.d.ts +0 -36
- package/dist/lib/api/auth-invitations-delete.js +0 -2883
- package/dist/lib/api/auth-invitations-delete.js.map +0 -1
- package/dist/lib/api/auth-invitations-resend.d.ts +0 -37
- package/dist/lib/api/auth-invitations-resend.js +0 -2883
- package/dist/lib/api/auth-invitations-resend.js.map +0 -1
- package/dist/lib/api/auth-invitations.d.ts +0 -109
- package/dist/lib/api/auth-invitations.js +0 -2887
- package/dist/lib/api/auth-invitations.js.map +0 -1
- package/dist/lib/api/auth-keys-rotate.d.ts +0 -37
- package/dist/lib/api/auth-keys-rotate.js +0 -2949
- package/dist/lib/api/auth-keys-rotate.js.map +0 -1
- package/dist/lib/api/auth-login.d.ts +0 -39
- package/dist/lib/api/auth-login.js +0 -2949
- package/dist/lib/api/auth-login.js.map +0 -1
- package/dist/lib/api/auth-logout.d.ts +0 -36
- package/dist/lib/api/auth-logout.js +0 -2949
- package/dist/lib/api/auth-logout.js.map +0 -1
- package/dist/lib/api/auth-me.d.ts +0 -50
- package/dist/lib/api/auth-me.js +0 -2949
- package/dist/lib/api/auth-me.js.map +0 -1
- package/dist/lib/api/auth-password.d.ts +0 -36
- package/dist/lib/api/auth-password.js +0 -2949
- package/dist/lib/api/auth-password.js.map +0 -1
- package/dist/lib/api/auth-register.d.ts +0 -38
- package/dist/lib/api/auth-register.js +0 -2949
- package/dist/lib/api/auth-register.js.map +0 -1
- package/dist/lib/api/index.d.ts +0 -356
- package/dist/lib/api/index.js +0 -3261
- package/dist/lib/api/index.js.map +0 -1
- package/dist/lib/config.d.ts +0 -70
- package/dist/lib/config.js +0 -64
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/contracts/auth.d.ts +0 -302
- package/dist/lib/contracts/auth.js +0 -2951
- package/dist/lib/contracts/auth.js.map +0 -1
- package/dist/lib/contracts/index.d.ts +0 -3
- package/dist/lib/contracts/index.js +0 -3190
- package/dist/lib/contracts/index.js.map +0 -1
- package/dist/lib/contracts/invitation.d.ts +0 -243
- package/dist/lib/contracts/invitation.js +0 -2883
- package/dist/lib/contracts/invitation.js.map +0 -1
- package/dist/lib/crypto.d.ts +0 -76
- package/dist/lib/crypto.js +0 -127
- package/dist/lib/crypto.js.map +0 -1
- package/dist/lib/index.d.ts +0 -4
- package/dist/lib/index.js +0 -313
- package/dist/lib/index.js.map +0 -1
- package/dist/lib/session.d.ts +0 -68
- package/dist/lib/session.js +0 -126
- package/dist/lib/session.js.map +0 -1
- package/dist/lib/types/api.d.ts +0 -45
- package/dist/lib/types/api.js +0 -1
- package/dist/lib/types/api.js.map +0 -1
- package/dist/lib/types/index.d.ts +0 -3
- package/dist/lib/types/index.js +0 -2647
- package/dist/lib/types/index.js.map +0 -1
- package/dist/lib/types/schemas.d.ts +0 -45
- package/dist/lib/types/schemas.js +0 -2647
- package/dist/lib/types/schemas.js.map +0 -1
- package/dist/lib.js +0 -1
- package/dist/lib.js.map +0 -1
- package/dist/plugin.d.ts +0 -12
- package/dist/plugin.js +0 -9083
- package/dist/plugin.js.map +0 -1
- package/dist/server/entities/index.d.ts +0 -11
- package/dist/server/entities/index.js +0 -395
- package/dist/server/entities/index.js.map +0 -1
- package/dist/server/entities/invitations.d.ts +0 -241
- package/dist/server/entities/invitations.js +0 -184
- package/dist/server/entities/invitations.js.map +0 -1
- package/dist/server/entities/permissions.d.ts +0 -196
- package/dist/server/entities/permissions.js +0 -49
- package/dist/server/entities/permissions.js.map +0 -1
- package/dist/server/entities/role-permissions.d.ts +0 -107
- package/dist/server/entities/role-permissions.js +0 -115
- package/dist/server/entities/role-permissions.js.map +0 -1
- package/dist/server/entities/roles.d.ts +0 -196
- package/dist/server/entities/roles.js +0 -50
- package/dist/server/entities/roles.js.map +0 -1
- package/dist/server/entities/schema.d.ts +0 -14
- package/dist/server/entities/schema.js +0 -7
- package/dist/server/entities/schema.js.map +0 -1
- package/dist/server/entities/user-permissions.d.ts +0 -163
- package/dist/server/entities/user-permissions.js +0 -193
- package/dist/server/entities/user-permissions.js.map +0 -1
- package/dist/server/entities/user-public-keys.d.ts +0 -227
- package/dist/server/entities/user-public-keys.js +0 -156
- package/dist/server/entities/user-public-keys.js.map +0 -1
- package/dist/server/entities/user-social-accounts.d.ts +0 -189
- package/dist/server/entities/user-social-accounts.js +0 -149
- package/dist/server/entities/user-social-accounts.js.map +0 -1
- package/dist/server/entities/users.d.ts +0 -235
- package/dist/server/entities/users.js +0 -117
- package/dist/server/entities/users.js.map +0 -1
- package/dist/server/entities/verification-codes.d.ts +0 -191
- package/dist/server/entities/verification-codes.js +0 -49
- package/dist/server/entities/verification-codes.js.map +0 -1
- package/dist/server/routes/auth/index.d.ts +0 -10
- package/dist/server/routes/auth/index.js +0 -4460
- package/dist/server/routes/auth/index.js.map +0 -1
- package/dist/server/routes/index.d.ts +0 -6
- package/dist/server/routes/index.js +0 -6584
- package/dist/server/routes/index.js.map +0 -1
- package/dist/server/routes/invitations/index.d.ts +0 -10
- package/dist/server/routes/invitations/index.js +0 -4395
- package/dist/server/routes/invitations/index.js.map +0 -1
- /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
|
-
|
|
4
|
-
|
|
3
|
+
**Version:** 0.1.0-alpha.88
|
|
4
|
+
**Status:** Alpha - Internal Development
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
import { generateKeyPair } from '@spfn/auth/client';
|
|
29
|
+
---
|
|
75
30
|
|
|
76
|
-
|
|
77
|
-
const keyPair = generateKeyPair('ES256');
|
|
31
|
+
## Overview
|
|
78
32
|
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
###
|
|
42
|
+
### Design Principles
|
|
94
43
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
await authSendCode({
|
|
100
|
-
target: 'user@example.com',
|
|
101
|
-
targetType: 'email',
|
|
102
|
-
purpose: 'registration'
|
|
103
|
-
});
|
|
50
|
+
---
|
|
104
51
|
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
56
|
+
```bash
|
|
57
|
+
pnpm add @spfn/auth
|
|
126
58
|
```
|
|
127
59
|
|
|
128
|
-
###
|
|
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
|
-
|
|
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 {
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
77
|
+
#### Register Router in `router.ts`
|
|
182
78
|
|
|
183
79
|
```typescript
|
|
184
|
-
import {
|
|
185
|
-
import {
|
|
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
|
-
|
|
83
|
+
export const appRouter = defineRouter({
|
|
84
|
+
// Auth routes (fixed namespace)
|
|
85
|
+
auth: authRouter,
|
|
199
86
|
|
|
200
|
-
|
|
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
|
-
|
|
93
|
+
#### Register Router Metadata and Errors in `api-client.ts`
|
|
219
94
|
|
|
220
95
|
```typescript
|
|
221
|
-
import {
|
|
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
|
-
###
|
|
225
|
-
|
|
226
|
-
The `authApi` object provides simplified authentication functions with automatic key generation and session management:
|
|
109
|
+
### 4. Environment Variables
|
|
227
110
|
|
|
228
|
-
```
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
'use client';
|
|
306
|
-
import { UniversalClient } from '@spfn/core/client';
|
|
237
|
+
---
|
|
307
238
|
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
334
|
+
## Module Exports
|
|
324
335
|
|
|
325
|
-
|
|
336
|
+
### Common Module (`@spfn/auth`)
|
|
326
337
|
|
|
338
|
+
**Entities:**
|
|
327
339
|
```typescript
|
|
328
340
|
import {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
import type {
|
|
357
|
+
User,
|
|
358
|
+
UserPublicKey,
|
|
359
|
+
VerificationCode,
|
|
360
|
+
Role,
|
|
361
|
+
Permission,
|
|
362
|
+
// ... etc
|
|
363
|
+
} from '@spfn/auth';
|
|
359
364
|
```
|
|
360
365
|
|
|
361
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
});
|
|
368
|
+
import {
|
|
369
|
+
BUILTIN_ROLES,
|
|
370
|
+
BUILTIN_PERMISSIONS,
|
|
371
|
+
BUILTIN_ROLE_PERMISSIONS
|
|
372
|
+
} from '@spfn/auth';
|
|
373
373
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
374
|
+
import type {
|
|
375
|
+
RoleConfig,
|
|
376
|
+
PermissionConfig,
|
|
377
|
+
InitializeAuthOptions,
|
|
378
|
+
BuiltinRoleName,
|
|
379
|
+
BuiltinPermissionName
|
|
380
|
+
} from '@spfn/auth';
|
|
380
381
|
```
|
|
381
382
|
|
|
382
|
-
|
|
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
|
-
###
|
|
385
|
+
### Server Module (`@spfn/auth/server`)
|
|
389
386
|
|
|
387
|
+
**Router:**
|
|
390
388
|
```typescript
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
478
|
+
**Repositories:**
|
|
485
479
|
```typescript
|
|
486
480
|
import {
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
493
|
-
|
|
493
|
+
**Middleware:**
|
|
494
494
|
```typescript
|
|
495
495
|
import {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
496
|
+
authenticate,
|
|
497
|
+
requirePermissions,
|
|
498
|
+
requireRole,
|
|
499
499
|
} from '@spfn/auth/server';
|
|
500
|
-
```
|
|
501
500
|
|
|
502
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
###
|
|
543
|
+
### Client Module (`@spfn/auth/client`)
|
|
517
544
|
|
|
518
|
-
|
|
519
|
-
import { createApp } from '@spfn/core/route';
|
|
520
|
-
import { loginService } from '@spfn/auth/server';
|
|
545
|
+
> **Status:** Work in Progress - Placeholders only
|
|
521
546
|
|
|
522
|
-
|
|
547
|
+
```typescript
|
|
548
|
+
// Currently empty exports
|
|
549
|
+
import {} from '@spfn/auth/client';
|
|
550
|
+
```
|
|
523
551
|
|
|
524
|
-
|
|
525
|
-
|
|
552
|
+
**Planned:**
|
|
553
|
+
- React hooks (useAuth, useSession)
|
|
554
|
+
- Zustand store
|
|
555
|
+
- UI components (LoginForm, etc.)
|
|
526
556
|
|
|
527
|
-
|
|
528
|
-
console.log(`Login attempt: ${body.email}`);
|
|
557
|
+
---
|
|
529
558
|
|
|
530
|
-
|
|
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
|
-
|
|
542
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
574
|
+
### Errors Module (`@spfn/auth/errors`)
|
|
567
575
|
|
|
568
576
|
```typescript
|
|
569
577
|
import {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
578
|
-
const { verificationToken } = await verifyCodeService({
|
|
579
|
-
target: body.email,
|
|
580
|
-
targetType: 'email',
|
|
581
|
-
code: body.otp,
|
|
582
|
-
purpose: 'registration',
|
|
583
|
-
});
|
|
601
|
+
---
|
|
584
602
|
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
609
|
-
|
|
607
|
+
```typescript
|
|
608
|
+
import {
|
|
609
|
+
authInterceptors,
|
|
610
|
+
loginRegisterInterceptor,
|
|
611
|
+
generalAuthInterceptor,
|
|
612
|
+
keyRotationInterceptor,
|
|
613
|
+
} from '@spfn/auth/nextjs/api';
|
|
610
614
|
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
632
|
-
|
|
656
|
+
// Get session (read-only, safe in Server Components)
|
|
657
|
+
const session = await getSession();
|
|
633
658
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
659
|
+
// Clear session
|
|
660
|
+
await clearSession();
|
|
661
|
+
```
|
|
637
662
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
663
|
+
**Guard Usage:**
|
|
664
|
+
```typescript
|
|
665
|
+
// app/dashboard/page.tsx
|
|
666
|
+
import { RequireAuth } from '@spfn/auth/nextjs/server';
|
|
641
667
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
650
|
-
}
|
|
678
|
+
---
|
|
651
679
|
|
|
652
|
-
|
|
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
|
-
|
|
662
|
-
return c.json({ step: 3, verificationToken });
|
|
663
|
-
}
|
|
682
|
+
### Email Service
|
|
664
683
|
|
|
665
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
715
|
+
### SMS Service
|
|
716
|
+
|
|
717
|
+
The SMS service uses AWS SNS by default.
|
|
687
718
|
|
|
719
|
+
**Send SMS:**
|
|
688
720
|
```typescript
|
|
689
|
-
import {
|
|
721
|
+
import { sendSMS } from '@spfn/auth/server';
|
|
690
722
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
723
|
+
await sendSMS({
|
|
724
|
+
phone: '+821012345678', // E.164 format
|
|
725
|
+
message: 'Your code is: 123456',
|
|
726
|
+
purpose: 'verification',
|
|
727
|
+
});
|
|
728
|
+
```
|
|
694
729
|
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
746
|
+
## Email Templates
|
|
747
|
+
|
|
748
|
+
### Built-in Templates
|
|
704
749
|
|
|
705
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
768
|
+
await sendEmail({ to: 'user@example.com', subject, text, html });
|
|
720
769
|
```
|
|
721
770
|
|
|
722
|
-
|
|
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
|
-
|
|
737
|
-
```
|
|
773
|
+
### Custom Templates
|
|
738
774
|
|
|
739
|
-
|
|
775
|
+
Register custom templates to override defaults with your brand design:
|
|
740
776
|
|
|
741
777
|
```typescript
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
|
820
|
+
## Server-Side API
|
|
755
821
|
|
|
756
|
-
### Public
|
|
822
|
+
### Public Routes (No Authentication)
|
|
757
823
|
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
765
|
-
|
|
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
|
-
|
|
774
|
-
|
|
841
|
+
exists: boolean;
|
|
842
|
+
identifier: string;
|
|
843
|
+
identifierType: 'email' | 'phone';
|
|
775
844
|
}
|
|
776
845
|
```
|
|
777
846
|
|
|
778
847
|
---
|
|
779
848
|
|
|
780
|
-
#### `POST /_auth/codes
|
|
781
|
-
|
|
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
|
-
|
|
797
|
-
|
|
865
|
+
success: boolean;
|
|
866
|
+
expiresAt: string; // ISO 8601
|
|
798
867
|
}
|
|
799
868
|
```
|
|
800
869
|
|
|
801
870
|
---
|
|
802
871
|
|
|
803
|
-
#### `POST /_auth/
|
|
804
|
-
|
|
872
|
+
#### `POST /_auth/codes/verify`
|
|
873
|
+
|
|
874
|
+
Verify OTP code.
|
|
805
875
|
|
|
806
876
|
**Request:**
|
|
807
877
|
```typescript
|
|
808
878
|
{
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
818
|
-
|
|
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
|
-
|
|
897
|
+
|
|
898
|
+
Register new user.
|
|
827
899
|
|
|
828
900
|
**Request:**
|
|
829
901
|
```typescript
|
|
830
902
|
{
|
|
831
|
-
email?: string;
|
|
832
|
-
phone?: string;
|
|
903
|
+
email?: string;
|
|
904
|
+
phone?: string;
|
|
833
905
|
verificationToken: string; // From /codes/verify
|
|
834
|
-
password: string;
|
|
835
|
-
publicKey: string;
|
|
836
|
-
keyId: string;
|
|
837
|
-
fingerprint: string;
|
|
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;
|
|
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
|
-
|
|
927
|
+
|
|
928
|
+
User login.
|
|
856
929
|
|
|
857
930
|
**Request:**
|
|
858
931
|
```typescript
|
|
859
932
|
{
|
|
860
|
-
email?: string;
|
|
933
|
+
email?: string;
|
|
861
934
|
phone?: string;
|
|
862
935
|
password: string;
|
|
863
|
-
publicKey: string;
|
|
864
|
-
keyId: string;
|
|
865
|
-
fingerprint: string;
|
|
866
|
-
oldKeyId?: string;
|
|
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;
|
|
951
|
+
passwordChangeRequired: boolean;
|
|
879
952
|
}
|
|
880
953
|
```
|
|
881
954
|
|
|
882
955
|
---
|
|
883
956
|
|
|
884
|
-
### Authenticated
|
|
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
|
-
|
|
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
|
-
|
|
985
|
+
|
|
986
|
+
Rotate public key before expiry (90 days).
|
|
905
987
|
|
|
906
988
|
**Request:**
|
|
907
989
|
```typescript
|
|
908
990
|
{
|
|
909
|
-
publicKey: string;
|
|
910
|
-
keyId: string;
|
|
911
|
-
fingerprint: string;
|
|
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;
|
|
1003
|
+
keyId: string;
|
|
922
1004
|
}
|
|
923
1005
|
```
|
|
924
1006
|
|
|
925
1007
|
---
|
|
926
1008
|
|
|
927
1009
|
#### `PUT /_auth/password`
|
|
928
|
-
|
|
1010
|
+
|
|
1011
|
+
Change password.
|
|
929
1012
|
|
|
930
1013
|
**Request:**
|
|
931
1014
|
```typescript
|
|
932
1015
|
{
|
|
933
1016
|
currentPassword: string;
|
|
934
|
-
newPassword: string;
|
|
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
|
-
###
|
|
1032
|
+
### Core Tables
|
|
1033
|
+
|
|
1034
|
+
#### `users`
|
|
950
1035
|
|
|
951
1036
|
Main user identity table.
|
|
952
1037
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
**
|
|
969
|
-
- At least one of `email` OR `phone`
|
|
970
|
-
-
|
|
971
|
-
- `roleId` references roles
|
|
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
|
-
|
|
1066
|
+
#### `user_public_keys`
|
|
976
1067
|
|
|
977
1068
|
Stores client public keys for JWT verification.
|
|
978
1069
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
**
|
|
995
|
-
- `
|
|
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
|
-
|
|
1099
|
+
#### `verification_codes`
|
|
1000
1100
|
|
|
1001
|
-
|
|
1101
|
+
OTP codes for email/SMS verification.
|
|
1002
1102
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1118
|
+
**Key Points:**
|
|
1119
|
+
- 6-digit numeric code
|
|
1120
|
+
- Expires in 5-10 minutes (configurable)
|
|
1121
|
+
- Single-use (marked via `usedAt`)
|
|
1017
1122
|
|
|
1018
|
-
|
|
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
|
-
|
|
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)
|
|
1053
|
-
- `superadmin` (priority 100)
|
|
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
|
-
|
|
1175
|
+
#### `role_permissions`
|
|
1058
1176
|
|
|
1059
|
-
|
|
1177
|
+
Many-to-many mapping between roles and permissions.
|
|
1060
1178
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
- `rbac:role:manage`, `rbac:permission:manage` - RBAC management
|
|
1187
|
+
UNIQUE(role_id, permission_id)
|
|
1188
|
+
);
|
|
1189
|
+
```
|
|
1078
1190
|
|
|
1079
1191
|
---
|
|
1080
1192
|
|
|
1081
|
-
|
|
1193
|
+
#### `user_permissions`
|
|
1082
1194
|
|
|
1083
|
-
|
|
1195
|
+
User-specific permission overrides.
|
|
1084
1196
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
-
**
|
|
1094
|
-
- `
|
|
1095
|
-
- `
|
|
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
|
-
###
|
|
1100
|
-
|
|
1101
|
-
User-specific permission overrides.
|
|
1219
|
+
### Supporting Tables
|
|
1102
1220
|
|
|
1103
|
-
|
|
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
|
-
|
|
1115
|
-
- `UNIQUE(userId, permissionId)`
|
|
1116
|
-
- `ON DELETE CASCADE` for both foreign keys
|
|
1223
|
+
User invitation system.
|
|
1117
1224
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
1259
|
+
---
|
|
1129
1260
|
|
|
1130
|
-
|
|
1261
|
+
#### `user_social_accounts`
|
|
1131
1262
|
|
|
1132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1280
|
+
---
|
|
1141
1281
|
|
|
1142
|
-
|
|
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
|
-
//
|
|
1289
|
+
// Minimal setup (built-in roles only)
|
|
1157
1290
|
await initializeAuth();
|
|
1158
|
-
```
|
|
1159
1291
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
```typescript
|
|
1292
|
+
// With presets
|
|
1163
1293
|
await initializeAuth({
|
|
1164
|
-
usePresets: true, // Adds
|
|
1294
|
+
usePresets: true, // Adds moderator, editor, viewer roles
|
|
1165
1295
|
});
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
#### Custom Roles & Permissions
|
|
1169
1296
|
|
|
1170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1358
|
+
// Role-based
|
|
1236
1359
|
app.bind(
|
|
1237
1360
|
adminDashboardContract,
|
|
1238
1361
|
[authenticate, requireRole('admin', 'superadmin')],
|
|
1239
1362
|
async (c) => {
|
|
1240
|
-
//
|
|
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
|
-
|
|
1368
|
+
---
|
|
1369
|
+
|
|
1370
|
+
### Programmatic Checks
|
|
1257
1371
|
|
|
1258
1372
|
```typescript
|
|
1259
1373
|
import { hasPermission, hasRole, getUserPermissions } from '@spfn/auth/server';
|
|
1260
1374
|
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
|
1392
|
+
// Create role
|
|
1289
1393
|
const role = await createRole({
|
|
1290
1394
|
name: 'moderator',
|
|
1291
|
-
displayName: '
|
|
1292
|
-
description: 'Manages community content',
|
|
1395
|
+
displayName: 'Moderator',
|
|
1293
1396
|
priority: 40,
|
|
1294
|
-
permissionIds: [1n, 2n
|
|
1397
|
+
permissionIds: [1n, 2n],
|
|
1295
1398
|
});
|
|
1296
1399
|
|
|
1297
|
-
// Add permission
|
|
1400
|
+
// Add permission
|
|
1298
1401
|
await addPermissionToRole(role.id, 5n);
|
|
1299
1402
|
|
|
1300
|
-
//
|
|
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
|
-
|
|
1407
|
+
---
|
|
1311
1408
|
|
|
1312
|
-
|
|
1409
|
+
## Next.js Adapter
|
|
1313
1410
|
|
|
1314
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
1432
|
+
---
|
|
1433
|
+
|
|
1434
|
+
### Server Component Guards
|
|
1326
1435
|
|
|
1327
1436
|
```typescript
|
|
1328
|
-
|
|
1437
|
+
// app/admin/page.tsx
|
|
1438
|
+
import { RequireAuth, RequireRole } from '@spfn/auth/nextjs/server';
|
|
1329
1439
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
1452
|
+
---
|
|
1341
1453
|
|
|
1342
|
-
|
|
1454
|
+
### Interceptors (API Routes)
|
|
1343
1455
|
|
|
1456
|
+
**Setup:**
|
|
1344
1457
|
```typescript
|
|
1345
|
-
|
|
1346
|
-
import
|
|
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
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1511
|
+
### Test Structure
|
|
1379
1512
|
|
|
1380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1556
|
+
expect(result.userId).toBeDefined();
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1403
1559
|
```
|
|
1404
1560
|
|
|
1405
|
-
|
|
1406
|
-
- Always send fingerprint with public key
|
|
1407
|
-
- Server validates fingerprint = SHA-256(publicKey)
|
|
1408
|
-
- Prevents key tampering during transmission
|
|
1561
|
+
---
|
|
1409
1562
|
|
|
1410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
1596
|
+
# Build package
|
|
1597
|
+
pnpm build
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
---
|
|
1601
|
+
|
|
1602
|
+
### Development
|
|
1428
1603
|
|
|
1429
1604
|
```bash
|
|
1430
|
-
|
|
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
|
-
|
|
1615
|
+
---
|
|
1434
1616
|
|
|
1435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1452
|
-
# .env
|
|
1643
|
+
---
|
|
1453
1644
|
|
|
1454
|
-
|
|
1455
|
-
# Core Authentication Settings (Required)
|
|
1456
|
-
# ========================================
|
|
1645
|
+
### Database Migrations
|
|
1457
1646
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1647
|
+
```bash
|
|
1648
|
+
# Generate new migration (after entity changes)
|
|
1649
|
+
pnpm db:generate
|
|
1461
1650
|
|
|
1462
|
-
#
|
|
1463
|
-
|
|
1464
|
-
|
|
1651
|
+
# Apply migrations (via SPFN CLI)
|
|
1652
|
+
cd ../../
|
|
1653
|
+
pnpm spfn db migrate
|
|
1465
1654
|
|
|
1466
|
-
#
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
|
|
1475
|
-
SPFN_AUTH_SESSION_SECRET=session-encryption-key # Required if using client-side session features
|
|
1661
|
+
---
|
|
1476
1662
|
|
|
1477
|
-
|
|
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
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1691
|
+
Follow the project's code style (see `/Users/launchscreen/PROJECTS/SPFN/workspaces/.claude/rules.md`):
|
|
1488
1692
|
|
|
1489
|
-
|
|
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
|
-
|
|
1713
|
+
---
|
|
1492
1714
|
|
|
1493
|
-
|
|
1715
|
+
### Environment Variables
|
|
1494
1716
|
|
|
1717
|
+
**Server-side:**
|
|
1495
1718
|
```bash
|
|
1496
|
-
#
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
1739
|
+
---
|
|
1514
1740
|
|
|
1515
|
-
|
|
1741
|
+
### Debugging
|
|
1516
1742
|
|
|
1517
|
-
|
|
1743
|
+
**Enable logging:**
|
|
1744
|
+
```typescript
|
|
1745
|
+
import { serverLogger } from '@/server/logger';
|
|
1518
1746
|
|
|
1519
|
-
|
|
1747
|
+
serverLogger.info('Debug message', { context });
|
|
1748
|
+
serverLogger.error('Error occurred', error);
|
|
1749
|
+
```
|
|
1520
1750
|
|
|
1751
|
+
**Inspect database:**
|
|
1521
1752
|
```bash
|
|
1522
|
-
|
|
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
|
-
**
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
-
|
|
1763
|
+
## Known Issues
|
|
1555
1764
|
|
|
1556
|
-
|
|
1765
|
+
### 1. Client Crypto Functions Missing
|
|
1557
1766
|
|
|
1558
|
-
|
|
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
|
-
**
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
1775
|
+
### 2. Next.js Proxy Route Not Implemented
|
|
1573
1776
|
|
|
1574
|
-
|
|
1777
|
+
**Issue:** Documentation mentions `@spfn/auth/nextjs/proxy` for client-side API proxying, but it doesn't exist.
|
|
1575
1778
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
-
|
|
1583
|
-
- `role: 'superadmin'`
|
|
1584
|
-
- `passwordChangeRequired: true`
|
|
1787
|
+
**Status:** Intentional removal. Use services or HTTP routes directly.
|
|
1585
1788
|
|
|
1586
1789
|
---
|
|
1587
1790
|
|
|
1588
|
-
|
|
1791
|
+
### 4. Test Coverage Below Target
|
|
1589
1792
|
|
|
1590
|
-
|
|
1793
|
+
**Current:** ~83%
|
|
1794
|
+
**Target:** 90%+
|
|
1591
1795
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1796
|
+
**Areas needing tests:**
|
|
1797
|
+
- Invitation service edge cases
|
|
1798
|
+
- RBAC permission checks
|
|
1799
|
+
- Key rotation scenarios
|
|
1800
|
+
- Session expiry handling
|
|
1595
1801
|
|
|
1596
|
-
|
|
1597
|
-
await ensureAdminExists();
|
|
1598
|
-
```
|
|
1802
|
+
---
|
|
1599
1803
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
-
**
|
|
1611
|
-
-
|
|
1612
|
-
-
|
|
1613
|
-
-
|
|
1614
|
-
-
|
|
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
|
-
###
|
|
1816
|
+
### Mid-term (Beta → v1.0)
|
|
1619
1817
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
1626
|
-
import { generateKeyPair, generateClientToken } from '@spfn/auth/client';
|
|
1828
|
+
### Long-term (Post v1.0)
|
|
1627
1829
|
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
##
|
|
1839
|
+
## Contributing
|
|
1635
1840
|
|
|
1636
|
-
###
|
|
1841
|
+
### Before Contributing
|
|
1637
1842
|
|
|
1638
|
-
|
|
1639
|
-
|
|
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
|
-
|
|
1848
|
+
---
|
|
1643
1849
|
|
|
1644
|
-
|
|
1645
|
-
pnpm test:coverage
|
|
1646
|
-
```
|
|
1850
|
+
### Pull Request Process
|
|
1647
1851
|
|
|
1648
|
-
|
|
1852
|
+
1. **Create feature branch**
|
|
1853
|
+
```bash
|
|
1854
|
+
git checkout -b feature/my-feature
|
|
1855
|
+
```
|
|
1649
1856
|
|
|
1650
|
-
|
|
1857
|
+
2. **Make changes**
|
|
1858
|
+
- Follow code style
|
|
1859
|
+
- Add tests
|
|
1860
|
+
- Update docs if needed
|
|
1651
1861
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1862
|
+
3. **Run checks**
|
|
1863
|
+
```bash
|
|
1864
|
+
pnpm type-check
|
|
1865
|
+
pnpm test
|
|
1866
|
+
pnpm build
|
|
1867
|
+
```
|
|
1655
1868
|
|
|
1656
|
-
|
|
1869
|
+
4. **Commit with conventional commits**
|
|
1870
|
+
```bash
|
|
1871
|
+
git commit -m "feat(auth): add password strength validation"
|
|
1872
|
+
```
|
|
1657
1873
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1874
|
+
5. **Push and create PR**
|
|
1875
|
+
```bash
|
|
1876
|
+
git push origin feature/my-feature
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
---
|
|
1661
1880
|
|
|
1662
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
-
|
|
1735
|
-
-
|
|
1736
|
-
-
|
|
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
|
-
|
|
1921
|
+
### Publishing
|
|
1741
1922
|
|
|
1742
|
-
|
|
1923
|
+
```bash
|
|
1924
|
+
# Alpha release
|
|
1925
|
+
pnpm run publish:alpha
|
|
1743
1926
|
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
- React UI components (LoginForm, RegisterForm)
|
|
1930
|
+
# Production release
|
|
1931
|
+
pnpm run publish:latest
|
|
1932
|
+
```
|
|
1757
1933
|
|
|
1758
|
-
**
|
|
1759
|
-
-
|
|
1760
|
-
-
|
|
1761
|
-
-
|
|
1762
|
-
-
|
|
1763
|
-
-
|
|
1764
|
-
-
|
|
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
|
-
##
|
|
1944
|
+
## Support
|
|
1769
1945
|
|
|
1770
|
-
|
|
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
|