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

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 +561 -1827
  3. package/dist/authenticate-ofdEmk6x.d.ts +1109 -0
  4. package/dist/config.d.ts +431 -39
  5. package/dist/config.js +217 -29
  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 +2019 -511
  22. package/dist/server.js +4435 -1157
  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,582 @@ 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
1678
+ * Apple OAuthProvider 구현 (native id_token 검증 전용)
1665
1679
  *
1666
- * @example
1667
- * ```typescript
1668
- * import { registerSMSProvider } from '@spfn/auth/server/services/sms';
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 없음).
1669
1683
  *
1670
- * const twilioProvider = {
1671
- * name: 'twilio',
1672
- * sendSMS: async (params) => {
1673
- * // Twilio implementation
1674
- * return { success: true, messageId: '...' };
1675
- * }
1676
- * };
1684
+ * 따라서 web 메서드(getAuthUrl/exchangeCodeForTokens/getUserInfo)는 지원하지 않는다.
1677
1685
  *
1678
- * registerSMSProvider(twilioProvider);
1679
- * ```
1686
+ * 이 모듈을 import 하는 것만으로 apple provider가 registry에 자기 등록된다.
1680
1687
  */
1681
- declare function registerSMSProvider(provider: SMSProvider): void;
1688
+
1689
+ declare const appleProvider: OAuthProvider;
1690
+
1682
1691
  /**
1683
- * Send SMS using the registered provider
1684
- *
1685
- * Falls back to development mode (console only) if no provider is registered
1686
- *
1687
- * @param params - SMS parameters
1688
- * @returns Send result
1692
+ * GitHub OAuthProvider (web authorization-code flow).
1689
1693
  */
1690
- declare function sendSMS(params: SendSMSParams): Promise<SendSMSResult>;
1694
+
1695
+ declare const githubProvider: OAuthProvider;
1691
1696
 
1692
1697
  /**
1693
- * @spfn/auth - Database Schema Definition
1694
- *
1695
- * Defines the 'spfn_auth' PostgreSQL schema for all auth-related tables
1698
+ * Kakao Login OAuthProvider (web authorization-code flow).
1696
1699
  */
1700
+
1701
+ declare const kakaoProvider: OAuthProvider;
1702
+
1697
1703
  /**
1698
- * Auth schema for all authentication and authorization tables
1699
- * Tables: users, roles, permissions, user_invitations, etc.
1704
+ * Naver Login OAuthProvider (web authorization-code flow).
1700
1705
  */
1701
- declare const authSchema: drizzle_orm_pg_core.PgSchema<string>;
1706
+
1707
+ declare const naverProvider: OAuthProvider;
1702
1708
 
1703
1709
  /**
1704
- * @spfn/auth - User Profiles Entity
1705
- *
1706
- * User profile information (separate from authentication data)
1710
+ * @spfn/auth - Account Deletion Requests Entity
1707
1711
  *
1708
- * Features:
1712
+ * Audit trail for account deletion/recovery. Rows are never deleted — even after
1713
+ * a hard-delete purge — so "who requested/recovered/purged what, when" survives
1714
+ * the user row itself (개인정보보호법 제21조 3항 분리 보존 원칙).
1715
+ */
1716
+ declare const accountDeletionRequests: drizzle_orm_pg_core.PgTableWithColumns<{
1717
+ name: "account_deletion_requests";
1718
+ schema: string;
1719
+ columns: {
1720
+ createdAt: drizzle_orm_pg_core.PgColumn<{
1721
+ name: "created_at";
1722
+ tableName: "account_deletion_requests";
1723
+ dataType: "date";
1724
+ columnType: "PgTimestamp";
1725
+ data: Date;
1726
+ driverParam: string;
1727
+ notNull: true;
1728
+ hasDefault: true;
1729
+ isPrimaryKey: false;
1730
+ isAutoincrement: false;
1731
+ hasRuntimeDefault: false;
1732
+ enumValues: undefined;
1733
+ baseColumn: never;
1734
+ identity: undefined;
1735
+ generated: undefined;
1736
+ }, {}, {}>;
1737
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
1738
+ name: "updated_at";
1739
+ tableName: "account_deletion_requests";
1740
+ dataType: "date";
1741
+ columnType: "PgTimestamp";
1742
+ data: Date;
1743
+ driverParam: string;
1744
+ notNull: true;
1745
+ hasDefault: true;
1746
+ isPrimaryKey: false;
1747
+ isAutoincrement: false;
1748
+ hasRuntimeDefault: false;
1749
+ enumValues: undefined;
1750
+ baseColumn: never;
1751
+ identity: undefined;
1752
+ generated: undefined;
1753
+ }, {}, {}>;
1754
+ id: drizzle_orm_pg_core.PgColumn<{
1755
+ name: "id";
1756
+ tableName: "account_deletion_requests";
1757
+ dataType: "number";
1758
+ columnType: "PgBigSerial53";
1759
+ data: number;
1760
+ driverParam: number;
1761
+ notNull: true;
1762
+ hasDefault: true;
1763
+ isPrimaryKey: true;
1764
+ isAutoincrement: false;
1765
+ hasRuntimeDefault: false;
1766
+ enumValues: undefined;
1767
+ baseColumn: never;
1768
+ identity: undefined;
1769
+ generated: undefined;
1770
+ }, {}, {}>;
1771
+ userId: drizzle_orm_pg_core.PgColumn<{
1772
+ name: `${string}_id`;
1773
+ tableName: "account_deletion_requests";
1774
+ dataType: "number";
1775
+ columnType: "PgBigInt53";
1776
+ data: number;
1777
+ driverParam: string | number;
1778
+ notNull: false;
1779
+ hasDefault: false;
1780
+ isPrimaryKey: false;
1781
+ isAutoincrement: false;
1782
+ hasRuntimeDefault: false;
1783
+ enumValues: undefined;
1784
+ baseColumn: never;
1785
+ identity: undefined;
1786
+ generated: undefined;
1787
+ }, {}, {}>;
1788
+ userPublicId: drizzle_orm_pg_core.PgColumn<{
1789
+ name: "user_public_id";
1790
+ tableName: "account_deletion_requests";
1791
+ dataType: "string";
1792
+ columnType: "PgText";
1793
+ data: string;
1794
+ driverParam: string;
1795
+ notNull: true;
1796
+ hasDefault: false;
1797
+ isPrimaryKey: false;
1798
+ isAutoincrement: false;
1799
+ hasRuntimeDefault: false;
1800
+ enumValues: [string, ...string[]];
1801
+ baseColumn: never;
1802
+ identity: undefined;
1803
+ generated: undefined;
1804
+ }, {}, {}>;
1805
+ requestedAt: drizzle_orm_pg_core.PgColumn<{
1806
+ name: string;
1807
+ tableName: "account_deletion_requests";
1808
+ dataType: "date";
1809
+ columnType: "PgTimestamp";
1810
+ data: Date;
1811
+ driverParam: string;
1812
+ notNull: true;
1813
+ hasDefault: true;
1814
+ isPrimaryKey: false;
1815
+ isAutoincrement: false;
1816
+ hasRuntimeDefault: false;
1817
+ enumValues: undefined;
1818
+ baseColumn: never;
1819
+ identity: undefined;
1820
+ generated: undefined;
1821
+ }, {}, {}>;
1822
+ purgeScheduledAt: drizzle_orm_pg_core.PgColumn<{
1823
+ name: string;
1824
+ tableName: "account_deletion_requests";
1825
+ dataType: "date";
1826
+ columnType: "PgTimestamp";
1827
+ data: Date;
1828
+ driverParam: string;
1829
+ notNull: true;
1830
+ hasDefault: false;
1831
+ isPrimaryKey: false;
1832
+ isAutoincrement: false;
1833
+ hasRuntimeDefault: false;
1834
+ enumValues: undefined;
1835
+ baseColumn: never;
1836
+ identity: undefined;
1837
+ generated: undefined;
1838
+ }, {}, {}>;
1839
+ status: drizzle_orm_pg_core.PgColumn<{
1840
+ name: string;
1841
+ tableName: "account_deletion_requests";
1842
+ dataType: "string";
1843
+ columnType: "PgText";
1844
+ data: "pending" | "cancelled" | "completed";
1845
+ driverParam: string;
1846
+ notNull: true;
1847
+ hasDefault: true;
1848
+ isPrimaryKey: false;
1849
+ isAutoincrement: false;
1850
+ hasRuntimeDefault: false;
1851
+ enumValues: ["pending", "cancelled", "completed"] & [string, ...string[]];
1852
+ baseColumn: never;
1853
+ identity: undefined;
1854
+ generated: undefined;
1855
+ }, {}, {}>;
1856
+ requestedBy: drizzle_orm_pg_core.PgColumn<{
1857
+ name: string;
1858
+ tableName: "account_deletion_requests";
1859
+ dataType: "string";
1860
+ columnType: "PgText";
1861
+ data: "admin" | "self";
1862
+ driverParam: string;
1863
+ notNull: true;
1864
+ hasDefault: true;
1865
+ isPrimaryKey: false;
1866
+ isAutoincrement: false;
1867
+ hasRuntimeDefault: false;
1868
+ enumValues: ["self", "admin"] & [string, ...string[]];
1869
+ baseColumn: never;
1870
+ identity: undefined;
1871
+ generated: undefined;
1872
+ }, {}, {}>;
1873
+ reason: drizzle_orm_pg_core.PgColumn<{
1874
+ name: "reason";
1875
+ tableName: "account_deletion_requests";
1876
+ dataType: "string";
1877
+ columnType: "PgText";
1878
+ data: string;
1879
+ driverParam: string;
1880
+ notNull: false;
1881
+ hasDefault: false;
1882
+ isPrimaryKey: false;
1883
+ isAutoincrement: false;
1884
+ hasRuntimeDefault: false;
1885
+ enumValues: [string, ...string[]];
1886
+ baseColumn: never;
1887
+ identity: undefined;
1888
+ generated: undefined;
1889
+ }, {}, {}>;
1890
+ cancelledAt: drizzle_orm_pg_core.PgColumn<{
1891
+ name: string;
1892
+ tableName: "account_deletion_requests";
1893
+ dataType: "date";
1894
+ columnType: "PgTimestamp";
1895
+ data: Date;
1896
+ driverParam: string;
1897
+ notNull: false;
1898
+ hasDefault: false;
1899
+ isPrimaryKey: false;
1900
+ isAutoincrement: false;
1901
+ hasRuntimeDefault: false;
1902
+ enumValues: undefined;
1903
+ baseColumn: never;
1904
+ identity: undefined;
1905
+ generated: undefined;
1906
+ }, {}, {}>;
1907
+ completedAt: drizzle_orm_pg_core.PgColumn<{
1908
+ name: string;
1909
+ tableName: "account_deletion_requests";
1910
+ dataType: "date";
1911
+ columnType: "PgTimestamp";
1912
+ data: Date;
1913
+ driverParam: string;
1914
+ notNull: false;
1915
+ hasDefault: false;
1916
+ isPrimaryKey: false;
1917
+ isAutoincrement: false;
1918
+ hasRuntimeDefault: false;
1919
+ enumValues: undefined;
1920
+ baseColumn: never;
1921
+ identity: undefined;
1922
+ generated: undefined;
1923
+ }, {}, {}>;
1924
+ purgeStrategy: drizzle_orm_pg_core.PgColumn<{
1925
+ name: string;
1926
+ tableName: "account_deletion_requests";
1927
+ dataType: "string";
1928
+ columnType: "PgText";
1929
+ data: "anonymize" | "hard-delete";
1930
+ driverParam: string;
1931
+ notNull: false;
1932
+ hasDefault: false;
1933
+ isPrimaryKey: false;
1934
+ isAutoincrement: false;
1935
+ hasRuntimeDefault: false;
1936
+ enumValues: ["anonymize", "hard-delete"] & [string, ...string[]];
1937
+ baseColumn: never;
1938
+ identity: undefined;
1939
+ generated: undefined;
1940
+ }, {}, {}>;
1941
+ };
1942
+ dialect: "pg";
1943
+ }>;
1944
+ type AccountDeletionRequest = typeof accountDeletionRequests.$inferSelect;
1945
+ type NewAccountDeletionRequest = typeof accountDeletionRequests.$inferInsert;
1946
+
1947
+ /**
1948
+ * @spfn/auth - Account Deletion Service
1949
+ *
1950
+ * Request / cancel (recovery) / purge for the account deletion lifecycle.
1951
+ *
1952
+ * Lifecycle:
1953
+ * active --request(reauth)--> pending_deletion --grace expires(cron)--> deleted (anonymize) | row removed (hard-delete)
1954
+ * ^ |
1955
+ * +-------cancel(reauth)---------+ immediate = grace period of 0, same pipeline
1956
+ */
1957
+
1958
+ interface PendingDeletionInfo {
1959
+ purgeScheduledAt: Date;
1960
+ }
1961
+ /**
1962
+ * Look up the pending deletion request's `purgeScheduledAt` for a `pending_deletion`
1963
+ * user, so login/OAuth/authenticate can surface it on `AccountPendingDeletionError`.
1964
+ * Returns null if there is no pending row (shouldn't normally happen for a user in
1965
+ * that status, but the caller falls back to an error without the date).
1966
+ *
1967
+ * Reads from the write primary, not a replica — this backs the same status gates
1968
+ * that decide whether to grant a session, so a replica-lag window must not let a
1969
+ * just-requested deletion go unnoticed.
1970
+ */
1971
+ declare function getPendingDeletionInfo(userId: number): Promise<PendingDeletionInfo | null>;
1972
+ interface RequestAccountDeletionParams {
1973
+ /** Who initiated this — 'self' requires re-auth via password/verificationToken; 'admin' does not. */
1974
+ requestedBy: AccountDeletionRequestedBy;
1975
+ password?: string;
1976
+ verificationToken?: string;
1977
+ reason?: string;
1978
+ /** Skip the grace period and purge inline. Self-service requires `deletion.allowSelfImmediate`. */
1979
+ immediate?: boolean;
1980
+ }
1981
+ interface RequestAccountDeletionResult {
1982
+ requestId: number;
1983
+ purgeScheduledAt: Date;
1984
+ }
1985
+ /**
1986
+ * Request account deletion (self-service re-auth gate, or trusted admin/DSR call).
1987
+ *
1988
+ * Sets status -> 'pending_deletion', records the request, revokes every active
1989
+ * session key, and (after commit) emits `auth.deletion.requested` and sends the
1990
+ * request-received notice. With a zero grace period (explicit `immediate: true`,
1991
+ * gated for self-service by `allowSelfImmediate`), purges inline instead of
1992
+ * waiting for the cron sweep.
1993
+ */
1994
+ declare function requestAccountDeletionService(userId: number, params: RequestAccountDeletionParams): Promise<RequestAccountDeletionResult>;
1995
+ interface CancelAccountDeletionParams {
1996
+ email?: string;
1997
+ phone?: string;
1998
+ password?: string;
1999
+ verificationToken?: string;
2000
+ }
2001
+ interface CancelAccountDeletionResult {
2002
+ userId: string;
2003
+ }
2004
+ /**
2005
+ * Cancel a pending deletion (recovery). All sessions were revoked at request
2006
+ * time, so this is credential-based (no Bearer token) rather than auth-context-based.
2007
+ *
2008
+ * Credential verification always runs before the caller learns whether the
2009
+ * account exists / is pending deletion — otherwise this endpoint could be used
2010
+ * to enumerate accounts or probe deletion status without a valid credential,
2011
+ * the same posture `loginService` takes for missing accounts.
2012
+ *
2013
+ * Claims the `account_deletion_requests` row (conditional `WHERE status =
2014
+ * 'pending'`) before touching `users` — see the comment at that claim below for
2015
+ * why the `users` row must not move until the claim succeeds.
2016
+ */
2017
+ declare function cancelAccountDeletionService(params: CancelAccountDeletionParams): Promise<CancelAccountDeletionResult>;
2018
+ interface PurgeUserResult {
2019
+ outcome: 'purged' | 'skipped' | 'not-found';
2020
+ }
2021
+ /**
2022
+ * Purge a user's pending deletion request immediately, ignoring `purgeScheduledAt`.
2023
+ * Admin/GDPR-response entry point — the route (if any) belongs to the app.
2024
+ *
2025
+ * @throws DeletionNotRequestedError if the user has no pending request
2026
+ */
2027
+ declare function purgeUserService(userId: number): Promise<PurgeUserResult>;
2028
+ interface SweepDuePurgesResult {
2029
+ processed: number;
2030
+ purged: number;
2031
+ skipped: number;
2032
+ }
2033
+ /**
2034
+ * Sweep every request whose grace period has elapsed and purge it. Called by the
2035
+ * `auth.deletion.purge` cron job. One user's failure (thrown error, not just an
2036
+ * `onBeforePurge` skip) is logged and does not stop the rest of the sweep — it
2037
+ * stays `pending` and is retried on the next tick.
2038
+ */
2039
+ declare function sweepDuePurges(now?: Date): Promise<SweepDuePurgesResult>;
2040
+
2041
+ /**
2042
+ * @spfn/auth - Database Schema Definition
2043
+ *
2044
+ * Defines the 'spfn_auth' PostgreSQL schema for all auth-related tables
2045
+ */
2046
+ /**
2047
+ * Auth schema for all authentication and authorization tables
2048
+ * Tables: users, roles, permissions, user_invitations, etc.
2049
+ */
2050
+ declare const authSchema: drizzle_orm_pg_core.PgSchema<string>;
2051
+
2052
+ /**
2053
+ * @spfn/auth - User Profiles Entity
2054
+ *
2055
+ * User profile information (separate from authentication data)
2056
+ *
2057
+ * Features:
1709
2058
  * - Display name and personal information
1710
2059
  * - Avatar and bio
1711
2060
  * - Locale and timezone preferences
@@ -1771,11 +2120,11 @@ declare const userProfiles: drizzle_orm_pg_core.PgTableWithColumns<{
1771
2120
  name: `${string}_id`;
1772
2121
  tableName: "user_profiles";
1773
2122
  dataType: "number";
1774
- columnType: "PgBigSerial53";
2123
+ columnType: "PgBigInt53";
1775
2124
  data: number;
1776
- driverParam: number;
2125
+ driverParam: string | number;
1777
2126
  notNull: true;
1778
- hasDefault: true;
2127
+ hasDefault: false;
1779
2128
  isPrimaryKey: false;
1780
2129
  isAutoincrement: false;
1781
2130
  hasRuntimeDefault: false;
@@ -1791,7 +2140,7 @@ declare const userProfiles: drizzle_orm_pg_core.PgTableWithColumns<{
1791
2140
  columnType: "PgText";
1792
2141
  data: string;
1793
2142
  driverParam: string;
1794
- notNull: true;
2143
+ notNull: false;
1795
2144
  hasDefault: false;
1796
2145
  isPrimaryKey: false;
1797
2146
  isAutoincrement: false;
@@ -2065,11 +2414,11 @@ declare const userPublicKeys: drizzle_orm_pg_core.PgTableWithColumns<{
2065
2414
  name: `${string}_id`;
2066
2415
  tableName: "user_public_keys";
2067
2416
  dataType: "number";
2068
- columnType: "PgBigSerial53";
2417
+ columnType: "PgBigInt53";
2069
2418
  data: number;
2070
- driverParam: number;
2419
+ driverParam: string | number;
2071
2420
  notNull: true;
2072
- hasDefault: true;
2421
+ hasDefault: false;
2073
2422
  isPrimaryKey: false;
2074
2423
  isAutoincrement: false;
2075
2424
  hasRuntimeDefault: false;
@@ -2318,11 +2667,11 @@ declare const userSocialAccounts: drizzle_orm_pg_core.PgTableWithColumns<{
2318
2667
  name: `${string}_id`;
2319
2668
  tableName: "user_social_accounts";
2320
2669
  dataType: "number";
2321
- columnType: "PgBigSerial53";
2670
+ columnType: "PgBigInt53";
2322
2671
  data: number;
2323
- driverParam: number;
2672
+ driverParam: string | number;
2324
2673
  notNull: true;
2325
- hasDefault: true;
2674
+ hasDefault: false;
2326
2675
  isPrimaryKey: false;
2327
2676
  isAutoincrement: false;
2328
2677
  hasRuntimeDefault: false;
@@ -2336,14 +2685,14 @@ declare const userSocialAccounts: drizzle_orm_pg_core.PgTableWithColumns<{
2336
2685
  tableName: "user_social_accounts";
2337
2686
  dataType: "string";
2338
2687
  columnType: "PgText";
2339
- data: "google" | "github" | "kakao" | "naver";
2688
+ data: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
2340
2689
  driverParam: string;
2341
2690
  notNull: true;
2342
2691
  hasDefault: false;
2343
2692
  isPrimaryKey: false;
2344
2693
  isAutoincrement: false;
2345
2694
  hasRuntimeDefault: false;
2346
- enumValues: ["google", "github", "kakao", "naver"] & [string, ...string[]];
2695
+ enumValues: ["google", "apple", "github", "kakao", "naver", "superself"] & [string, ...string[]];
2347
2696
  baseColumn: never;
2348
2697
  identity: undefined;
2349
2698
  generated: undefined;
@@ -2556,14 +2905,14 @@ declare const verificationCodes: drizzle_orm_pg_core.PgTableWithColumns<{
2556
2905
  tableName: "verification_codes";
2557
2906
  dataType: "string";
2558
2907
  columnType: "PgText";
2559
- data: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
2908
+ data: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
2560
2909
  driverParam: string;
2561
2910
  notNull: true;
2562
2911
  hasDefault: false;
2563
2912
  isPrimaryKey: false;
2564
2913
  isAutoincrement: false;
2565
2914
  hasRuntimeDefault: false;
2566
- enumValues: ["registration", "login", "password_reset", "email_change", "phone_change"] & [string, ...string[]];
2915
+ enumValues: ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"] & [string, ...string[]];
2567
2916
  baseColumn: never;
2568
2917
  identity: undefined;
2569
2918
  generated: undefined;
@@ -2747,7 +3096,7 @@ declare const permissions: drizzle_orm_pg_core.PgTableWithColumns<{
2747
3096
  tableName: "permissions";
2748
3097
  dataType: "string";
2749
3098
  columnType: "PgText";
2750
- data: "custom" | "user" | "auth" | "rbac" | "system";
3099
+ data: "auth" | "custom" | "user" | "rbac" | "system";
2751
3100
  driverParam: string;
2752
3101
  notNull: false;
2753
3102
  hasDefault: false;
@@ -2905,11 +3254,11 @@ declare const rolePermissions: drizzle_orm_pg_core.PgTableWithColumns<{
2905
3254
  name: `${string}_id`;
2906
3255
  tableName: "role_permissions";
2907
3256
  dataType: "number";
2908
- columnType: "PgBigSerial53";
3257
+ columnType: "PgBigInt53";
2909
3258
  data: number;
2910
- driverParam: number;
3259
+ driverParam: string | number;
2911
3260
  notNull: true;
2912
- hasDefault: true;
3261
+ hasDefault: false;
2913
3262
  isPrimaryKey: false;
2914
3263
  isAutoincrement: false;
2915
3264
  hasRuntimeDefault: false;
@@ -2922,11 +3271,11 @@ declare const rolePermissions: drizzle_orm_pg_core.PgTableWithColumns<{
2922
3271
  name: `${string}_id`;
2923
3272
  tableName: "role_permissions";
2924
3273
  dataType: "number";
2925
- columnType: "PgBigSerial53";
3274
+ columnType: "PgBigInt53";
2926
3275
  data: number;
2927
- driverParam: number;
3276
+ driverParam: string | number;
2928
3277
  notNull: true;
2929
- hasDefault: true;
3278
+ hasDefault: false;
2930
3279
  isPrimaryKey: false;
2931
3280
  isAutoincrement: false;
2932
3281
  hasRuntimeDefault: false;
@@ -3014,11 +3363,11 @@ declare const userPermissions: drizzle_orm_pg_core.PgTableWithColumns<{
3014
3363
  name: `${string}_id`;
3015
3364
  tableName: "user_permissions";
3016
3365
  dataType: "number";
3017
- columnType: "PgBigSerial53";
3366
+ columnType: "PgBigInt53";
3018
3367
  data: number;
3019
- driverParam: number;
3368
+ driverParam: string | number;
3020
3369
  notNull: true;
3021
- hasDefault: true;
3370
+ hasDefault: false;
3022
3371
  isPrimaryKey: false;
3023
3372
  isAutoincrement: false;
3024
3373
  hasRuntimeDefault: false;
@@ -3031,11 +3380,11 @@ declare const userPermissions: drizzle_orm_pg_core.PgTableWithColumns<{
3031
3380
  name: `${string}_id`;
3032
3381
  tableName: "user_permissions";
3033
3382
  dataType: "number";
3034
- columnType: "PgBigSerial53";
3383
+ columnType: "PgBigInt53";
3035
3384
  data: number;
3036
- driverParam: number;
3385
+ driverParam: string | number;
3037
3386
  notNull: true;
3038
- hasDefault: true;
3387
+ hasDefault: false;
3039
3388
  isPrimaryKey: false;
3040
3389
  isAutoincrement: false;
3041
3390
  hasRuntimeDefault: false;
@@ -3101,6 +3450,73 @@ declare const userPermissions: drizzle_orm_pg_core.PgTableWithColumns<{
3101
3450
  type UserPermission = typeof userPermissions.$inferSelect;
3102
3451
  type NewUserPermission = typeof userPermissions.$inferInsert;
3103
3452
 
3453
+ /**
3454
+ * @spfn/auth - Auth Metadata Entity
3455
+ *
3456
+ * Key-value store for auth system metadata
3457
+ * Used for storing RBAC config hash and other system-level settings
3458
+ */
3459
+ declare const authMetadata: drizzle_orm_pg_core.PgTableWithColumns<{
3460
+ name: "auth_metadata";
3461
+ schema: string;
3462
+ columns: {
3463
+ key: drizzle_orm_pg_core.PgColumn<{
3464
+ name: "key";
3465
+ tableName: "auth_metadata";
3466
+ dataType: "string";
3467
+ columnType: "PgText";
3468
+ data: string;
3469
+ driverParam: string;
3470
+ notNull: true;
3471
+ hasDefault: false;
3472
+ isPrimaryKey: true;
3473
+ isAutoincrement: false;
3474
+ hasRuntimeDefault: false;
3475
+ enumValues: [string, ...string[]];
3476
+ baseColumn: never;
3477
+ identity: undefined;
3478
+ generated: undefined;
3479
+ }, {}, {}>;
3480
+ value: drizzle_orm_pg_core.PgColumn<{
3481
+ name: "value";
3482
+ tableName: "auth_metadata";
3483
+ dataType: "string";
3484
+ columnType: "PgText";
3485
+ data: string;
3486
+ driverParam: string;
3487
+ notNull: true;
3488
+ hasDefault: false;
3489
+ isPrimaryKey: false;
3490
+ isAutoincrement: false;
3491
+ hasRuntimeDefault: false;
3492
+ enumValues: [string, ...string[]];
3493
+ baseColumn: never;
3494
+ identity: undefined;
3495
+ generated: undefined;
3496
+ }, {}, {}>;
3497
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
3498
+ name: "updated_at";
3499
+ tableName: "auth_metadata";
3500
+ dataType: "date";
3501
+ columnType: "PgTimestamp";
3502
+ data: Date;
3503
+ driverParam: string;
3504
+ notNull: true;
3505
+ hasDefault: true;
3506
+ isPrimaryKey: false;
3507
+ isAutoincrement: false;
3508
+ hasRuntimeDefault: false;
3509
+ enumValues: undefined;
3510
+ baseColumn: never;
3511
+ identity: undefined;
3512
+ generated: undefined;
3513
+ }, {}, {}>;
3514
+ };
3515
+ dialect: "pg";
3516
+ }>;
3517
+ type AuthMetadataEntity = typeof authMetadata.$inferSelect;
3518
+ type NewAuthMetadataEntity = typeof authMetadata.$inferInsert;
3519
+
3104
3520
  /**
3105
3521
  * Users Repository
3106
3522
  *
@@ -3122,15 +3538,44 @@ declare class UsersRepository extends BaseRepository {
3122
3538
  * Read replica 사용
3123
3539
  */
3124
3540
  findById(id: number): Promise<{
3541
+ deletedAt: Date | null;
3542
+ deletedBy: string | null;
3543
+ createdAt: Date;
3544
+ updatedAt: Date;
3545
+ id: number;
3546
+ publicId: string;
3547
+ email: string | null;
3548
+ phone: string | null;
3549
+ username: string | null;
3550
+ passwordHash: string | null;
3551
+ passwordChangeRequired: boolean;
3552
+ roleId: number;
3553
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3554
+ emailVerifiedAt: Date | null;
3555
+ phoneVerifiedAt: Date | null;
3556
+ lastLoginAt: Date | null;
3557
+ }>;
3558
+ /**
3559
+ * ID로 사용자 조회 — Write primary에서 직접 읽는다.
3560
+ *
3561
+ * 복제 지연 창에서 status 전이(예: 삭제 요청으로 pending_deletion 전환)를 놓치면
3562
+ * 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
3563
+ * 계속 사용할 것.
3564
+ */
3565
+ findByIdOnPrimary(id: number): Promise<{
3566
+ deletedAt: Date | null;
3567
+ deletedBy: string | null;
3125
3568
  createdAt: Date;
3126
3569
  updatedAt: Date;
3127
3570
  id: number;
3571
+ publicId: string;
3128
3572
  email: string | null;
3129
3573
  phone: string | null;
3574
+ username: string | null;
3130
3575
  passwordHash: string | null;
3131
3576
  passwordChangeRequired: boolean;
3132
3577
  roleId: number;
3133
- status: "active" | "inactive" | "suspended";
3578
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3134
3579
  emailVerifiedAt: Date | null;
3135
3580
  phoneVerifiedAt: Date | null;
3136
3581
  lastLoginAt: Date | null;
@@ -3140,15 +3585,19 @@ declare class UsersRepository extends BaseRepository {
3140
3585
  * Read replica 사용
3141
3586
  */
3142
3587
  findByEmail(email: string): Promise<{
3588
+ deletedAt: Date | null;
3589
+ deletedBy: string | null;
3143
3590
  createdAt: Date;
3144
3591
  updatedAt: Date;
3145
3592
  id: number;
3593
+ publicId: string;
3146
3594
  email: string | null;
3147
3595
  phone: string | null;
3596
+ username: string | null;
3148
3597
  passwordHash: string | null;
3149
3598
  passwordChangeRequired: boolean;
3150
3599
  roleId: number;
3151
- status: "active" | "inactive" | "suspended";
3600
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3152
3601
  emailVerifiedAt: Date | null;
3153
3602
  phoneVerifiedAt: Date | null;
3154
3603
  lastLoginAt: Date | null;
@@ -3158,15 +3607,63 @@ declare class UsersRepository extends BaseRepository {
3158
3607
  * Read replica 사용
3159
3608
  */
3160
3609
  findByPhone(phone: string): Promise<{
3610
+ deletedAt: Date | null;
3611
+ deletedBy: string | null;
3161
3612
  createdAt: Date;
3162
3613
  updatedAt: Date;
3163
3614
  id: number;
3615
+ publicId: string;
3164
3616
  email: string | null;
3165
3617
  phone: string | null;
3618
+ username: string | null;
3166
3619
  passwordHash: string | null;
3167
3620
  passwordChangeRequired: boolean;
3168
3621
  roleId: number;
3169
- status: "active" | "inactive" | "suspended";
3622
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3623
+ emailVerifiedAt: Date | null;
3624
+ phoneVerifiedAt: Date | null;
3625
+ lastLoginAt: Date | null;
3626
+ }>;
3627
+ /**
3628
+ * 사용자명으로 사용자 조회
3629
+ * Read replica 사용
3630
+ */
3631
+ findByUsername(username: string): Promise<{
3632
+ deletedAt: Date | null;
3633
+ deletedBy: string | null;
3634
+ createdAt: Date;
3635
+ updatedAt: Date;
3636
+ id: number;
3637
+ publicId: string;
3638
+ email: string | null;
3639
+ phone: string | null;
3640
+ username: string | null;
3641
+ passwordHash: string | null;
3642
+ passwordChangeRequired: boolean;
3643
+ roleId: number;
3644
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3645
+ emailVerifiedAt: Date | null;
3646
+ phoneVerifiedAt: Date | null;
3647
+ lastLoginAt: Date | null;
3648
+ }>;
3649
+ /**
3650
+ * Public ID(UUID)로 사용자 조회
3651
+ * Read replica 사용
3652
+ */
3653
+ findByPublicId(publicId: string): Promise<{
3654
+ deletedAt: Date | null;
3655
+ deletedBy: string | null;
3656
+ createdAt: Date;
3657
+ updatedAt: Date;
3658
+ id: number;
3659
+ publicId: string;
3660
+ email: string | null;
3661
+ phone: string | null;
3662
+ username: string | null;
3663
+ passwordHash: string | null;
3664
+ passwordChangeRequired: boolean;
3665
+ roleId: number;
3666
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3170
3667
  emailVerifiedAt: Date | null;
3171
3668
  phoneVerifiedAt: Date | null;
3172
3669
  lastLoginAt: Date | null;
@@ -3176,19 +3673,54 @@ declare class UsersRepository extends BaseRepository {
3176
3673
  * Read replica 사용
3177
3674
  */
3178
3675
  findByEmailOrPhone(email?: string, phone?: string): Promise<{
3676
+ deletedAt: Date | null;
3677
+ deletedBy: string | null;
3179
3678
  createdAt: Date;
3180
3679
  updatedAt: Date;
3181
3680
  id: number;
3681
+ publicId: string;
3182
3682
  email: string | null;
3183
3683
  phone: string | null;
3684
+ username: string | null;
3184
3685
  passwordHash: string | null;
3185
3686
  passwordChangeRequired: boolean;
3186
3687
  roleId: number;
3187
- status: "active" | "inactive" | "suspended";
3688
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3188
3689
  emailVerifiedAt: Date | null;
3189
3690
  phoneVerifiedAt: Date | null;
3190
3691
  lastLoginAt: Date | null;
3191
3692
  } | null>;
3693
+ /**
3694
+ * ID로 사용자 + Role 조회 (leftJoin)
3695
+ * Read replica 사용
3696
+ *
3697
+ * roleId가 null인 유저는 role: null 반환
3698
+ */
3699
+ findByIdWithRole(id: number): Promise<{
3700
+ user: {
3701
+ deletedAt: Date | null;
3702
+ deletedBy: string | null;
3703
+ createdAt: Date;
3704
+ updatedAt: Date;
3705
+ id: number;
3706
+ publicId: string;
3707
+ email: string | null;
3708
+ phone: string | null;
3709
+ username: string | null;
3710
+ passwordHash: string | null;
3711
+ passwordChangeRequired: boolean;
3712
+ roleId: number;
3713
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3714
+ emailVerifiedAt: Date | null;
3715
+ phoneVerifiedAt: Date | null;
3716
+ lastLoginAt: Date | null;
3717
+ };
3718
+ role: {
3719
+ name: string;
3720
+ displayName: string;
3721
+ priority: number;
3722
+ } | null;
3723
+ } | null>;
3192
3724
  /**
3193
3725
  * 사용자 생성
3194
3726
  * Write primary 사용
@@ -3196,31 +3728,66 @@ declare class UsersRepository extends BaseRepository {
3196
3728
  create(data: NewUser): Promise<{
3197
3729
  email: string | null;
3198
3730
  phone: string | null;
3199
- status: "active" | "inactive" | "suspended";
3200
3731
  id: number;
3201
- createdAt: Date;
3202
- updatedAt: Date;
3732
+ publicId: string;
3733
+ username: string | null;
3203
3734
  passwordHash: string | null;
3204
3735
  passwordChangeRequired: boolean;
3205
3736
  roleId: number;
3737
+ createdAt: Date;
3738
+ updatedAt: Date;
3739
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3206
3740
  emailVerifiedAt: Date | null;
3207
3741
  phoneVerifiedAt: Date | null;
3208
3742
  lastLoginAt: Date | null;
3743
+ deletedAt: Date | null;
3744
+ deletedBy: string | null;
3209
3745
  }>;
3210
3746
  /**
3211
3747
  * 사용자 정보 업데이트
3212
3748
  * Write primary 사용
3213
3749
  */
3214
3750
  updateById(id: number, data: Partial<NewUser>): Promise<{
3751
+ deletedAt: Date | null;
3752
+ deletedBy: string | null;
3215
3753
  createdAt: Date;
3216
3754
  updatedAt: Date;
3217
3755
  id: number;
3756
+ publicId: string;
3218
3757
  email: string | null;
3219
3758
  phone: string | null;
3759
+ username: string | null;
3220
3760
  passwordHash: string | null;
3221
3761
  passwordChangeRequired: boolean;
3222
3762
  roleId: number;
3223
- status: "active" | "inactive" | "suspended";
3763
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3764
+ emailVerifiedAt: Date | null;
3765
+ phoneVerifiedAt: Date | null;
3766
+ lastLoginAt: Date | null;
3767
+ }>;
3768
+ /**
3769
+ * 계정 삭제 취소(복구) — `WHERE status = 'pending_deletion'` 조건부 UPDATE.
3770
+ *
3771
+ * cancelAccountDeletionService가 account_deletion_requests claim(조건부
3772
+ * markCancelled)에 성공한 **이후에만** 호출한다. 그 claim이 이미 purge와의
3773
+ * 경합을 해결하므로 이 조건은 방어적 이중 안전장치 — 0 row 매치(= 이미
3774
+ * status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
3775
+ * Write primary 사용
3776
+ */
3777
+ reactivateFromPendingDeletion(id: number): Promise<{
3778
+ deletedAt: Date | null;
3779
+ deletedBy: string | null;
3780
+ createdAt: Date;
3781
+ updatedAt: Date;
3782
+ id: number;
3783
+ publicId: string;
3784
+ email: string | null;
3785
+ phone: string | null;
3786
+ username: string | null;
3787
+ passwordHash: string | null;
3788
+ passwordChangeRequired: boolean;
3789
+ roleId: number;
3790
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3224
3791
  emailVerifiedAt: Date | null;
3225
3792
  phoneVerifiedAt: Date | null;
3226
3793
  lastLoginAt: Date | null;
@@ -3230,15 +3797,19 @@ declare class UsersRepository extends BaseRepository {
3230
3797
  * Write primary 사용
3231
3798
  */
3232
3799
  updatePassword(id: number, passwordHash: string, clearPasswordChangeRequired?: boolean): Promise<{
3800
+ deletedAt: Date | null;
3801
+ deletedBy: string | null;
3233
3802
  createdAt: Date;
3234
3803
  updatedAt: Date;
3235
3804
  id: number;
3805
+ publicId: string;
3236
3806
  email: string | null;
3237
3807
  phone: string | null;
3808
+ username: string | null;
3238
3809
  passwordHash: string | null;
3239
3810
  passwordChangeRequired: boolean;
3240
3811
  roleId: number;
3241
- status: "active" | "inactive" | "suspended";
3812
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3242
3813
  emailVerifiedAt: Date | null;
3243
3814
  phoneVerifiedAt: Date | null;
3244
3815
  lastLoginAt: Date | null;
@@ -3248,15 +3819,19 @@ declare class UsersRepository extends BaseRepository {
3248
3819
  * Write primary 사용
3249
3820
  */
3250
3821
  updateLastLogin(id: number): Promise<{
3822
+ deletedAt: Date | null;
3823
+ deletedBy: string | null;
3251
3824
  createdAt: Date;
3252
3825
  updatedAt: Date;
3253
3826
  id: number;
3827
+ publicId: string;
3254
3828
  email: string | null;
3255
3829
  phone: string | null;
3830
+ username: string | null;
3256
3831
  passwordHash: string | null;
3257
3832
  passwordChangeRequired: boolean;
3258
3833
  roleId: number;
3259
- status: "active" | "inactive" | "suspended";
3834
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3260
3835
  emailVerifiedAt: Date | null;
3261
3836
  phoneVerifiedAt: Date | null;
3262
3837
  lastLoginAt: Date | null;
@@ -3268,16 +3843,20 @@ declare class UsersRepository extends BaseRepository {
3268
3843
  deleteById(id: number): Promise<{
3269
3844
  email: string | null;
3270
3845
  phone: string | null;
3271
- status: "active" | "inactive" | "suspended";
3272
3846
  id: number;
3273
- createdAt: Date;
3274
- updatedAt: Date;
3847
+ publicId: string;
3848
+ username: string | null;
3275
3849
  passwordHash: string | null;
3276
3850
  passwordChangeRequired: boolean;
3277
3851
  roleId: number;
3852
+ createdAt: Date;
3853
+ updatedAt: Date;
3854
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3278
3855
  emailVerifiedAt: Date | null;
3279
3856
  phoneVerifiedAt: Date | null;
3280
3857
  lastLoginAt: Date | null;
3858
+ deletedAt: Date | null;
3859
+ deletedBy: string | null;
3281
3860
  }>;
3282
3861
  /**
3283
3862
  * User의 Role과 Permissions 조회 (JOIN)
@@ -3297,7 +3876,7 @@ declare class UsersRepository extends BaseRepository {
3297
3876
  id: number;
3298
3877
  name: string;
3299
3878
  displayName: string;
3300
- category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
3879
+ category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
3301
3880
  }[];
3302
3881
  }>;
3303
3882
  /**
@@ -3309,9 +3888,12 @@ declare class UsersRepository extends BaseRepository {
3309
3888
  */
3310
3889
  fetchMinimalUserData(userId: number): Promise<{
3311
3890
  userId: number;
3891
+ publicId: string;
3312
3892
  email: string | null;
3893
+ username: string | null;
3313
3894
  isEmailVerified: boolean;
3314
3895
  isPhoneVerified: boolean;
3896
+ hasPassword: boolean;
3315
3897
  }>;
3316
3898
  /**
3317
3899
  * Full user data 조회 (user profile용)
@@ -3322,7 +3904,9 @@ declare class UsersRepository extends BaseRepository {
3322
3904
  */
3323
3905
  fetchFullUserData(userId: number): Promise<{
3324
3906
  userId: number;
3907
+ publicId: string;
3325
3908
  email: string | null;
3909
+ username: string | null;
3326
3910
  isEmailVerified: boolean;
3327
3911
  isPhoneVerified: boolean;
3328
3912
  lastLoginAt: Date | null;
@@ -3407,24 +3991,45 @@ declare class KeysRepository extends BaseRepository {
3407
3991
  * Write primary 사용
3408
3992
  */
3409
3993
  create(data: NewUserPublicKey): Promise<{
3410
- userId: number;
3994
+ publicKey: string;
3411
3995
  keyId: string;
3996
+ fingerprint: string;
3997
+ algorithm: "ES256" | "RS256";
3998
+ userId: number;
3412
3999
  id: number;
3413
4000
  isActive: boolean;
3414
4001
  createdAt: Date;
4002
+ expiresAt: Date | null;
4003
+ lastUsedAt: Date | null;
4004
+ revokedAt: Date | null;
4005
+ revokedReason: string | null;
4006
+ }>;
4007
+ /**
4008
+ * 공개키 revoke (비활성화)
4009
+ * Write primary 사용
4010
+ */
4011
+ revokeByKeyIdAndUserId(keyId: string, userId: number, reason: string): Promise<{
4012
+ id: number;
4013
+ userId: number;
4014
+ keyId: string;
3415
4015
  publicKey: string;
3416
4016
  algorithm: "ES256" | "RS256";
3417
4017
  fingerprint: string;
4018
+ isActive: boolean;
4019
+ createdAt: Date;
3418
4020
  lastUsedAt: Date | null;
3419
4021
  expiresAt: Date | null;
3420
4022
  revokedAt: Date | null;
3421
4023
  revokedReason: string | null;
3422
4024
  }>;
3423
4025
  /**
3424
- * 공개키 revoke (비활성화)
4026
+ * 사용자의 모든 활성 공개키 revoke (비활성화)
4027
+ *
4028
+ * 비번 변경 시 전체 세션 로그아웃에 사용. authenticate는 활성 키만 검증하므로,
4029
+ * revoke된 키로 서명한 기존 세션의 요청은 즉시 401이 된다.
3425
4030
  * Write primary 사용
3426
4031
  */
3427
- revokeByKeyIdAndUserId(keyId: string, userId: number, reason: string): Promise<{
4032
+ revokeAllActiveByUserId(userId: number, reason: string): Promise<{
3428
4033
  id: number;
3429
4034
  userId: number;
3430
4035
  keyId: string;
@@ -3437,25 +4042,33 @@ declare class KeysRepository extends BaseRepository {
3437
4042
  expiresAt: Date | null;
3438
4043
  revokedAt: Date | null;
3439
4044
  revokedReason: string | null;
3440
- }>;
4045
+ }[]>;
3441
4046
  /**
3442
4047
  * 공개키 삭제
3443
4048
  * Write primary 사용
3444
4049
  */
3445
4050
  deleteByKeyIdAndUserId(keyId: string, userId: number): Promise<{
3446
- userId: number;
4051
+ publicKey: string;
3447
4052
  keyId: string;
4053
+ fingerprint: string;
4054
+ algorithm: "ES256" | "RS256";
4055
+ userId: number;
3448
4056
  id: number;
3449
4057
  isActive: boolean;
3450
4058
  createdAt: Date;
3451
- publicKey: string;
3452
- algorithm: "ES256" | "RS256";
3453
- fingerprint: string;
3454
- lastUsedAt: Date | null;
3455
4059
  expiresAt: Date | null;
4060
+ lastUsedAt: Date | null;
3456
4061
  revokedAt: Date | null;
3457
4062
  revokedReason: string | null;
3458
4063
  }>;
4064
+ /**
4065
+ * 사용자의 모든 공개키 삭제 (계정 익명화 파기용)
4066
+ *
4067
+ * hard-delete는 FK cascade로 자동 처리되지만, anonymize 모드는 users row를
4068
+ * 남기므로 자식 row를 직접 지워야 한다.
4069
+ * Write primary 사용
4070
+ */
4071
+ deleteAllByUserId(userId: number): Promise<number>;
3459
4072
  /**
3460
4073
  * 마지막 사용 시간 업데이트
3461
4074
  * Write primary 사용
@@ -3494,22 +4107,14 @@ declare class KeysRepository extends BaseRepository {
3494
4107
  }>;
3495
4108
  /**
3496
4109
  * Primary key로 마지막 사용 시간 업데이트 (authenticate용)
3497
- * Write primary 사용
4110
+ * Write primary 사용.
4111
+ *
4112
+ * Throttled: only writes when lastUsedAt is stale (older than
4113
+ * LAST_USED_THROTTLE_MS), so a busy key isn't UPDATEd on every request. The
4114
+ * throttle lives in the WHERE clause — atomic, no read-then-write race. No
4115
+ * RETURNING (callers fire-and-forget and discard the row).
3498
4116
  */
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
- }>;
4117
+ updateLastUsedById(id: number): Promise<void>;
3513
4118
  }
3514
4119
  declare const keysRepository: KeysRepository;
3515
4120
 
@@ -3541,7 +4146,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3541
4146
  target: string;
3542
4147
  targetType: "email" | "phone";
3543
4148
  code: string;
3544
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4149
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3545
4150
  expiresAt: Date;
3546
4151
  usedAt: Date | null;
3547
4152
  attempts: number;
@@ -3557,7 +4162,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3557
4162
  target: string;
3558
4163
  targetType: "email" | "phone";
3559
4164
  code: string;
3560
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4165
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3561
4166
  expiresAt: Date;
3562
4167
  usedAt: Date | null;
3563
4168
  attempts: number;
@@ -3567,14 +4172,14 @@ declare class VerificationCodesRepository extends BaseRepository {
3567
4172
  * Write primary 사용
3568
4173
  */
3569
4174
  create(data: NewVerificationCode): Promise<{
4175
+ target: string;
4176
+ targetType: "email" | "phone";
4177
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
4178
+ code: string;
3570
4179
  id: number;
3571
4180
  createdAt: Date;
3572
4181
  updatedAt: Date;
3573
4182
  expiresAt: Date;
3574
- target: string;
3575
- targetType: "email" | "phone";
3576
- code: string;
3577
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
3578
4183
  usedAt: Date | null;
3579
4184
  attempts: number;
3580
4185
  }>;
@@ -3589,7 +4194,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3589
4194
  target: string;
3590
4195
  targetType: "email" | "phone";
3591
4196
  code: string;
3592
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4197
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3593
4198
  expiresAt: Date;
3594
4199
  usedAt: Date | null;
3595
4200
  attempts: number;
@@ -3605,7 +4210,7 @@ declare class VerificationCodesRepository extends BaseRepository {
3605
4210
  target: string;
3606
4211
  targetType: "email" | "phone";
3607
4212
  code: string;
3608
- purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change";
4213
+ purpose: "registration" | "login" | "password_reset" | "email_change" | "phone_change" | "account_deletion";
3609
4214
  expiresAt: Date;
3610
4215
  usedAt: Date | null;
3611
4216
  attempts: number;
@@ -3615,6 +4220,14 @@ declare class VerificationCodesRepository extends BaseRepository {
3615
4220
  * Write primary 사용
3616
4221
  */
3617
4222
  deleteExpired(): Promise<number>;
4223
+ /**
4224
+ * Target(email/phone)의 모든 인증 코드 삭제 (계정 파기용)
4225
+ *
4226
+ * verificationCodes는 userId FK가 없고 target 텍스트로만 연결되므로, 파기 시
4227
+ * 원래 email/phone을 이 메서드로 직접 정리해야 한다.
4228
+ * Write primary 사용
4229
+ */
4230
+ deleteByTarget(target: string): Promise<number>;
3618
4231
  /**
3619
4232
  * Target의 모든 이전 코드 무효화 (새 코드 발급 시)
3620
4233
  * Write primary 사용
@@ -3763,7 +4376,7 @@ declare class PermissionsRepository extends BaseRepository {
3763
4376
  name: string;
3764
4377
  displayName: string;
3765
4378
  description: string | null;
3766
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4379
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3767
4380
  isBuiltin: boolean;
3768
4381
  isSystem: boolean;
3769
4382
  isActive: boolean;
@@ -3779,7 +4392,7 @@ declare class PermissionsRepository extends BaseRepository {
3779
4392
  name: string;
3780
4393
  displayName: string;
3781
4394
  description: string | null;
3782
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4395
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3783
4396
  isBuiltin: boolean;
3784
4397
  isSystem: boolean;
3785
4398
  isActive: boolean;
@@ -3819,7 +4432,7 @@ declare class PermissionsRepository extends BaseRepository {
3819
4432
  name: string;
3820
4433
  displayName: string;
3821
4434
  description: string | null;
3822
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4435
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3823
4436
  isBuiltin: boolean;
3824
4437
  isSystem: boolean;
3825
4438
  isActive: boolean;
@@ -3830,6 +4443,7 @@ declare class PermissionsRepository extends BaseRepository {
3830
4443
  */
3831
4444
  deleteById(id: number): Promise<{
3832
4445
  description: string | null;
4446
+ metadata: Record<string, any> | null;
3833
4447
  id: number;
3834
4448
  name: string;
3835
4449
  displayName: string;
@@ -3838,8 +4452,7 @@ declare class PermissionsRepository extends BaseRepository {
3838
4452
  isActive: boolean;
3839
4453
  createdAt: Date;
3840
4454
  updatedAt: Date;
3841
- metadata: Record<string, any> | null;
3842
- category: "custom" | "user" | "auth" | "rbac" | "system" | null;
4455
+ category: "auth" | "custom" | "user" | "rbac" | "system" | null;
3843
4456
  }>;
3844
4457
  }
3845
4458
  declare const permissionsRepository: PermissionsRepository;
@@ -3884,9 +4497,9 @@ declare class RolePermissionsRepository extends BaseRepository {
3884
4497
  */
3885
4498
  createMany(data: NewRolePermission[]): Promise<{
3886
4499
  id: number;
4500
+ roleId: number;
3887
4501
  createdAt: Date;
3888
4502
  updatedAt: Date;
3889
- roleId: number;
3890
4503
  permissionId: number;
3891
4504
  }[]>;
3892
4505
  /**
@@ -3902,9 +4515,9 @@ declare class RolePermissionsRepository extends BaseRepository {
3902
4515
  */
3903
4516
  setPermissionsForRole(roleId: number, permissionIds: number[]): Promise<{
3904
4517
  id: number;
4518
+ roleId: number;
3905
4519
  createdAt: Date;
3906
4520
  updatedAt: Date;
3907
- roleId: number;
3908
4521
  permissionId: number;
3909
4522
  }[]>;
3910
4523
  }
@@ -3969,10 +4582,10 @@ declare class UserPermissionsRepository extends BaseRepository {
3969
4582
  id: number;
3970
4583
  createdAt: Date;
3971
4584
  updatedAt: Date;
3972
- expiresAt: Date | null;
3973
4585
  permissionId: number;
3974
- granted: boolean;
3975
4586
  reason: string | null;
4587
+ expiresAt: Date | null;
4588
+ granted: boolean;
3976
4589
  }>;
3977
4590
  /**
3978
4591
  * 사용자 권한 오버라이드 업데이트
@@ -3995,10 +4608,10 @@ declare class UserPermissionsRepository extends BaseRepository {
3995
4608
  id: number;
3996
4609
  createdAt: Date;
3997
4610
  updatedAt: Date;
3998
- expiresAt: Date | null;
3999
4611
  permissionId: number;
4000
- granted: boolean;
4001
4612
  reason: string | null;
4613
+ expiresAt: Date | null;
4614
+ granted: boolean;
4002
4615
  }>;
4003
4616
  /**
4004
4617
  * 사용자의 모든 권한 오버라이드 삭제
@@ -4030,7 +4643,7 @@ declare class UserProfilesRepository extends BaseRepository {
4030
4643
  updatedAt: Date;
4031
4644
  id: number;
4032
4645
  userId: number;
4033
- displayName: string;
4646
+ displayName: string | null;
4034
4647
  firstName: string | null;
4035
4648
  lastName: string | null;
4036
4649
  avatarUrl: string | null;
@@ -4045,6 +4658,10 @@ declare class UserProfilesRepository extends BaseRepository {
4045
4658
  jobTitle: string | null;
4046
4659
  metadata: Record<string, any> | null;
4047
4660
  }>;
4661
+ /**
4662
+ * User ID로 locale만 조회 (경량)
4663
+ */
4664
+ findLocaleByUserId(userId: number): Promise<string>;
4048
4665
  /**
4049
4666
  * User ID로 프로필 조회
4050
4667
  */
@@ -4053,7 +4670,7 @@ declare class UserProfilesRepository extends BaseRepository {
4053
4670
  updatedAt: Date;
4054
4671
  id: number;
4055
4672
  userId: number;
4056
- displayName: string;
4673
+ displayName: string | null;
4057
4674
  firstName: string | null;
4058
4675
  lastName: string | null;
4059
4676
  avatarUrl: string | null;
@@ -4072,9 +4689,10 @@ declare class UserProfilesRepository extends BaseRepository {
4072
4689
  * 프로필 생성
4073
4690
  */
4074
4691
  create(data: NewUserProfile): Promise<{
4692
+ metadata: Record<string, any> | null;
4075
4693
  userId: number;
4076
4694
  id: number;
4077
- displayName: string;
4695
+ displayName: string | null;
4078
4696
  createdAt: Date;
4079
4697
  updatedAt: Date;
4080
4698
  firstName: string | null;
@@ -4089,7 +4707,6 @@ declare class UserProfilesRepository extends BaseRepository {
4089
4707
  location: string | null;
4090
4708
  company: string | null;
4091
4709
  jobTitle: string | null;
4092
- metadata: Record<string, any> | null;
4093
4710
  }>;
4094
4711
  /**
4095
4712
  * 프로필 업데이트 (by ID)
@@ -4099,7 +4716,7 @@ declare class UserProfilesRepository extends BaseRepository {
4099
4716
  updatedAt: Date;
4100
4717
  id: number;
4101
4718
  userId: number;
4102
- displayName: string;
4719
+ displayName: string | null;
4103
4720
  firstName: string | null;
4104
4721
  lastName: string | null;
4105
4722
  avatarUrl: string | null;
@@ -4122,7 +4739,7 @@ declare class UserProfilesRepository extends BaseRepository {
4122
4739
  updatedAt: Date;
4123
4740
  id: number;
4124
4741
  userId: number;
4125
- displayName: string;
4742
+ displayName: string | null;
4126
4743
  firstName: string | null;
4127
4744
  lastName: string | null;
4128
4745
  avatarUrl: string | null;
@@ -4141,9 +4758,10 @@ declare class UserProfilesRepository extends BaseRepository {
4141
4758
  * 프로필 삭제 (by ID)
4142
4759
  */
4143
4760
  deleteById(id: number): Promise<{
4761
+ metadata: Record<string, any> | null;
4144
4762
  userId: number;
4145
4763
  id: number;
4146
- displayName: string;
4764
+ displayName: string | null;
4147
4765
  createdAt: Date;
4148
4766
  updatedAt: Date;
4149
4767
  firstName: string | null;
@@ -4158,15 +4776,15 @@ declare class UserProfilesRepository extends BaseRepository {
4158
4776
  location: string | null;
4159
4777
  company: string | null;
4160
4778
  jobTitle: string | null;
4161
- metadata: Record<string, any> | null;
4162
4779
  }>;
4163
4780
  /**
4164
4781
  * 프로필 삭제 (by User ID)
4165
4782
  */
4166
4783
  deleteByUserId(userId: number): Promise<{
4784
+ metadata: Record<string, any> | null;
4167
4785
  userId: number;
4168
4786
  id: number;
4169
- displayName: string;
4787
+ displayName: string | null;
4170
4788
  createdAt: Date;
4171
4789
  updatedAt: Date;
4172
4790
  firstName: string | null;
@@ -4181,7 +4799,6 @@ declare class UserProfilesRepository extends BaseRepository {
4181
4799
  location: string | null;
4182
4800
  company: string | null;
4183
4801
  jobTitle: string | null;
4184
- metadata: Record<string, any> | null;
4185
4802
  }>;
4186
4803
  /**
4187
4804
  * 프로필 Upsert (by User ID)
@@ -4190,9 +4807,10 @@ declare class UserProfilesRepository extends BaseRepository {
4190
4807
  * 새로 생성 시 displayName은 필수 (없으면 'User'로 설정)
4191
4808
  */
4192
4809
  upsertByUserId(userId: number, data: Partial<Omit<NewUserProfile, 'userId'>>): Promise<{
4810
+ metadata: Record<string, any> | null;
4193
4811
  userId: number;
4194
4812
  id: number;
4195
- displayName: string;
4813
+ displayName: string | null;
4196
4814
  createdAt: Date;
4197
4815
  updatedAt: Date;
4198
4816
  firstName: string | null;
@@ -4207,7 +4825,6 @@ declare class UserProfilesRepository extends BaseRepository {
4207
4825
  location: string | null;
4208
4826
  company: string | null;
4209
4827
  jobTitle: string | null;
4210
- metadata: Record<string, any> | null;
4211
4828
  }>;
4212
4829
  /**
4213
4830
  * User ID로 프로필 데이터 조회 (formatted)
@@ -4216,7 +4833,7 @@ declare class UserProfilesRepository extends BaseRepository {
4216
4833
  */
4217
4834
  fetchProfileData(userId: number): Promise<{
4218
4835
  profileId: number;
4219
- displayName: string;
4836
+ displayName: string | null;
4220
4837
  firstName: string | null;
4221
4838
  lastName: string | null;
4222
4839
  avatarUrl: string | null;
@@ -4335,15 +4952,15 @@ declare class InvitationsRepository extends BaseRepository {
4335
4952
  */
4336
4953
  create(data: NewInvitation): Promise<{
4337
4954
  email: string;
4338
- status: "pending" | "accepted" | "expired" | "cancelled";
4955
+ metadata: Record<string, any> | null;
4339
4956
  id: number;
4957
+ roleId: number;
4340
4958
  createdAt: Date;
4341
4959
  updatedAt: Date;
4342
- roleId: number;
4343
- metadata: Record<string, any> | null;
4344
- expiresAt: Date;
4960
+ status: "pending" | "accepted" | "expired" | "cancelled";
4345
4961
  token: string;
4346
4962
  invitedBy: number;
4963
+ expiresAt: Date;
4347
4964
  acceptedAt: Date | null;
4348
4965
  cancelledAt: Date | null;
4349
4966
  }>;
@@ -4369,15 +4986,15 @@ declare class InvitationsRepository extends BaseRepository {
4369
4986
  */
4370
4987
  deleteById(id: number): Promise<{
4371
4988
  email: string;
4372
- status: "pending" | "accepted" | "expired" | "cancelled";
4989
+ metadata: Record<string, any> | null;
4373
4990
  id: number;
4991
+ roleId: number;
4374
4992
  createdAt: Date;
4375
4993
  updatedAt: Date;
4376
- roleId: number;
4377
- metadata: Record<string, any> | null;
4378
- expiresAt: Date;
4994
+ status: "pending" | "accepted" | "expired" | "cancelled";
4379
4995
  token: string;
4380
4996
  invitedBy: number;
4997
+ expiresAt: Date;
4381
4998
  acceptedAt: Date | null;
4382
4999
  cancelledAt: Date | null;
4383
5000
  }>;
@@ -4480,35 +5097,369 @@ declare class InvitationsRepository extends BaseRepository {
4480
5097
  expiresAt: Date;
4481
5098
  acceptedAt: Date | null;
4482
5099
  cancelledAt: Date | null;
4483
- metadata: Record<string, any> | null;
5100
+ metadata: Record<string, any> | null;
5101
+ }>;
5102
+ /**
5103
+ * 초대 취소 (status, metadata 동시 업데이트)
5104
+ */
5105
+ cancel(id: number, cancelledBy: number, reason?: string, currentMetadata?: Record<string, any> | null): Promise<{
5106
+ createdAt: Date;
5107
+ updatedAt: Date;
5108
+ id: number;
5109
+ email: string;
5110
+ token: string;
5111
+ roleId: number;
5112
+ invitedBy: number;
5113
+ status: "pending" | "accepted" | "expired" | "cancelled";
5114
+ expiresAt: Date;
5115
+ acceptedAt: Date | null;
5116
+ cancelledAt: Date | null;
5117
+ metadata: Record<string, any> | null;
5118
+ }>;
5119
+ }
5120
+ declare const invitationsRepository: InvitationsRepository;
5121
+
5122
+ /**
5123
+ * Social Accounts Repository
5124
+ *
5125
+ * OAuth 소셜 계정 데이터 관리를 위한 Repository
5126
+ * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리
5127
+ */
5128
+
5129
+ /**
5130
+ * Social Accounts Repository 클래스
5131
+ */
5132
+ declare class SocialAccountsRepository extends BaseRepository {
5133
+ /**
5134
+ * 저장 row 의 토큰을 평문으로 복호화해 반환한다.
5135
+ *
5136
+ * 레거시 평문(마커 없음)이 감지되면 즉시 재암호화해 저장하는
5137
+ * self-healing 마이그레이션을 수행한다. 호출자에게는 항상 평문이 반환되어
5138
+ * 외부 API 계약(평문 토큰)이 유지된다.
5139
+ */
5140
+ private decryptAccount;
5141
+ /**
5142
+ * provider와 providerUserId로 소셜 계정 조회
5143
+ * Read replica 사용
5144
+ */
5145
+ findByProviderAndProviderId(provider: SocialProvider, providerUserId: string): Promise<{
5146
+ accessToken: string | null;
5147
+ refreshToken: string | null;
5148
+ userId: number;
5149
+ id: number;
5150
+ createdAt: Date;
5151
+ updatedAt: Date;
5152
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5153
+ providerUserId: string;
5154
+ providerEmail: string | null;
5155
+ tokenExpiresAt: Date | null;
5156
+ } | null>;
5157
+ /**
5158
+ * userId로 모든 소셜 계정 조회
5159
+ * Read replica 사용
5160
+ */
5161
+ findByUserId(userId: number): Promise<({
5162
+ accessToken: string | null;
5163
+ refreshToken: string | null;
5164
+ userId: number;
5165
+ id: number;
5166
+ createdAt: Date;
5167
+ updatedAt: Date;
5168
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5169
+ providerUserId: string;
5170
+ providerEmail: string | null;
5171
+ tokenExpiresAt: Date | null;
5172
+ } | null)[]>;
5173
+ /**
5174
+ * userId와 provider로 소셜 계정 조회
5175
+ * Read replica 사용
5176
+ */
5177
+ findByUserIdAndProvider(userId: number, provider: SocialProvider): Promise<{
5178
+ accessToken: string | null;
5179
+ refreshToken: string | null;
5180
+ userId: number;
5181
+ id: number;
5182
+ createdAt: Date;
5183
+ updatedAt: Date;
5184
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5185
+ providerUserId: string;
5186
+ providerEmail: string | null;
5187
+ tokenExpiresAt: Date | null;
5188
+ } | null>;
5189
+ /**
5190
+ * 소셜 계정 생성
5191
+ * Write primary 사용
5192
+ */
5193
+ create(data: NewUserSocialAccount): Promise<{
5194
+ accessToken: string | null;
5195
+ refreshToken: string | null;
5196
+ userId: number;
5197
+ id: number;
5198
+ createdAt: Date;
5199
+ updatedAt: Date;
5200
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5201
+ providerUserId: string;
5202
+ providerEmail: string | null;
5203
+ tokenExpiresAt: Date | null;
5204
+ } | null>;
5205
+ /**
5206
+ * 토큰 정보 업데이트
5207
+ * Write primary 사용
5208
+ */
5209
+ updateTokens(id: number, data: {
5210
+ accessToken?: string | null;
5211
+ refreshToken?: string | null;
5212
+ tokenExpiresAt?: Date | null;
5213
+ }): Promise<{
5214
+ accessToken: string | null;
5215
+ refreshToken: string | null;
5216
+ userId: number;
5217
+ id: number;
5218
+ createdAt: Date;
5219
+ updatedAt: Date;
5220
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5221
+ providerUserId: string;
5222
+ providerEmail: string | null;
5223
+ tokenExpiresAt: Date | null;
5224
+ } | null>;
5225
+ /**
5226
+ * 소셜 계정 삭제
5227
+ * Write primary 사용
5228
+ */
5229
+ deleteById(id: number): Promise<{
5230
+ userId: number;
5231
+ id: number;
5232
+ createdAt: Date;
5233
+ updatedAt: Date;
5234
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5235
+ providerUserId: string;
5236
+ providerEmail: string | null;
5237
+ accessToken: string | null;
5238
+ refreshToken: string | null;
5239
+ tokenExpiresAt: Date | null;
5240
+ }>;
5241
+ /**
5242
+ * userId와 provider로 소셜 계정 삭제
5243
+ * Write primary 사용
5244
+ */
5245
+ deleteByUserIdAndProvider(userId: number, provider: SocialProvider): Promise<{
5246
+ userId: number;
5247
+ id: number;
5248
+ createdAt: Date;
5249
+ updatedAt: Date;
5250
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself";
5251
+ providerUserId: string;
5252
+ providerEmail: string | null;
5253
+ accessToken: string | null;
5254
+ refreshToken: string | null;
5255
+ tokenExpiresAt: Date | null;
5256
+ }>;
5257
+ /**
5258
+ * 사용자의 모든 소셜 계정 삭제 (계정 익명화 파기용)
5259
+ *
5260
+ * provider unique index(provider, providerUserId)를 해제해 같은 소셜 계정으로
5261
+ * 재가입할 수 있게 한다.
5262
+ * Write primary 사용
5263
+ */
5264
+ deleteAllByUserId(userId: number): Promise<number>;
5265
+ }
5266
+ declare const socialAccountsRepository: SocialAccountsRepository;
5267
+
5268
+ /**
5269
+ * Auth Metadata Repository
5270
+ *
5271
+ * Key-value 기반 시스템 메타데이터 저장소
5272
+ * RBAC 설정 해시 등 시스템 설정값 관리
5273
+ */
5274
+
5275
+ declare class AuthMetadataRepository extends BaseRepository {
5276
+ /**
5277
+ * 키로 값 조회
5278
+ */
5279
+ get(key: string): Promise<string | null>;
5280
+ /**
5281
+ * 키-값 저장 (upsert)
5282
+ */
5283
+ set(key: string, value: string): Promise<void>;
5284
+ }
5285
+ declare const authMetadataRepository: AuthMetadataRepository;
5286
+
5287
+ /**
5288
+ * Account Deletion Requests Repository
5289
+ *
5290
+ * 계정 탈퇴 요청/복구/파기 감사 이력 관리를 위한 Repository
5291
+ * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리
5292
+ *
5293
+ * Row는 절대 삭제하지 않는다 — status 전이(pending -> cancelled|completed)로만 갱신.
5294
+ */
5295
+
5296
+ /**
5297
+ * Account Deletion Requests Repository 클래스
5298
+ */
5299
+ declare class AccountDeletionRequestsRepository extends BaseRepository {
5300
+ /**
5301
+ * ID로 요청 조회
5302
+ * Read replica 사용
5303
+ */
5304
+ findById(id: number): Promise<{
5305
+ createdAt: Date;
5306
+ updatedAt: Date;
5307
+ id: number;
5308
+ userId: number | null;
5309
+ userPublicId: string;
5310
+ requestedAt: Date;
5311
+ purgeScheduledAt: Date;
5312
+ status: "pending" | "cancelled" | "completed";
5313
+ requestedBy: "admin" | "self";
5314
+ reason: string | null;
5315
+ cancelledAt: Date | null;
5316
+ completedAt: Date | null;
5317
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5318
+ }>;
5319
+ /**
5320
+ * User ID로 pending 요청 조회 (유저당 최대 1건, partial unique index로 보장)
5321
+ * Read replica 사용
5322
+ */
5323
+ findPendingByUserId(userId: number): Promise<{
5324
+ createdAt: Date;
5325
+ updatedAt: Date;
5326
+ id: number;
5327
+ userId: number | null;
5328
+ userPublicId: string;
5329
+ requestedAt: Date;
5330
+ purgeScheduledAt: Date;
5331
+ status: "pending" | "cancelled" | "completed";
5332
+ requestedBy: "admin" | "self";
5333
+ reason: string | null;
5334
+ cancelledAt: Date | null;
5335
+ completedAt: Date | null;
5336
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5337
+ }>;
5338
+ /**
5339
+ * User ID로 pending 요청 조회 — Write primary에서 직접 읽는다.
5340
+ *
5341
+ * 로그인/OAuth/authenticate 게이트가 pending_deletion 유저의 purgeScheduledAt을
5342
+ * 표시하는 데 쓴다. 이 경로들은 복제 지연이 있으면 방금 커밋된 상태 전이를 놓칠 수
5343
+ * 있어(예: 삭제 요청 직후 OAuth 로그인 시도), replica가 아닌 primary에서 읽는다.
5344
+ */
5345
+ findPendingByUserIdOnPrimary(userId: number): Promise<{
5346
+ createdAt: Date;
5347
+ updatedAt: Date;
5348
+ id: number;
5349
+ userId: number | null;
5350
+ userPublicId: string;
5351
+ requestedAt: Date;
5352
+ purgeScheduledAt: Date;
5353
+ status: "pending" | "cancelled" | "completed";
5354
+ requestedBy: "admin" | "self";
5355
+ reason: string | null;
5356
+ cancelledAt: Date | null;
5357
+ completedAt: Date | null;
5358
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5359
+ }>;
5360
+ /**
5361
+ * 파기 스윕 대상 조회 — status='pending' AND purgeScheduledAt <= now
5362
+ * Read replica 사용
5363
+ */
5364
+ findDueForPurge(now: Date): Promise<{
5365
+ createdAt: Date;
5366
+ updatedAt: Date;
5367
+ id: number;
5368
+ userId: number | null;
5369
+ userPublicId: string;
5370
+ requestedAt: Date;
5371
+ purgeScheduledAt: Date;
5372
+ status: "pending" | "cancelled" | "completed";
5373
+ requestedBy: "admin" | "self";
5374
+ reason: string | null;
5375
+ cancelledAt: Date | null;
5376
+ completedAt: Date | null;
5377
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5378
+ }[]>;
5379
+ /**
5380
+ * 요청 생성
5381
+ * Write primary 사용
5382
+ */
5383
+ create(data: NewAccountDeletionRequest): Promise<{
5384
+ userId: number | null;
5385
+ id: number;
5386
+ createdAt: Date;
5387
+ updatedAt: Date;
5388
+ status: "pending" | "cancelled" | "completed";
5389
+ reason: string | null;
5390
+ purgeScheduledAt: Date;
5391
+ cancelledAt: Date | null;
5392
+ userPublicId: string;
5393
+ requestedAt: Date;
5394
+ requestedBy: "admin" | "self";
5395
+ completedAt: Date | null;
5396
+ purgeStrategy: "anonymize" | "hard-delete" | null;
5397
+ }>;
5398
+ /**
5399
+ * 요청 취소 (복구)
5400
+ *
5401
+ * `WHERE status = 'pending'` 조건부 UPDATE — 이미 completed(파기 완료)된 row를
5402
+ * 뒤늦게 도착한 cancel이 덮어쓰지 않도록 한다. 0 row 매치(= 이미 completed거나
5403
+ * cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
5404
+ * Write primary 사용
5405
+ */
5406
+ markCancelled(id: number): Promise<{
5407
+ createdAt: Date;
5408
+ updatedAt: Date;
5409
+ id: number;
5410
+ userId: number | null;
5411
+ userPublicId: string;
5412
+ requestedAt: Date;
5413
+ purgeScheduledAt: Date;
5414
+ status: "pending" | "cancelled" | "completed";
5415
+ requestedBy: "admin" | "self";
5416
+ reason: string | null;
5417
+ cancelledAt: Date | null;
5418
+ completedAt: Date | null;
5419
+ purgeStrategy: "anonymize" | "hard-delete" | null;
4484
5420
  }>;
4485
5421
  /**
4486
- * 초대 취소 (status, metadata 동시 업데이트)
5422
+ * 요청 완료 처리 (파기 실행 직전에 호출하는 "claim" — 파기 실행 후가 아니다)
5423
+ *
5424
+ * `WHERE status = 'pending'` 조건부 UPDATE. Postgres UPDATE는 매치되는 row에 락을
5425
+ * 걸고 최신 커밋 상태로 WHERE를 재평가하므로, 이 호출 하나로 (1) 동시에 취소된
5426
+ * row를 안전하게 걸러내고 (2) row-level 락으로 동시 파기 시도를 직렬화한다.
5427
+ * 0 row 매치(= 이미 취소되었거나 다른 파기가 먼저 처리함) 시 null을 반환 — 호출자는
5428
+ * destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
5429
+ * Write primary 사용
4487
5430
  */
4488
- cancel(id: number, cancelledBy: number, reason?: string, currentMetadata?: Record<string, any> | null): Promise<{
5431
+ markCompleted(id: number, purgeStrategy: PurgeStrategy): Promise<{
4489
5432
  createdAt: Date;
4490
5433
  updatedAt: Date;
4491
5434
  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;
5435
+ userId: number | null;
5436
+ userPublicId: string;
5437
+ requestedAt: Date;
5438
+ purgeScheduledAt: Date;
5439
+ status: "pending" | "cancelled" | "completed";
5440
+ requestedBy: "admin" | "self";
5441
+ reason: string | null;
4499
5442
  cancelledAt: Date | null;
4500
- metadata: Record<string, any> | null;
5443
+ completedAt: Date | null;
5444
+ purgeStrategy: "anonymize" | "hard-delete" | null;
4501
5445
  }>;
4502
5446
  }
4503
- declare const invitationsRepository: InvitationsRepository;
5447
+ declare const accountDeletionRequestsRepository: AccountDeletionRequestsRepository;
4504
5448
 
4505
5449
  /**
4506
5450
  * @spfn/auth - Password Helpers
4507
5451
  *
4508
- * Password hashing and verification using bcrypt
5452
+ * Password hashing and verification using bcrypt (@node-rs/bcrypt).
5453
+ *
5454
+ * Uses the native (Rust/napi) implementation, which runs the CPU-bound key
5455
+ * derivation on the libuv threadpool instead of the main event loop — so
5456
+ * concurrent logins run in parallel and don't head-of-line-block other requests.
5457
+ * Hashes are standard bcrypt ($2*$) and verify against existing bcryptjs hashes.
5458
+ * For very high concurrent-login load, raise UV_THREADPOOL_SIZE toward the core
5459
+ * count (default pool is 4).
4509
5460
  *
4510
5461
  * Security:
4511
- * - Adaptive hashing (configurable rounds)
5462
+ * - Adaptive hashing (configurable rounds, default 12)
4512
5463
  * - Automatic salt generation (per-password)
4513
5464
  * - Constant-time comparison (timing attack protection)
4514
5465
  * - Rainbow table protection
@@ -4521,10 +5472,10 @@ declare const invitationsRepository: InvitationsRepository;
4521
5472
  * 2. Apply bcrypt key derivation (2^rounds iterations)
4522
5473
  * 3. Return $2b$rounds$[salt][hash] (60 chars)
4523
5474
  *
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)
5475
+ * Salt rounds are configured via SPFN_AUTH_BCRYPT_SALT_ROUNDS (native timings):
5476
+ * - 12 rounds: ~200ms (default — OWASP-aligned, off the event loop)
5477
+ * - 10 rounds: ~55ms (faster, lower work factor)
5478
+ * - 14 rounds: ~800ms (very secure, heavy)
4528
5479
  *
4529
5480
  * @param password - Plain text password to hash
4530
5481
  * @returns Bcrypt hash string (includes salt)
@@ -4558,6 +5509,7 @@ declare function hashPassword(password: string): Promise<string>;
4558
5509
  * ```
4559
5510
  */
4560
5511
  declare function verifyPassword(password: string, hash: string): Promise<boolean>;
5512
+ declare function getDummyPasswordHash(): Promise<string>;
4561
5513
  /**
4562
5514
  * Validate password strength
4563
5515
  *
@@ -4846,6 +5798,29 @@ interface RoleGuardOptions {
4846
5798
  */
4847
5799
  declare const roleGuard: _spfn_core_route.NamedMiddlewareFactory<"roleGuard", [options: RoleGuardOptions]>;
4848
5800
 
5801
+ /**
5802
+ * One-Time Token Authentication Middleware
5803
+ *
5804
+ * Authenticates requests using a one-time token instead of JWT.
5805
+ * Extracts token from query parameter `?token=xxx` or `Authorization: OTT xxx` header.
5806
+ *
5807
+ * On success, injects AuthContext identical to the `authenticate` middleware,
5808
+ * making it transparent to downstream handlers using `getAuth(c)`.
5809
+ *
5810
+ * Auto-skips the global 'auth' middleware.
5811
+ *
5812
+ * @example
5813
+ * ```typescript
5814
+ * export const uploadFile = route.post('/files/upload')
5815
+ * .use([oneTimeTokenAuth])
5816
+ * .handler(async (c) => {
5817
+ * const { userId } = getAuth(c);
5818
+ * // handle file upload...
5819
+ * });
5820
+ * ```
5821
+ */
5822
+ declare const oneTimeTokenAuth: _spfn_core_route.NamedMiddleware<"oneTimeTokenAuth">;
5823
+
4849
5824
  /**
4850
5825
  * Auth Context Helpers
4851
5826
  *
@@ -4872,6 +5847,32 @@ declare const roleGuard: _spfn_core_route.NamedMiddlewareFactory<"roleGuard", [o
4872
5847
  declare function getAuth(c: Context | {
4873
5848
  raw: Context;
4874
5849
  }): AuthContext;
5850
+ /**
5851
+ * Get optional auth context from route context
5852
+ *
5853
+ * Returns AuthContext if authenticated, undefined otherwise.
5854
+ * Use with `optionalAuth` middleware for routes that serve both
5855
+ * authenticated and unauthenticated users.
5856
+ *
5857
+ * @example
5858
+ * ```typescript
5859
+ * export const getProducts = route.get('/products')
5860
+ * .use([optionalAuth])
5861
+ * .handler(async (c) => {
5862
+ * const auth = getOptionalAuth(c);
5863
+ *
5864
+ * if (auth)
5865
+ * {
5866
+ * return getPersonalizedProducts(auth.userId);
5867
+ * }
5868
+ *
5869
+ * return getPublicProducts();
5870
+ * });
5871
+ * ```
5872
+ */
5873
+ declare function getOptionalAuth(c: Context | {
5874
+ raw: Context;
5875
+ }): AuthContext | undefined;
4875
5876
  /**
4876
5877
  * Get authenticated user from route context
4877
5878
  *
@@ -4888,16 +5889,20 @@ declare function getUser(c: Context | {
4888
5889
  }): {
4889
5890
  email: string | null;
4890
5891
  phone: string | null;
4891
- status: "active" | "inactive" | "suspended";
4892
5892
  id: number;
4893
- createdAt: Date;
4894
- updatedAt: Date;
5893
+ publicId: string;
5894
+ username: string | null;
4895
5895
  passwordHash: string | null;
4896
5896
  passwordChangeRequired: boolean;
4897
5897
  roleId: number;
5898
+ createdAt: Date;
5899
+ updatedAt: Date;
5900
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
4898
5901
  emailVerifiedAt: Date | null;
4899
5902
  phoneVerifiedAt: Date | null;
4900
5903
  lastLoginAt: Date | null;
5904
+ deletedAt: Date | null;
5905
+ deletedBy: string | null;
4901
5906
  };
4902
5907
  /**
4903
5908
  * Get authenticated user ID from route context
@@ -4913,6 +5918,38 @@ declare function getUser(c: Context | {
4913
5918
  declare function getUserId(c: Context | {
4914
5919
  raw: Context;
4915
5920
  }): string;
5921
+ /**
5922
+ * Get authenticated user's role from route context
5923
+ *
5924
+ * @returns Role name or null if user has no role
5925
+ *
5926
+ * @example
5927
+ * ```typescript
5928
+ * app.bind(adminContract, [authenticate], async (c) => {
5929
+ * const role = getRole(c);
5930
+ * // 'admin' | 'superadmin' | null
5931
+ * });
5932
+ * ```
5933
+ */
5934
+ declare function getRole(c: Context | {
5935
+ raw: Context;
5936
+ }): string | null;
5937
+ /**
5938
+ * Get authenticated user's locale from route context
5939
+ *
5940
+ * @returns Locale string (e.g., 'en', 'ko')
5941
+ *
5942
+ * @example
5943
+ * ```typescript
5944
+ * app.bind(contract, [authenticate], async (c) => {
5945
+ * const locale = getLocale(c);
5946
+ * // 'en' | 'ko' | ...
5947
+ * });
5948
+ * ```
5949
+ */
5950
+ declare function getLocale(c: Context | {
5951
+ raw: Context;
5952
+ }): string;
4916
5953
  /**
4917
5954
  * Get current key ID from route context
4918
5955
  *
@@ -4939,7 +5976,7 @@ declare function getKeyId(c: Context | {
4939
5976
  * - RS256 (RSA 2048): ~294 bytes (Base64: ~392 chars)
4940
5977
  */
4941
5978
 
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";
5979
+ 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
5980
  type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;
4944
5981
  type StringValue = `${number}` | `${number}${UnitAnyCase}` | `${number} ${UnitAnyCase}`;
4945
5982
  interface KeyPair {
@@ -4985,70 +6022,40 @@ declare function shouldRotateKey(createdAt: Date, rotationDays?: number): {
4985
6022
  daysRemaining: number;
4986
6023
  };
4987
6024
 
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
6025
  /**
5039
6026
  * @spfn/auth - Global Configuration
5040
6027
  *
5041
6028
  * Manages global auth configuration including session TTL
5042
6029
  */
6030
+
5043
6031
  /**
5044
6032
  * Cookie names used by SPFN Auth
6033
+ *
6034
+ * Names include a port-based suffix so that multiple dev instances
6035
+ * on different ports don't overwrite each other's cookies.
5045
6036
  */
5046
6037
  declare const COOKIE_NAMES: {
5047
6038
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
5048
- readonly SESSION: "spfn_session";
6039
+ readonly SESSION: string;
5049
6040
  /** Current key ID (for key rotation) */
5050
- readonly SESSION_KEY_ID: "spfn_session_key_id";
6041
+ readonly SESSION_KEY_ID: string;
6042
+ /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
6043
+ readonly OAUTH_PENDING: string;
6044
+ /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
6045
+ readonly OAUTH_CSRF: string;
5051
6046
  };
6047
+ /**
6048
+ * OAuth CSRF 쿠키를 PORT 접미사와 무관하게 전부 수집한다.
6049
+ *
6050
+ * 쿠키를 심는 쪽은 Next.js 프로세스, 읽는 쪽은 API 프로세스라 분리 배포에서는
6051
+ * 두 프로세스의 PORT가 달라 COOKIE_NAMES.OAUTH_CSRF 정확 일치 조회가 빗나간다.
6052
+ * nonce 자체가 랜덤값이고 암호화된 state의 nonce와 대조되므로, 접미사가 다른
6053
+ * spfn_oauth_csrf* 후보를 모두 대조 대상으로 넘겨도 안전하다.
6054
+ */
6055
+ declare function matchOAuthCsrfCookies(cookies: Record<string, string>): {
6056
+ name: string;
6057
+ value: string;
6058
+ }[];
5052
6059
  /**
5053
6060
  * Parse duration string to seconds
5054
6061
  *
@@ -5061,6 +6068,36 @@ declare const COOKIE_NAMES: {
5061
6068
  * parseDuration('3600') // 3600
5062
6069
  */
5063
6070
  declare function parseDuration(duration: string | number): number;
6071
+ /**
6072
+ * Registration channel passed to the beforeRegister hook
6073
+ *
6074
+ * - credentials: email/phone + password registration
6075
+ * - oauth: new-user signup through a social provider (web or native flow)
6076
+ * - invitation: invitation acceptance
6077
+ */
6078
+ type RegisterChannel = 'credentials' | 'oauth' | 'invitation';
6079
+ /**
6080
+ * Context passed to the beforeRegister hook
6081
+ *
6082
+ * Credentials (password, keys) are intentionally excluded — the hook is a
6083
+ * policy gate, not a credential handler.
6084
+ */
6085
+ interface BeforeRegisterContext {
6086
+ channel: RegisterChannel;
6087
+ /** Social provider — only set when channel is 'oauth' */
6088
+ provider?: SocialProvider;
6089
+ email?: string;
6090
+ /**
6091
+ * Whether the email is verified — only set when channel is 'oauth'.
6092
+ * OAuth providers may report an unverified (spoofable) email; the created
6093
+ * account stores it as null in that case, so email-based policies must
6094
+ * check this flag. credentials/invitation emails are already verified.
6095
+ */
6096
+ emailVerified?: boolean;
6097
+ phone?: string;
6098
+ /** App-supplied registration metadata (register params / OAuth start params / invitation) */
6099
+ metadata?: Record<string, unknown>;
6100
+ }
5064
6101
  /**
5065
6102
  * Auth configuration
5066
6103
  */
@@ -5075,6 +6112,35 @@ interface AuthConfig {
5075
6112
  * @default 7d (7 days)
5076
6113
  */
5077
6114
  sessionTtl?: string | number;
6115
+ /**
6116
+ * App-injected validator that runs before a new user row is created,
6117
+ * on every registration channel (credentials, oauth, invitation).
6118
+ *
6119
+ * Throw to reject the registration — RegistrationRejectedError (403) is
6120
+ * the recommended error; any HttpError subclass keeps its own status.
6121
+ * Runs after built-in checks (verification token, duplicate account),
6122
+ * so existing error precedence is unchanged. Not called for admin
6123
+ * seeding (initializeAuth) or when linking a social account to an
6124
+ * existing user.
6125
+ *
6126
+ * Runs inside the registration DB transaction on every channel — keep it
6127
+ * fast. A slow call (e.g. an external policy API) holds a pooled DB
6128
+ * connection open for its full duration on every signup.
6129
+ *
6130
+ * @example
6131
+ * ```typescript
6132
+ * configureAuth({
6133
+ * beforeRegister: async ({ channel, metadata }) =>
6134
+ * {
6135
+ * if (channel === 'credentials' && !isOldEnough(metadata?.birthDate))
6136
+ * {
6137
+ * throw new RegistrationRejectedError({ message: 'Age requirement not met' });
6138
+ * }
6139
+ * },
6140
+ * });
6141
+ * ```
6142
+ */
6143
+ beforeRegister?: (context: BeforeRegisterContext) => void | Promise<void>;
5078
6144
  }
5079
6145
  /**
5080
6146
  * Configure global auth settings
@@ -5093,6 +6159,14 @@ declare function configureAuth(config: AuthConfig): void;
5093
6159
  * Get current auth configuration
5094
6160
  */
5095
6161
  declare function getAuthConfig(): AuthConfig;
6162
+ /**
6163
+ * Run the app-injected beforeRegister hook if configured — throws to reject.
6164
+ *
6165
+ * Single entry point for every registration channel so a new channel cannot
6166
+ * forget the configured-check. Callers invoke this right before creating the
6167
+ * user row.
6168
+ */
6169
+ declare function runBeforeRegister(context: BeforeRegisterContext): Promise<void>;
5096
6170
  /**
5097
6171
  * Get session TTL in seconds
5098
6172
  *
@@ -5104,6 +6178,107 @@ declare function getAuthConfig(): AuthConfig;
5104
6178
  */
5105
6179
  declare function getSessionTtl(override?: string | number): number;
5106
6180
 
6181
+ /**
6182
+ * One-Time Token Manager
6183
+ *
6184
+ * Singleton wrapper around SSETokenManager for one-time token authentication.
6185
+ * Used for direct API access (file uploads, SSE streaming, etc.) bypassing RPC proxy.
6186
+ */
6187
+
6188
+ /**
6189
+ * Initialize the one-time token manager
6190
+ *
6191
+ * Called during auth lifecycle initialization.
6192
+ * Creates a singleton SSETokenManager instance.
6193
+ *
6194
+ * @param config - Optional configuration
6195
+ * @param config.ttl - Token time-to-live in milliseconds (default: 30000)
6196
+ * @param config.store - Custom token store (e.g., CacheTokenStore for Redis)
6197
+ */
6198
+ declare function initOneTimeTokenManager(config?: {
6199
+ ttl?: number;
6200
+ store?: SSETokenStore;
6201
+ }): void;
6202
+ /**
6203
+ * Get the one-time token manager instance
6204
+ *
6205
+ * @throws Error if initOneTimeTokenManager() has not been called
6206
+ *
6207
+ * @example
6208
+ * ```typescript
6209
+ * import { getOneTimeTokenManager } from '@spfn/auth/server';
6210
+ *
6211
+ * // Use as SSE tokenManager
6212
+ * .eventsConfig({
6213
+ * auth: {
6214
+ * enabled: true,
6215
+ * tokenManager: getOneTimeTokenManager(),
6216
+ * },
6217
+ * })
6218
+ * ```
6219
+ */
6220
+ declare function getOneTimeTokenManager(): SSETokenManager;
6221
+
6222
+ /**
6223
+ * Account Deletion Configuration
6224
+ *
6225
+ * Singleton config for the account deletion/recovery lifecycle, mirroring the
6226
+ * one-time-token manager pattern (`lib/one-time-token.ts`): a mutable module-level
6227
+ * value, set once during `createAuthLifecycle()` and read at request/handler time.
6228
+ *
6229
+ * `purgeCron` is the one field this module does NOT make effective on its own —
6230
+ * `job('auth.deletion.purge').cron(expression)` bakes the cron string in at
6231
+ * *module-import* time, which always runs before `createAuthLifecycle()` executes
6232
+ * (ESM evaluates imports before the importing module's own statements). So a
6233
+ * statically-exported job router can never see a `purgeCron` set here. Apps that
6234
+ * need a non-default cron must build the router themselves, after configuring
6235
+ * deletion, with `createAuthDeletionJobRouter({ purgeCron })` — see
6236
+ * `server/jobs/deletion-purge.ts` and the README.
6237
+ */
6238
+
6239
+ interface AccountDeletionPurgeUser {
6240
+ id: number;
6241
+ publicId: string;
6242
+ email: string | null;
6243
+ phone: string | null;
6244
+ }
6245
+ interface AuthDeletionConfig {
6246
+ /** Days between a deletion request and the purge becoming eligible. 0 = immediate. */
6247
+ gracePeriodDays: number;
6248
+ /** How the purge job destroys the account once the grace period elapses. */
6249
+ purgeStrategy: PurgeStrategy;
6250
+ /** Whether a self-service caller may request `immediate: true` (skip the grace period). */
6251
+ allowSelfImmediate: boolean;
6252
+ /**
6253
+ * Cron schedule for the purge sweep. NOT read automatically by the static
6254
+ * `authJobRouter` export — see the module doc comment above.
6255
+ */
6256
+ purgeCron: string;
6257
+ /** Whether to email users (when they have one) at request/recovery/purge time. */
6258
+ sendNotifications: boolean;
6259
+ /**
6260
+ * Synchronous hook invoked immediately before a user is purged. Throw to skip
6261
+ * that user for this sweep (they stay `pending`, retried on the next tick).
6262
+ */
6263
+ onBeforePurge?: (user: AccountDeletionPurgeUser) => Promise<void>;
6264
+ }
6265
+ declare const DEFAULT_DELETION_GRACE_PERIOD_DAYS = 30;
6266
+ declare const DEFAULT_DELETION_PURGE_STRATEGY: PurgeStrategy;
6267
+ declare const DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE = false;
6268
+ declare const DEFAULT_DELETION_PURGE_CRON = "0 4 * * *";
6269
+ declare const DEFAULT_DELETION_SEND_NOTIFICATIONS = true;
6270
+ /**
6271
+ * Set the resolved deletion config. Called synchronously from `createAuthLifecycle()`
6272
+ * (not from `afterInfrastructure`) so it takes effect before any route/job handler
6273
+ * that reads `getDeletionConfig()` can run.
6274
+ */
6275
+ declare function configureDeletion(options?: Partial<AuthDeletionConfig>): void;
6276
+ /**
6277
+ * Read the current deletion config. Safe to call any time — defaults apply even
6278
+ * if `createAuthLifecycle()` was never given a `deletion` block.
6279
+ */
6280
+ declare function getDeletionConfig(): AuthDeletionConfig;
6281
+
5107
6282
  /**
5108
6283
  * @spfn/auth - Centralized Logger
5109
6284
  *
@@ -5116,7 +6291,9 @@ declare const authLogger: {
5116
6291
  general: _spfn_core_logger.Logger;
5117
6292
  login: _spfn_core_logger.Logger;
5118
6293
  keyRotation: _spfn_core_logger.Logger;
6294
+ oauth: _spfn_core_logger.Logger;
5119
6295
  };
6296
+ session: _spfn_core_logger.Logger;
5120
6297
  service: _spfn_core_logger.Logger;
5121
6298
  setup: _spfn_core_logger.Logger;
5122
6299
  email: _spfn_core_logger.Logger;
@@ -5197,6 +6374,337 @@ interface AuthLifecycleConfig {
5197
6374
  * .build();
5198
6375
  * ```
5199
6376
  */
5200
- declare function createAuthLifecycle(options?: AuthInitOptions): AuthLifecycleConfig;
6377
+ /**
6378
+ * Options for createAuthLifecycle
6379
+ */
6380
+ interface AuthLifecycleOptions extends AuthInitOptions {
6381
+ /**
6382
+ * One-time token configuration
6383
+ *
6384
+ * Enables one-time token issuance for direct API access
6385
+ * (file uploads, SSE streaming, etc.)
6386
+ *
6387
+ * @example
6388
+ * ```typescript
6389
+ * createAuthLifecycle({
6390
+ * oneTimeToken: { ttl: 60000 }, // 60 seconds
6391
+ * })
6392
+ * ```
6393
+ */
6394
+ oneTimeToken?: {
6395
+ /**
6396
+ * Token time-to-live in milliseconds
6397
+ * @default 30000
6398
+ */
6399
+ ttl?: number;
6400
+ /**
6401
+ * Custom token store (e.g., CacheTokenStore for Redis/Valkey)
6402
+ *
6403
+ * When provided, tokens are stored in the external store instead of in-memory Map.
6404
+ * Required for multi-instance deployments where token issuance and verification
6405
+ * may happen on different server instances.
6406
+ *
6407
+ * @example
6408
+ * ```typescript
6409
+ * import { CacheTokenStore } from '@spfn/core/event/sse';
6410
+ * import { getCache } from '@spfn/core/cache';
6411
+ *
6412
+ * createAuthLifecycle({
6413
+ * oneTimeToken: {
6414
+ * store: new CacheTokenStore(getCache()),
6415
+ * },
6416
+ * })
6417
+ * ```
6418
+ */
6419
+ store?: SSETokenStore;
6420
+ };
6421
+ /**
6422
+ * Account deletion/recovery lifecycle configuration
6423
+ *
6424
+ * Controls the grace-period request → recover → purge flow exposed by
6425
+ * `POST /_auth/deletion/request` and `POST /_auth/deletion/cancel`.
6426
+ * Registering the purge job itself is a separate step — see `authJobRouter`
6427
+ * (`@spfn/auth/server`) and the README, since job registration happens after
6428
+ * this lifecycle hook runs and can't be triggered from here.
6429
+ *
6430
+ * @example
6431
+ * ```typescript
6432
+ * createAuthLifecycle({
6433
+ * deletion: {
6434
+ * gracePeriodDays: 14,
6435
+ * purgeStrategy: 'anonymize',
6436
+ * allowSelfImmediate: false,
6437
+ * sendNotifications: true,
6438
+ * onBeforePurge: async (user) => {
6439
+ * await appDataCleanup(user.id); // throw to skip this user this sweep
6440
+ * },
6441
+ * },
6442
+ * })
6443
+ * ```
6444
+ */
6445
+ deletion?: {
6446
+ /**
6447
+ * Days between a deletion request and the purge becoming eligible.
6448
+ * 0 = immediate (still goes through the same request/purge pipeline).
6449
+ * @default 30
6450
+ */
6451
+ gracePeriodDays?: number;
6452
+ /**
6453
+ * How the purge job destroys the account once the grace period elapses.
6454
+ * - 'anonymize': scrub PII, keep the row (status -> 'deleted') — recommended default
6455
+ * - 'hard-delete': physically remove the `users` row (cascades to child rows)
6456
+ * @default 'anonymize'
6457
+ */
6458
+ purgeStrategy?: PurgeStrategy;
6459
+ /**
6460
+ * Whether a self-service caller may pass `immediate: true` on
6461
+ * `POST /_auth/deletion/request` to skip the grace period entirely.
6462
+ * @default false
6463
+ */
6464
+ allowSelfImmediate?: boolean;
6465
+ /**
6466
+ * Cron schedule for the purge sweep. NOTE: this does not reach the static
6467
+ * `authJobRouter` export automatically — job schedules are fixed at
6468
+ * module-import time, before this lifecycle hook runs. For a non-default
6469
+ * cron, build the router yourself after this call with
6470
+ * `createAuthDeletionJobRouter({ purgeCron })` (see `@spfn/auth/server`).
6471
+ * @default '0 4 * * *'
6472
+ */
6473
+ purgeCron?: string;
6474
+ /**
6475
+ * Whether to email users (when they have one on file) at request,
6476
+ * recovery, and final-purge time.
6477
+ * @default true
6478
+ */
6479
+ sendNotifications?: boolean;
6480
+ /**
6481
+ * Invoked immediately before a user is purged (both the immediate inline
6482
+ * path and the cron sweep). Throw to skip that user for this run — they
6483
+ * stay `pending` and are retried on the next sweep.
6484
+ */
6485
+ onBeforePurge?: (user: AccountDeletionPurgeUser) => Promise<void>;
6486
+ };
6487
+ }
6488
+ declare function createAuthLifecycle(options?: AuthLifecycleOptions): AuthLifecycleConfig;
6489
+
6490
+ /**
6491
+ * @spfn/auth - Account Deletion Purge Job
6492
+ *
6493
+ * The package's first background job (`@spfn/core/job`). Sweeps
6494
+ * `account_deletion_requests` for rows whose grace period elapsed and purges
6495
+ * each one (see `sweepDuePurges` in `account-deletion.service.ts`).
6496
+ *
6497
+ * Registration is NOT automatic. `createAuthLifecycle()`'s `afterInfrastructure`
6498
+ * hook runs *before* `initBoss`/`registerJobs` in `@spfn/core`'s server startup
6499
+ * (`initializeInfrastructure()`), so the lifecycle has no opportunity to register
6500
+ * jobs itself. Apps must register explicitly:
6501
+ *
6502
+ * ```typescript
6503
+ * import { authJobRouter } from '@spfn/auth/server';
6504
+ *
6505
+ * export default defineServerConfig()
6506
+ * .lifecycle(createAuthLifecycle())
6507
+ * .jobs(authJobRouter)
6508
+ * .routes(appRouter)
6509
+ * .build();
6510
+ * ```
6511
+ *
6512
+ * Cron schedule caveat: `job(...).cron(expression)` bakes the cron string in at
6513
+ * *module-import* time, which always happens before `createAuthLifecycle()` runs
6514
+ * in the app's own module (ESM evaluates imports before the importing module's
6515
+ * statements). So the static `authJobRouter` export below always uses the default
6516
+ * cron — `deletion.purgeCron` passed to `createAuthLifecycle()` cannot reach it.
6517
+ * To use a custom cron, build the router yourself, after calling
6518
+ * `createAuthLifecycle()`, with `createAuthDeletionJobRouter({ purgeCron })` and
6519
+ * register that instead of the static export.
6520
+ */
6521
+ /**
6522
+ * Build the `auth.deletion.purge` job with a given cron schedule.
6523
+ *
6524
+ * @param cronExpression - Defaults to `deletion-config`'s default (daily at 04:00).
6525
+ */
6526
+ declare function createAuthDeletionPurgeJob(cronExpression?: string): _spfn_core_job.JobDef<void, void>;
6527
+ /**
6528
+ * Build a job router containing only the deletion purge job, with a given cron.
6529
+ * Use this (after calling `createAuthLifecycle({ deletion: { purgeCron } })`) when
6530
+ * you need a non-default schedule — see the module doc comment above for why the
6531
+ * static `authJobRouter` export can't pick up `deletion.purgeCron` automatically.
6532
+ */
6533
+ declare function createAuthDeletionJobRouter(options?: {
6534
+ purgeCron?: string;
6535
+ }): _spfn_core_job.JobRouter<{
6536
+ deletionPurge: _spfn_core_job.JobDef<void, void>;
6537
+ }>;
6538
+ /**
6539
+ * Default job router — the default cron (`0 4 * * *`). Register with
6540
+ * `.jobs(authJobRouter)` in `server.config.ts`.
6541
+ */
6542
+ declare const authJobRouter: _spfn_core_job.JobRouter<{
6543
+ deletionPurge: _spfn_core_job.JobDef<void, void>;
6544
+ }>;
6545
+
6546
+ /**
6547
+ * @spfn/auth - Auth Events
6548
+ *
6549
+ * 인증 관련 이벤트 정의
6550
+ * - auth.login: 로그인 성공 시 (기존 사용자만)
6551
+ * - auth.register: 회원가입 성공 시 (OAuth 신규 가입 포함)
6552
+ */
6553
+ /**
6554
+ * Auth provider type
6555
+ *
6556
+ * 직접 인증(email/phone) + 등록 가능한 모든 소셜 provider(SOCIAL_PROVIDERS).
6557
+ */
6558
+ declare const AuthProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">, ..._sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]]>;
6559
+ /**
6560
+ * auth.login - 로그인 성공 이벤트
6561
+ *
6562
+ * 발행 시점:
6563
+ * - 이메일/전화 로그인 성공 시
6564
+ * - OAuth 기존 사용자 로그인 시
6565
+ *
6566
+ * @example
6567
+ * ```typescript
6568
+ * authLoginEvent.subscribe(async (payload) => {
6569
+ * await analytics.trackLogin(payload.userId, payload.provider);
6570
+ * });
6571
+ * ```
6572
+ */
6573
+ declare const authLoginEvent: _spfn_core_event.EventDef<{
6574
+ email?: string | undefined;
6575
+ phone?: string | undefined;
6576
+ userId: string;
6577
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
6578
+ }>;
6579
+ /**
6580
+ * auth.register - 회원가입 성공 이벤트
6581
+ *
6582
+ * 발행 시점:
6583
+ * - 이메일/전화 회원가입 성공 시
6584
+ * - OAuth 신규 사용자 가입 시
6585
+ *
6586
+ * @example
6587
+ * ```typescript
6588
+ * authRegisterEvent.subscribe(async (payload) => {
6589
+ * await emailService.sendWelcome(payload.email);
6590
+ * });
6591
+ * ```
6592
+ */
6593
+ declare const authRegisterEvent: _spfn_core_event.EventDef<{
6594
+ email?: string | undefined;
6595
+ phone?: string | undefined;
6596
+ metadata?: {
6597
+ [x: string]: unknown;
6598
+ } | undefined;
6599
+ userId: string;
6600
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
6601
+ }>;
6602
+ /**
6603
+ * auth.invitation.created - 초대 생성 이벤트
6604
+ *
6605
+ * 발행 시점:
6606
+ * - createInvitation() 성공 시
6607
+ * - resendInvitation() 성공 시
6608
+ *
6609
+ * @example
6610
+ * ```typescript
6611
+ * invitationCreatedEvent.subscribe(async (payload) => {
6612
+ * const inviteUrl = `${APP_URL}/invite/${payload.token}`;
6613
+ * await notificationService.send({
6614
+ * channel: 'email',
6615
+ * to: payload.email,
6616
+ * subject: 'You are invited!',
6617
+ * html: renderInviteEmail({ inviteUrl, ...payload.metadata }),
6618
+ * });
6619
+ * });
6620
+ * ```
6621
+ */
6622
+ declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
6623
+ metadata?: {
6624
+ [x: string]: unknown;
6625
+ } | undefined;
6626
+ email: string;
6627
+ roleId: number;
6628
+ token: string;
6629
+ invitedBy: string;
6630
+ expiresAt: string;
6631
+ invitationId: string;
6632
+ isResend: boolean;
6633
+ }>;
6634
+ /**
6635
+ * auth.invitation.accepted - 초대 수락 이벤트
6636
+ *
6637
+ * 발행 시점:
6638
+ * - acceptInvitation() 성공 시
6639
+ *
6640
+ * @example
6641
+ * ```typescript
6642
+ * invitationAcceptedEvent.subscribe(async (payload) => {
6643
+ * await onboardingService.start(payload.userId);
6644
+ * });
6645
+ * ```
6646
+ */
6647
+ declare const invitationAcceptedEvent: _spfn_core_event.EventDef<{
6648
+ metadata?: {
6649
+ [x: string]: unknown;
6650
+ } | undefined;
6651
+ email: string;
6652
+ userId: string;
6653
+ roleId: number;
6654
+ invitedBy: string;
6655
+ invitationId: string;
6656
+ }>;
6657
+ /**
6658
+ * auth.deletion.requested - 계정 탈퇴 요청 이벤트
6659
+ *
6660
+ * 발행 시점:
6661
+ * - requestAccountDeletionService() 성공 시 (self/admin 공통)
6662
+ *
6663
+ * @example
6664
+ * ```typescript
6665
+ * authDeletionRequestedEvent.subscribe(async (payload) => {
6666
+ * await analytics.trackChurnRisk(payload.userId);
6667
+ * });
6668
+ * ```
6669
+ */
6670
+ declare const authDeletionRequestedEvent: _spfn_core_event.EventDef<{
6671
+ userId: string;
6672
+ purgeScheduledAt: string;
6673
+ userPublicId: string;
6674
+ requestedBy: "admin" | "self";
6675
+ }>;
6676
+ /**
6677
+ * auth.deletion.cancelled - 계정 탈퇴 복구 이벤트
6678
+ *
6679
+ * 발행 시점:
6680
+ * - cancelAccountDeletionService() 성공 시 (유예 기간 내 복구)
6681
+ */
6682
+ declare const authDeletionCancelledEvent: _spfn_core_event.EventDef<{
6683
+ userId: string;
6684
+ userPublicId: string;
6685
+ }>;
6686
+ /**
6687
+ * auth.deletion.completed - 계정 파기 완료 이벤트
6688
+ *
6689
+ * 발행 시점:
6690
+ * - purge job(또는 즉시 파기 경로)이 유저를 파기한 직후
6691
+ *
6692
+ * PII를 담지 않는다 — userId(내부 순번)/email/phone 없이 userPublicId만 실어
6693
+ * 파기 완료 이후에도 구독자가 식별 정보를 다시 축적하지 않도록 한다.
6694
+ */
6695
+ declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
6696
+ userPublicId: string;
6697
+ purgeStrategy: "anonymize" | "hard-delete";
6698
+ }>;
6699
+ /**
6700
+ * Auth event payload types
6701
+ */
6702
+ type AuthLoginPayload = typeof authLoginEvent._payload;
6703
+ type AuthRegisterPayload = typeof authRegisterEvent._payload;
6704
+ type InvitationCreatedPayload = typeof invitationCreatedEvent._payload;
6705
+ type InvitationAcceptedPayload = typeof invitationAcceptedEvent._payload;
6706
+ type AuthDeletionRequestedPayload = typeof authDeletionRequestedEvent._payload;
6707
+ type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
6708
+ type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
5201
6709
 
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 };
6710
+ 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, githubProvider, 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 };