@spfn/auth 0.2.0-beta.8 → 0.2.0-beta.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +549 -1819
  3. package/dist/authenticate-ofdEmk6x.d.ts +1109 -0
  4. package/dist/config.d.ts +359 -41
  5. package/dist/config.js +186 -30
  6. package/dist/config.js.map +1 -1
  7. package/dist/errors.d.ts +117 -3
  8. package/dist/errors.js +83 -1
  9. package/dist/errors.js.map +1 -1
  10. package/dist/index.d.ts +351 -109
  11. package/dist/index.js +124 -7
  12. package/dist/index.js.map +1 -1
  13. package/dist/nextjs/api.js +591 -61
  14. package/dist/nextjs/api.js.map +1 -1
  15. package/dist/nextjs/client.d.ts +28 -0
  16. package/dist/nextjs/client.js +80 -0
  17. package/dist/nextjs/client.js.map +1 -0
  18. package/dist/nextjs/server.d.ts +92 -3
  19. package/dist/nextjs/server.js +288 -24
  20. package/dist/nextjs/server.js.map +1 -1
  21. package/dist/server.d.ts +2015 -513
  22. package/dist/server.js +4198 -1086
  23. package/dist/server.js.map +1 -1
  24. package/dist/session-CGxgH3C9.d.ts +53 -0
  25. package/dist/types-1BMx0OX1.d.ts +84 -0
  26. package/migrations/0001_smooth_the_fury.sql +3 -0
  27. package/migrations/0002_deep_iceman.sql +11 -0
  28. package/migrations/0003_perfect_deathbird.sql +3 -0
  29. package/migrations/0004_concerned_rawhide_kid.sql +5 -0
  30. package/migrations/0005_lethal_lifeguard.sql +32 -0
  31. package/migrations/0006_easy_hardball.sql +24 -0
  32. package/migrations/0007_glossy_major_mapleleaf.sql +1 -0
  33. package/migrations/meta/0001_snapshot.json +1660 -0
  34. package/migrations/meta/0002_snapshot.json +1660 -0
  35. package/migrations/meta/0003_snapshot.json +1689 -0
  36. package/migrations/meta/0004_snapshot.json +1721 -0
  37. package/migrations/meta/0005_snapshot.json +1721 -0
  38. package/migrations/meta/0006_snapshot.json +1921 -0
  39. package/migrations/meta/0007_snapshot.json +1916 -0
  40. package/migrations/meta/_journal.json +49 -0
  41. package/package.json +43 -39
  42. package/dist/dto-lZmWuObc.d.ts +0 -645
package/dist/server.d.ts CHANGED
@@ -1,13 +1,20 @@
1
- import { k as AuthInitOptions, l as KeyAlgorithmType, n as InvitationStatus, f as VerificationPurpose, j as PermissionCategory, q as AuthContext } from './dto-lZmWuObc.js';
2
- export { B as ChangePasswordParams, w as CheckAccountExistsParams, C as CheckAccountExistsResult, X as EmailSchema, I as INVITATION_STATUSES, K as KEY_ALGORITHM, y as LoginParams, L as LoginResult, z as LogoutParams, Z as PasswordSchema, Y as PhoneSchema, x as RegisterParams, O as RegisterPublicKeyParams, b as RegisterResult, T as RevokeKeyParams, Q as RotateKeyParams, c as RotateKeyResult, e as SOCIAL_PROVIDERS, F as SendVerificationCodeParams, S as SendVerificationCodeResult, p as SocialProvider, _ as TargetTypeSchema, d as USER_STATUSES, o as UserStatus, h as VERIFICATION_PURPOSES, g as VERIFICATION_TARGET_TYPES, $ as VerificationPurposeSchema, V as VerificationTargetType, G as VerifyCodeParams, H as VerifyCodeResult, m as authRouter, W as authenticate, v as changePasswordService, r as checkAccountExistsService, t as loginService, u as logoutService, J as registerPublicKeyService, s as registerService, N as revokeKeyService, M as rotateKeyService, D as sendVerificationCodeService, E as verifyCodeService } from './dto-lZmWuObc.js';
1
+ import { j as AuthInitOptions, k as OAuthProvider, e as VerificationPurpose, i as PermissionCategory, l as AuthContext } from './authenticate-ofdEmk6x.js';
2
+ export { C as ChangePasswordParams, a3 as EmailSchema, I as IssueOneTimeTokenResult, s as LoginParams, L as LoginResult, t as LogoutParams, aa as NativeVerifyOptions, a8 as NormalizedIdentity, Z as OAuthCallbackParams, _ as OAuthCallbackResult, ab as OAuthCodeExchangeOptions, a0 as OAuthNativeParams, c as OAuthNativeResult, Y as OAuthStartParams, O as OAuthStartResult, a9 as OAuthTokens, a5 as PasswordSchema, a4 as PhoneSchema, q as RegisterParams, E as RegisterPublicKeyParams, a as RegisterResult, G as RevokeKeyParams, F as RotateKeyParams, b as RotateKeyResult, w as SendVerificationCodeParams, S as SendVerificationCodeResult, a6 as TargetTypeSchema, g as VERIFICATION_PURPOSES, f as VERIFICATION_TARGET_TYPES, a7 as VerificationPurposeSchema, V as VerificationTargetType, x as VerifyCodeParams, y as VerifyCodeResult, m as authRouter, a1 as authenticate, N as buildOAuthErrorUrl, p as changePasswordService, W as getEnabledOAuthProviders, X as getGoogleAccessToken, ad as getOAuthProvider, ae as getRegisteredProviders, Q as isOAuthProviderEnabled, H as issueOneTimeTokenService, n as loginService, o as logoutService, M as oauthCallbackService, $ as oauthNativeService, K as oauthStartService, a2 as optionalAuth, ac as registerOAuthProvider, z as registerPublicKeyService, r as registerService, T as requireEnabledProvider, D as revokeKeyService, B as rotateKeyService, u as sendVerificationCodeService, v as verifyCodeService, J as verifyOneTimeTokenService } from './authenticate-ofdEmk6x.js';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
+ import { K as KeyAlgorithmType, c as InvitationStatus, e as SocialProvider, g as AccountDeletionRequestedBy, h as PurgeStrategy } from './types-1BMx0OX1.js';
5
+ export { b as ACCOUNT_DELETION_REQUESTED_BY, A as ACCOUNT_DELETION_REQUEST_STATUSES, f as AccountDeletionRequestStatus, I as INVITATION_STATUSES, a as KEY_ALGORITHM, P as PURGE_STRATEGIES, S as SOCIAL_PROVIDERS, U as USER_STATUSES, d as UserStatus } from './types-1BMx0OX1.js';
4
6
  import { UserProfile as UserProfile$1, ProfileInfo } from '@spfn/auth';
5
7
  import { BaseRepository } from '@spfn/core/db';
6
8
  import { Context } from 'hono';
7
9
  import * as _spfn_core_route from '@spfn/core/route';
8
10
  import { Algorithm } from 'jsonwebtoken';
11
+ export { S as SessionData, g as getSessionInfo, s as sealSession, a as shouldRefreshSession, u as unsealSession } from './session-CGxgH3C9.js';
12
+ import { JWTPayload } from 'jose';
13
+ import { SSETokenStore, SSETokenManager } from '@spfn/core/event/sse';
9
14
  import * as _spfn_core_logger from '@spfn/core/logger';
10
- import '@sinclair/typebox';
15
+ import * as _spfn_core_job from '@spfn/core/job';
16
+ import * as _spfn_core_event from '@spfn/core/event';
17
+ import * as _sinclair_typebox from '@sinclair/typebox';
11
18
  import '@spfn/auth/server';
12
19
 
13
20
  /**
@@ -27,6 +34,40 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
27
34
  name: "users";
28
35
  schema: string;
29
36
  columns: {
37
+ deletedAt: drizzle_orm_pg_core.PgColumn<{
38
+ name: "deleted_at";
39
+ tableName: "users";
40
+ dataType: "date";
41
+ columnType: "PgTimestamp";
42
+ data: Date;
43
+ driverParam: string;
44
+ notNull: false;
45
+ hasDefault: false;
46
+ isPrimaryKey: false;
47
+ isAutoincrement: false;
48
+ hasRuntimeDefault: false;
49
+ enumValues: undefined;
50
+ baseColumn: never;
51
+ identity: undefined;
52
+ generated: undefined;
53
+ }, {}, {}>;
54
+ deletedBy: drizzle_orm_pg_core.PgColumn<{
55
+ name: "deleted_by";
56
+ tableName: "users";
57
+ dataType: "string";
58
+ columnType: "PgText";
59
+ data: string;
60
+ driverParam: string;
61
+ notNull: false;
62
+ hasDefault: false;
63
+ isPrimaryKey: false;
64
+ isAutoincrement: false;
65
+ hasRuntimeDefault: false;
66
+ enumValues: [string, ...string[]];
67
+ baseColumn: never;
68
+ identity: undefined;
69
+ generated: undefined;
70
+ }, {}, {}>;
30
71
  createdAt: drizzle_orm_pg_core.PgColumn<{
31
72
  name: "created_at";
32
73
  tableName: "users";
@@ -78,6 +119,23 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
78
119
  identity: undefined;
79
120
  generated: undefined;
80
121
  }, {}, {}>;
122
+ publicId: drizzle_orm_pg_core.PgColumn<{
123
+ name: "public_id";
124
+ tableName: "users";
125
+ dataType: "string";
126
+ columnType: "PgUUID";
127
+ data: string;
128
+ driverParam: string;
129
+ notNull: true;
130
+ hasDefault: true;
131
+ isPrimaryKey: false;
132
+ isAutoincrement: false;
133
+ hasRuntimeDefault: false;
134
+ enumValues: undefined;
135
+ baseColumn: never;
136
+ identity: undefined;
137
+ generated: undefined;
138
+ }, {}, {}>;
81
139
  email: drizzle_orm_pg_core.PgColumn<{
82
140
  name: "email";
83
141
  tableName: "users";
@@ -112,6 +170,23 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
112
170
  identity: undefined;
113
171
  generated: undefined;
114
172
  }, {}, {}>;
173
+ username: drizzle_orm_pg_core.PgColumn<{
174
+ name: "username";
175
+ tableName: "users";
176
+ dataType: "string";
177
+ columnType: "PgText";
178
+ data: string;
179
+ driverParam: string;
180
+ notNull: false;
181
+ hasDefault: false;
182
+ isPrimaryKey: false;
183
+ isAutoincrement: false;
184
+ hasRuntimeDefault: false;
185
+ enumValues: [string, ...string[]];
186
+ baseColumn: never;
187
+ identity: undefined;
188
+ generated: undefined;
189
+ }, {}, {}>;
115
190
  passwordHash: drizzle_orm_pg_core.PgColumn<{
116
191
  name: "password_hash";
117
192
  tableName: "users";
@@ -150,11 +225,11 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
150
225
  name: `${string}_id`;
151
226
  tableName: "users";
152
227
  dataType: "number";
153
- columnType: "PgBigSerial53";
228
+ columnType: "PgBigInt53";
154
229
  data: number;
155
- driverParam: number;
230
+ driverParam: string | number;
156
231
  notNull: true;
157
- hasDefault: true;
232
+ hasDefault: false;
158
233
  isPrimaryKey: false;
159
234
  isAutoincrement: false;
160
235
  hasRuntimeDefault: false;
@@ -168,14 +243,14 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
168
243
  tableName: "users";
169
244
  dataType: "string";
170
245
  columnType: "PgText";
171
- data: "active" | "inactive" | "suspended";
246
+ data: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
172
247
  driverParam: string;
173
248
  notNull: true;
174
249
  hasDefault: true;
175
250
  isPrimaryKey: false;
176
251
  isAutoincrement: false;
177
252
  hasRuntimeDefault: false;
178
- enumValues: ["active", "inactive", "suspended"] & [string, ...string[]];
253
+ enumValues: ["active", "inactive", "suspended", "pending_deletion", "deleted"] & [string, ...string[]];
179
254
  baseColumn: never;
180
255
  identity: undefined;
181
256
  generated: undefined;
@@ -247,15 +322,19 @@ type NewUser = typeof users.$inferInsert;
247
322
  * Get user by ID
248
323
  */
249
324
  declare function getUserByIdService(userId: number): Promise<{
325
+ deletedAt: Date | null;
326
+ deletedBy: string | null;
250
327
  createdAt: Date;
251
328
  updatedAt: Date;
252
329
  id: number;
330
+ publicId: string;
253
331
  email: string | null;
254
332
  phone: string | null;
333
+ username: string | null;
255
334
  passwordHash: string | null;
256
335
  passwordChangeRequired: boolean;
257
336
  roleId: number;
258
- status: "active" | "inactive" | "suspended";
337
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
259
338
  emailVerifiedAt: Date | null;
260
339
  phoneVerifiedAt: Date | null;
261
340
  lastLoginAt: Date | null;
@@ -264,15 +343,19 @@ declare function getUserByIdService(userId: number): Promise<{
264
343
  * Get user by email
265
344
  */
266
345
  declare function getUserByEmailService(email: string): Promise<{
346
+ deletedAt: Date | null;
347
+ deletedBy: string | null;
267
348
  createdAt: Date;
268
349
  updatedAt: Date;
269
350
  id: number;
351
+ publicId: string;
270
352
  email: string | null;
271
353
  phone: string | null;
354
+ username: string | null;
272
355
  passwordHash: string | null;
273
356
  passwordChangeRequired: boolean;
274
357
  roleId: number;
275
- status: "active" | "inactive" | "suspended";
358
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
276
359
  emailVerifiedAt: Date | null;
277
360
  phoneVerifiedAt: Date | null;
278
361
  lastLoginAt: Date | null;
@@ -281,15 +364,19 @@ declare function getUserByEmailService(email: string): Promise<{
281
364
  * Get user by phone
282
365
  */
283
366
  declare function getUserByPhoneService(phone: string): Promise<{
367
+ deletedAt: Date | null;
368
+ deletedBy: string | null;
284
369
  createdAt: Date;
285
370
  updatedAt: Date;
286
371
  id: number;
372
+ publicId: string;
287
373
  email: string | null;
288
374
  phone: string | null;
375
+ username: string | null;
289
376
  passwordHash: string | null;
290
377
  passwordChangeRequired: boolean;
291
378
  roleId: number;
292
- status: "active" | "inactive" | "suspended";
379
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
293
380
  emailVerifiedAt: Date | null;
294
381
  phoneVerifiedAt: Date | null;
295
382
  lastLoginAt: Date | null;
@@ -302,17 +389,51 @@ declare function updateLastLoginService(userId: number): Promise<void>;
302
389
  * Update user data
303
390
  */
304
391
  declare function updateUserService(userId: number, updates: Partial<NewUser>): Promise<void>;
392
+ /**
393
+ * Check if username is available
394
+ *
395
+ * @returns true if the username is available (not taken and not reserved)
396
+ */
397
+ declare function checkUsernameAvailableService(username: string): Promise<boolean>;
398
+ /**
399
+ * Update username with reserved word and duplicate check
400
+ *
401
+ * @param userId - User ID (string, number, or bigint)
402
+ * @param username - New username or null to clear
403
+ * @throws ReservedUsernameError if username is reserved
404
+ * @throws UsernameAlreadyTakenError if username is already in use by another user
405
+ */
406
+ declare function updateUsernameService(userId: string | number | bigint, username: string | null): Promise<{
407
+ deletedAt: Date | null;
408
+ deletedBy: string | null;
409
+ createdAt: Date;
410
+ updatedAt: Date;
411
+ id: number;
412
+ publicId: string;
413
+ email: string | null;
414
+ phone: string | null;
415
+ username: string | null;
416
+ passwordHash: string | null;
417
+ passwordChangeRequired: boolean;
418
+ roleId: number;
419
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
420
+ emailVerifiedAt: Date | null;
421
+ phoneVerifiedAt: Date | null;
422
+ lastLoginAt: Date | null;
423
+ }>;
305
424
 
306
425
  /**
307
426
  * @spfn/auth - RBAC Initialization Service
308
427
  *
309
428
  * Initialize roles, permissions, and their mappings
429
+ * Uses hash-based change detection to skip unnecessary DB operations
310
430
  */
311
431
 
312
432
  /**
313
433
  * Initialize auth package with RBAC system
314
434
  *
315
- * Creates built-in roles, permissions, and custom configurations
435
+ * Creates built-in roles, permissions, and custom configurations.
436
+ * Uses hash-based change detection - skips DB operations when config is unchanged.
316
437
  *
317
438
  * @param options - Initialization options
318
439
  *
@@ -442,6 +563,24 @@ declare function hasRole(userId: string | number | bigint, roleName: string): Pr
442
563
  * @returns true if user has at least one role
443
564
  */
444
565
  declare function hasAnyRole(userId: string | number | bigint, roleNames: string[]): Promise<boolean>;
566
+ /**
567
+ * Assert that the caller is allowed to assign the given role.
568
+ *
569
+ * Centralizes the privilege rule shared by direct role assignment and invitation:
570
+ * a non-superadmin caller may never grant the superadmin role, and may grant the
571
+ * admin role only when they hold the `admin:promote` permission. Assigning any
572
+ * lower role is unrestricted (the route's own permission guard still applies).
573
+ *
574
+ * @param callerUserId - User performing the assignment
575
+ * @param targetRoleId - Role being assigned
576
+ * @throws ForbiddenError when the caller lacks the authority for the target role
577
+ *
578
+ * @example
579
+ * ```typescript
580
+ * await assertCanAssignRole(auth.userId, body.roleId);
581
+ * ```
582
+ */
583
+ declare function assertCanAssignRole(callerUserId: string | number | bigint, targetRoleId: number): Promise<void>;
445
584
 
446
585
  /**
447
586
  * @spfn/auth - Roles Entity
@@ -880,11 +1019,11 @@ declare const userInvitations: drizzle_orm_pg_core.PgTableWithColumns<{
880
1019
  name: `${string}_id`;
881
1020
  tableName: "user_invitations";
882
1021
  dataType: "number";
883
- columnType: "PgBigSerial53";
1022
+ columnType: "PgBigInt53";
884
1023
  data: number;
885
- driverParam: number;
1024
+ driverParam: string | number;
886
1025
  notNull: true;
887
- hasDefault: true;
1026
+ hasDefault: false;
888
1027
  isPrimaryKey: false;
889
1028
  isAutoincrement: false;
890
1029
  hasRuntimeDefault: false;
@@ -897,11 +1036,11 @@ declare const userInvitations: drizzle_orm_pg_core.PgTableWithColumns<{
897
1036
  name: `${string}_id`;
898
1037
  tableName: "user_invitations";
899
1038
  dataType: "number";
900
- columnType: "PgBigSerial53";
1039
+ columnType: "PgBigInt53";
901
1040
  data: number;
902
- driverParam: number;
1041
+ driverParam: string | number;
903
1042
  notNull: true;
904
- hasDefault: true;
1043
+ hasDefault: false;
905
1044
  isPrimaryKey: false;
906
1045
  isAutoincrement: false;
907
1046
  hasRuntimeDefault: false;
@@ -1032,6 +1171,7 @@ declare function createInvitation(params: {
1032
1171
  roleId: number;
1033
1172
  invitedBy: number;
1034
1173
  expiresInDays?: number;
1174
+ expiresAt?: Date;
1035
1175
  metadata?: Record<string, any>;
1036
1176
  }): Promise<Invitation>;
1037
1177
  /**
@@ -1255,12 +1395,14 @@ declare function getAuthSessionService(userId: string | number | bigint): Promis
1255
1395
  id: number;
1256
1396
  name: string;
1257
1397
  displayName: string;
1258
- category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
1398
+ category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
1259
1399
  }[];
1260
1400
  userId: number;
1401
+ publicId: string;
1261
1402
  email: string | null;
1262
1403
  emailVerified: boolean;
1263
1404
  phoneVerified: boolean;
1405
+ hasPassword: boolean;
1264
1406
  }>;
1265
1407
 
1266
1408
  /**
@@ -1305,6 +1447,16 @@ interface UpdateProfileParams {
1305
1447
  * ```
1306
1448
  */
1307
1449
  declare function getUserProfileService(userId: string | number | bigint): Promise<UserProfile$1>;
1450
+ /**
1451
+ * Update user locale
1452
+ *
1453
+ * @param userId - User ID
1454
+ * @param locale - Locale code (e.g., 'en', 'ko', 'ja')
1455
+ * @returns Updated locale
1456
+ */
1457
+ declare function updateLocaleService(userId: string | number | bigint, locale: string): Promise<{
1458
+ locale: string;
1459
+ }>;
1308
1460
  /**
1309
1461
  * Update user profile (upsert)
1310
1462
  *
@@ -1327,385 +1479,576 @@ declare function getUserProfileService(userId: string | number | bigint): Promis
1327
1479
  declare function updateUserProfileService(userId: string | number | bigint, params: UpdateProfileParams): Promise<ProfileInfo>;
1328
1480
 
1329
1481
  /**
1330
- * @spfn/auth - Email Template Types
1482
+ * Google OAuth 2.0 Client
1331
1483
  *
1332
- * Type definitions for customizable email templates
1333
- */
1334
- /**
1335
- * Common template result
1484
+ * Authorization Code Flow 구현
1485
+ * - getGoogleAuthUrl: Google 로그인 URL 생성
1486
+ * - exchangeCodeForTokens: Code를 Token으로 교환
1487
+ * - getGoogleUserInfo: 사용자 정보 조회
1336
1488
  */
1337
- interface EmailTemplateResult {
1338
- subject: string;
1339
- text: string;
1340
- html: string;
1489
+ interface GoogleTokenResponse {
1490
+ access_token: string;
1491
+ expires_in: number;
1492
+ refresh_token?: string;
1493
+ scope: string;
1494
+ token_type: string;
1495
+ id_token?: string;
1341
1496
  }
1342
- /**
1343
- * Verification code template parameters
1344
- */
1345
- interface VerificationCodeParams {
1346
- code: string;
1347
- purpose: 'registration' | 'login' | 'password_reset' | string;
1348
- expiresInMinutes?: number;
1349
- appName?: string;
1497
+ interface GoogleUserInfo {
1498
+ id: string;
1499
+ email: string;
1500
+ verified_email: boolean;
1501
+ name?: string;
1502
+ given_name?: string;
1503
+ family_name?: string;
1504
+ picture?: string;
1505
+ locale?: string;
1350
1506
  }
1351
1507
  /**
1352
- * Email template provider interface
1353
- *
1354
- * Implement this interface to create custom email templates
1355
- *
1356
- * @example
1357
- * ```typescript
1358
- * import { registerEmailTemplates } from '@spfn/auth/server';
1359
- *
1360
- * registerEmailTemplates({
1361
- * verificationCode: (params) => ({
1362
- * subject: 'Your Code',
1363
- * text: `Code: ${params.code}`,
1364
- * html: `<h1>Code: ${params.code}</h1>`,
1365
- * }),
1366
- * });
1367
- * ```
1508
+ * Google OAuth가 활성화되어 있는지 확인
1368
1509
  */
1369
- interface EmailTemplateProvider {
1370
- /**
1371
- * Verification code email template
1372
- */
1373
- verificationCode?(params: VerificationCodeParams): EmailTemplateResult;
1374
- /**
1375
- * Welcome email template (after registration)
1376
- */
1377
- welcome?(params: {
1378
- email: string;
1379
- appName?: string;
1380
- }): EmailTemplateResult;
1381
- /**
1382
- * Password reset email template
1383
- */
1384
- passwordReset?(params: {
1385
- resetLink: string;
1386
- expiresInMinutes?: number;
1387
- appName?: string;
1388
- }): EmailTemplateResult;
1389
- /**
1390
- * Invitation email template
1391
- */
1392
- invitation?(params: {
1393
- inviteLink: string;
1394
- inviterName?: string;
1395
- roleName?: string;
1396
- appName?: string;
1397
- }): EmailTemplateResult;
1398
- }
1399
-
1510
+ declare function isGoogleOAuthEnabled(): boolean;
1400
1511
  /**
1401
- * @spfn/auth - Email Template Registry
1402
- *
1403
- * Manages custom email template registration and fallback to defaults
1512
+ * Google OAuth 설정 가져오기
1404
1513
  */
1405
-
1514
+ declare function getGoogleOAuthConfig(): {
1515
+ clientId: string;
1516
+ clientSecret: string;
1517
+ redirectUri: string;
1518
+ };
1406
1519
  /**
1407
- * Register custom email templates
1520
+ * Google 로그인 URL 생성
1408
1521
  *
1409
- * Templates not provided will fall back to defaults
1410
- *
1411
- * @param templates - Custom template implementations
1412
- *
1413
- * @example
1414
- * ```typescript
1415
- * import { registerEmailTemplates } from '@spfn/auth/server';
1416
- *
1417
- * // Override verification code template with custom design
1418
- * registerEmailTemplates({
1419
- * verificationCode: ({ code, purpose, expiresInMinutes }) => ({
1420
- * subject: `[MyApp] Your verification code`,
1421
- * text: `Your code is: ${code}`,
1422
- * html: `
1423
- * <div style="font-family: Arial;">
1424
- * <h1>Welcome to MyApp!</h1>
1425
- * <p>Your code: <strong>${code}</strong></p>
1426
- * </div>
1427
- * `,
1428
- * }),
1429
- * });
1430
- * ```
1522
+ * @param state - CSRF 방지용 state 파라미터 (암호화된 returnUrl + nonce 포함)
1523
+ * @param scopes - 요청할 OAuth scopes (기본: env 또는 email, profile)
1431
1524
  */
1432
- declare function registerEmailTemplates(templates: Partial<EmailTemplateProvider>): void;
1525
+ declare function getGoogleAuthUrl(state: string, scopes?: string[]): string;
1433
1526
  /**
1434
- * Get verification code template
1527
+ * Authorization Code를 Token으로 교환
1435
1528
  *
1436
- * Uses custom template if registered, otherwise falls back to default
1437
- */
1438
- declare function getVerificationCodeTemplate(params: VerificationCodeParams): EmailTemplateResult;
1439
- /**
1440
- * Get welcome template
1529
+ * @param code - Google에서 받은 authorization code
1441
1530
  */
1442
- declare function getWelcomeTemplate(params: {
1443
- email: string;
1444
- appName?: string;
1445
- }): EmailTemplateResult;
1531
+ declare function exchangeCodeForTokens(code: string): Promise<GoogleTokenResponse>;
1446
1532
  /**
1447
- * Get password reset template
1533
+ * Access Token으로 Google 사용자 정보 조회
1534
+ *
1535
+ * @param accessToken - Google access token
1448
1536
  */
1449
- declare function getPasswordResetTemplate(params: {
1450
- resetLink: string;
1451
- expiresInMinutes?: number;
1452
- appName?: string;
1453
- }): EmailTemplateResult;
1537
+ declare function getGoogleUserInfo(accessToken: string): Promise<GoogleUserInfo>;
1454
1538
  /**
1455
- * Get invitation template
1539
+ * Refresh Token으로 새 Access Token 획득
1540
+ *
1541
+ * @param refreshToken - Google refresh token
1456
1542
  */
1457
- declare function getInvitationTemplate(params: {
1458
- inviteLink: string;
1459
- inviterName?: string;
1460
- roleName?: string;
1461
- appName?: string;
1462
- }): EmailTemplateResult;
1543
+ declare function refreshAccessToken(refreshToken: string): Promise<GoogleTokenResponse>;
1463
1544
 
1464
1545
  /**
1465
- * @spfn/auth - Email Service Types
1546
+ * OAuth State Management
1466
1547
  *
1467
- * Type definitions for email sending service
1468
- */
1469
- /**
1470
- * Parameters for sending email
1548
+ * CSRF 방지를 위한 state 파라미터 암호화/복호화
1549
+ * - returnUrl: OAuth 성공 후 리다이렉트할 URL
1550
+ * - nonce: CSRF 방지용 일회용 토큰
1551
+ * - provider: OAuth provider (google, github 등)
1552
+ * - publicKey, keyId, fingerprint, algorithm: 클라이언트 키 정보
1553
+ * - expiresAt: state 만료 시간
1471
1554
  */
1472
- interface SendEmailParams {
1473
- /**
1474
- * Recipient email address
1475
- */
1476
- to: string;
1477
- /**
1478
- * Email subject
1479
- */
1480
- subject: string;
1481
- /**
1482
- * Plain text content
1483
- */
1484
- text?: string;
1485
- /**
1486
- * HTML content
1487
- */
1488
- html?: string;
1489
- /**
1490
- * Purpose of the email (for logging)
1491
- */
1492
- purpose?: string;
1555
+
1556
+ interface OAuthState {
1557
+ returnUrl: string;
1558
+ nonce: string;
1559
+ provider: string;
1560
+ publicKey: string;
1561
+ keyId: string;
1562
+ fingerprint: string;
1563
+ algorithm: KeyAlgorithmType;
1564
+ metadata?: Record<string, unknown>;
1493
1565
  }
1494
1566
  /**
1495
- * Result of sending email
1567
+ * Generate a CSRF nonce for the OAuth flow. The caller passes it to
1568
+ * createOAuthState AND sets it as the oauth_csrf cookie, so the callback can
1569
+ * double-submit-verify the flow was initiated by this same browser.
1496
1570
  */
1497
- interface SendEmailResult {
1498
- /**
1499
- * Whether email was sent successfully
1500
- */
1501
- success: boolean;
1502
- /**
1503
- * Message ID from email provider (if successful)
1504
- */
1505
- messageId?: string;
1571
+ declare function generateOAuthNonce(): string;
1572
+ interface CreateOAuthStateParams {
1573
+ provider: string;
1574
+ returnUrl: string;
1575
+ publicKey: string;
1576
+ keyId: string;
1577
+ fingerprint: string;
1578
+ algorithm: KeyAlgorithmType;
1579
+ metadata?: Record<string, unknown>;
1506
1580
  /**
1507
- * Error message (if failed)
1581
+ * CSRF nonce bound into the state. Pass the same value as the oauth_csrf
1582
+ * cookie. Defaults to a fresh nonce (unbound — legacy/no-CSRF callers).
1508
1583
  */
1509
- error?: string;
1584
+ nonce?: string;
1510
1585
  }
1511
1586
  /**
1512
- * Email Provider Interface
1513
- *
1514
- * Implement this interface to create custom email providers
1515
- *
1516
- * @example
1517
- * ```typescript
1518
- * import { EmailProvider, registerEmailProvider } from '@spfn/auth/server/services/email';
1519
- *
1520
- * const sendgridProvider: EmailProvider = {
1521
- * name: 'sendgrid',
1522
- * sendEmail: async (params) => {
1523
- * // Your SendGrid implementation
1524
- * return { success: true, messageId: '...' };
1525
- * }
1526
- * };
1587
+ * OAuth state 생성 및 암호화
1527
1588
  *
1528
- * registerEmailProvider(sendgridProvider);
1529
- * ```
1589
+ * @param params - state 생성에 필요한 파라미터
1590
+ * @returns 암호화된 state 문자열
1530
1591
  */
1531
- interface EmailProvider {
1532
- /**
1533
- * Provider name (e.g., 'aws-ses', 'sendgrid', 'custom')
1534
- */
1535
- name: string;
1536
- /**
1537
- * Send email via this provider
1538
- *
1539
- * @param params - Email parameters
1540
- * @returns Send result
1541
- */
1542
- sendEmail(params: SendEmailParams): Promise<SendEmailResult>;
1543
- }
1544
-
1592
+ declare function createOAuthState(params: CreateOAuthStateParams): Promise<string>;
1545
1593
  /**
1546
- * @spfn/auth - Email Provider Management
1594
+ * OAuth state 복호화 검증
1547
1595
  *
1548
- * Manages email provider registration and fallback behavior
1596
+ * @param encryptedState - 암호화된 state 문자열
1597
+ * @returns 복호화된 state 객체
1598
+ * @throws Error if state is invalid or expired (JWE exp claim으로 자동 검증)
1549
1599
  */
1600
+ declare function verifyOAuthState(encryptedState: string): Promise<OAuthState>;
1550
1601
 
1551
1602
  /**
1552
- * Register a custom email provider
1553
- *
1554
- * @param provider - Custom email provider implementation
1555
- *
1556
- * @example
1557
- * ```typescript
1558
- * import { registerEmailProvider } from '@spfn/auth/server/services/email';
1559
- *
1560
- * const sendgridProvider = {
1561
- * name: 'sendgrid',
1562
- * sendEmail: async (params) => {
1563
- * // SendGrid implementation
1564
- * return { success: true, messageId: '...' };
1565
- * }
1566
- * };
1567
- *
1568
- * registerEmailProvider(sendgridProvider);
1569
- * ```
1570
- */
1571
- declare function registerEmailProvider(provider: EmailProvider): void;
1572
- /**
1573
- * Send email using the registered provider
1603
+ * 소셜 provider id_token JWKS 검증
1574
1604
  *
1575
- * Falls back to development mode (console only) if no provider is registered
1576
- *
1577
- * @param params - Email parameters
1578
- * @returns Send result
1605
+ * 네이티브/웹 SDK가 받은 id_token을 provider의 공개키(JWKS) 검증한다.
1606
+ * authorization code 교환이 없으므로 client secret을 쓰지 않는다.
1607
+ * 서명·issuer·audience·만료는 jose가, nonce는 여기서 직접 대조한다.
1579
1608
  */
1580
- declare function sendEmail(params: SendEmailParams): Promise<SendEmailResult>;
1581
1609
 
1582
- /**
1583
- * @spfn/auth - SMS Service Types
1584
- *
1585
- * Type definitions for SMS sending service
1586
- */
1587
- /**
1588
- * Parameters for sending SMS
1589
- */
1590
- interface SendSMSParams {
1591
- /**
1592
- * Phone number in E.164 format (e.g., +821012345678)
1593
- */
1594
- phone: string;
1595
- /**
1596
- * SMS message content
1597
- */
1598
- message: string;
1599
- /**
1600
- * Purpose of the SMS (for logging)
1601
- */
1602
- purpose?: string;
1610
+ interface VerifyIdTokenParams {
1611
+ idToken: string;
1612
+ jwksUri: string;
1613
+ issuer: string | string[];
1614
+ audiences: string[];
1615
+ /** 허용 서명 알고리즘 화이트리스트(alg confusion 방어). Google/Apple은 'RS256'. */
1616
+ algorithms: string[];
1617
+ /** id_token의 nonce claim과 정확히 일치해야 하는 값(provider별로 raw 또는 SHA-256 해시). */
1618
+ expectedNonce: string;
1603
1619
  }
1604
1620
  /**
1605
- * Result of sending SMS
1621
+ * id_token을 검증하고 claims(payload)를 반환한다.
1622
+ *
1623
+ * @throws InvalidSocialTokenError 서명/issuer/audience/만료/알고리즘/nonce/sub 검증 실패 시
1606
1624
  */
1607
- interface SendSMSResult {
1608
- /**
1609
- * Whether SMS was sent successfully
1610
- */
1611
- success: boolean;
1612
- /**
1613
- * Message ID from SMS provider (if successful)
1614
- */
1615
- messageId?: string;
1616
- /**
1617
- * Error message (if failed)
1618
- */
1619
- error?: string;
1620
- }
1625
+ declare function verifyIdToken(params: VerifyIdTokenParams): Promise<JWTPayload>;
1626
+
1621
1627
  /**
1622
- * SMS Provider Interface
1628
+ * @spfn/auth - Social Token At-Rest Encryption
1623
1629
  *
1624
- * Implement this interface to create custom SMS providers
1630
+ * OAuth access/refresh tokens are encrypted with a backend-only rotating keyring.
1631
+ * The active key is the first entry in SPFN_AUTH_TOKEN_ENCRYPTION_KEYS; remaining
1632
+ * keys are decrypt-only grace keys. Ciphertext is bound to its provider account
1633
+ * and token type with AES-GCM additional authenticated data (AAD).
1625
1634
  *
1626
- * @example
1627
- * ```typescript
1628
- * import { SMSProvider, registerSMSProvider } from '@spfn/auth/server/services/sms';
1629
- *
1630
- * const twilioProvider: SMSProvider = {
1631
- * name: 'twilio',
1632
- * sendSMS: async (params) => {
1633
- * // Your Twilio implementation
1634
- * return { success: true, messageId: '...' };
1635
- * }
1636
- * };
1635
+ * v2 format: enc:v2:<keyId>:<base64url(iv | authTag | ciphertext)>
1636
+ * v1 format: enc:v1:<base64(iv | authTag | ciphertext)> (legacy session-secret key)
1637
1637
  *
1638
- * registerSMSProvider(twilioProvider);
1639
- * ```
1638
+ * Never include token or key material in logs or error messages.
1640
1639
  */
1641
- interface SMSProvider {
1642
- /**
1643
- * Provider name (e.g., 'aws-sns', 'twilio', 'custom')
1644
- */
1645
- name: string;
1646
- /**
1647
- * Send SMS via this provider
1648
- *
1649
- * @param params - SMS parameters
1650
- * @returns Send result
1651
- */
1652
- sendSMS(params: SendSMSParams): Promise<SendSMSResult>;
1640
+
1641
+ type OAuthTokenType = 'access' | 'refresh';
1642
+ interface OAuthTokenContext {
1643
+ provider: SocialProvider;
1644
+ providerUserId: string;
1645
+ tokenType: OAuthTokenType;
1653
1646
  }
1647
+ interface DecryptedOAuthToken {
1648
+ value: string;
1649
+ /** True for plaintext, v1, or ciphertext encrypted with a non-active v2 key. */
1650
+ needsRotation: boolean;
1651
+ }
1652
+ interface OAuthTokenCipher {
1653
+ encrypt(value: string, context: OAuthTokenContext): Promise<string>;
1654
+ decrypt(value: string, context: OAuthTokenContext): Promise<DecryptedOAuthToken>;
1655
+ }
1656
+ declare class EnvironmentKeyringTokenCipher implements OAuthTokenCipher {
1657
+ encrypt(value: string, context: OAuthTokenContext): Promise<string>;
1658
+ decrypt(value: string, context: OAuthTokenContext): Promise<DecryptedOAuthToken>;
1659
+ }
1660
+ /** Configure an external/KMS-backed cipher before the auth server starts. */
1661
+ declare function configureOAuthTokenCipher(cipher: OAuthTokenCipher): void;
1662
+ declare function encryptToken(value: string, context: OAuthTokenContext): Promise<string>;
1663
+ declare function decryptToken(value: string, context: OAuthTokenContext): Promise<DecryptedOAuthToken>;
1664
+ declare function isEncrypted(value: string): boolean;
1654
1665
 
1655
1666
  /**
1656
- * @spfn/auth - SMS Provider Management
1667
+ * Google OAuthProvider 구현
1668
+ *
1669
+ * 기존 google.ts의 함수를 OAuthProvider 인터페이스로 래핑한다.
1670
+ * google.ts 자체는 그대로 유지(테스트·google 전용 route가 직접 의존).
1657
1671
  *
1658
- * Manages SMS provider registration and fallback behavior
1672
+ * 모듈을 import 하는 것만으로 google provider registry에 자기 등록된다.
1659
1673
  */
1660
1674
 
1675
+ declare const googleProvider: OAuthProvider;
1676
+
1661
1677
  /**
1662
- * Register a custom SMS provider
1663
- *
1664
- * @param provider - Custom SMS provider implementation
1665
- *
1666
- * @example
1667
- * ```typescript
1668
- * import { registerSMSProvider } from '@spfn/auth/server/services/sms';
1669
- *
1670
- * const twilioProvider = {
1671
- * name: 'twilio',
1672
- * sendSMS: async (params) => {
1673
- * // Twilio implementation
1674
- * return { success: true, messageId: '...' };
1675
- * }
1676
- * };
1678
+ * Apple OAuthProvider 구현 (native id_token 검증 전용)
1677
1679
  *
1678
- * registerSMSProvider(twilioProvider);
1679
- * ```
1680
- */
1681
- declare function registerSMSProvider(provider: SMSProvider): void;
1682
- /**
1683
- * Send SMS using the registered provider
1680
+ * Apple은 Android·웹에 네이티브 SDK를 제공하지 않으므로, web 흐름(Sign in with Apple JS,
1681
+ * Android Custom Tab)도 결국 id_token을 클라이언트가 받아 서버로 보낸다. 서버는 그 id_token을
1682
+ * Apple JWKS로 검증만 한다(authorization code 교환·client secret 없음).
1684
1683
  *
1685
- * Falls back to development mode (console only) if no provider is registered
1684
+ * 따라서 web 메서드(getAuthUrl/exchangeCodeForTokens/getUserInfo) 지원하지 않는다.
1686
1685
  *
1687
- * @param params - SMS parameters
1688
- * @returns Send result
1686
+ * 모듈을 import 하는 것만으로 apple provider가 registry에 자기 등록된다.
1689
1687
  */
1690
- declare function sendSMS(params: SendSMSParams): Promise<SendSMSResult>;
1688
+
1689
+ declare const appleProvider: OAuthProvider;
1691
1690
 
1692
1691
  /**
1693
- * @spfn/auth - Database Schema Definition
1694
- *
1695
- * Defines the 'spfn_auth' PostgreSQL schema for all auth-related tables
1692
+ * Kakao Login OAuthProvider (web authorization-code flow).
1696
1693
  */
1694
+
1695
+ declare const kakaoProvider: OAuthProvider;
1696
+
1697
1697
  /**
1698
- * Auth schema for all authentication and authorization tables
1699
- * Tables: users, roles, permissions, user_invitations, etc.
1698
+ * Naver Login OAuthProvider (web authorization-code flow).
1700
1699
  */
1701
- declare const authSchema: drizzle_orm_pg_core.PgSchema<string>;
1700
+
1701
+ declare const naverProvider: OAuthProvider;
1702
1702
 
1703
1703
  /**
1704
- * @spfn/auth - User Profiles Entity
1705
- *
1706
- * User profile information (separate from authentication data)
1704
+ * @spfn/auth - Account Deletion Requests Entity
1707
1705
  *
1708
- * Features:
1706
+ * Audit trail for account deletion/recovery. Rows are never deleted — even after
1707
+ * a hard-delete purge — so "who requested/recovered/purged what, when" survives
1708
+ * the user row itself (개인정보보호법 제21조 3항 분리 보존 원칙).
1709
+ */
1710
+ declare const accountDeletionRequests: drizzle_orm_pg_core.PgTableWithColumns<{
1711
+ name: "account_deletion_requests";
1712
+ schema: string;
1713
+ columns: {
1714
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1715
+ name: "created_at";
1716
+ tableName: "account_deletion_requests";
1717
+ dataType: "date";
1718
+ columnType: "PgTimestamp";
1719
+ data: Date;
1720
+ driverParam: string;
1721
+ notNull: true;
1722
+ hasDefault: true;
1723
+ isPrimaryKey: false;
1724
+ isAutoincrement: false;
1725
+ hasRuntimeDefault: false;
1726
+ enumValues: undefined;
1727
+ baseColumn: never;
1728
+ identity: undefined;
1729
+ generated: undefined;
1730
+ }, {}, {}>;
1731
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1732
+ name: "updated_at";
1733
+ tableName: "account_deletion_requests";
1734
+ dataType: "date";
1735
+ columnType: "PgTimestamp";
1736
+ data: Date;
1737
+ driverParam: string;
1738
+ notNull: true;
1739
+ hasDefault: true;
1740
+ isPrimaryKey: false;
1741
+ isAutoincrement: false;
1742
+ hasRuntimeDefault: false;
1743
+ enumValues: undefined;
1744
+ baseColumn: never;
1745
+ identity: undefined;
1746
+ generated: undefined;
1747
+ }, {}, {}>;
1748
+ id: drizzle_orm_pg_core.PgColumn<{
1749
+ name: "id";
1750
+ tableName: "account_deletion_requests";
1751
+ dataType: "number";
1752
+ columnType: "PgBigSerial53";
1753
+ data: number;
1754
+ driverParam: number;
1755
+ notNull: true;
1756
+ hasDefault: true;
1757
+ isPrimaryKey: true;
1758
+ isAutoincrement: false;
1759
+ hasRuntimeDefault: false;
1760
+ enumValues: undefined;
1761
+ baseColumn: never;
1762
+ identity: undefined;
1763
+ generated: undefined;
1764
+ }, {}, {}>;
1765
+ userId: drizzle_orm_pg_core.PgColumn<{
1766
+ name: `${string}_id`;
1767
+ tableName: "account_deletion_requests";
1768
+ dataType: "number";
1769
+ columnType: "PgBigInt53";
1770
+ data: number;
1771
+ driverParam: string | number;
1772
+ notNull: false;
1773
+ hasDefault: false;
1774
+ isPrimaryKey: false;
1775
+ isAutoincrement: false;
1776
+ hasRuntimeDefault: false;
1777
+ enumValues: undefined;
1778
+ baseColumn: never;
1779
+ identity: undefined;
1780
+ generated: undefined;
1781
+ }, {}, {}>;
1782
+ userPublicId: drizzle_orm_pg_core.PgColumn<{
1783
+ name: "user_public_id";
1784
+ tableName: "account_deletion_requests";
1785
+ dataType: "string";
1786
+ columnType: "PgText";
1787
+ data: string;
1788
+ driverParam: string;
1789
+ notNull: true;
1790
+ hasDefault: false;
1791
+ isPrimaryKey: false;
1792
+ isAutoincrement: false;
1793
+ hasRuntimeDefault: false;
1794
+ enumValues: [string, ...string[]];
1795
+ baseColumn: never;
1796
+ identity: undefined;
1797
+ generated: undefined;
1798
+ }, {}, {}>;
1799
+ requestedAt: drizzle_orm_pg_core.PgColumn<{
1800
+ name: string;
1801
+ tableName: "account_deletion_requests";
1802
+ dataType: "date";
1803
+ columnType: "PgTimestamp";
1804
+ data: Date;
1805
+ driverParam: string;
1806
+ notNull: true;
1807
+ hasDefault: true;
1808
+ isPrimaryKey: false;
1809
+ isAutoincrement: false;
1810
+ hasRuntimeDefault: false;
1811
+ enumValues: undefined;
1812
+ baseColumn: never;
1813
+ identity: undefined;
1814
+ generated: undefined;
1815
+ }, {}, {}>;
1816
+ purgeScheduledAt: drizzle_orm_pg_core.PgColumn<{
1817
+ name: string;
1818
+ tableName: "account_deletion_requests";
1819
+ dataType: "date";
1820
+ columnType: "PgTimestamp";
1821
+ data: Date;
1822
+ driverParam: string;
1823
+ notNull: true;
1824
+ hasDefault: false;
1825
+ isPrimaryKey: false;
1826
+ isAutoincrement: false;
1827
+ hasRuntimeDefault: false;
1828
+ enumValues: undefined;
1829
+ baseColumn: never;
1830
+ identity: undefined;
1831
+ generated: undefined;
1832
+ }, {}, {}>;
1833
+ status: drizzle_orm_pg_core.PgColumn<{
1834
+ name: string;
1835
+ tableName: "account_deletion_requests";
1836
+ dataType: "string";
1837
+ columnType: "PgText";
1838
+ data: "pending" | "cancelled" | "completed";
1839
+ driverParam: string;
1840
+ notNull: true;
1841
+ hasDefault: true;
1842
+ isPrimaryKey: false;
1843
+ isAutoincrement: false;
1844
+ hasRuntimeDefault: false;
1845
+ enumValues: ["pending", "cancelled", "completed"] & [string, ...string[]];
1846
+ baseColumn: never;
1847
+ identity: undefined;
1848
+ generated: undefined;
1849
+ }, {}, {}>;
1850
+ requestedBy: drizzle_orm_pg_core.PgColumn<{
1851
+ name: string;
1852
+ tableName: "account_deletion_requests";
1853
+ dataType: "string";
1854
+ columnType: "PgText";
1855
+ data: "admin" | "self";
1856
+ driverParam: string;
1857
+ notNull: true;
1858
+ hasDefault: true;
1859
+ isPrimaryKey: false;
1860
+ isAutoincrement: false;
1861
+ hasRuntimeDefault: false;
1862
+ enumValues: ["self", "admin"] & [string, ...string[]];
1863
+ baseColumn: never;
1864
+ identity: undefined;
1865
+ generated: undefined;
1866
+ }, {}, {}>;
1867
+ reason: drizzle_orm_pg_core.PgColumn<{
1868
+ name: "reason";
1869
+ tableName: "account_deletion_requests";
1870
+ dataType: "string";
1871
+ columnType: "PgText";
1872
+ data: string;
1873
+ driverParam: string;
1874
+ notNull: false;
1875
+ hasDefault: false;
1876
+ isPrimaryKey: false;
1877
+ isAutoincrement: false;
1878
+ hasRuntimeDefault: false;
1879
+ enumValues: [string, ...string[]];
1880
+ baseColumn: never;
1881
+ identity: undefined;
1882
+ generated: undefined;
1883
+ }, {}, {}>;
1884
+ cancelledAt: drizzle_orm_pg_core.PgColumn<{
1885
+ name: string;
1886
+ tableName: "account_deletion_requests";
1887
+ dataType: "date";
1888
+ columnType: "PgTimestamp";
1889
+ data: Date;
1890
+ driverParam: string;
1891
+ notNull: false;
1892
+ hasDefault: false;
1893
+ isPrimaryKey: false;
1894
+ isAutoincrement: false;
1895
+ hasRuntimeDefault: false;
1896
+ enumValues: undefined;
1897
+ baseColumn: never;
1898
+ identity: undefined;
1899
+ generated: undefined;
1900
+ }, {}, {}>;
1901
+ completedAt: drizzle_orm_pg_core.PgColumn<{
1902
+ name: string;
1903
+ tableName: "account_deletion_requests";
1904
+ dataType: "date";
1905
+ columnType: "PgTimestamp";
1906
+ data: Date;
1907
+ driverParam: string;
1908
+ notNull: false;
1909
+ hasDefault: false;
1910
+ isPrimaryKey: false;
1911
+ isAutoincrement: false;
1912
+ hasRuntimeDefault: false;
1913
+ enumValues: undefined;
1914
+ baseColumn: never;
1915
+ identity: undefined;
1916
+ generated: undefined;
1917
+ }, {}, {}>;
1918
+ purgeStrategy: drizzle_orm_pg_core.PgColumn<{
1919
+ name: string;
1920
+ tableName: "account_deletion_requests";
1921
+ dataType: "string";
1922
+ columnType: "PgText";
1923
+ data: "anonymize" | "hard-delete";
1924
+ driverParam: string;
1925
+ notNull: false;
1926
+ hasDefault: false;
1927
+ isPrimaryKey: false;
1928
+ isAutoincrement: false;
1929
+ hasRuntimeDefault: false;
1930
+ enumValues: ["anonymize", "hard-delete"] & [string, ...string[]];
1931
+ baseColumn: never;
1932
+ identity: undefined;
1933
+ generated: undefined;
1934
+ }, {}, {}>;
1935
+ };
1936
+ dialect: "pg";
1937
+ }>;
1938
+ type AccountDeletionRequest = typeof accountDeletionRequests.$inferSelect;
1939
+ type NewAccountDeletionRequest = typeof accountDeletionRequests.$inferInsert;
1940
+
1941
+ /**
1942
+ * @spfn/auth - Account Deletion Service
1943
+ *
1944
+ * Request / cancel (recovery) / purge for the account deletion lifecycle.
1945
+ *
1946
+ * Lifecycle:
1947
+ * active --request(reauth)--> pending_deletion --grace expires(cron)--> deleted (anonymize) | row removed (hard-delete)
1948
+ * ^ |
1949
+ * +-------cancel(reauth)---------+ immediate = grace period of 0, same pipeline
1950
+ */
1951
+
1952
+ interface PendingDeletionInfo {
1953
+ purgeScheduledAt: Date;
1954
+ }
1955
+ /**
1956
+ * Look up the pending deletion request's `purgeScheduledAt` for a `pending_deletion`
1957
+ * user, so login/OAuth/authenticate can surface it on `AccountPendingDeletionError`.
1958
+ * Returns null if there is no pending row (shouldn't normally happen for a user in
1959
+ * that status, but the caller falls back to an error without the date).
1960
+ *
1961
+ * Reads from the write primary, not a replica — this backs the same status gates
1962
+ * that decide whether to grant a session, so a replica-lag window must not let a
1963
+ * just-requested deletion go unnoticed.
1964
+ */
1965
+ declare function getPendingDeletionInfo(userId: number): Promise<PendingDeletionInfo | null>;
1966
+ interface RequestAccountDeletionParams {
1967
+ /** Who initiated this — 'self' requires re-auth via password/verificationToken; 'admin' does not. */
1968
+ requestedBy: AccountDeletionRequestedBy;
1969
+ password?: string;
1970
+ verificationToken?: string;
1971
+ reason?: string;
1972
+ /** Skip the grace period and purge inline. Self-service requires `deletion.allowSelfImmediate`. */
1973
+ immediate?: boolean;
1974
+ }
1975
+ interface RequestAccountDeletionResult {
1976
+ requestId: number;
1977
+ purgeScheduledAt: Date;
1978
+ }
1979
+ /**
1980
+ * Request account deletion (self-service re-auth gate, or trusted admin/DSR call).
1981
+ *
1982
+ * Sets status -> 'pending_deletion', records the request, revokes every active
1983
+ * session key, and (after commit) emits `auth.deletion.requested` and sends the
1984
+ * request-received notice. With a zero grace period (explicit `immediate: true`,
1985
+ * gated for self-service by `allowSelfImmediate`), purges inline instead of
1986
+ * waiting for the cron sweep.
1987
+ */
1988
+ declare function requestAccountDeletionService(userId: number, params: RequestAccountDeletionParams): Promise<RequestAccountDeletionResult>;
1989
+ interface CancelAccountDeletionParams {
1990
+ email?: string;
1991
+ phone?: string;
1992
+ password?: string;
1993
+ verificationToken?: string;
1994
+ }
1995
+ interface CancelAccountDeletionResult {
1996
+ userId: string;
1997
+ }
1998
+ /**
1999
+ * Cancel a pending deletion (recovery). All sessions were revoked at request
2000
+ * time, so this is credential-based (no Bearer token) rather than auth-context-based.
2001
+ *
2002
+ * Credential verification always runs before the caller learns whether the
2003
+ * account exists / is pending deletion — otherwise this endpoint could be used
2004
+ * to enumerate accounts or probe deletion status without a valid credential,
2005
+ * the same posture `loginService` takes for missing accounts.
2006
+ *
2007
+ * Claims the `account_deletion_requests` row (conditional `WHERE status =
2008
+ * 'pending'`) before touching `users` — see the comment at that claim below for
2009
+ * why the `users` row must not move until the claim succeeds.
2010
+ */
2011
+ declare function cancelAccountDeletionService(params: CancelAccountDeletionParams): Promise<CancelAccountDeletionResult>;
2012
+ interface PurgeUserResult {
2013
+ outcome: 'purged' | 'skipped' | 'not-found';
2014
+ }
2015
+ /**
2016
+ * Purge a user's pending deletion request immediately, ignoring `purgeScheduledAt`.
2017
+ * Admin/GDPR-response entry point — the route (if any) belongs to the app.
2018
+ *
2019
+ * @throws DeletionNotRequestedError if the user has no pending request
2020
+ */
2021
+ declare function purgeUserService(userId: number): Promise<PurgeUserResult>;
2022
+ interface SweepDuePurgesResult {
2023
+ processed: number;
2024
+ purged: number;
2025
+ skipped: number;
2026
+ }
2027
+ /**
2028
+ * Sweep every request whose grace period has elapsed and purge it. Called by the
2029
+ * `auth.deletion.purge` cron job. One user's failure (thrown error, not just an
2030
+ * `onBeforePurge` skip) is logged and does not stop the rest of the sweep — it
2031
+ * stays `pending` and is retried on the next tick.
2032
+ */
2033
+ declare function sweepDuePurges(now?: Date): Promise<SweepDuePurgesResult>;
2034
+
2035
+ /**
2036
+ * @spfn/auth - Database Schema Definition
2037
+ *
2038
+ * Defines the 'spfn_auth' PostgreSQL schema for all auth-related tables
2039
+ */
2040
+ /**
2041
+ * Auth schema for all authentication and authorization tables
2042
+ * Tables: users, roles, permissions, user_invitations, etc.
2043
+ */
2044
+ declare const authSchema: drizzle_orm_pg_core.PgSchema<string>;
2045
+
2046
+ /**
2047
+ * @spfn/auth - User Profiles Entity
2048
+ *
2049
+ * User profile information (separate from authentication data)
2050
+ *
2051
+ * Features:
1709
2052
  * - Display name and personal information
1710
2053
  * - Avatar and bio
1711
2054
  * - Locale and timezone preferences
@@ -1771,11 +2114,11 @@ declare const userProfiles: drizzle_orm_pg_core.PgTableWithColumns<{
1771
2114
  name: `${string}_id`;
1772
2115
  tableName: "user_profiles";
1773
2116
  dataType: "number";
1774
- columnType: "PgBigSerial53";
2117
+ columnType: "PgBigInt53";
1775
2118
  data: number;
1776
- driverParam: number;
2119
+ driverParam: string | number;
1777
2120
  notNull: true;
1778
- hasDefault: true;
2121
+ hasDefault: false;
1779
2122
  isPrimaryKey: false;
1780
2123
  isAutoincrement: false;
1781
2124
  hasRuntimeDefault: false;
@@ -1791,7 +2134,7 @@ declare const userProfiles: drizzle_orm_pg_core.PgTableWithColumns<{
1791
2134
  columnType: "PgText";
1792
2135
  data: string;
1793
2136
  driverParam: string;
1794
- notNull: true;
2137
+ notNull: false;
1795
2138
  hasDefault: false;
1796
2139
  isPrimaryKey: false;
1797
2140
  isAutoincrement: false;
@@ -2065,11 +2408,11 @@ declare const userPublicKeys: drizzle_orm_pg_core.PgTableWithColumns<{
2065
2408
  name: `${string}_id`;
2066
2409
  tableName: "user_public_keys";
2067
2410
  dataType: "number";
2068
- columnType: "PgBigSerial53";
2411
+ columnType: "PgBigInt53";
2069
2412
  data: number;
2070
- driverParam: number;
2413
+ driverParam: string | number;
2071
2414
  notNull: true;
2072
- hasDefault: true;
2415
+ hasDefault: false;
2073
2416
  isPrimaryKey: false;
2074
2417
  isAutoincrement: false;
2075
2418
  hasRuntimeDefault: false;
@@ -2318,11 +2661,11 @@ declare const userSocialAccounts: drizzle_orm_pg_core.PgTableWithColumns<{
2318
2661
  name: `${string}_id`;
2319
2662
  tableName: "user_social_accounts";
2320
2663
  dataType: "number";
2321
- columnType: "PgBigSerial53";
2664
+ columnType: "PgBigInt53";
2322
2665
  data: number;
2323
- driverParam: number;
2666
+ driverParam: string | number;
2324
2667
  notNull: true;
2325
- hasDefault: true;
2668
+ hasDefault: false;
2326
2669
  isPrimaryKey: false;
2327
2670
  isAutoincrement: false;
2328
2671
  hasRuntimeDefault: false;
@@ -2336,14 +2679,14 @@ declare const userSocialAccounts: drizzle_orm_pg_core.PgTableWithColumns<{
2336
2679
  tableName: "user_social_accounts";
2337
2680
  dataType: "string";
2338
2681
  columnType: "PgText";
2339
- data: "google" | "github" | "kakao" | "naver";
2682
+ data: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
2340
2683
  driverParam: string;
2341
2684
  notNull: true;
2342
2685
  hasDefault: false;
2343
2686
  isPrimaryKey: false;
2344
2687
  isAutoincrement: false;
2345
2688
  hasRuntimeDefault: false;
2346
- enumValues: ["google", "github", "kakao", "naver"] & [string, ...string[]];
2689
+ enumValues: ["google", "apple", "github", "kakao", "naver", "superself"] & [string, ...string[]];
2347
2690
  baseColumn: never;
2348
2691
  identity: undefined;
2349
2692
  generated: undefined;
@@ -2556,14 +2899,14 @@ declare const verificationCodes: drizzle_orm_pg_core.PgTableWithColumns<{
2556
2899
  tableName: "verification_codes";
2557
2900
  dataType: "string";
2558
2901
  columnType: "PgText";
2559
- data: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
2902
+ data: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
2560
2903
  driverParam: string;
2561
2904
  notNull: true;
2562
2905
  hasDefault: false;
2563
2906
  isPrimaryKey: false;
2564
2907
  isAutoincrement: false;
2565
2908
  hasRuntimeDefault: false;
2566
- enumValues: ["registration", "login", "password_reset", "email_change", "phone_change"] & [string, ...string[]];
2909
+ enumValues: ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"] & [string, ...string[]];
2567
2910
  baseColumn: never;
2568
2911
  identity: undefined;
2569
2912
  generated: undefined;
@@ -2747,7 +3090,7 @@ declare const permissions: drizzle_orm_pg_core.PgTableWithColumns<{
2747
3090
  tableName: "permissions";
2748
3091
  dataType: "string";
2749
3092
  columnType: "PgText";
2750
- data: "custom" | "user" | "auth" | "rbac" | "system";
3093
+ data: "auth" | "custom" | "user" | "rbac" | "system";
2751
3094
  driverParam: string;
2752
3095
  notNull: false;
2753
3096
  hasDefault: false;
@@ -2905,11 +3248,11 @@ declare const rolePermissions: drizzle_orm_pg_core.PgTableWithColumns<{
2905
3248
  name: `${string}_id`;
2906
3249
  tableName: "role_permissions";
2907
3250
  dataType: "number";
2908
- columnType: "PgBigSerial53";
3251
+ columnType: "PgBigInt53";
2909
3252
  data: number;
2910
- driverParam: number;
3253
+ driverParam: string | number;
2911
3254
  notNull: true;
2912
- hasDefault: true;
3255
+ hasDefault: false;
2913
3256
  isPrimaryKey: false;
2914
3257
  isAutoincrement: false;
2915
3258
  hasRuntimeDefault: false;
@@ -2922,11 +3265,11 @@ declare const rolePermissions: drizzle_orm_pg_core.PgTableWithColumns<{
2922
3265
  name: `${string}_id`;
2923
3266
  tableName: "role_permissions";
2924
3267
  dataType: "number";
2925
- columnType: "PgBigSerial53";
3268
+ columnType: "PgBigInt53";
2926
3269
  data: number;
2927
- driverParam: number;
3270
+ driverParam: string | number;
2928
3271
  notNull: true;
2929
- hasDefault: true;
3272
+ hasDefault: false;
2930
3273
  isPrimaryKey: false;
2931
3274
  isAutoincrement: false;
2932
3275
  hasRuntimeDefault: false;
@@ -3014,11 +3357,11 @@ declare const userPermissions: drizzle_orm_pg_core.PgTableWithColumns<{
3014
3357
  name: `${string}_id`;
3015
3358
  tableName: "user_permissions";
3016
3359
  dataType: "number";
3017
- columnType: "PgBigSerial53";
3360
+ columnType: "PgBigInt53";
3018
3361
  data: number;
3019
- driverParam: number;
3362
+ driverParam: string | number;
3020
3363
  notNull: true;
3021
- hasDefault: true;
3364
+ hasDefault: false;
3022
3365
  isPrimaryKey: false;
3023
3366
  isAutoincrement: false;
3024
3367
  hasRuntimeDefault: false;
@@ -3031,11 +3374,11 @@ declare const userPermissions: drizzle_orm_pg_core.PgTableWithColumns<{
3031
3374
  name: `${string}_id`;
3032
3375
  tableName: "user_permissions";
3033
3376
  dataType: "number";
3034
- columnType: "PgBigSerial53";
3377
+ columnType: "PgBigInt53";
3035
3378
  data: number;
3036
- driverParam: number;
3379
+ driverParam: string | number;
3037
3380
  notNull: true;
3038
- hasDefault: true;
3381
+ hasDefault: false;
3039
3382
  isPrimaryKey: false;
3040
3383
  isAutoincrement: false;
3041
3384
  hasRuntimeDefault: false;
@@ -3101,6 +3444,73 @@ declare const userPermissions: drizzle_orm_pg_core.PgTableWithColumns<{
3101
3444
  type UserPermission = typeof userPermissions.$inferSelect;
3102
3445
  type NewUserPermission = typeof userPermissions.$inferInsert;
3103
3446
 
3447
+ /**
3448
+ * @spfn/auth - Auth Metadata Entity
3449
+ *
3450
+ * Key-value store for auth system metadata
3451
+ * Used for storing RBAC config hash and other system-level settings
3452
+ */
3453
+ declare const authMetadata: drizzle_orm_pg_core.PgTableWithColumns<{
3454
+ name: "auth_metadata";
3455
+ schema: string;
3456
+ columns: {
3457
+ key: drizzle_orm_pg_core.PgColumn<{
3458
+ name: "key";
3459
+ tableName: "auth_metadata";
3460
+ dataType: "string";
3461
+ columnType: "PgText";
3462
+ data: string;
3463
+ driverParam: string;
3464
+ notNull: true;
3465
+ hasDefault: false;
3466
+ isPrimaryKey: true;
3467
+ isAutoincrement: false;
3468
+ hasRuntimeDefault: false;
3469
+ enumValues: [string, ...string[]];
3470
+ baseColumn: never;
3471
+ identity: undefined;
3472
+ generated: undefined;
3473
+ }, {}, {}>;
3474
+ value: drizzle_orm_pg_core.PgColumn<{
3475
+ name: "value";
3476
+ tableName: "auth_metadata";
3477
+ dataType: "string";
3478
+ columnType: "PgText";
3479
+ data: string;
3480
+ driverParam: string;
3481
+ notNull: true;
3482
+ hasDefault: false;
3483
+ isPrimaryKey: false;
3484
+ isAutoincrement: false;
3485
+ hasRuntimeDefault: false;
3486
+ enumValues: [string, ...string[]];
3487
+ baseColumn: never;
3488
+ identity: undefined;
3489
+ generated: undefined;
3490
+ }, {}, {}>;
3491
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
3492
+ name: "updated_at";
3493
+ tableName: "auth_metadata";
3494
+ dataType: "date";
3495
+ columnType: "PgTimestamp";
3496
+ data: Date;
3497
+ driverParam: string;
3498
+ notNull: true;
3499
+ hasDefault: true;
3500
+ isPrimaryKey: false;
3501
+ isAutoincrement: false;
3502
+ hasRuntimeDefault: false;
3503
+ enumValues: undefined;
3504
+ baseColumn: never;
3505
+ identity: undefined;
3506
+ generated: undefined;
3507
+ }, {}, {}>;
3508
+ };
3509
+ dialect: "pg";
3510
+ }>;
3511
+ type AuthMetadataEntity = typeof authMetadata.$inferSelect;
3512
+ type NewAuthMetadataEntity = typeof authMetadata.$inferInsert;
3513
+
3104
3514
  /**
3105
3515
  * Users Repository
3106
3516
  *
@@ -3122,15 +3532,44 @@ declare class UsersRepository extends BaseRepository {
3122
3532
  * Read replica 사용
3123
3533
  */
3124
3534
  findById(id: number): Promise<{
3535
+ deletedAt: Date | null;
3536
+ deletedBy: string | null;
3537
+ createdAt: Date;
3538
+ updatedAt: Date;
3539
+ id: number;
3540
+ publicId: string;
3541
+ email: string | null;
3542
+ phone: string | null;
3543
+ username: string | null;
3544
+ passwordHash: string | null;
3545
+ passwordChangeRequired: boolean;
3546
+ roleId: number;
3547
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3548
+ emailVerifiedAt: Date | null;
3549
+ phoneVerifiedAt: Date | null;
3550
+ lastLoginAt: Date | null;
3551
+ }>;
3552
+ /**
3553
+ * ID로 사용자 조회 — Write primary에서 직접 읽는다.
3554
+ *
3555
+ * 복제 지연 창에서 status 전이(예: 삭제 요청으로 pending_deletion 전환)를 놓치면
3556
+ * 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
3557
+ * 계속 사용할 것.
3558
+ */
3559
+ findByIdOnPrimary(id: number): Promise<{
3560
+ deletedAt: Date | null;
3561
+ deletedBy: string | null;
3125
3562
  createdAt: Date;
3126
3563
  updatedAt: Date;
3127
3564
  id: number;
3565
+ publicId: string;
3128
3566
  email: string | null;
3129
3567
  phone: string | null;
3568
+ username: string | null;
3130
3569
  passwordHash: string | null;
3131
3570
  passwordChangeRequired: boolean;
3132
3571
  roleId: number;
3133
- status: "active" | "inactive" | "suspended";
3572
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3134
3573
  emailVerifiedAt: Date | null;
3135
3574
  phoneVerifiedAt: Date | null;
3136
3575
  lastLoginAt: Date | null;
@@ -3140,15 +3579,19 @@ declare class UsersRepository extends BaseRepository {
3140
3579
  * Read replica 사용
3141
3580
  */
3142
3581
  findByEmail(email: string): Promise<{
3582
+ deletedAt: Date | null;
3583
+ deletedBy: string | null;
3143
3584
  createdAt: Date;
3144
3585
  updatedAt: Date;
3145
3586
  id: number;
3587
+ publicId: string;
3146
3588
  email: string | null;
3147
3589
  phone: string | null;
3590
+ username: string | null;
3148
3591
  passwordHash: string | null;
3149
3592
  passwordChangeRequired: boolean;
3150
3593
  roleId: number;
3151
- status: "active" | "inactive" | "suspended";
3594
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3152
3595
  emailVerifiedAt: Date | null;
3153
3596
  phoneVerifiedAt: Date | null;
3154
3597
  lastLoginAt: Date | null;
@@ -3158,15 +3601,63 @@ declare class UsersRepository extends BaseRepository {
3158
3601
  * Read replica 사용
3159
3602
  */
3160
3603
  findByPhone(phone: string): Promise<{
3604
+ deletedAt: Date | null;
3605
+ deletedBy: string | null;
3161
3606
  createdAt: Date;
3162
3607
  updatedAt: Date;
3163
3608
  id: number;
3609
+ publicId: string;
3164
3610
  email: string | null;
3165
3611
  phone: string | null;
3612
+ username: string | null;
3166
3613
  passwordHash: string | null;
3167
3614
  passwordChangeRequired: boolean;
3168
3615
  roleId: number;
3169
- status: "active" | "inactive" | "suspended";
3616
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3617
+ emailVerifiedAt: Date | null;
3618
+ phoneVerifiedAt: Date | null;
3619
+ lastLoginAt: Date | null;
3620
+ }>;
3621
+ /**
3622
+ * 사용자명으로 사용자 조회
3623
+ * Read replica 사용
3624
+ */
3625
+ findByUsername(username: string): Promise<{
3626
+ deletedAt: Date | null;
3627
+ deletedBy: string | null;
3628
+ createdAt: Date;
3629
+ updatedAt: Date;
3630
+ id: number;
3631
+ publicId: string;
3632
+ email: string | null;
3633
+ phone: string | null;
3634
+ username: string | null;
3635
+ passwordHash: string | null;
3636
+ passwordChangeRequired: boolean;
3637
+ roleId: number;
3638
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3639
+ emailVerifiedAt: Date | null;
3640
+ phoneVerifiedAt: Date | null;
3641
+ lastLoginAt: Date | null;
3642
+ }>;
3643
+ /**
3644
+ * Public ID(UUID)로 사용자 조회
3645
+ * Read replica 사용
3646
+ */
3647
+ findByPublicId(publicId: string): Promise<{
3648
+ deletedAt: Date | null;
3649
+ deletedBy: string | null;
3650
+ createdAt: Date;
3651
+ updatedAt: Date;
3652
+ id: number;
3653
+ publicId: string;
3654
+ email: string | null;
3655
+ phone: string | null;
3656
+ username: string | null;
3657
+ passwordHash: string | null;
3658
+ passwordChangeRequired: boolean;
3659
+ roleId: number;
3660
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3170
3661
  emailVerifiedAt: Date | null;
3171
3662
  phoneVerifiedAt: Date | null;
3172
3663
  lastLoginAt: Date | null;
@@ -3176,19 +3667,54 @@ declare class UsersRepository extends BaseRepository {
3176
3667
  * Read replica 사용
3177
3668
  */
3178
3669
  findByEmailOrPhone(email?: string, phone?: string): Promise<{
3670
+ deletedAt: Date | null;
3671
+ deletedBy: string | null;
3179
3672
  createdAt: Date;
3180
3673
  updatedAt: Date;
3181
3674
  id: number;
3675
+ publicId: string;
3182
3676
  email: string | null;
3183
3677
  phone: string | null;
3678
+ username: string | null;
3184
3679
  passwordHash: string | null;
3185
3680
  passwordChangeRequired: boolean;
3186
3681
  roleId: number;
3187
- status: "active" | "inactive" | "suspended";
3682
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3188
3683
  emailVerifiedAt: Date | null;
3189
3684
  phoneVerifiedAt: Date | null;
3190
3685
  lastLoginAt: Date | null;
3191
3686
  } | null>;
3687
+ /**
3688
+ * ID로 사용자 + Role 조회 (leftJoin)
3689
+ * Read replica 사용
3690
+ *
3691
+ * roleId가 null인 유저는 role: null 반환
3692
+ */
3693
+ findByIdWithRole(id: number): Promise<{
3694
+ user: {
3695
+ deletedAt: Date | null;
3696
+ deletedBy: string | null;
3697
+ createdAt: Date;
3698
+ updatedAt: Date;
3699
+ id: number;
3700
+ publicId: string;
3701
+ email: string | null;
3702
+ phone: string | null;
3703
+ username: string | null;
3704
+ passwordHash: string | null;
3705
+ passwordChangeRequired: boolean;
3706
+ roleId: number;
3707
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3708
+ emailVerifiedAt: Date | null;
3709
+ phoneVerifiedAt: Date | null;
3710
+ lastLoginAt: Date | null;
3711
+ };
3712
+ role: {
3713
+ name: string;
3714
+ displayName: string;
3715
+ priority: number;
3716
+ } | null;
3717
+ } | null>;
3192
3718
  /**
3193
3719
  * 사용자 생성
3194
3720
  * Write primary 사용
@@ -3196,31 +3722,66 @@ declare class UsersRepository extends BaseRepository {
3196
3722
  create(data: NewUser): Promise<{
3197
3723
  email: string | null;
3198
3724
  phone: string | null;
3199
- status: "active" | "inactive" | "suspended";
3200
3725
  id: number;
3201
- createdAt: Date;
3202
- updatedAt: Date;
3726
+ publicId: string;
3727
+ username: string | null;
3203
3728
  passwordHash: string | null;
3204
3729
  passwordChangeRequired: boolean;
3205
3730
  roleId: number;
3731
+ createdAt: Date;
3732
+ updatedAt: Date;
3733
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3206
3734
  emailVerifiedAt: Date | null;
3207
3735
  phoneVerifiedAt: Date | null;
3208
3736
  lastLoginAt: Date | null;
3737
+ deletedAt: Date | null;
3738
+ deletedBy: string | null;
3209
3739
  }>;
3210
3740
  /**
3211
3741
  * 사용자 정보 업데이트
3212
3742
  * Write primary 사용
3213
3743
  */
3214
3744
  updateById(id: number, data: Partial<NewUser>): Promise<{
3745
+ deletedAt: Date | null;
3746
+ deletedBy: string | null;
3215
3747
  createdAt: Date;
3216
3748
  updatedAt: Date;
3217
3749
  id: number;
3750
+ publicId: string;
3218
3751
  email: string | null;
3219
3752
  phone: string | null;
3753
+ username: string | null;
3220
3754
  passwordHash: string | null;
3221
3755
  passwordChangeRequired: boolean;
3222
3756
  roleId: number;
3223
- status: "active" | "inactive" | "suspended";
3757
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3758
+ emailVerifiedAt: Date | null;
3759
+ phoneVerifiedAt: Date | null;
3760
+ lastLoginAt: Date | null;
3761
+ }>;
3762
+ /**
3763
+ * 계정 삭제 취소(복구) — `WHERE status = 'pending_deletion'` 조건부 UPDATE.
3764
+ *
3765
+ * cancelAccountDeletionService가 account_deletion_requests claim(조건부
3766
+ * markCancelled)에 성공한 **이후에만** 호출한다. 그 claim이 이미 purge와의
3767
+ * 경합을 해결하므로 이 조건은 방어적 이중 안전장치 — 0 row 매치(= 이미
3768
+ * status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
3769
+ * Write primary 사용
3770
+ */
3771
+ reactivateFromPendingDeletion(id: number): Promise<{
3772
+ deletedAt: Date | null;
3773
+ deletedBy: string | null;
3774
+ createdAt: Date;
3775
+ updatedAt: Date;
3776
+ id: number;
3777
+ publicId: string;
3778
+ email: string | null;
3779
+ phone: string | null;
3780
+ username: string | null;
3781
+ passwordHash: string | null;
3782
+ passwordChangeRequired: boolean;
3783
+ roleId: number;
3784
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3224
3785
  emailVerifiedAt: Date | null;
3225
3786
  phoneVerifiedAt: Date | null;
3226
3787
  lastLoginAt: Date | null;
@@ -3230,15 +3791,19 @@ declare class UsersRepository extends BaseRepository {
3230
3791
  * Write primary 사용
3231
3792
  */
3232
3793
  updatePassword(id: number, passwordHash: string, clearPasswordChangeRequired?: boolean): Promise<{
3794
+ deletedAt: Date | null;
3795
+ deletedBy: string | null;
3233
3796
  createdAt: Date;
3234
3797
  updatedAt: Date;
3235
3798
  id: number;
3799
+ publicId: string;
3236
3800
  email: string | null;
3237
3801
  phone: string | null;
3802
+ username: string | null;
3238
3803
  passwordHash: string | null;
3239
3804
  passwordChangeRequired: boolean;
3240
3805
  roleId: number;
3241
- status: "active" | "inactive" | "suspended";
3806
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3242
3807
  emailVerifiedAt: Date | null;
3243
3808
  phoneVerifiedAt: Date | null;
3244
3809
  lastLoginAt: Date | null;
@@ -3248,15 +3813,19 @@ declare class UsersRepository extends BaseRepository {
3248
3813
  * Write primary 사용
3249
3814
  */
3250
3815
  updateLastLogin(id: number): Promise<{
3816
+ deletedAt: Date | null;
3817
+ deletedBy: string | null;
3251
3818
  createdAt: Date;
3252
3819
  updatedAt: Date;
3253
3820
  id: number;
3821
+ publicId: string;
3254
3822
  email: string | null;
3255
3823
  phone: string | null;
3824
+ username: string | null;
3256
3825
  passwordHash: string | null;
3257
3826
  passwordChangeRequired: boolean;
3258
3827
  roleId: number;
3259
- status: "active" | "inactive" | "suspended";
3828
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3260
3829
  emailVerifiedAt: Date | null;
3261
3830
  phoneVerifiedAt: Date | null;
3262
3831
  lastLoginAt: Date | null;
@@ -3268,16 +3837,20 @@ declare class UsersRepository extends BaseRepository {
3268
3837
  deleteById(id: number): Promise<{
3269
3838
  email: string | null;
3270
3839
  phone: string | null;
3271
- status: "active" | "inactive" | "suspended";
3272
3840
  id: number;
3273
- createdAt: Date;
3274
- updatedAt: Date;
3841
+ publicId: string;
3842
+ username: string | null;
3275
3843
  passwordHash: string | null;
3276
3844
  passwordChangeRequired: boolean;
3277
3845
  roleId: number;
3846
+ createdAt: Date;
3847
+ updatedAt: Date;
3848
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3278
3849
  emailVerifiedAt: Date | null;
3279
3850
  phoneVerifiedAt: Date | null;
3280
3851
  lastLoginAt: Date | null;
3852
+ deletedAt: Date | null;
3853
+ deletedBy: string | null;
3281
3854
  }>;
3282
3855
  /**
3283
3856
  * User의 Role과 Permissions 조회 (JOIN)
@@ -3297,7 +3870,7 @@ declare class UsersRepository extends BaseRepository {
3297
3870
  id: number;
3298
3871
  name: string;
3299
3872
  displayName: string;
3300
- category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
3873
+ category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
3301
3874
  }[];
3302
3875
  }>;
3303
3876
  /**
@@ -3309,9 +3882,12 @@ declare class UsersRepository extends BaseRepository {
3309
3882
  */
3310
3883
  fetchMinimalUserData(userId: number): Promise<{
3311
3884
  userId: number;
3885
+ publicId: string;
3312
3886
  email: string | null;
3887
+ username: string | null;
3313
3888
  isEmailVerified: boolean;
3314
3889
  isPhoneVerified: boolean;
3890
+ hasPassword: boolean;
3315
3891
  }>;
3316
3892
  /**
3317
3893
  * Full user data 조회 (user profile용)
@@ -3322,7 +3898,9 @@ declare class UsersRepository extends BaseRepository {
3322
3898
  */
3323
3899
  fetchFullUserData(userId: number): Promise<{
3324
3900
  userId: number;
3901
+ publicId: string;
3325
3902
  email: string | null;
3903
+ username: string | null;
3326
3904
  isEmailVerified: boolean;
3327
3905
  isPhoneVerified: boolean;
3328
3906
  lastLoginAt: Date | null;
@@ -3407,24 +3985,45 @@ declare class KeysRepository extends BaseRepository {
3407
3985
  * Write primary 사용
3408
3986
  */
3409
3987
  create(data: NewUserPublicKey): Promise<{
3410
- userId: number;
3988
+ publicKey: string;
3411
3989
  keyId: string;
3990
+ fingerprint: string;
3991
+ algorithm: "ES256" | "RS256";
3992
+ userId: number;
3412
3993
  id: number;
3413
3994
  isActive: boolean;
3414
3995
  createdAt: Date;
3996
+ expiresAt: Date | null;
3997
+ lastUsedAt: Date | null;
3998
+ revokedAt: Date | null;
3999
+ revokedReason: string | null;
4000
+ }>;
4001
+ /**
4002
+ * 공개키 revoke (비활성화)
4003
+ * Write primary 사용
4004
+ */
4005
+ revokeByKeyIdAndUserId(keyId: string, userId: number, reason: string): Promise<{
4006
+ id: number;
4007
+ userId: number;
4008
+ keyId: string;
3415
4009
  publicKey: string;
3416
4010
  algorithm: "ES256" | "RS256";
3417
4011
  fingerprint: string;
4012
+ isActive: boolean;
4013
+ createdAt: Date;
3418
4014
  lastUsedAt: Date | null;
3419
4015
  expiresAt: Date | null;
3420
4016
  revokedAt: Date | null;
3421
4017
  revokedReason: string | null;
3422
4018
  }>;
3423
4019
  /**
3424
- * 공개키 revoke (비활성화)
4020
+ * 사용자의 모든 활성 공개키 revoke (비활성화)
4021
+ *
4022
+ * 비번 변경 시 전체 세션 로그아웃에 사용. authenticate는 활성 키만 검증하므로,
4023
+ * revoke된 키로 서명한 기존 세션의 요청은 즉시 401이 된다.
3425
4024
  * Write primary 사용
3426
4025
  */
3427
- revokeByKeyIdAndUserId(keyId: string, userId: number, reason: string): Promise<{
4026
+ revokeAllActiveByUserId(userId: number, reason: string): Promise<{
3428
4027
  id: number;
3429
4028
  userId: number;
3430
4029
  keyId: string;
@@ -3437,25 +4036,33 @@ declare class KeysRepository extends BaseRepository {
3437
4036
  expiresAt: Date | null;
3438
4037
  revokedAt: Date | null;
3439
4038
  revokedReason: string | null;
3440
- }>;
4039
+ }[]>;
3441
4040
  /**
3442
4041
  * 공개키 삭제
3443
4042
  * Write primary 사용
3444
4043
  */
3445
4044
  deleteByKeyIdAndUserId(keyId: string, userId: number): Promise<{
3446
- userId: number;
4045
+ publicKey: string;
3447
4046
  keyId: string;
4047
+ fingerprint: string;
4048
+ algorithm: "ES256" | "RS256";
4049
+ userId: number;
3448
4050
  id: number;
3449
4051
  isActive: boolean;
3450
4052
  createdAt: Date;
3451
- publicKey: string;
3452
- algorithm: "ES256" | "RS256";
3453
- fingerprint: string;
3454
- lastUsedAt: Date | null;
3455
4053
  expiresAt: Date | null;
4054
+ lastUsedAt: Date | null;
3456
4055
  revokedAt: Date | null;
3457
4056
  revokedReason: string | null;
3458
4057
  }>;
4058
+ /**
4059
+ * 사용자의 모든 공개키 삭제 (계정 익명화 파기용)
4060
+ *
4061
+ * hard-delete는 FK cascade로 자동 처리되지만, anonymize 모드는 users row를
4062
+ * 남기므로 자식 row를 직접 지워야 한다.
4063
+ * Write primary 사용
4064
+ */
4065
+ deleteAllByUserId(userId: number): Promise<number>;
3459
4066
  /**
3460
4067
  * 마지막 사용 시간 업데이트
3461
4068
  * Write primary 사용
@@ -3494,22 +4101,14 @@ declare class KeysRepository extends BaseRepository {
3494
4101
  }>;
3495
4102
  /**
3496
4103
  * Primary key로 마지막 사용 시간 업데이트 (authenticate용)
3497
- * Write primary 사용
4104
+ * Write primary 사용.
4105
+ *
4106
+ * Throttled: only writes when lastUsedAt is stale (older than
4107
+ * LAST_USED_THROTTLE_MS), so a busy key isn't UPDATEd on every request. The
4108
+ * throttle lives in the WHERE clause — atomic, no read-then-write race. No
4109
+ * RETURNING (callers fire-and-forget and discard the row).
3498
4110
  */
3499
- updateLastUsedById(id: number): Promise<{
3500
- id: number;
3501
- userId: number;
3502
- keyId: string;
3503
- publicKey: string;
3504
- algorithm: "ES256" | "RS256";
3505
- fingerprint: string;
3506
- isActive: boolean;
3507
- createdAt: Date;
3508
- lastUsedAt: Date | null;
3509
- expiresAt: Date | null;
3510
- revokedAt: Date | null;
3511
- revokedReason: string | null;
3512
- }>;
4111
+ updateLastUsedById(id: number): Promise<void>;
3513
4112
  }
3514
4113
  declare const keysRepository: KeysRepository;
3515
4114
 
@@ -3541,7 +4140,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3541
4140
  target: string;
3542
4141
  targetType: "email" | "phone";
3543
4142
  code: string;
3544
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4143
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3545
4144
  expiresAt: Date;
3546
4145
  usedAt: Date | null;
3547
4146
  attempts: number;
@@ -3557,7 +4156,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3557
4156
  target: string;
3558
4157
  targetType: "email" | "phone";
3559
4158
  code: string;
3560
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4159
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3561
4160
  expiresAt: Date;
3562
4161
  usedAt: Date | null;
3563
4162
  attempts: number;
@@ -3567,14 +4166,14 @@ declare class VerificationCodesRepository extends BaseRepository {
3567
4166
  * Write primary 사용
3568
4167
  */
3569
4168
  create(data: NewVerificationCode): Promise<{
4169
+ target: string;
4170
+ targetType: "email" | "phone";
4171
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
4172
+ code: string;
3570
4173
  id: number;
3571
4174
  createdAt: Date;
3572
4175
  updatedAt: Date;
3573
4176
  expiresAt: Date;
3574
- target: string;
3575
- targetType: "email" | "phone";
3576
- code: string;
3577
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
3578
4177
  usedAt: Date | null;
3579
4178
  attempts: number;
3580
4179
  }>;
@@ -3589,7 +4188,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3589
4188
  target: string;
3590
4189
  targetType: "email" | "phone";
3591
4190
  code: string;
3592
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4191
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3593
4192
  expiresAt: Date;
3594
4193
  usedAt: Date | null;
3595
4194
  attempts: number;
@@ -3605,7 +4204,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3605
4204
  target: string;
3606
4205
  targetType: "email" | "phone";
3607
4206
  code: string;
3608
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4207
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3609
4208
  expiresAt: Date;
3610
4209
  usedAt: Date | null;
3611
4210
  attempts: number;
@@ -3615,6 +4214,14 @@ declare class VerificationCodesRepository extends BaseRepository {
3615
4214
  * Write primary 사용
3616
4215
  */
3617
4216
  deleteExpired(): Promise<number>;
4217
+ /**
4218
+ * Target(email/phone)의 모든 인증 코드 삭제 (계정 파기용)
4219
+ *
4220
+ * verificationCodes는 userId FK가 없고 target 텍스트로만 연결되므로, 파기 시
4221
+ * 원래 email/phone을 이 메서드로 직접 정리해야 한다.
4222
+ * Write primary 사용
4223
+ */
4224
+ deleteByTarget(target: string): Promise<number>;
3618
4225
  /**
3619
4226
  * Target의 모든 이전 코드 무효화 (새 코드 발급 시)
3620
4227
  * Write primary 사용
@@ -3763,7 +4370,7 @@ declare class PermissionsRepository extends BaseRepository {
3763
4370
  name: string;
3764
4371
  displayName: string;
3765
4372
  description: string | null;
3766
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4373
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3767
4374
  isBuiltin: boolean;
3768
4375
  isSystem: boolean;
3769
4376
  isActive: boolean;
@@ -3779,7 +4386,7 @@ declare class PermissionsRepository extends BaseRepository {
3779
4386
  name: string;
3780
4387
  displayName: string;
3781
4388
  description: string | null;
3782
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4389
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3783
4390
  isBuiltin: boolean;
3784
4391
  isSystem: boolean;
3785
4392
  isActive: boolean;
@@ -3819,7 +4426,7 @@ declare class PermissionsRepository extends BaseRepository {
3819
4426
  name: string;
3820
4427
  displayName: string;
3821
4428
  description: string | null;
3822
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4429
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3823
4430
  isBuiltin: boolean;
3824
4431
  isSystem: boolean;
3825
4432
  isActive: boolean;
@@ -3830,6 +4437,7 @@ declare class PermissionsRepository extends BaseRepository {
3830
4437
  */
3831
4438
  deleteById(id: number): Promise<{
3832
4439
  description: string | null;
4440
+ metadata: Record<string, any> | null;
3833
4441
  id: number;
3834
4442
  name: string;
3835
4443
  displayName: string;
@@ -3838,8 +4446,7 @@ declare class PermissionsRepository extends BaseRepository {
3838
4446
  isActive: boolean;
3839
4447
  createdAt: Date;
3840
4448
  updatedAt: Date;
3841
- metadata: Record<string, any> | null;
3842
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4449
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3843
4450
  }>;
3844
4451
  }
3845
4452
  declare const permissionsRepository: PermissionsRepository;
@@ -3884,9 +4491,9 @@ declare class RolePermissionsRepository extends BaseRepository {
3884
4491
  */
3885
4492
  createMany(data: NewRolePermission[]): Promise<{
3886
4493
  id: number;
4494
+ roleId: number;
3887
4495
  createdAt: Date;
3888
4496
  updatedAt: Date;
3889
- roleId: number;
3890
4497
  permissionId: number;
3891
4498
  }[]>;
3892
4499
  /**
@@ -3902,9 +4509,9 @@ declare class RolePermissionsRepository extends BaseRepository {
3902
4509
  */
3903
4510
  setPermissionsForRole(roleId: number, permissionIds: number[]): Promise<{
3904
4511
  id: number;
4512
+ roleId: number;
3905
4513
  createdAt: Date;
3906
4514
  updatedAt: Date;
3907
- roleId: number;
3908
4515
  permissionId: number;
3909
4516
  }[]>;
3910
4517
  }
@@ -3969,10 +4576,10 @@ declare class UserPermissionsRepository extends BaseRepository {
3969
4576
  id: number;
3970
4577
  createdAt: Date;
3971
4578
  updatedAt: Date;
3972
- expiresAt: Date | null;
3973
4579
  permissionId: number;
3974
- granted: boolean;
3975
4580
  reason: string | null;
4581
+ expiresAt: Date | null;
4582
+ granted: boolean;
3976
4583
  }>;
3977
4584
  /**
3978
4585
  * 사용자 권한 오버라이드 업데이트
@@ -3995,10 +4602,10 @@ declare class UserPermissionsRepository extends BaseRepository {
3995
4602
  id: number;
3996
4603
  createdAt: Date;
3997
4604
  updatedAt: Date;
3998
- expiresAt: Date | null;
3999
4605
  permissionId: number;
4000
- granted: boolean;
4001
4606
  reason: string | null;
4607
+ expiresAt: Date | null;
4608
+ granted: boolean;
4002
4609
  }>;
4003
4610
  /**
4004
4611
  * 사용자의 모든 권한 오버라이드 삭제
@@ -4030,7 +4637,7 @@ declare class UserProfilesRepository extends BaseRepository {
4030
4637
  updatedAt: Date;
4031
4638
  id: number;
4032
4639
  userId: number;
4033
- displayName: string;
4640
+ displayName: string | null;
4034
4641
  firstName: string | null;
4035
4642
  lastName: string | null;
4036
4643
  avatarUrl: string | null;
@@ -4045,6 +4652,10 @@ declare class UserProfilesRepository extends BaseRepository {
4045
4652
  jobTitle: string | null;
4046
4653
  metadata: Record<string, any> | null;
4047
4654
  }>;
4655
+ /**
4656
+ * User ID로 locale만 조회 (경량)
4657
+ */
4658
+ findLocaleByUserId(userId: number): Promise<string>;
4048
4659
  /**
4049
4660
  * User ID로 프로필 조회
4050
4661
  */
@@ -4053,7 +4664,7 @@ declare class UserProfilesRepository extends BaseRepository {
4053
4664
  updatedAt: Date;
4054
4665
  id: number;
4055
4666
  userId: number;
4056
- displayName: string;
4667
+ displayName: string | null;
4057
4668
  firstName: string | null;
4058
4669
  lastName: string | null;
4059
4670
  avatarUrl: string | null;
@@ -4072,9 +4683,10 @@ declare class UserProfilesRepository extends BaseRepository {
4072
4683
  * 프로필 생성
4073
4684
  */
4074
4685
  create(data: NewUserProfile): Promise<{
4686
+ metadata: Record<string, any> | null;
4075
4687
  userId: number;
4076
4688
  id: number;
4077
- displayName: string;
4689
+ displayName: string | null;
4078
4690
  createdAt: Date;
4079
4691
  updatedAt: Date;
4080
4692
  firstName: string | null;
@@ -4089,7 +4701,6 @@ declare class UserProfilesRepository extends BaseRepository {
4089
4701
  location: string | null;
4090
4702
  company: string | null;
4091
4703
  jobTitle: string | null;
4092
- metadata: Record<string, any> | null;
4093
4704
  }>;
4094
4705
  /**
4095
4706
  * 프로필 업데이트 (by ID)
@@ -4099,7 +4710,7 @@ declare class UserProfilesRepository extends BaseRepository {
4099
4710
  updatedAt: Date;
4100
4711
  id: number;
4101
4712
  userId: number;
4102
- displayName: string;
4713
+ displayName: string | null;
4103
4714
  firstName: string | null;
4104
4715
  lastName: string | null;
4105
4716
  avatarUrl: string | null;
@@ -4122,7 +4733,7 @@ declare class UserProfilesRepository extends BaseRepository {
4122
4733
  updatedAt: Date;
4123
4734
  id: number;
4124
4735
  userId: number;
4125
- displayName: string;
4736
+ displayName: string | null;
4126
4737
  firstName: string | null;
4127
4738
  lastName: string | null;
4128
4739
  avatarUrl: string | null;
@@ -4141,9 +4752,10 @@ declare class UserProfilesRepository extends BaseRepository {
4141
4752
  * 프로필 삭제 (by ID)
4142
4753
  */
4143
4754
  deleteById(id: number): Promise<{
4755
+ metadata: Record<string, any> | null;
4144
4756
  userId: number;
4145
4757
  id: number;
4146
- displayName: string;
4758
+ displayName: string | null;
4147
4759
  createdAt: Date;
4148
4760
  updatedAt: Date;
4149
4761
  firstName: string | null;
@@ -4158,15 +4770,15 @@ declare class UserProfilesRepository extends BaseRepository {
4158
4770
  location: string | null;
4159
4771
  company: string | null;
4160
4772
  jobTitle: string | null;
4161
- metadata: Record<string, any> | null;
4162
4773
  }>;
4163
4774
  /**
4164
4775
  * 프로필 삭제 (by User ID)
4165
4776
  */
4166
4777
  deleteByUserId(userId: number): Promise<{
4778
+ metadata: Record<string, any> | null;
4167
4779
  userId: number;
4168
4780
  id: number;
4169
- displayName: string;
4781
+ displayName: string | null;
4170
4782
  createdAt: Date;
4171
4783
  updatedAt: Date;
4172
4784
  firstName: string | null;
@@ -4181,7 +4793,6 @@ declare class UserProfilesRepository extends BaseRepository {
4181
4793
  location: string | null;
4182
4794
  company: string | null;
4183
4795
  jobTitle: string | null;
4184
- metadata: Record<string, any> | null;
4185
4796
  }>;
4186
4797
  /**
4187
4798
  * 프로필 Upsert (by User ID)
@@ -4190,9 +4801,10 @@ declare class UserProfilesRepository extends BaseRepository {
4190
4801
  * 새로 생성 시 displayName은 필수 (없으면 'User'로 설정)
4191
4802
  */
4192
4803
  upsertByUserId(userId: number, data: Partial<Omit<NewUserProfile, 'userId'>>): Promise<{
4804
+ metadata: Record<string, any> | null;
4193
4805
  userId: number;
4194
4806
  id: number;
4195
- displayName: string;
4807
+ displayName: string | null;
4196
4808
  createdAt: Date;
4197
4809
  updatedAt: Date;
4198
4810
  firstName: string | null;
@@ -4207,7 +4819,6 @@ declare class UserProfilesRepository extends BaseRepository {
4207
4819
  location: string | null;
4208
4820
  company: string | null;
4209
4821
  jobTitle: string | null;
4210
- metadata: Record<string, any> | null;
4211
4822
  }>;
4212
4823
  /**
4213
4824
  * User ID로 프로필 데이터 조회 (formatted)
@@ -4216,7 +4827,7 @@ declare class UserProfilesRepository extends BaseRepository {
4216
4827
  */
4217
4828
  fetchProfileData(userId: number): Promise<{
4218
4829
  profileId: number;
4219
- displayName: string;
4830
+ displayName: string | null;
4220
4831
  firstName: string | null;
4221
4832
  lastName: string | null;
4222
4833
  avatarUrl: string | null;
@@ -4335,15 +4946,15 @@ declare class InvitationsRepository extends BaseRepository {
4335
4946
  */
4336
4947
  create(data: NewInvitation): Promise<{
4337
4948
  email: string;
4338
- status: "pending" | "accepted" | "expired" | "cancelled";
4949
+ metadata: Record<string, any> | null;
4339
4950
  id: number;
4951
+ roleId: number;
4340
4952
  createdAt: Date;
4341
4953
  updatedAt: Date;
4342
- roleId: number;
4343
- metadata: Record<string, any> | null;
4344
- expiresAt: Date;
4954
+ status: "pending" | "accepted" | "expired" | "cancelled";
4345
4955
  token: string;
4346
4956
  invitedBy: number;
4957
+ expiresAt: Date;
4347
4958
  acceptedAt: Date | null;
4348
4959
  cancelledAt: Date | null;
4349
4960
  }>;
@@ -4369,15 +4980,15 @@ declare class InvitationsRepository extends BaseRepository {
4369
4980
  */
4370
4981
  deleteById(id: number): Promise<{
4371
4982
  email: string;
4372
- status: "pending" | "accepted" | "expired" | "cancelled";
4983
+ metadata: Record<string, any> | null;
4373
4984
  id: number;
4985
+ roleId: number;
4374
4986
  createdAt: Date;
4375
4987
  updatedAt: Date;
4376
- roleId: number;
4377
- metadata: Record<string, any> | null;
4378
- expiresAt: Date;
4988
+ status: "pending" | "accepted" | "expired" | "cancelled";
4379
4989
  token: string;
4380
4990
  invitedBy: number;
4991
+ expiresAt: Date;
4381
4992
  acceptedAt: Date | null;
4382
4993
  cancelledAt: Date | null;
4383
4994
  }>;
@@ -4480,35 +5091,369 @@ declare class InvitationsRepository extends BaseRepository {
4480
5091
  expiresAt: Date;
4481
5092
  acceptedAt: Date | null;
4482
5093
  cancelledAt: Date | null;
4483
- metadata: Record<string, any> | null;
5094
+ metadata: Record<string, any> | null;
5095
+ }>;
5096
+ /**
5097
+ * 초대 취소 (status, metadata 동시 업데이트)
5098
+ */
5099
+ cancel(id: number, cancelledBy: number, reason?: string, currentMetadata?: Record<string, any> | null): Promise<{
5100
+ createdAt: Date;
5101
+ updatedAt: Date;
5102
+ id: number;
5103
+ email: string;
5104
+ token: string;
5105
+ roleId: number;
5106
+ invitedBy: number;
5107
+ status: "pending" | "accepted" | "expired" | "cancelled";
5108
+ expiresAt: Date;
5109
+ acceptedAt: Date | null;
5110
+ cancelledAt: Date | null;
5111
+ metadata: Record<string, any> | null;
5112
+ }>;
5113
+ }
5114
+ declare const invitationsRepository: InvitationsRepository;
5115
+
5116
+ /**
5117
+ * Social Accounts Repository
5118
+ *
5119
+ * OAuth 소셜 계정 데이터 관리를 위한 Repository
5120
+ * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리
5121
+ */
5122
+
5123
+ /**
5124
+ * Social Accounts Repository 클래스
5125
+ */
5126
+ declare class SocialAccountsRepository extends BaseRepository {
5127
+ /**
5128
+ * 저장 row 의 토큰을 평문으로 복호화해 반환한다.
5129
+ *
5130
+ * 레거시 평문(마커 없음)이 감지되면 즉시 재암호화해 저장하는
5131
+ * self-healing 마이그레이션을 수행한다. 호출자에게는 항상 평문이 반환되어
5132
+ * 외부 API 계약(평문 토큰)이 유지된다.
5133
+ */
5134
+ private decryptAccount;
5135
+ /**
5136
+ * provider와 providerUserId로 소셜 계정 조회
5137
+ * Read replica 사용
5138
+ */
5139
+ findByProviderAndProviderId(provider: SocialProvider, providerUserId: string): Promise<{
5140
+ accessToken: string | null;
5141
+ refreshToken: string | null;
5142
+ userId: number;
5143
+ id: number;
5144
+ createdAt: Date;
5145
+ updatedAt: Date;
5146
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5147
+ providerUserId: string;
5148
+ providerEmail: string | null;
5149
+ tokenExpiresAt: Date | null;
5150
+ } | null>;
5151
+ /**
5152
+ * userId로 모든 소셜 계정 조회
5153
+ * Read replica 사용
5154
+ */
5155
+ findByUserId(userId: number): Promise<({
5156
+ accessToken: string | null;
5157
+ refreshToken: string | null;
5158
+ userId: number;
5159
+ id: number;
5160
+ createdAt: Date;
5161
+ updatedAt: Date;
5162
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5163
+ providerUserId: string;
5164
+ providerEmail: string | null;
5165
+ tokenExpiresAt: Date | null;
5166
+ } | null)[]>;
5167
+ /**
5168
+ * userId와 provider로 소셜 계정 조회
5169
+ * Read replica 사용
5170
+ */
5171
+ findByUserIdAndProvider(userId: number, provider: SocialProvider): Promise<{
5172
+ accessToken: string | null;
5173
+ refreshToken: string | null;
5174
+ userId: number;
5175
+ id: number;
5176
+ createdAt: Date;
5177
+ updatedAt: Date;
5178
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5179
+ providerUserId: string;
5180
+ providerEmail: string | null;
5181
+ tokenExpiresAt: Date | null;
5182
+ } | null>;
5183
+ /**
5184
+ * 소셜 계정 생성
5185
+ * Write primary 사용
5186
+ */
5187
+ create(data: NewUserSocialAccount): Promise<{
5188
+ accessToken: string | null;
5189
+ refreshToken: string | null;
5190
+ userId: number;
5191
+ id: number;
5192
+ createdAt: Date;
5193
+ updatedAt: Date;
5194
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5195
+ providerUserId: string;
5196
+ providerEmail: string | null;
5197
+ tokenExpiresAt: Date | null;
5198
+ } | null>;
5199
+ /**
5200
+ * 토큰 정보 업데이트
5201
+ * Write primary 사용
5202
+ */
5203
+ updateTokens(id: number, data: {
5204
+ accessToken?: string | null;
5205
+ refreshToken?: string | null;
5206
+ tokenExpiresAt?: Date | null;
5207
+ }): Promise<{
5208
+ accessToken: string | null;
5209
+ refreshToken: string | null;
5210
+ userId: number;
5211
+ id: number;
5212
+ createdAt: Date;
5213
+ updatedAt: Date;
5214
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5215
+ providerUserId: string;
5216
+ providerEmail: string | null;
5217
+ tokenExpiresAt: Date | null;
5218
+ } | null>;
5219
+ /**
5220
+ * 소셜 계정 삭제
5221
+ * Write primary 사용
5222
+ */
5223
+ deleteById(id: number): Promise<{
5224
+ userId: number;
5225
+ id: number;
5226
+ createdAt: Date;
5227
+ updatedAt: Date;
5228
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5229
+ providerUserId: string;
5230
+ providerEmail: string | null;
5231
+ accessToken: string | null;
5232
+ refreshToken: string | null;
5233
+ tokenExpiresAt: Date | null;
5234
+ }>;
5235
+ /**
5236
+ * userId와 provider로 소셜 계정 삭제
5237
+ * Write primary 사용
5238
+ */
5239
+ deleteByUserIdAndProvider(userId: number, provider: SocialProvider): Promise<{
5240
+ userId: number;
5241
+ id: number;
5242
+ createdAt: Date;
5243
+ updatedAt: Date;
5244
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5245
+ providerUserId: string;
5246
+ providerEmail: string | null;
5247
+ accessToken: string | null;
5248
+ refreshToken: string | null;
5249
+ tokenExpiresAt: Date | null;
5250
+ }>;
5251
+ /**
5252
+ * 사용자의 모든 소셜 계정 삭제 (계정 익명화 파기용)
5253
+ *
5254
+ * provider unique index(provider, providerUserId)를 해제해 같은 소셜 계정으로
5255
+ * 재가입할 수 있게 한다.
5256
+ * Write primary 사용
5257
+ */
5258
+ deleteAllByUserId(userId: number): Promise<number>;
5259
+ }
5260
+ declare const socialAccountsRepository: SocialAccountsRepository;
5261
+
5262
+ /**
5263
+ * Auth Metadata Repository
5264
+ *
5265
+ * Key-value 기반 시스템 메타데이터 저장소
5266
+ * RBAC 설정 해시 등 시스템 설정값 관리
5267
+ */
5268
+
5269
+ declare class AuthMetadataRepository extends BaseRepository {
5270
+ /**
5271
+ * 키로 값 조회
5272
+ */
5273
+ get(key: string): Promise<string | null>;
5274
+ /**
5275
+ * 키-값 저장 (upsert)
5276
+ */
5277
+ set(key: string, value: string): Promise<void>;
5278
+ }
5279
+ declare const authMetadataRepository: AuthMetadataRepository;
5280
+
5281
+ /**
5282
+ * Account Deletion Requests Repository
5283
+ *
5284
+ * 계정 탈퇴 요청/복구/파기 감사 이력 관리를 위한 Repository
5285
+ * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리
5286
+ *
5287
+ * Row는 절대 삭제하지 않는다 — status 전이(pending -> cancelled|completed)로만 갱신.
5288
+ */
5289
+
5290
+ /**
5291
+ * Account Deletion Requests Repository 클래스
5292
+ */
5293
+ declare class AccountDeletionRequestsRepository extends BaseRepository {
5294
+ /**
5295
+ * ID로 요청 조회
5296
+ * Read replica 사용
5297
+ */
5298
+ findById(id: number): Promise<{
5299
+ createdAt: Date;
5300
+ updatedAt: Date;
5301
+ id: number;
5302
+ userId: number | null;
5303
+ userPublicId: string;
5304
+ requestedAt: Date;
5305
+ purgeScheduledAt: Date;
5306
+ status: "pending" | "cancelled" | "completed";
5307
+ requestedBy: "admin" | "self";
5308
+ reason: string | null;
5309
+ cancelledAt: Date | null;
5310
+ completedAt: Date | null;
5311
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5312
+ }>;
5313
+ /**
5314
+ * User ID로 pending 요청 조회 (유저당 최대 1건, partial unique index로 보장)
5315
+ * Read replica 사용
5316
+ */
5317
+ findPendingByUserId(userId: number): Promise<{
5318
+ createdAt: Date;
5319
+ updatedAt: Date;
5320
+ id: number;
5321
+ userId: number | null;
5322
+ userPublicId: string;
5323
+ requestedAt: Date;
5324
+ purgeScheduledAt: Date;
5325
+ status: "pending" | "cancelled" | "completed";
5326
+ requestedBy: "admin" | "self";
5327
+ reason: string | null;
5328
+ cancelledAt: Date | null;
5329
+ completedAt: Date | null;
5330
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5331
+ }>;
5332
+ /**
5333
+ * User ID로 pending 요청 조회 — Write primary에서 직접 읽는다.
5334
+ *
5335
+ * 로그인/OAuth/authenticate 게이트가 pending_deletion 유저의 purgeScheduledAt을
5336
+ * 표시하는 데 쓴다. 이 경로들은 복제 지연이 있으면 방금 커밋된 상태 전이를 놓칠 수
5337
+ * 있어(예: 삭제 요청 직후 OAuth 로그인 시도), replica가 아닌 primary에서 읽는다.
5338
+ */
5339
+ findPendingByUserIdOnPrimary(userId: number): Promise<{
5340
+ createdAt: Date;
5341
+ updatedAt: Date;
5342
+ id: number;
5343
+ userId: number | null;
5344
+ userPublicId: string;
5345
+ requestedAt: Date;
5346
+ purgeScheduledAt: Date;
5347
+ status: "pending" | "cancelled" | "completed";
5348
+ requestedBy: "admin" | "self";
5349
+ reason: string | null;
5350
+ cancelledAt: Date | null;
5351
+ completedAt: Date | null;
5352
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5353
+ }>;
5354
+ /**
5355
+ * 파기 스윕 대상 조회 — status='pending' AND purgeScheduledAt <= now
5356
+ * Read replica 사용
5357
+ */
5358
+ findDueForPurge(now: Date): Promise<{
5359
+ createdAt: Date;
5360
+ updatedAt: Date;
5361
+ id: number;
5362
+ userId: number | null;
5363
+ userPublicId: string;
5364
+ requestedAt: Date;
5365
+ purgeScheduledAt: Date;
5366
+ status: "pending" | "cancelled" | "completed";
5367
+ requestedBy: "admin" | "self";
5368
+ reason: string | null;
5369
+ cancelledAt: Date | null;
5370
+ completedAt: Date | null;
5371
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5372
+ }[]>;
5373
+ /**
5374
+ * 요청 생성
5375
+ * Write primary 사용
5376
+ */
5377
+ create(data: NewAccountDeletionRequest): Promise<{
5378
+ userId: number | null;
5379
+ id: number;
5380
+ createdAt: Date;
5381
+ updatedAt: Date;
5382
+ status: "pending" | "cancelled" | "completed";
5383
+ reason: string | null;
5384
+ purgeScheduledAt: Date;
5385
+ cancelledAt: Date | null;
5386
+ userPublicId: string;
5387
+ requestedAt: Date;
5388
+ requestedBy: "admin" | "self";
5389
+ completedAt: Date | null;
5390
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5391
+ }>;
5392
+ /**
5393
+ * 요청 취소 (복구)
5394
+ *
5395
+ * `WHERE status = 'pending'` 조건부 UPDATE — 이미 completed(파기 완료)된 row를
5396
+ * 뒤늦게 도착한 cancel이 덮어쓰지 않도록 한다. 0 row 매치(= 이미 completed거나
5397
+ * cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
5398
+ * Write primary 사용
5399
+ */
5400
+ markCancelled(id: number): Promise<{
5401
+ createdAt: Date;
5402
+ updatedAt: Date;
5403
+ id: number;
5404
+ userId: number | null;
5405
+ userPublicId: string;
5406
+ requestedAt: Date;
5407
+ purgeScheduledAt: Date;
5408
+ status: "pending" | "cancelled" | "completed";
5409
+ requestedBy: "admin" | "self";
5410
+ reason: string | null;
5411
+ cancelledAt: Date | null;
5412
+ completedAt: Date | null;
5413
+ purgeStrategy: "anonymize" | "hard-delete" | null;
4484
5414
  }>;
4485
5415
  /**
4486
- * 초대 취소 (status, metadata 동시 업데이트)
5416
+ * 요청 완료 처리 (파기 실행 직전에 호출하는 "claim" — 파기 실행 후가 아니다)
5417
+ *
5418
+ * `WHERE status = 'pending'` 조건부 UPDATE. Postgres UPDATE는 매치되는 row에 락을
5419
+ * 걸고 최신 커밋 상태로 WHERE를 재평가하므로, 이 호출 하나로 (1) 동시에 취소된
5420
+ * row를 안전하게 걸러내고 (2) row-level 락으로 동시 파기 시도를 직렬화한다.
5421
+ * 0 row 매치(= 이미 취소되었거나 다른 파기가 먼저 처리함) 시 null을 반환 — 호출자는
5422
+ * destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
5423
+ * Write primary 사용
4487
5424
  */
4488
- cancel(id: number, cancelledBy: number, reason?: string, currentMetadata?: Record<string, any> | null): Promise<{
5425
+ markCompleted(id: number, purgeStrategy: PurgeStrategy): Promise<{
4489
5426
  createdAt: Date;
4490
5427
  updatedAt: Date;
4491
5428
  id: number;
4492
- email: string;
4493
- token: string;
4494
- roleId: number;
4495
- invitedBy: number;
4496
- status: "pending" | "accepted" | "expired" | "cancelled";
4497
- expiresAt: Date;
4498
- acceptedAt: Date | null;
5429
+ userId: number | null;
5430
+ userPublicId: string;
5431
+ requestedAt: Date;
5432
+ purgeScheduledAt: Date;
5433
+ status: "pending" | "cancelled" | "completed";
5434
+ requestedBy: "admin" | "self";
5435
+ reason: string | null;
4499
5436
  cancelledAt: Date | null;
4500
- metadata: Record<string, any> | null;
5437
+ completedAt: Date | null;
5438
+ purgeStrategy: "anonymize" | "hard-delete" | null;
4501
5439
  }>;
4502
5440
  }
4503
- declare const invitationsRepository: InvitationsRepository;
5441
+ declare const accountDeletionRequestsRepository: AccountDeletionRequestsRepository;
4504
5442
 
4505
5443
  /**
4506
5444
  * @spfn/auth - Password Helpers
4507
5445
  *
4508
- * Password hashing and verification using bcrypt
5446
+ * Password hashing and verification using bcrypt (@node-rs/bcrypt).
5447
+ *
5448
+ * Uses the native (Rust/napi) implementation, which runs the CPU-bound key
5449
+ * derivation on the libuv threadpool instead of the main event loop — so
5450
+ * concurrent logins run in parallel and don't head-of-line-block other requests.
5451
+ * Hashes are standard bcrypt ($2*$) and verify against existing bcryptjs hashes.
5452
+ * For very high concurrent-login load, raise UV_THREADPOOL_SIZE toward the core
5453
+ * count (default pool is 4).
4509
5454
  *
4510
5455
  * Security:
4511
- * - Adaptive hashing (configurable rounds)
5456
+ * - Adaptive hashing (configurable rounds, default 12)
4512
5457
  * - Automatic salt generation (per-password)
4513
5458
  * - Constant-time comparison (timing attack protection)
4514
5459
  * - Rainbow table protection
@@ -4521,10 +5466,10 @@ declare const invitationsRepository: InvitationsRepository;
4521
5466
  * 2. Apply bcrypt key derivation (2^rounds iterations)
4522
5467
  * 3. Return $2b$rounds$[salt][hash] (60 chars)
4523
5468
  *
4524
- * Salt rounds are configured via SPFN_AUTH_BCRYPT_SALT_ROUNDS:
4525
- * - 10 rounds: ~100ms (default, balanced)
4526
- * - 12 rounds: ~400ms (more secure, slower)
4527
- * - 14 rounds: ~1600ms (very secure, too slow for most apps)
5469
+ * Salt rounds are configured via SPFN_AUTH_BCRYPT_SALT_ROUNDS (native timings):
5470
+ * - 12 rounds: ~200ms (default — OWASP-aligned, off the event loop)
5471
+ * - 10 rounds: ~55ms (faster, lower work factor)
5472
+ * - 14 rounds: ~800ms (very secure, heavy)
4528
5473
  *
4529
5474
  * @param password - Plain text password to hash
4530
5475
  * @returns Bcrypt hash string (includes salt)
@@ -4558,6 +5503,7 @@ declare function hashPassword(password: string): Promise<string>;
4558
5503
  * ```
4559
5504
  */
4560
5505
  declare function verifyPassword(password: string, hash: string): Promise<boolean>;
5506
+ declare function getDummyPasswordHash(): Promise<string>;
4561
5507
  /**
4562
5508
  * Validate password strength
4563
5509
  *
@@ -4846,6 +5792,29 @@ interface RoleGuardOptions {
4846
5792
  */
4847
5793
  declare const roleGuard: _spfn_core_route.NamedMiddlewareFactory<"roleGuard", [options: RoleGuardOptions]>;
4848
5794
 
5795
+ /**
5796
+ * One-Time Token Authentication Middleware
5797
+ *
5798
+ * Authenticates requests using a one-time token instead of JWT.
5799
+ * Extracts token from query parameter `?token=xxx` or `Authorization: OTT xxx` header.
5800
+ *
5801
+ * On success, injects AuthContext identical to the `authenticate` middleware,
5802
+ * making it transparent to downstream handlers using `getAuth(c)`.
5803
+ *
5804
+ * Auto-skips the global 'auth' middleware.
5805
+ *
5806
+ * @example
5807
+ * ```typescript
5808
+ * export const uploadFile = route.post('/files/upload')
5809
+ * .use([oneTimeTokenAuth])
5810
+ * .handler(async (c) => {
5811
+ * const { userId } = getAuth(c);
5812
+ * // handle file upload...
5813
+ * });
5814
+ * ```
5815
+ */
5816
+ declare const oneTimeTokenAuth: _spfn_core_route.NamedMiddleware<"oneTimeTokenAuth">;
5817
+
4849
5818
  /**
4850
5819
  * Auth Context Helpers
4851
5820
  *
@@ -4872,6 +5841,32 @@ declare const roleGuard: _spfn_core_route.NamedMiddlewareFactory<"roleGuard", [o
4872
5841
  declare function getAuth(c: Context | {
4873
5842
  raw: Context;
4874
5843
  }): AuthContext;
5844
+ /**
5845
+ * Get optional auth context from route context
5846
+ *
5847
+ * Returns AuthContext if authenticated, undefined otherwise.
5848
+ * Use with `optionalAuth` middleware for routes that serve both
5849
+ * authenticated and unauthenticated users.
5850
+ *
5851
+ * @example
5852
+ * ```typescript
5853
+ * export const getProducts = route.get('/products')
5854
+ * .use([optionalAuth])
5855
+ * .handler(async (c) => {
5856
+ * const auth = getOptionalAuth(c);
5857
+ *
5858
+ * if (auth)
5859
+ * {
5860
+ * return getPersonalizedProducts(auth.userId);
5861
+ * }
5862
+ *
5863
+ * return getPublicProducts();
5864
+ * });
5865
+ * ```
5866
+ */
5867
+ declare function getOptionalAuth(c: Context | {
5868
+ raw: Context;
5869
+ }): AuthContext | undefined;
4875
5870
  /**
4876
5871
  * Get authenticated user from route context
4877
5872
  *
@@ -4888,16 +5883,20 @@ declare function getUser(c: Context | {
4888
5883
  }): {
4889
5884
  email: string | null;
4890
5885
  phone: string | null;
4891
- status: "active" | "inactive" | "suspended";
4892
5886
  id: number;
4893
- createdAt: Date;
4894
- updatedAt: Date;
5887
+ publicId: string;
5888
+ username: string | null;
4895
5889
  passwordHash: string | null;
4896
5890
  passwordChangeRequired: boolean;
4897
5891
  roleId: number;
5892
+ createdAt: Date;
5893
+ updatedAt: Date;
5894
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
4898
5895
  emailVerifiedAt: Date | null;
4899
5896
  phoneVerifiedAt: Date | null;
4900
5897
  lastLoginAt: Date | null;
5898
+ deletedAt: Date | null;
5899
+ deletedBy: string | null;
4901
5900
  };
4902
5901
  /**
4903
5902
  * Get authenticated user ID from route context
@@ -4913,6 +5912,38 @@ declare function getUser(c: Context | {
4913
5912
  declare function getUserId(c: Context | {
4914
5913
  raw: Context;
4915
5914
  }): string;
5915
+ /**
5916
+ * Get authenticated user's role from route context
5917
+ *
5918
+ * @returns Role name or null if user has no role
5919
+ *
5920
+ * @example
5921
+ * ```typescript
5922
+ * app.bind(adminContract, [authenticate], async (c) => {
5923
+ * const role = getRole(c);
5924
+ * // 'admin' | 'superadmin' | null
5925
+ * });
5926
+ * ```
5927
+ */
5928
+ declare function getRole(c: Context | {
5929
+ raw: Context;
5930
+ }): string | null;
5931
+ /**
5932
+ * Get authenticated user's locale from route context
5933
+ *
5934
+ * @returns Locale string (e.g., 'en', 'ko')
5935
+ *
5936
+ * @example
5937
+ * ```typescript
5938
+ * app.bind(contract, [authenticate], async (c) => {
5939
+ * const locale = getLocale(c);
5940
+ * // 'en' | 'ko' | ...
5941
+ * });
5942
+ * ```
5943
+ */
5944
+ declare function getLocale(c: Context | {
5945
+ raw: Context;
5946
+ }): string;
4916
5947
  /**
4917
5948
  * Get current key ID from route context
4918
5949
  *
@@ -4939,7 +5970,7 @@ declare function getKeyId(c: Context | {
4939
5970
  * - RS256 (RSA 2048): ~294 bytes (Base64: ~392 chars)
4940
5971
  */
4941
5972
 
4942
- type Unit = "Years" | "Year" | "Yrs" | "Yr" | "Y" | "Weeks" | "Week" | "W" | "Days" | "Day" | "D" | "Hours" | "Hour" | "Hrs" | "Hr" | "H" | "Minutes" | "Minute" | "Mins" | "Min" | "M" | "Seconds" | "Second" | "Secs" | "Sec" | "s" | "Milliseconds" | "Millisecond" | "Msecs" | "Msec" | "Ms";
5973
+ type Unit = 'Years' | 'Year' | 'Yrs' | 'Yr' | 'Y' | 'Weeks' | 'Week' | 'W' | 'Days' | 'Day' | 'D' | 'Hours' | 'Hour' | 'Hrs' | 'Hr' | 'H' | 'Minutes' | 'Minute' | 'Mins' | 'Min' | 'M' | 'Seconds' | 'Second' | 'Secs' | 'Sec' | 's' | 'Milliseconds' | 'Millisecond' | 'Msecs' | 'Msec' | 'Ms';
4943
5974
  type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;
4944
5975
  type StringValue = `${number}` | `${number}${UnitAnyCase}` | `${number} ${UnitAnyCase}`;
4945
5976
  interface KeyPair {
@@ -4985,70 +6016,40 @@ declare function shouldRotateKey(createdAt: Date, rotationDays?: number): {
4985
6016
  daysRemaining: number;
4986
6017
  };
4987
6018
 
4988
- /**
4989
- * @spfn/auth - Client Session Management
4990
- *
4991
- * Uses Jose JWE (JSON Web Encryption) to securely store session data in cookies
4992
- * More efficient than Iron Session with better Edge Runtime support
4993
- */
4994
-
4995
- interface SessionData {
4996
- userId: string;
4997
- privateKey: string;
4998
- keyId: string;
4999
- algorithm: KeyAlgorithmType;
5000
- }
5001
- /**
5002
- * Seal session data into encrypted JWT (JWE)
5003
- *
5004
- * @param data - Session data to encrypt
5005
- * @param ttl - Time to live in seconds (default: 7 days)
5006
- * @returns Encrypted JWT string
5007
- */
5008
- declare function sealSession(data: SessionData, ttl?: number): Promise<string>;
5009
- /**
5010
- * Unseal encrypted JWT (JWE) to session data
5011
- *
5012
- * @param jwt - Encrypted JWT string
5013
- * @returns Session data
5014
- * @throws Error if session is invalid or expired
5015
- */
5016
- declare function unsealSession(jwt: string): Promise<SessionData>;
5017
- /**
5018
- * Get session metadata without decrypting
5019
- *
5020
- * @param jwt - Encrypted JWT string
5021
- * @returns Session metadata or null if invalid
5022
- */
5023
- declare function getSessionInfo(jwt: string): Promise<{
5024
- issuedAt: Date;
5025
- expiresAt: Date;
5026
- issuer: string;
5027
- audience: string;
5028
- } | null>;
5029
- /**
5030
- * Check if session is about to expire (within threshold)
5031
- *
5032
- * @param jwt - Encrypted JWT string
5033
- * @param thresholdHours - Hours before expiry to trigger refresh (default: 24)
5034
- * @returns True if session should be refreshed
5035
- */
5036
- declare function shouldRefreshSession(jwt: string, thresholdHours?: number): Promise<boolean>;
5037
-
5038
6019
  /**
5039
6020
  * @spfn/auth - Global Configuration
5040
6021
  *
5041
6022
  * Manages global auth configuration including session TTL
5042
6023
  */
6024
+
5043
6025
  /**
5044
6026
  * Cookie names used by SPFN Auth
6027
+ *
6028
+ * Names include a port-based suffix so that multiple dev instances
6029
+ * on different ports don't overwrite each other's cookies.
5045
6030
  */
5046
6031
  declare const COOKIE_NAMES: {
5047
6032
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
5048
- readonly SESSION: "spfn_session";
6033
+ readonly SESSION: string;
5049
6034
  /** Current key ID (for key rotation) */
5050
- readonly SESSION_KEY_ID: "spfn_session_key_id";
6035
+ readonly SESSION_KEY_ID: string;
6036
+ /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
6037
+ readonly OAUTH_PENDING: string;
6038
+ /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
6039
+ readonly OAUTH_CSRF: string;
5051
6040
  };
6041
+ /**
6042
+ * OAuth CSRF 쿠키를 PORT 접미사와 무관하게 전부 수집한다.
6043
+ *
6044
+ * 쿠키를 심는 쪽은 Next.js 프로세스, 읽는 쪽은 API 프로세스라 분리 배포에서는
6045
+ * 두 프로세스의 PORT가 달라 COOKIE_NAMES.OAUTH_CSRF 정확 일치 조회가 빗나간다.
6046
+ * nonce 자체가 랜덤값이고 암호화된 state의 nonce와 대조되므로, 접미사가 다른
6047
+ * spfn_oauth_csrf* 후보를 모두 대조 대상으로 넘겨도 안전하다.
6048
+ */
6049
+ declare function matchOAuthCsrfCookies(cookies: Record<string, string>): {
6050
+ name: string;
6051
+ value: string;
6052
+ }[];
5052
6053
  /**
5053
6054
  * Parse duration string to seconds
5054
6055
  *
@@ -5061,6 +6062,36 @@ declare const COOKIE_NAMES: {
5061
6062
  * parseDuration('3600') // 3600
5062
6063
  */
5063
6064
  declare function parseDuration(duration: string | number): number;
6065
+ /**
6066
+ * Registration channel passed to the beforeRegister hook
6067
+ *
6068
+ * - credentials: email/phone + password registration
6069
+ * - oauth: new-user signup through a social provider (web or native flow)
6070
+ * - invitation: invitation acceptance
6071
+ */
6072
+ type RegisterChannel = 'credentials' | 'oauth' | 'invitation';
6073
+ /**
6074
+ * Context passed to the beforeRegister hook
6075
+ *
6076
+ * Credentials (password, keys) are intentionally excluded — the hook is a
6077
+ * policy gate, not a credential handler.
6078
+ */
6079
+ interface BeforeRegisterContext {
6080
+ channel: RegisterChannel;
6081
+ /** Social provider — only set when channel is 'oauth' */
6082
+ provider?: SocialProvider;
6083
+ email?: string;
6084
+ /**
6085
+ * Whether the email is verified — only set when channel is 'oauth'.
6086
+ * OAuth providers may report an unverified (spoofable) email; the created
6087
+ * account stores it as null in that case, so email-based policies must
6088
+ * check this flag. credentials/invitation emails are already verified.
6089
+ */
6090
+ emailVerified?: boolean;
6091
+ phone?: string;
6092
+ /** App-supplied registration metadata (register params / OAuth start params / invitation) */
6093
+ metadata?: Record<string, unknown>;
6094
+ }
5064
6095
  /**
5065
6096
  * Auth configuration
5066
6097
  */
@@ -5075,6 +6106,35 @@ interface AuthConfig {
5075
6106
  * @default 7d (7 days)
5076
6107
  */
5077
6108
  sessionTtl?: string | number;
6109
+ /**
6110
+ * App-injected validator that runs before a new user row is created,
6111
+ * on every registration channel (credentials, oauth, invitation).
6112
+ *
6113
+ * Throw to reject the registration — RegistrationRejectedError (403) is
6114
+ * the recommended error; any HttpError subclass keeps its own status.
6115
+ * Runs after built-in checks (verification token, duplicate account),
6116
+ * so existing error precedence is unchanged. Not called for admin
6117
+ * seeding (initializeAuth) or when linking a social account to an
6118
+ * existing user.
6119
+ *
6120
+ * Runs inside the registration DB transaction on every channel — keep it
6121
+ * fast. A slow call (e.g. an external policy API) holds a pooled DB
6122
+ * connection open for its full duration on every signup.
6123
+ *
6124
+ * @example
6125
+ * ```typescript
6126
+ * configureAuth({
6127
+ * beforeRegister: async ({ channel, metadata }) =>
6128
+ * {
6129
+ * if (channel === 'credentials' && !isOldEnough(metadata?.birthDate))
6130
+ * {
6131
+ * throw new RegistrationRejectedError({ message: 'Age requirement not met' });
6132
+ * }
6133
+ * },
6134
+ * });
6135
+ * ```
6136
+ */
6137
+ beforeRegister?: (context: BeforeRegisterContext) => void | Promise<void>;
5078
6138
  }
5079
6139
  /**
5080
6140
  * Configure global auth settings
@@ -5093,6 +6153,14 @@ declare function configureAuth(config: AuthConfig): void;
5093
6153
  * Get current auth configuration
5094
6154
  */
5095
6155
  declare function getAuthConfig(): AuthConfig;
6156
+ /**
6157
+ * Run the app-injected beforeRegister hook if configured — throws to reject.
6158
+ *
6159
+ * Single entry point for every registration channel so a new channel cannot
6160
+ * forget the configured-check. Callers invoke this right before creating the
6161
+ * user row.
6162
+ */
6163
+ declare function runBeforeRegister(context: BeforeRegisterContext): Promise<void>;
5096
6164
  /**
5097
6165
  * Get session TTL in seconds
5098
6166
  *
@@ -5104,6 +6172,107 @@ declare function getAuthConfig(): AuthConfig;
5104
6172
  */
5105
6173
  declare function getSessionTtl(override?: string | number): number;
5106
6174
 
6175
+ /**
6176
+ * One-Time Token Manager
6177
+ *
6178
+ * Singleton wrapper around SSETokenManager for one-time token authentication.
6179
+ * Used for direct API access (file uploads, SSE streaming, etc.) bypassing RPC proxy.
6180
+ */
6181
+
6182
+ /**
6183
+ * Initialize the one-time token manager
6184
+ *
6185
+ * Called during auth lifecycle initialization.
6186
+ * Creates a singleton SSETokenManager instance.
6187
+ *
6188
+ * @param config - Optional configuration
6189
+ * @param config.ttl - Token time-to-live in milliseconds (default: 30000)
6190
+ * @param config.store - Custom token store (e.g., CacheTokenStore for Redis)
6191
+ */
6192
+ declare function initOneTimeTokenManager(config?: {
6193
+ ttl?: number;
6194
+ store?: SSETokenStore;
6195
+ }): void;
6196
+ /**
6197
+ * Get the one-time token manager instance
6198
+ *
6199
+ * @throws Error if initOneTimeTokenManager() has not been called
6200
+ *
6201
+ * @example
6202
+ * ```typescript
6203
+ * import { getOneTimeTokenManager } from '@spfn/auth/server';
6204
+ *
6205
+ * // Use as SSE tokenManager
6206
+ * .eventsConfig({
6207
+ * auth: {
6208
+ * enabled: true,
6209
+ * tokenManager: getOneTimeTokenManager(),
6210
+ * },
6211
+ * })
6212
+ * ```
6213
+ */
6214
+ declare function getOneTimeTokenManager(): SSETokenManager;
6215
+
6216
+ /**
6217
+ * Account Deletion Configuration
6218
+ *
6219
+ * Singleton config for the account deletion/recovery lifecycle, mirroring the
6220
+ * one-time-token manager pattern (`lib/one-time-token.ts`): a mutable module-level
6221
+ * value, set once during `createAuthLifecycle()` and read at request/handler time.
6222
+ *
6223
+ * `purgeCron` is the one field this module does NOT make effective on its own —
6224
+ * `job('auth.deletion.purge').cron(expression)` bakes the cron string in at
6225
+ * *module-import* time, which always runs before `createAuthLifecycle()` executes
6226
+ * (ESM evaluates imports before the importing module's own statements). So a
6227
+ * statically-exported job router can never see a `purgeCron` set here. Apps that
6228
+ * need a non-default cron must build the router themselves, after configuring
6229
+ * deletion, with `createAuthDeletionJobRouter({ purgeCron })` — see
6230
+ * `server/jobs/deletion-purge.ts` and the README.
6231
+ */
6232
+
6233
+ interface AccountDeletionPurgeUser {
6234
+ id: number;
6235
+ publicId: string;
6236
+ email: string | null;
6237
+ phone: string | null;
6238
+ }
6239
+ interface AuthDeletionConfig {
6240
+ /** Days between a deletion request and the purge becoming eligible. 0 = immediate. */
6241
+ gracePeriodDays: number;
6242
+ /** How the purge job destroys the account once the grace period elapses. */
6243
+ purgeStrategy: PurgeStrategy;
6244
+ /** Whether a self-service caller may request `immediate: true` (skip the grace period). */
6245
+ allowSelfImmediate: boolean;
6246
+ /**
6247
+ * Cron schedule for the purge sweep. NOT read automatically by the static
6248
+ * `authJobRouter` export — see the module doc comment above.
6249
+ */
6250
+ purgeCron: string;
6251
+ /** Whether to email users (when they have one) at request/recovery/purge time. */
6252
+ sendNotifications: boolean;
6253
+ /**
6254
+ * Synchronous hook invoked immediately before a user is purged. Throw to skip
6255
+ * that user for this sweep (they stay `pending`, retried on the next tick).
6256
+ */
6257
+ onBeforePurge?: (user: AccountDeletionPurgeUser) => Promise<void>;
6258
+ }
6259
+ declare const DEFAULT_DELETION_GRACE_PERIOD_DAYS = 30;
6260
+ declare const DEFAULT_DELETION_PURGE_STRATEGY: PurgeStrategy;
6261
+ declare const DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE = false;
6262
+ declare const DEFAULT_DELETION_PURGE_CRON = "0 4 * * *";
6263
+ declare const DEFAULT_DELETION_SEND_NOTIFICATIONS = true;
6264
+ /**
6265
+ * Set the resolved deletion config. Called synchronously from `createAuthLifecycle()`
6266
+ * (not from `afterInfrastructure`) so it takes effect before any route/job handler
6267
+ * that reads `getDeletionConfig()` can run.
6268
+ */
6269
+ declare function configureDeletion(options?: Partial<AuthDeletionConfig>): void;
6270
+ /**
6271
+ * Read the current deletion config. Safe to call any time — defaults apply even
6272
+ * if `createAuthLifecycle()` was never given a `deletion` block.
6273
+ */
6274
+ declare function getDeletionConfig(): AuthDeletionConfig;
6275
+
5107
6276
  /**
5108
6277
  * @spfn/auth - Centralized Logger
5109
6278
  *
@@ -5116,7 +6285,9 @@ declare const authLogger: {
5116
6285
  general: _spfn_core_logger.Logger;
5117
6286
  login: _spfn_core_logger.Logger;
5118
6287
  keyRotation: _spfn_core_logger.Logger;
6288
+ oauth: _spfn_core_logger.Logger;
5119
6289
  };
6290
+ session: _spfn_core_logger.Logger;
5120
6291
  service: _spfn_core_logger.Logger;
5121
6292
  setup: _spfn_core_logger.Logger;
5122
6293
  email: _spfn_core_logger.Logger;
@@ -5197,6 +6368,337 @@ interface AuthLifecycleConfig {
5197
6368
  * .build();
5198
6369
  * ```
5199
6370
  */
5200
- declare function createAuthLifecycle(options?: AuthInitOptions): AuthLifecycleConfig;
6371
+ /**
6372
+ * Options for createAuthLifecycle
6373
+ */
6374
+ interface AuthLifecycleOptions extends AuthInitOptions {
6375
+ /**
6376
+ * One-time token configuration
6377
+ *
6378
+ * Enables one-time token issuance for direct API access
6379
+ * (file uploads, SSE streaming, etc.)
6380
+ *
6381
+ * @example
6382
+ * ```typescript
6383
+ * createAuthLifecycle({
6384
+ * oneTimeToken: { ttl: 60000 }, // 60 seconds
6385
+ * })
6386
+ * ```
6387
+ */
6388
+ oneTimeToken?: {
6389
+ /**
6390
+ * Token time-to-live in milliseconds
6391
+ * @default 30000
6392
+ */
6393
+ ttl?: number;
6394
+ /**
6395
+ * Custom token store (e.g., CacheTokenStore for Redis/Valkey)
6396
+ *
6397
+ * When provided, tokens are stored in the external store instead of in-memory Map.
6398
+ * Required for multi-instance deployments where token issuance and verification
6399
+ * may happen on different server instances.
6400
+ *
6401
+ * @example
6402
+ * ```typescript
6403
+ * import { CacheTokenStore } from '@spfn/core/event/sse';
6404
+ * import { getCache } from '@spfn/core/cache';
6405
+ *
6406
+ * createAuthLifecycle({
6407
+ * oneTimeToken: {
6408
+ * store: new CacheTokenStore(getCache()),
6409
+ * },
6410
+ * })
6411
+ * ```
6412
+ */
6413
+ store?: SSETokenStore;
6414
+ };
6415
+ /**
6416
+ * Account deletion/recovery lifecycle configuration
6417
+ *
6418
+ * Controls the grace-period request → recover → purge flow exposed by
6419
+ * `POST /_auth/deletion/request` and `POST /_auth/deletion/cancel`.
6420
+ * Registering the purge job itself is a separate step — see `authJobRouter`
6421
+ * (`@spfn/auth/server`) and the README, since job registration happens after
6422
+ * this lifecycle hook runs and can't be triggered from here.
6423
+ *
6424
+ * @example
6425
+ * ```typescript
6426
+ * createAuthLifecycle({
6427
+ * deletion: {
6428
+ * gracePeriodDays: 14,
6429
+ * purgeStrategy: 'anonymize',
6430
+ * allowSelfImmediate: false,
6431
+ * sendNotifications: true,
6432
+ * onBeforePurge: async (user) => {
6433
+ * await appDataCleanup(user.id); // throw to skip this user this sweep
6434
+ * },
6435
+ * },
6436
+ * })
6437
+ * ```
6438
+ */
6439
+ deletion?: {
6440
+ /**
6441
+ * Days between a deletion request and the purge becoming eligible.
6442
+ * 0 = immediate (still goes through the same request/purge pipeline).
6443
+ * @default 30
6444
+ */
6445
+ gracePeriodDays?: number;
6446
+ /**
6447
+ * How the purge job destroys the account once the grace period elapses.
6448
+ * - 'anonymize': scrub PII, keep the row (status -> 'deleted') — recommended default
6449
+ * - 'hard-delete': physically remove the `users` row (cascades to child rows)
6450
+ * @default 'anonymize'
6451
+ */
6452
+ purgeStrategy?: PurgeStrategy;
6453
+ /**
6454
+ * Whether a self-service caller may pass `immediate: true` on
6455
+ * `POST /_auth/deletion/request` to skip the grace period entirely.
6456
+ * @default false
6457
+ */
6458
+ allowSelfImmediate?: boolean;
6459
+ /**
6460
+ * Cron schedule for the purge sweep. NOTE: this does not reach the static
6461
+ * `authJobRouter` export automatically — job schedules are fixed at
6462
+ * module-import time, before this lifecycle hook runs. For a non-default
6463
+ * cron, build the router yourself after this call with
6464
+ * `createAuthDeletionJobRouter({ purgeCron })` (see `@spfn/auth/server`).
6465
+ * @default '0 4 * * *'
6466
+ */
6467
+ purgeCron?: string;
6468
+ /**
6469
+ * Whether to email users (when they have one on file) at request,
6470
+ * recovery, and final-purge time.
6471
+ * @default true
6472
+ */
6473
+ sendNotifications?: boolean;
6474
+ /**
6475
+ * Invoked immediately before a user is purged (both the immediate inline
6476
+ * path and the cron sweep). Throw to skip that user for this run — they
6477
+ * stay `pending` and are retried on the next sweep.
6478
+ */
6479
+ onBeforePurge?: (user: AccountDeletionPurgeUser) => Promise<void>;
6480
+ };
6481
+ }
6482
+ declare function createAuthLifecycle(options?: AuthLifecycleOptions): AuthLifecycleConfig;
6483
+
6484
+ /**
6485
+ * @spfn/auth - Account Deletion Purge Job
6486
+ *
6487
+ * The package's first background job (`@spfn/core/job`). Sweeps
6488
+ * `account_deletion_requests` for rows whose grace period elapsed and purges
6489
+ * each one (see `sweepDuePurges` in `account-deletion.service.ts`).
6490
+ *
6491
+ * Registration is NOT automatic. `createAuthLifecycle()`'s `afterInfrastructure`
6492
+ * hook runs *before* `initBoss`/`registerJobs` in `@spfn/core`'s server startup
6493
+ * (`initializeInfrastructure()`), so the lifecycle has no opportunity to register
6494
+ * jobs itself. Apps must register explicitly:
6495
+ *
6496
+ * ```typescript
6497
+ * import { authJobRouter } from '@spfn/auth/server';
6498
+ *
6499
+ * export default defineServerConfig()
6500
+ * .lifecycle(createAuthLifecycle())
6501
+ * .jobs(authJobRouter)
6502
+ * .routes(appRouter)
6503
+ * .build();
6504
+ * ```
6505
+ *
6506
+ * Cron schedule caveat: `job(...).cron(expression)` bakes the cron string in at
6507
+ * *module-import* time, which always happens before `createAuthLifecycle()` runs
6508
+ * in the app's own module (ESM evaluates imports before the importing module's
6509
+ * statements). So the static `authJobRouter` export below always uses the default
6510
+ * cron — `deletion.purgeCron` passed to `createAuthLifecycle()` cannot reach it.
6511
+ * To use a custom cron, build the router yourself, after calling
6512
+ * `createAuthLifecycle()`, with `createAuthDeletionJobRouter({ purgeCron })` and
6513
+ * register that instead of the static export.
6514
+ */
6515
+ /**
6516
+ * Build the `auth.deletion.purge` job with a given cron schedule.
6517
+ *
6518
+ * @param cronExpression - Defaults to `deletion-config`'s default (daily at 04:00).
6519
+ */
6520
+ declare function createAuthDeletionPurgeJob(cronExpression?: string): _spfn_core_job.JobDef<void, void>;
6521
+ /**
6522
+ * Build a job router containing only the deletion purge job, with a given cron.
6523
+ * Use this (after calling `createAuthLifecycle({ deletion: { purgeCron } })`) when
6524
+ * you need a non-default schedule — see the module doc comment above for why the
6525
+ * static `authJobRouter` export can't pick up `deletion.purgeCron` automatically.
6526
+ */
6527
+ declare function createAuthDeletionJobRouter(options?: {
6528
+ purgeCron?: string;
6529
+ }): _spfn_core_job.JobRouter<{
6530
+ deletionPurge: _spfn_core_job.JobDef<void, void>;
6531
+ }>;
6532
+ /**
6533
+ * Default job router — the default cron (`0 4 * * *`). Register with
6534
+ * `.jobs(authJobRouter)` in `server.config.ts`.
6535
+ */
6536
+ declare const authJobRouter: _spfn_core_job.JobRouter<{
6537
+ deletionPurge: _spfn_core_job.JobDef<void, void>;
6538
+ }>;
6539
+
6540
+ /**
6541
+ * @spfn/auth - Auth Events
6542
+ *
6543
+ * 인증 관련 이벤트 정의
6544
+ * - auth.login: 로그인 성공 시 (기존 사용자만)
6545
+ * - auth.register: 회원가입 성공 시 (OAuth 신규 가입 포함)
6546
+ */
6547
+ /**
6548
+ * Auth provider type
6549
+ *
6550
+ * 직접 인증(email/phone) + 등록 가능한 모든 소셜 provider(SOCIAL_PROVIDERS).
6551
+ */
6552
+ declare const AuthProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">, ..._sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]]>;
6553
+ /**
6554
+ * auth.login - 로그인 성공 이벤트
6555
+ *
6556
+ * 발행 시점:
6557
+ * - 이메일/전화 로그인 성공 시
6558
+ * - OAuth 기존 사용자 로그인 시
6559
+ *
6560
+ * @example
6561
+ * ```typescript
6562
+ * authLoginEvent.subscribe(async (payload) => {
6563
+ * await analytics.trackLogin(payload.userId, payload.provider);
6564
+ * });
6565
+ * ```
6566
+ */
6567
+ declare const authLoginEvent: _spfn_core_event.EventDef<{
6568
+ email?: string | undefined;
6569
+ phone?: string | undefined;
6570
+ userId: string;
6571
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
6572
+ }>;
6573
+ /**
6574
+ * auth.register - 회원가입 성공 이벤트
6575
+ *
6576
+ * 발행 시점:
6577
+ * - 이메일/전화 회원가입 성공 시
6578
+ * - OAuth 신규 사용자 가입 시
6579
+ *
6580
+ * @example
6581
+ * ```typescript
6582
+ * authRegisterEvent.subscribe(async (payload) => {
6583
+ * await emailService.sendWelcome(payload.email);
6584
+ * });
6585
+ * ```
6586
+ */
6587
+ declare const authRegisterEvent: _spfn_core_event.EventDef<{
6588
+ email?: string | undefined;
6589
+ phone?: string | undefined;
6590
+ metadata?: {
6591
+ [x: string]: unknown;
6592
+ } | undefined;
6593
+ userId: string;
6594
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
6595
+ }>;
6596
+ /**
6597
+ * auth.invitation.created - 초대 생성 이벤트
6598
+ *
6599
+ * 발행 시점:
6600
+ * - createInvitation() 성공 시
6601
+ * - resendInvitation() 성공 시
6602
+ *
6603
+ * @example
6604
+ * ```typescript
6605
+ * invitationCreatedEvent.subscribe(async (payload) => {
6606
+ * const inviteUrl = `${APP_URL}/invite/${payload.token}`;
6607
+ * await notificationService.send({
6608
+ * channel: 'email',
6609
+ * to: payload.email,
6610
+ * subject: 'You are invited!',
6611
+ * html: renderInviteEmail({ inviteUrl, ...payload.metadata }),
6612
+ * });
6613
+ * });
6614
+ * ```
6615
+ */
6616
+ declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
6617
+ metadata?: {
6618
+ [x: string]: unknown;
6619
+ } | undefined;
6620
+ email: string;
6621
+ roleId: number;
6622
+ token: string;
6623
+ invitedBy: string;
6624
+ expiresAt: string;
6625
+ invitationId: string;
6626
+ isResend: boolean;
6627
+ }>;
6628
+ /**
6629
+ * auth.invitation.accepted - 초대 수락 이벤트
6630
+ *
6631
+ * 발행 시점:
6632
+ * - acceptInvitation() 성공 시
6633
+ *
6634
+ * @example
6635
+ * ```typescript
6636
+ * invitationAcceptedEvent.subscribe(async (payload) => {
6637
+ * await onboardingService.start(payload.userId);
6638
+ * });
6639
+ * ```
6640
+ */
6641
+ declare const invitationAcceptedEvent: _spfn_core_event.EventDef<{
6642
+ metadata?: {
6643
+ [x: string]: unknown;
6644
+ } | undefined;
6645
+ email: string;
6646
+ userId: string;
6647
+ roleId: number;
6648
+ invitedBy: string;
6649
+ invitationId: string;
6650
+ }>;
6651
+ /**
6652
+ * auth.deletion.requested - 계정 탈퇴 요청 이벤트
6653
+ *
6654
+ * 발행 시점:
6655
+ * - requestAccountDeletionService() 성공 시 (self/admin 공통)
6656
+ *
6657
+ * @example
6658
+ * ```typescript
6659
+ * authDeletionRequestedEvent.subscribe(async (payload) => {
6660
+ * await analytics.trackChurnRisk(payload.userId);
6661
+ * });
6662
+ * ```
6663
+ */
6664
+ declare const authDeletionRequestedEvent: _spfn_core_event.EventDef<{
6665
+ userId: string;
6666
+ purgeScheduledAt: string;
6667
+ userPublicId: string;
6668
+ requestedBy: "admin" | "self";
6669
+ }>;
6670
+ /**
6671
+ * auth.deletion.cancelled - 계정 탈퇴 복구 이벤트
6672
+ *
6673
+ * 발행 시점:
6674
+ * - cancelAccountDeletionService() 성공 시 (유예 기간 내 복구)
6675
+ */
6676
+ declare const authDeletionCancelledEvent: _spfn_core_event.EventDef<{
6677
+ userId: string;
6678
+ userPublicId: string;
6679
+ }>;
6680
+ /**
6681
+ * auth.deletion.completed - 계정 파기 완료 이벤트
6682
+ *
6683
+ * 발행 시점:
6684
+ * - purge job(또는 즉시 파기 경로)이 유저를 파기한 직후
6685
+ *
6686
+ * PII를 담지 않는다 — userId(내부 순번)/email/phone 없이 userPublicId만 실어
6687
+ * 파기 완료 이후에도 구독자가 식별 정보를 다시 축적하지 않도록 한다.
6688
+ */
6689
+ declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
6690
+ userPublicId: string;
6691
+ purgeStrategy: "anonymize" | "hard-delete";
6692
+ }>;
6693
+ /**
6694
+ * Auth event payload types
6695
+ */
6696
+ type AuthLoginPayload = typeof authLoginEvent._payload;
6697
+ type AuthRegisterPayload = typeof authRegisterEvent._payload;
6698
+ type InvitationCreatedPayload = typeof invitationCreatedEvent._payload;
6699
+ type InvitationAcceptedPayload = typeof invitationAcceptedEvent._payload;
6700
+ type AuthDeletionRequestedPayload = typeof authDeletionRequestedEvent._payload;
6701
+ type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
6702
+ type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
5201
6703
 
5202
- export { type AuthConfig, AuthContext, COOKIE_NAMES, type EmailProvider, type EmailTemplateProvider, type EmailTemplateResult, type Invitation, InvitationStatus, InvitationsRepository, KeyAlgorithmType, type KeyPair, KeysRepository, type NewInvitation, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, type Permission, type PermissionEntity, PermissionsRepository, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SMSProvider, type SendEmailParams, type SendEmailResult, type SendSMSParams, type SendSMSResult, type SessionData, type SessionPayload, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, type VerificationCodeParams, VerificationCodesRepository, VerificationPurpose, acceptInvitation, addPermissionToRole, authLogger, authSchema, cancelInvitation, configureAuth, createAuthLifecycle, createInvitation, createRole, decodeToken, deleteInvitation, deleteRole, expireOldInvitations, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getInvitationByToken, getInvitationTemplate, getInvitationWithDetails, getKeyId, getKeySize, getPasswordResetTemplate, getRoleByName, getRolePermissions, getSessionInfo, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, getVerificationCodeTemplate, getWelcomeTemplate, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initializeAuth, invitationsRepository, keysRepository, listInvitations, parseDuration, permissions, permissionsRepository, registerEmailProvider, registerEmailTemplates, registerSMSProvider, removePermissionFromRole, requireAnyPermission, requirePermissions, requireRole, resendInvitation, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, sealSession, sendEmail, sendSMS, setRolePermissions, shouldRefreshSession, shouldRotateKey, unsealSession, updateLastLoginService, updateRole, updateUserProfileService, updateUserService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyKeyFingerprint, verifyPassword, verifyToken };
6704
+ export { type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthDeletionCancelledPayload, type AuthDeletionCompletedPayload, type AuthDeletionConfig, type AuthDeletionRequestedPayload, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, type BeforeRegisterContext, COOKIE_NAMES, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, type DecryptedOAuthToken, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, KeyAlgorithmType, type KeyPair, KeysRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewInvitation, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, OAuthProvider, type OAuthState, type OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifyIdTokenParams, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertCanAssignRole, authDeletionCancelledEvent, authDeletionCompletedEvent, authDeletionRequestedEvent, authJobRouter, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureDeletion, configureOAuthTokenCipher, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, encryptToken, exchangeCodeForTokens, expireOldInvitations, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getDeletionConfig, getDummyPasswordHash, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getKeySize, getLocale, getOneTimeTokenManager, getOptionalAuth, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isEncrypted, isGoogleOAuthEnabled, kakaoProvider, keysRepository, listInvitations, matchOAuthCsrfCookies, naverProvider, oneTimeTokenAuth, parseDuration, permissions, permissionsRepository, purgeUserService, refreshAccessToken, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requirePermissions, requireRole, resendInvitation, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, setRolePermissions, shouldRotateKey, socialAccountsRepository, sweepDuePurges, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyPassword, verifyToken };