@spfn/auth 0.2.0-beta.5 → 0.2.0-beta.51

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @spfn/auth - Technical Documentation
2
2
 
3
- **Version:** 0.1.0-alpha.88
3
+ **Version:** 0.2.0-beta.15
4
4
  **Status:** Alpha - Internal Development
5
5
 
6
6
  > **Note:** This is a technical documentation for developers working on the @spfn/auth package.
@@ -12,12 +12,14 @@
12
12
 
13
13
  - [Overview](#overview)
14
14
  - [Installation](#installation)
15
+ - [Admin Account Setup](#6-admin-account-setup)
15
16
  - [Architecture](#architecture)
16
17
  - [Package Structure](#package-structure)
17
18
  - [Module Exports](#module-exports)
18
19
  - [Email & SMS Services](#email--sms-services)
19
- - [Email Templates](#email-templates)
20
20
  - [Server-Side API](#server-side-api)
21
+ - [Events](#events)
22
+ - [OAuth Authentication](#oauth-authentication)
21
23
  - [Database Schema](#database-schema)
22
24
  - [RBAC System](#rbac-system)
23
25
  - [Next.js Adapter](#nextjs-adapter)
@@ -34,10 +36,11 @@
34
36
 
35
37
  - **Asymmetric JWT Authentication** - Client-signed tokens using ES256/RS256
36
38
  - **User Management** - Email/phone-based identity with bcrypt hashing
39
+ - **OAuth Authentication** - Google OAuth 2.0 (Authorization Code Flow), extensible to other providers
37
40
  - **Multi-Factor Authentication** - OTP verification via email/SMS
38
41
  - **Session Management** - Public key rotation with 90-day expiry
39
42
  - **Role-Based Access Control** - Flexible RBAC with runtime role/permission management
40
- - **Next.js Integration** - Session helpers and server-side guards
43
+ - **Next.js Integration** - Session helpers, server-side guards, and OAuth interceptors
41
44
 
42
45
  ### Design Principles
43
46
 
@@ -74,64 +77,98 @@ export default defineServerConfig()
74
77
  .build();
75
78
  ```
76
79
 
77
- #### Register Router in `router.ts`
80
+ #### Register Router and Global Middleware in `router.ts`
78
81
 
79
82
  ```typescript
80
83
  import { defineRouter } from '@spfn/core/route';
81
- import { authRouter } from '@spfn/auth/server';
84
+ import { authRouter, authenticate } from '@spfn/auth/server';
85
+ import { getHealth } from './routes/health';
86
+ import { createOrder } from './routes/orders';
82
87
 
83
88
  export const appRouter = defineRouter({
84
- // Auth routes (fixed namespace)
85
- auth: authRouter,
86
-
89
+ getHealth,
90
+ createOrder,
87
91
  // ... your other routes
88
- });
92
+ })
93
+ .packages([authRouter]) // Auth routes (/_auth/* namespace)
94
+ .use([authenticate]); // Global auth middleware on all routes
95
+
96
+ export type AppRouter = typeof appRouter;
89
97
  ```
90
98
 
91
- ### 3. Configure Client (Next.js)
99
+ > **Important:** Public routes must explicitly skip auth with `.skip(['auth'])`.
100
+ > See the [Authentication Guide](https://spfn.dev/docs/guides/authentication) for details.
92
101
 
93
- #### Register Router Metadata and Errors in `api-client.ts`
102
+ ### 3. Configure Next.js Interceptor
103
+
104
+ Register the auth interceptor in your RPC proxy route. This handles session cookies, JWT signing, and key management automatically.
94
105
 
95
106
  ```typescript
107
+ // app/api/rpc/[routeName]/route.ts
108
+ import '@spfn/auth/nextjs/api'; // Must be first! Registers auth interceptor
109
+ import { appRouter } from '@/server/router';
110
+ import { createRpcProxy } from '@spfn/core/nextjs/server';
111
+
112
+ export const { GET, POST } = createRpcProxy({ router: appRouter });
113
+ ```
114
+
115
+ Your API client needs no auth-specific configuration:
116
+
117
+ ```typescript
118
+ // src/lib/api-client.ts
96
119
  import { createApi } from '@spfn/core/nextjs';
97
120
  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
- });
121
+
122
+ export const api = createApi<AppRouter>();
123
+ ```
124
+
125
+ The built-in `authApi` is also available for auth-only calls:
126
+
127
+ ```typescript
128
+ import { authApi } from '@spfn/auth';
129
+ const session = await authApi.getAuthSession.call({});
107
130
  ```
108
131
 
109
132
  ### 4. Environment Variables
110
133
 
134
+ Auth requires variables in **two separate files**: `.env.server` (SPFN backend) and `.env.local` (Next.js).
135
+
136
+ #### `.env.server` (SPFN Backend)
137
+
111
138
  ```bash
112
139
  # Required
113
- SPFN_AUTH_JWT_SECRET=your-secret-key
140
+ DATABASE_URL=postgresql://user:pass@localhost:5432/myapp_dev
114
141
  SPFN_AUTH_VERIFICATION_TOKEN_SECRET=your-verification-secret
115
- DATABASE_URL=postgresql://...
116
142
 
117
- # Next.js (required)
118
- SPFN_AUTH_SESSION_SECRET=your-32-char-secret
143
+ # Admin account (required — at least one format)
144
+ SPFN_AUTH_ADMIN_ACCOUNTS='[{"email":"admin@example.com","password":"Admin!@34","role":"superadmin"}]'
119
145
 
120
146
  # Optional
147
+ SPFN_AUTH_JWT_SECRET=your-jwt-secret
121
148
  SPFN_AUTH_JWT_EXPIRES_IN=7d
122
149
  SPFN_AUTH_BCRYPT_SALT_ROUNDS=10
123
150
  SPFN_AUTH_SESSION_TTL=7d
124
151
 
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
152
+ # Google OAuth (optional)
153
+ SPFN_AUTH_GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
154
+ SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-...
155
+ ```
156
+
157
+ #### `.env.local` (Next.js)
158
+
159
+ ```bash
160
+ # Required
161
+ DATABASE_URL=postgresql://user:pass@localhost:5432/myapp_dev
162
+ SPFN_API_URL=http://localhost:8790
163
+
164
+ # Required for session cookies (minimum 32 characters)
165
+ SPFN_AUTH_SESSION_SECRET=my-super-secret-session-key-at-least-32-chars-long
130
166
 
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
167
+ # Optional
168
+ SPFN_AUTH_SESSION_TTL=7d
169
+
170
+ # Email/SMS — configure via @spfn/notification
171
+ # See @spfn/notification README for AWS SES/SNS settings
135
172
  ```
136
173
 
137
174
  ### 5. Run Migrations
@@ -144,6 +181,83 @@ pnpm spfn db generate
144
181
  pnpm spfn db migrate
145
182
  ```
146
183
 
184
+ ### 6. Admin Account Setup
185
+
186
+ Admin accounts are automatically created on server startup via `createAuthLifecycle()`.
187
+ Choose one of the following methods:
188
+
189
+ #### Method 1: JSON Format (Recommended)
190
+
191
+ Best for multiple accounts with full configuration:
192
+
193
+ ```bash
194
+ SPFN_AUTH_ADMIN_ACCOUNTS='[
195
+ {"email": "superadmin@example.com", "password": "secure-pass-1", "role": "superadmin"},
196
+ {"email": "admin@example.com", "password": "secure-pass-2", "role": "admin"},
197
+ {"email": "manager@example.com", "password": "secure-pass-3", "role": "user"}
198
+ ]'
199
+ ```
200
+
201
+ **JSON Schema:**
202
+ ```typescript
203
+ interface AdminAccountConfig {
204
+ email: string; // Required
205
+ password: string; // Required
206
+ role?: string; // Default: 'user' (options: 'user', 'admin', 'superadmin')
207
+ phone?: string; // Optional
208
+ passwordChangeRequired?: boolean; // Default: true
209
+ }
210
+ ```
211
+
212
+ #### Method 2: CSV Format
213
+
214
+ For multiple accounts with simpler configuration:
215
+
216
+ ```bash
217
+ SPFN_AUTH_ADMIN_EMAILS=admin@example.com,manager@example.com
218
+ SPFN_AUTH_ADMIN_PASSWORDS=admin-pass,manager-pass
219
+ SPFN_AUTH_ADMIN_ROLES=superadmin,admin
220
+ ```
221
+
222
+ #### Method 3: Single Account (Legacy)
223
+
224
+ Simplest format for a single superadmin:
225
+
226
+ ```bash
227
+ SPFN_AUTH_ADMIN_EMAIL=admin@example.com
228
+ SPFN_AUTH_ADMIN_PASSWORD=secure-password
229
+ ```
230
+
231
+ > **Note:** This method always creates a `superadmin` role account.
232
+
233
+ #### Default Behavior
234
+
235
+ All admin accounts created via environment variables have:
236
+ - `emailVerifiedAt`: Auto-verified (current timestamp)
237
+ - `passwordChangeRequired`: `true` (must change on first login)
238
+ - `status`: `active`
239
+
240
+ #### Programmatic Creation
241
+
242
+ You can also create admin accounts programmatically:
243
+
244
+ ```typescript
245
+ import { usersRepository, getRoleByName, hashPassword } from '@spfn/auth/server';
246
+
247
+ // After initializeAuth() has been called
248
+ const role = await getRoleByName('admin');
249
+ const passwordHash = await hashPassword('secure-password');
250
+
251
+ await usersRepository.create({
252
+ email: 'admin@example.com',
253
+ passwordHash,
254
+ roleId: role.id,
255
+ emailVerifiedAt: new Date(),
256
+ passwordChangeRequired: true,
257
+ status: 'active',
258
+ });
259
+ ```
260
+
147
261
  ---
148
262
 
149
263
  ## Architecture
@@ -335,20 +449,15 @@ packages/auth/
335
449
 
336
450
  ### Common Module (`@spfn/auth`)
337
451
 
338
- **Entities:**
452
+ **API Client:**
339
453
  ```typescript
340
- import {
341
- users,
342
- userPublicKeys,
343
- verificationCodes,
344
- roles,
345
- permissions,
346
- rolePermissions,
347
- userPermissions,
348
- userInvitations,
349
- userSocialAccounts,
350
- userProfiles
351
- } from '@spfn/auth';
454
+ import { authApi } from '@spfn/auth';
455
+
456
+ // Type-safe API calls
457
+ const session = await authApi.getAuthSession.call({});
458
+ const result = await authApi.login.call({
459
+ body: { email, password, fingerprint, publicKey, keyId }
460
+ });
352
461
  ```
353
462
 
354
463
  **Types:**
@@ -359,6 +468,9 @@ import type {
359
468
  VerificationCode,
360
469
  Role,
361
470
  Permission,
471
+ AuthSession,
472
+ UserProfile,
473
+ ProfileInfo,
362
474
  // ... etc
363
475
  } from '@spfn/auth';
364
476
  ```
@@ -380,6 +492,34 @@ import type {
380
492
  } from '@spfn/auth';
381
493
  ```
382
494
 
495
+ **Validation Patterns:**
496
+ ```typescript
497
+ import {
498
+ UUID_PATTERN,
499
+ EMAIL_PATTERN,
500
+ BASE64_PATTERN,
501
+ FINGERPRINT_PATTERN,
502
+ PHONE_PATTERN,
503
+ } from '@spfn/auth';
504
+ ```
505
+
506
+ **Route Map (for RPC Proxy):**
507
+ ```typescript
508
+ import { authRouteMap } from '@spfn/auth';
509
+
510
+ // Use in Next.js RPC proxy (app/api/rpc/[routeName]/route.ts)
511
+ import '@spfn/auth/nextjs/api'; // Auto-register auth interceptors
512
+ import { routeMap } from '@/generated/route-map';
513
+ import { authRouteMap } from '@spfn/auth';
514
+ import { createRpcProxy } from '@spfn/core/nextjs/proxy';
515
+
516
+ export const { GET, POST } = createRpcProxy({
517
+ routeMap: { ...routeMap, ...authRouteMap }
518
+ });
519
+ ```
520
+
521
+ > **Note:** Database entities (`users`, `userPublicKeys`, etc.) are exported from `@spfn/auth/server`, not the common module.
522
+
383
523
  ---
384
524
 
385
525
  ### Server Module (`@spfn/auth/server`)
@@ -456,22 +596,13 @@ import {
456
596
 
457
597
  // Session
458
598
  getAuthSessionService,
459
- getUserProfileService,
460
599
 
461
- // Email
462
- sendEmail,
463
- registerEmailProvider,
464
-
465
- // SMS
466
- sendSMS,
467
- registerSMSProvider,
600
+ // User Profile
601
+ getUserProfileService,
602
+ updateUserProfileService,
468
603
 
469
- // Email Templates
470
- registerEmailTemplates,
471
- getVerificationCodeTemplate,
472
- getWelcomeTemplate,
473
- getPasswordResetTemplate,
474
- getInvitationTemplate,
604
+ // OAuth - Google API Access
605
+ getGoogleAccessToken,
475
606
  } from '@spfn/auth/server';
476
607
  ```
477
608
 
@@ -494,11 +625,13 @@ import {
494
625
  ```typescript
495
626
  import {
496
627
  authenticate,
628
+ optionalAuth,
497
629
  requirePermissions,
630
+ requireAnyPermission,
498
631
  requireRole,
499
632
  } from '@spfn/auth/server';
500
633
 
501
- // Usage
634
+ // Usage - all permissions required
502
635
  app.bind(
503
636
  myContract,
504
637
  [authenticate, requirePermissions('user:delete')],
@@ -506,6 +639,27 @@ app.bind(
506
639
  // Handler
507
640
  }
508
641
  );
642
+
643
+ // Usage - any of the permissions
644
+ app.bind(
645
+ myContract,
646
+ [authenticate, requireAnyPermission('content:read', 'admin:access')],
647
+ async (c) => {
648
+ // User has either content:read OR admin:access
649
+ }
650
+ );
651
+
652
+ // Usage - optional auth (public route with optional user context)
653
+ // Auto-skips global 'auth' middleware — no .skip(['auth']) needed
654
+ export const getProducts = route.get('/products')
655
+ .use([optionalAuth])
656
+ .handler(async (c) => {
657
+ const auth = getOptionalAuth(c); // AuthContext | undefined
658
+ if (auth) {
659
+ return getPersonalizedProducts(auth.userId);
660
+ }
661
+ return getPublicProducts();
662
+ });
509
663
  ```
510
664
 
511
665
  **Helpers:**
@@ -513,6 +667,7 @@ app.bind(
513
667
  import {
514
668
  // Context
515
669
  getAuth,
670
+ getOptionalAuth,
516
671
  getUser,
517
672
  getUserId,
518
673
  getKeyId,
@@ -610,9 +765,11 @@ import {
610
765
  loginRegisterInterceptor,
611
766
  generalAuthInterceptor,
612
767
  keyRotationInterceptor,
768
+ oauthUrlInterceptor,
769
+ oauthFinalizeInterceptor,
613
770
  } from '@spfn/auth/nextjs/api';
614
771
 
615
- // Auto-registers interceptors on import
772
+ // Auto-registers interceptors on import (including OAuth)
616
773
  import '@spfn/auth/nextjs/api';
617
774
  ```
618
775
 
@@ -679,141 +836,24 @@ export default async function DashboardPage()
679
836
 
680
837
  ## Email & SMS Services
681
838
 
682
- ### Email Service
839
+ > **⚠️ DEPRECATED:** Email and SMS functionality has been moved to `@spfn/notification` package.
683
840
 
684
- The email service uses AWS SES by default, with fallback to console logging in development.
841
+ ### Migration Guide
685
842
 
686
- **Send Email:**
687
843
  ```typescript
688
- import { sendEmail } from '@spfn/auth/server';
844
+ // Before (deprecated)
845
+ import { sendEmail, sendSMS } from '@spfn/auth/server';
689
846
 
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
- });
847
+ // After (recommended)
848
+ import { sendEmail, sendSMS } from '@spfn/notification/server';
697
849
  ```
698
850
 
699
- **Custom Email Provider:**
700
- ```typescript
701
- import { registerEmailProvider } from '@spfn/auth/server';
851
+ The `@spfn/notification` package provides:
852
+ - Multi-channel support (Email, SMS, Slack, Push)
853
+ - Template system with variable substitution
854
+ - Multiple provider support (AWS SES, SNS, SendGrid, Twilio, etc.)
702
855
 
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
- },
710
- });
711
- ```
712
-
713
- ---
714
-
715
- ### SMS Service
716
-
717
- The SMS service uses AWS SNS by default.
718
-
719
- **Send SMS:**
720
- ```typescript
721
- import { sendSMS } from '@spfn/auth/server';
722
-
723
- await sendSMS({
724
- phone: '+821012345678', // E.164 format
725
- message: 'Your code is: 123456',
726
- purpose: 'verification',
727
- });
728
- ```
729
-
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
- });
742
- ```
743
-
744
- ---
745
-
746
- ## Email Templates
747
-
748
- ### Built-in Templates
749
-
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 |
756
-
757
- **Usage:**
758
- ```typescript
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',
766
- });
767
-
768
- await sendEmail({ to: 'user@example.com', subject, text, html });
769
- ```
770
-
771
- ---
772
-
773
- ### Custom Templates
774
-
775
- Register custom templates to override defaults with your brand design:
776
-
777
- ```typescript
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
- }),
806
- });
807
- ```
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?` |
856
+ For documentation, see `@spfn/notification` package README.
817
857
 
818
858
  ---
819
859
 
@@ -1027,6 +1067,574 @@ Change password.
1027
1067
 
1028
1068
  ---
1029
1069
 
1070
+ #### `GET /_auth/users/username/check`
1071
+
1072
+ Check if a username is available.
1073
+
1074
+ **Query:**
1075
+ ```typescript
1076
+ {
1077
+ username: string; // Min 1 char
1078
+ }
1079
+ ```
1080
+
1081
+ **Response:**
1082
+ ```typescript
1083
+ {
1084
+ available: boolean;
1085
+ }
1086
+ ```
1087
+
1088
+ ---
1089
+
1090
+ #### `PATCH /_auth/users/username`
1091
+
1092
+ Update authenticated user's username. Validates uniqueness before updating.
1093
+
1094
+ **Request:**
1095
+ ```typescript
1096
+ {
1097
+ username: string | null; // New username or null to clear
1098
+ }
1099
+ ```
1100
+
1101
+ **Response:** Updated user object.
1102
+
1103
+ **Errors:**
1104
+ - `409 UsernameAlreadyTakenError` - Username is already in use by another user
1105
+
1106
+ ---
1107
+
1108
+ ## Events
1109
+
1110
+ `@spfn/auth`는 `@spfn/core/event`를 사용하여 인증 관련 이벤트를 발행합니다. 이를 통해 로그인/회원가입 시 추가 로직(환영 이메일, 분석, 알림 등)을 디커플링된 방식으로 처리할 수 있습니다.
1111
+
1112
+ ### Available Events
1113
+
1114
+ | Event | Description | Trigger |
1115
+ |-------|-------------|---------|
1116
+ | `auth.login` | 로그인 성공 | 이메일/전화 로그인, OAuth 기존 사용자 |
1117
+ | `auth.register` | 회원가입 성공 | 이메일/전화 회원가입, OAuth 신규 사용자 |
1118
+ | `auth.invitation.created` | 초대 생성/재발송 | createInvitation, resendInvitation |
1119
+ | `auth.invitation.accepted` | 초대 수락 | acceptInvitation |
1120
+
1121
+ ---
1122
+
1123
+ ### Event Payloads
1124
+
1125
+ #### `auth.login`
1126
+
1127
+ ```typescript
1128
+ {
1129
+ userId: string;
1130
+ provider: 'email' | 'phone' | 'google';
1131
+ email?: string;
1132
+ phone?: string;
1133
+ }
1134
+ ```
1135
+
1136
+ #### `auth.register`
1137
+
1138
+ ```typescript
1139
+ {
1140
+ userId: string;
1141
+ provider: 'email' | 'phone' | 'google';
1142
+ email?: string;
1143
+ phone?: string;
1144
+ metadata?: Record<string, unknown>; // 가입 시 전달된 커스텀 메타데이터
1145
+ }
1146
+ ```
1147
+
1148
+ `metadata`는 클라이언트가 register/OAuth 요청 body에 포함한 값이 그대로 전달됩니다.
1149
+ 레퍼럴 코드, UTM 파라미터 등 앱 고유 데이터를 이벤트 구독자에게 전달할 때 사용합니다.
1150
+
1151
+ #### `auth.invitation.created`
1152
+
1153
+ ```typescript
1154
+ {
1155
+ invitationId: string;
1156
+ email: string;
1157
+ token: string;
1158
+ roleId: number;
1159
+ invitedBy: string;
1160
+ expiresAt: string; // ISO 8601
1161
+ isResend: boolean; // true면 재발송
1162
+ metadata?: Record<string, unknown>;
1163
+ }
1164
+ ```
1165
+
1166
+ #### `auth.invitation.accepted`
1167
+
1168
+ ```typescript
1169
+ {
1170
+ invitationId: string;
1171
+ email: string;
1172
+ userId: string; // 생성된 사용자 ID
1173
+ roleId: number;
1174
+ invitedBy: string;
1175
+ metadata?: Record<string, unknown>;
1176
+ }
1177
+ ```
1178
+
1179
+ ---
1180
+
1181
+ ### Subscribing to Events
1182
+
1183
+ ```typescript
1184
+ import { authLoginEvent, authRegisterEvent } from '@spfn/auth/server';
1185
+
1186
+ // 로그인 이벤트 구독
1187
+ authLoginEvent.subscribe(async (payload) => {
1188
+ console.log('User logged in:', payload.userId, payload.provider);
1189
+ await analytics.trackLogin(payload.userId);
1190
+ });
1191
+
1192
+ // 회원가입 이벤트 구독 (metadata 활용)
1193
+ authRegisterEvent.subscribe(async (payload) => {
1194
+ console.log('New user registered:', payload.userId);
1195
+ if (payload.email) {
1196
+ await emailService.sendWelcome(payload.email);
1197
+ }
1198
+
1199
+ // 레퍼럴 코드 처리
1200
+ const refCode = payload.metadata?.refCode as string;
1201
+ if (refCode) {
1202
+ await referralService.link(payload.userId, refCode);
1203
+ }
1204
+ });
1205
+ ```
1206
+
1207
+ 클라이언트에서 metadata를 전달하는 방법:
1208
+
1209
+ ```typescript
1210
+ // 이메일/전화 가입
1211
+ authApi.register.call({
1212
+ body: { email, password, metadata: { refCode: 'CODE', utm_source: 'google' } }
1213
+ });
1214
+
1215
+ // OAuth 가입
1216
+ authApi.oauthStart.call({
1217
+ body: { provider: 'google', returnUrl: '/dashboard', metadata: { refCode: 'CODE' } }
1218
+ });
1219
+ ```
1220
+
1221
+ #### 초대 이벤트 구독 (이메일 발송 연동)
1222
+
1223
+ ```typescript
1224
+ import { invitationCreatedEvent, invitationAcceptedEvent } from '@spfn/auth/server';
1225
+
1226
+ // 초대 생성 시 이메일 발송
1227
+ invitationCreatedEvent.subscribe(async (payload) => {
1228
+ const inviteUrl = `${APP_URL}/invite/${payload.token}`;
1229
+
1230
+ await notificationService.send({
1231
+ channel: 'email',
1232
+ to: payload.email,
1233
+ subject: payload.isResend ? '초대가 재발송되었습니다' : '초대장이 도착했습니다',
1234
+ html: renderInviteEmail({
1235
+ inviteUrl,
1236
+ inviterName: payload.metadata?.inviterName,
1237
+ message: payload.metadata?.message,
1238
+ }),
1239
+ tracking: {
1240
+ category: 'invitation',
1241
+ metadata: { invitationId: payload.invitationId },
1242
+ },
1243
+ });
1244
+ });
1245
+
1246
+ // 초대 수락 시 온보딩 처리
1247
+ invitationAcceptedEvent.subscribe(async (payload) => {
1248
+ await onboardingService.start(payload.userId);
1249
+ });
1250
+ ```
1251
+
1252
+ 초대 생성 시 커스텀 만료 시간 지정:
1253
+
1254
+ ```typescript
1255
+ // expiresAt이 expiresInDays보다 우선
1256
+ authApi.createInvitation.call({
1257
+ body: {
1258
+ email: 'user@example.com',
1259
+ roleId: 2,
1260
+ expiresAt: '2026-03-20T00:00:00Z',
1261
+ metadata: { inviterName: '홍길동', message: '함께 일해요!' },
1262
+ }
1263
+ });
1264
+ ```
1265
+
1266
+ ---
1267
+
1268
+ ### Job Integration
1269
+
1270
+ `@spfn/core/job`과 연동하여 백그라운드 작업을 실행할 수 있습니다.
1271
+
1272
+ ```typescript
1273
+ import { job, defineJobRouter } from '@spfn/core/job';
1274
+ import { authRegisterEvent } from '@spfn/auth/server';
1275
+
1276
+ // 회원가입 시 환영 이메일 발송 Job
1277
+ const sendWelcomeEmailJob = job('send-welcome-email')
1278
+ .on(authRegisterEvent)
1279
+ .handler(async ({ userId, email }) => {
1280
+ if (email) {
1281
+ await emailService.sendWelcome(email);
1282
+ }
1283
+ });
1284
+
1285
+ // 회원가입 시 기본 설정 생성 Job
1286
+ const createDefaultSettingsJob = job('create-default-settings')
1287
+ .on(authRegisterEvent)
1288
+ .handler(async ({ userId }) => {
1289
+ await settingsService.createDefaults(userId);
1290
+ });
1291
+
1292
+ export const jobRouter = defineJobRouter({
1293
+ sendWelcomeEmailJob,
1294
+ createDefaultSettingsJob,
1295
+ });
1296
+ ```
1297
+
1298
+ ---
1299
+
1300
+ ### Event Flow
1301
+
1302
+ ```
1303
+ ┌─────────────────────────────────────────────────────────────────┐
1304
+ │ loginService() / registerService() │
1305
+ │ oauthCallbackService() │
1306
+ └─────────────────────────────────────────────────────────────────┘
1307
+
1308
+
1309
+ authLoginEvent.emit()
1310
+ authRegisterEvent.emit()
1311
+
1312
+ ┌───────────────────┼───────────────────┐
1313
+ ▼ ▼ ▼
1314
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
1315
+ │ Backend │ │ Job │ │ SSE │
1316
+ │ Handler │ │ Queue │ │ Stream │
1317
+ └──────────┘ └──────────┘ └──────────┘
1318
+ .subscribe() .on(event) (optional)
1319
+ │ │
1320
+ ▼ ▼
1321
+ [Analytics, [Background
1322
+ Logging] Processing]
1323
+ ```
1324
+
1325
+ ---
1326
+
1327
+ ### Type Exports
1328
+
1329
+ ```typescript
1330
+ import type {
1331
+ AuthLoginPayload,
1332
+ AuthRegisterPayload,
1333
+ } from '@spfn/auth/server';
1334
+ ```
1335
+
1336
+ ---
1337
+
1338
+ ## OAuth Authentication
1339
+
1340
+ ### Overview
1341
+
1342
+ `@spfn/auth`는 OAuth 2.0 Authorization Code Flow를 지원합니다. 현재 Google OAuth가 구현되어 있으며, 다른 provider (GitHub, Kakao, Naver)는 동일한 패턴으로 확장 가능합니다.
1343
+
1344
+ **핵심 설계:**
1345
+ - 환경 변수만으로 설정 (`SPFN_AUTH_GOOGLE_CLIENT_ID`, `SPFN_AUTH_GOOGLE_CLIENT_SECRET`)
1346
+ - Next.js 인터셉터 기반 자동 세션 관리 (키쌍 생성 → pending session → full session)
1347
+ - 기존 이메일 계정과 자동 연결 (Google verified_email 확인 시에만)
1348
+
1349
+ ---
1350
+
1351
+ ### Authentication Flow
1352
+
1353
+ ```
1354
+ ┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
1355
+ │ Client │ │ Next.js RPC │ │ Backend │ │ Google │
1356
+ │ (Browser)│ │ (Interceptor)│ │ (SPFN) │ │ OAuth │
1357
+ └────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
1358
+ │ │ │ │
1359
+ │ 1. Click Login │ │ │
1360
+ ├──────────────────>│ │ │
1361
+ │ │ │ │
1362
+ │ 2. Generate keypair (ES256) │ │
1363
+ │ 3. Create encrypted state │ │
1364
+ │ (publicKey, keyId in JWE) │ │
1365
+ │ 4. Save privateKey to │ │
1366
+ │ pending session cookie │ │
1367
+ │ │ │ │
1368
+ │ │ 5. Forward with │ │
1369
+ │ │ state in body │ │
1370
+ │ ├─────────────────>│ │
1371
+ │ │ │ │
1372
+ │ │ 6. Return Google │ │
1373
+ │ │ Auth URL │ │
1374
+ │ │<─────────────────┤ │
1375
+ │ │ │ │
1376
+ │ 7. Redirect to Google │ │
1377
+ │<──────────────────┤ │ │
1378
+ │ │ │ │
1379
+ │ 8. User consents │ │ │
1380
+ ├───────────────────┼──────────────────┼────────────────>│
1381
+ │ │ │ │
1382
+ │ │ 9. Callback with code + state │
1383
+ │ │ │<────────────────┤
1384
+ │ │ │ │
1385
+ │ │ 10. Verify state, exchange code │
1386
+ │ │ Create/link user account │
1387
+ │ │ Register publicKey │
1388
+ │ │ │ │
1389
+ │ 11. Redirect to /auth/callback │ │
1390
+ │ ?userId=X&keyId=Y&returnUrl=/ │ │
1391
+ │<─────────────────────────────────────┤ │
1392
+ │ │ │ │
1393
+ │ 12. OAuthCallback │ │ │
1394
+ │ component │ │ │
1395
+ │ calls finalize│ │ │
1396
+ ├──────────────────>│ │ │
1397
+ │ │ │ │
1398
+ │ 13. Interceptor reads pending │ │
1399
+ │ session cookie, verifies │ │
1400
+ │ keyId match, creates full │ │
1401
+ │ session cookie │ │
1402
+ │ │ │ │
1403
+ │ 14. Session set, │ │ │
1404
+ │ redirect to │ │ │
1405
+ │ returnUrl │ │ │
1406
+ │<──────────────────┤ │ │
1407
+ │ │ │ │
1408
+ ```
1409
+
1410
+ ---
1411
+
1412
+ ### Setup
1413
+
1414
+ #### 1. Google Cloud Console
1415
+
1416
+ 1. [Google Cloud Console](https://console.cloud.google.com/) > APIs & Services > Credentials
1417
+ 2. Create OAuth 2.0 Client ID (Web application)
1418
+ 3. Add Authorized redirect URI: `http://localhost:8790/_auth/oauth/google/callback`
1419
+ 4. Copy Client ID and Client Secret
1420
+
1421
+ #### 2. Environment Variables
1422
+
1423
+ ```bash
1424
+ # Required
1425
+ SPFN_AUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
1426
+ SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
1427
+
1428
+ # Next.js app URL (for OAuth callback redirect)
1429
+ SPFN_APP_URL=http://localhost:3000
1430
+
1431
+ # Optional
1432
+ SPFN_AUTH_GOOGLE_SCOPES=email,profile # default (comma-separated)
1433
+ SPFN_AUTH_GOOGLE_REDIRECT_URI=http://localhost:8790/_auth/oauth/google/callback # default
1434
+ SPFN_AUTH_OAUTH_SUCCESS_URL=/auth/callback # default
1435
+ ```
1436
+
1437
+ #### 3. Next.js Callback Page
1438
+
1439
+ ```tsx
1440
+ // app/auth/callback/page.tsx
1441
+ export { OAuthCallback as default } from '@spfn/auth/nextjs/client';
1442
+ ```
1443
+
1444
+ #### 4. Login Button
1445
+
1446
+ ```typescript
1447
+ import { authApi } from '@spfn/auth';
1448
+
1449
+ const handleGoogleLogin = async () =>
1450
+ {
1451
+ const response = await authApi.getGoogleOAuthUrl.call({
1452
+ body: { returnUrl: '/dashboard' },
1453
+ });
1454
+ window.location.href = response.authUrl;
1455
+ };
1456
+ ```
1457
+
1458
+ ---
1459
+
1460
+ ### OAuth Routes
1461
+
1462
+ #### `GET /_auth/oauth/google`
1463
+
1464
+ Google OAuth 시작 (리다이렉트 방식). 브라우저를 Google 로그인 페이지로 직접 리다이렉트합니다.
1465
+
1466
+ **Query:**
1467
+ ```typescript
1468
+ {
1469
+ state: string; // Encrypted OAuth state (JWE)
1470
+ }
1471
+ ```
1472
+
1473
+ ---
1474
+
1475
+ #### `POST /_auth/oauth/google/url`
1476
+
1477
+ Google OAuth URL 획득 (인터셉터 방식). 인터셉터가 state를 자동 생성하여 주입합니다.
1478
+
1479
+ **Request:**
1480
+ ```typescript
1481
+ {
1482
+ returnUrl?: string; // Default: '/'
1483
+ }
1484
+ ```
1485
+
1486
+ **Response:**
1487
+ ```typescript
1488
+ {
1489
+ authUrl: string; // Google OAuth URL
1490
+ }
1491
+ ```
1492
+
1493
+ ---
1494
+
1495
+ #### `GET /_auth/oauth/google/callback`
1496
+
1497
+ Google에서 리다이렉트되는 콜백. code를 token으로 교환하고 사용자를 생성/연결합니다.
1498
+
1499
+ **Query (from Google):**
1500
+ ```typescript
1501
+ {
1502
+ code?: string; // Authorization code
1503
+ state?: string; // OAuth state
1504
+ error?: string; // Error code
1505
+ error_description?: string; // Error description
1506
+ }
1507
+ ```
1508
+
1509
+ **Result:** Next.js 콜백 페이지로 리다이렉트 (`/auth/callback?userId=X&keyId=Y&returnUrl=/`)
1510
+
1511
+ ---
1512
+
1513
+ #### `POST /_auth/oauth/finalize`
1514
+
1515
+ OAuth 세션 완료. 인터셉터가 pending session에서 full session을 생성합니다.
1516
+
1517
+ **Request:**
1518
+ ```typescript
1519
+ {
1520
+ userId: string;
1521
+ keyId: string;
1522
+ returnUrl?: string;
1523
+ }
1524
+ ```
1525
+
1526
+ **Response:**
1527
+ ```typescript
1528
+ {
1529
+ success: boolean;
1530
+ returnUrl: string;
1531
+ }
1532
+ ```
1533
+
1534
+ ---
1535
+
1536
+ #### `GET /_auth/oauth/providers`
1537
+
1538
+ 활성화된 OAuth provider 목록을 반환합니다.
1539
+
1540
+ **Response:**
1541
+ ```typescript
1542
+ {
1543
+ providers: ('google' | 'github' | 'kakao' | 'naver')[];
1544
+ }
1545
+ ```
1546
+
1547
+ ---
1548
+
1549
+ ### Google API Access
1550
+
1551
+ OAuth 로그인 후 저장된 access token으로 Google API를 호출할 수 있습니다.
1552
+
1553
+ #### Custom Scopes 설정
1554
+
1555
+ `SPFN_AUTH_GOOGLE_SCOPES` 환경변수로 추가 스코프를 요청합니다. 미설정 시 `email,profile`이 기본값입니다.
1556
+
1557
+ ```bash
1558
+ # Gmail + Calendar 읽기 권한 추가
1559
+ SPFN_AUTH_GOOGLE_SCOPES=email,profile,https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/calendar.readonly
1560
+ ```
1561
+
1562
+ > **Note:** Google Cloud Console에서 해당 API를 활성화해야 합니다.
1563
+
1564
+ #### Access Token 사용
1565
+
1566
+ `getGoogleAccessToken(userId)`은 유효한 access token을 반환합니다. 토큰이 만료 임박(5분 이내) 또는 만료 상태이면 자동으로 refresh token을 사용하여 갱신합니다.
1567
+
1568
+ ```typescript
1569
+ import { getGoogleAccessToken } from '@spfn/auth/server';
1570
+
1571
+ // 항상 유효한 토큰 반환 (만료 시 자동 갱신)
1572
+ const token = await getGoogleAccessToken(userId);
1573
+
1574
+ // Gmail API 호출
1575
+ const response = await fetch(
1576
+ 'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=10',
1577
+ { headers: { Authorization: `Bearer ${token}` } }
1578
+ );
1579
+ const data = await response.json();
1580
+ ```
1581
+
1582
+ **에러 케이스:**
1583
+ - Google 계정 미연결 → `'No Google account linked'`
1584
+ - Refresh token 없음 → `'Google refresh token not available'` (재로그인 필요)
1585
+
1586
+ ---
1587
+
1588
+ ### Security
1589
+
1590
+ - **State 암호화**: JWE (A256GCM)로 state 파라미터 암호화. CSRF 방지용 nonce 포함.
1591
+ - **Pending Session**: OAuth 리다이렉트 중 privateKey를 JWE로 암호화한 HttpOnly 쿠키에 저장. 10분 TTL.
1592
+ - **KeyId 검증**: finalize 시 pending session의 keyId와 응답의 keyId 일치 확인.
1593
+ - **Email 검증**: `verified_email`이 true인 경우에만 기존 계정에 자동 연결. 미검증 이메일로 기존 계정 연결 시도 시 에러.
1594
+ - **Session Cookie**: `HttpOnly`, `Secure` (production), `SameSite=strict`.
1595
+
1596
+ ---
1597
+
1598
+ ### OAuthCallback Component
1599
+
1600
+ `@spfn/auth/nextjs/client`에서 제공하는 클라이언트 컴포넌트입니다.
1601
+
1602
+ ```tsx
1603
+ import { OAuthCallback } from '@spfn/auth/nextjs/client';
1604
+
1605
+ // 기본 사용
1606
+ export default function CallbackPage()
1607
+ {
1608
+ return <OAuthCallback />;
1609
+ }
1610
+
1611
+ // 커스터마이징
1612
+ export default function CallbackPage()
1613
+ {
1614
+ return (
1615
+ <OAuthCallback
1616
+ apiBasePath="/api/rpc"
1617
+ loadingComponent={<MySpinner />}
1618
+ errorComponent={(error) => <MyError message={error} />}
1619
+ onSuccess={(userId) => console.log('Logged in:', userId)}
1620
+ onError={(error) => console.error(error)}
1621
+ />
1622
+ );
1623
+ }
1624
+ ```
1625
+
1626
+ **Props:**
1627
+
1628
+ | Prop | Type | Default | Description |
1629
+ |------|------|---------|-------------|
1630
+ | `apiBasePath` | `string` | `'/api/rpc'` | RPC API base path |
1631
+ | `loadingComponent` | `ReactNode` | Built-in | 로딩 중 표시할 컴포넌트 |
1632
+ | `errorComponent` | `(error: string) => ReactNode` | Built-in | 에러 표시 컴포넌트 |
1633
+ | `onSuccess` | `(userId: string) => void` | - | 성공 콜백 |
1634
+ | `onError` | `(error: string) => void` | - | 에러 콜백 |
1635
+
1636
+ ---
1637
+
1030
1638
  ## Database Schema
1031
1639
 
1032
1640
  ### Core Tables
@@ -1038,8 +1646,10 @@ Main user identity table.
1038
1646
  ```sql
1039
1647
  CREATE TABLE users (
1040
1648
  id BIGSERIAL PRIMARY KEY,
1649
+ public_id UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
1041
1650
  email TEXT UNIQUE,
1042
1651
  phone TEXT UNIQUE,
1652
+ username TEXT UNIQUE,
1043
1653
  password_hash TEXT NOT NULL,
1044
1654
  password_change_required BOOLEAN DEFAULT false,
1045
1655
  role_id BIGINT REFERENCES roles(id) NOT NULL,
@@ -1057,7 +1667,9 @@ CREATE TABLE users (
1057
1667
  ```
1058
1668
 
1059
1669
  **Key Points:**
1670
+ - `public_id` is a UUID v4 for external-facing URLs and APIs (never expose internal `id`)
1060
1671
  - At least one of `email` OR `phone` required
1672
+ - `username` is unique and nullable (optional display/mention identifier)
1061
1673
  - `passwordHash` is bcrypt ($2b$10$..., 60 chars)
1062
1674
  - `roleId` references roles table (NOT NULL)
1063
1675
 
@@ -1167,7 +1779,7 @@ CREATE TABLE permissions (
1167
1779
 
1168
1780
  **Built-in Permissions:**
1169
1781
  - `auth:self:manage`
1170
- - `user:read`, `user:write`, `user:delete`
1782
+ - `user:read`, `user:write`, `user:delete`, `user:invite`
1171
1783
  - `rbac:role:manage`, `rbac:permission:manage`
1172
1784
 
1173
1785
  ---
@@ -1260,7 +1872,7 @@ CREATE TABLE user_profiles (
1260
1872
 
1261
1873
  #### `user_social_accounts`
1262
1874
 
1263
- OAuth provider accounts (future feature).
1875
+ OAuth provider accounts (Google, GitHub, etc.).
1264
1876
 
1265
1877
  ```sql
1266
1878
  CREATE TABLE user_social_accounts (
@@ -1327,7 +1939,7 @@ await initializeAuth({
1327
1939
 
1328
1940
  **Permissions:**
1329
1941
  - `auth:self:manage` - Change password, rotate keys
1330
- - `user:read`, `user:write`, `user:delete`
1942
+ - `user:read`, `user:write`, `user:delete`, `user:invite`
1331
1943
  - `rbac:role:manage`, `rbac:permission:manage`
1332
1944
 
1333
1945
  ---
@@ -1335,7 +1947,7 @@ await initializeAuth({
1335
1947
  ### Middleware Usage
1336
1948
 
1337
1949
  ```typescript
1338
- import { authenticate, requirePermissions, requireRole } from '@spfn/auth/server';
1950
+ import { authenticate, requirePermissions, requireAnyPermission, requireRole } from '@spfn/auth/server';
1339
1951
 
1340
1952
  // Single permission
1341
1953
  app.bind(
@@ -1355,6 +1967,15 @@ app.bind(
1355
1967
  }
1356
1968
  );
1357
1969
 
1970
+ // Any of the permissions (at least one required)
1971
+ app.bind(
1972
+ viewContentContract,
1973
+ [authenticate, requireAnyPermission('content:read', 'admin:access')],
1974
+ async (c) => {
1975
+ // User has either content:read OR admin:access
1976
+ }
1977
+ );
1978
+
1358
1979
  // Role-based
1359
1980
  app.bind(
1360
1981
  adminDashboardContract,
@@ -1468,10 +2089,22 @@ import '@spfn/auth/nextjs/api';
1468
2089
  **Target Routes:**
1469
2090
  - `/_auth/login`, `/_auth/register` - Login/register interceptor
1470
2091
  - `/_auth/keys/rotate` - Key rotation interceptor
2092
+ - `/_auth/oauth/:provider/url` - OAuth URL interceptor (keypair + state generation)
2093
+ - `/_auth/oauth/finalize` - OAuth finalize interceptor (pending session → full session)
1471
2094
  - All other authenticated routes - General auth interceptor
1472
2095
 
1473
2096
  ---
1474
2097
 
2098
+ ### OAuth Client Component (`@spfn/auth/nextjs/client`)
2099
+
2100
+ ```typescript
2101
+ import { OAuthCallback, type OAuthCallbackProps } from '@spfn/auth/nextjs/client';
2102
+ ```
2103
+
2104
+ OAuth 콜백 페이지용 `'use client'` 컴포넌트. 자세한 사용법은 [OAuth Authentication](#oauth-authentication) 섹션 참조.
2105
+
2106
+ ---
2107
+
1475
2108
  ## Testing
1476
2109
 
1477
2110
  ### Setup Test Environment
@@ -1817,7 +2450,7 @@ ls migrations/
1817
2450
 
1818
2451
  - [ ] **React hooks** - useAuth, useSession, usePermissions
1819
2452
  - [ ] **UI components** - LoginForm, RegisterForm, AuthProvider
1820
- - [ ] **OAuth integration** - Google, GitHub, etc.
2453
+ - [x] **OAuth integration** - Google (implemented), GitHub/Kakao/Naver (planned)
1821
2454
  - [ ] **2FA support** - TOTP/authenticator apps
1822
2455
  - [ ] **Password reset flow** - Complete email-based reset
1823
2456
  - [ ] **Email change flow** - Verification for email updates
@@ -1957,6 +2590,6 @@ MIT License - See LICENSE file for details.
1957
2590
 
1958
2591
  ---
1959
2592
 
1960
- **Last Updated:** 2025-12-07
1961
- **Document Version:** 2.2.0 (Technical Documentation)
1962
- **Package Version:** 0.1.0-alpha.88
2593
+ **Last Updated:** 2026-02-23
2594
+ **Document Version:** 2.6.0 (Technical Documentation)
2595
+ **Package Version:** 0.2.0-beta.15