@spfn/auth 0.2.0-beta.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,11 +1,5 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
- }) : x)(function(x) {
6
- if (typeof require !== "undefined") return require.apply(this, arguments);
7
- throw Error('Dynamic require of "' + x + '" is not supported');
8
- });
9
3
  var __esm = (fn, res) => function __init() {
10
4
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
5
  };
@@ -1365,7 +1359,7 @@ var init_literal2 = __esm({
1365
1359
  });
1366
1360
 
1367
1361
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/boolean/boolean.mjs
1368
- function Boolean(options) {
1362
+ function Boolean2(options) {
1369
1363
  return CreateType({ [Kind]: "Boolean", type: "boolean" }, options);
1370
1364
  }
1371
1365
  var init_boolean = __esm({
@@ -1447,7 +1441,7 @@ var init_string2 = __esm({
1447
1441
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/template-literal/syntax.mjs
1448
1442
  function* FromUnion(syntax) {
1449
1443
  const trim = syntax.trim().replace(/"|'/g, "");
1450
- return trim === "boolean" ? yield Boolean() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt() : trim === "string" ? yield String2() : yield (() => {
1444
+ return trim === "boolean" ? yield Boolean2() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt() : trim === "string" ? yield String2() : yield (() => {
1451
1445
  const literals = trim.split("|").map((literal) => Literal(literal.trim()));
1452
1446
  return literals.length === 0 ? Never() : literals.length === 1 ? literals[0] : UnionEvaluated(literals);
1453
1447
  })();
@@ -4250,7 +4244,7 @@ __export(type_exports3, {
4250
4244
  AsyncIterator: () => AsyncIterator,
4251
4245
  Awaited: () => Awaited,
4252
4246
  BigInt: () => BigInt,
4253
- Boolean: () => Boolean,
4247
+ Boolean: () => Boolean2,
4254
4248
  Capitalize: () => Capitalize,
4255
4249
  Composite: () => Composite,
4256
4250
  Const: () => Const,
@@ -4570,7 +4564,7 @@ var init_roles = __esm({
4570
4564
  });
4571
4565
 
4572
4566
  // src/server/entities/users.ts
4573
- import { text as text2, check, boolean as boolean2, index as index2 } from "drizzle-orm/pg-core";
4567
+ import { text as text2, check, boolean as boolean2, index as index2, uuid } from "drizzle-orm/pg-core";
4574
4568
  import { id as id2, timestamps as timestamps2, enumText, utcTimestamp, foreignKey } from "@spfn/core/db";
4575
4569
  import { sql } from "drizzle-orm";
4576
4570
  var users;
@@ -4585,6 +4579,9 @@ var init_users = __esm({
4585
4579
  {
4586
4580
  // Identity
4587
4581
  id: id2(),
4582
+ // Public-facing UUID (for URLs, external APIs)
4583
+ // Never expose internal bigserial ID externally
4584
+ publicId: uuid("public_id").notNull().unique().defaultRandom(),
4588
4585
  // Email address (unique identifier)
4589
4586
  // Used for: login, password reset, notifications
4590
4587
  email: text2("email").unique(),
@@ -4592,6 +4589,9 @@ var init_users = __esm({
4592
4589
  // Format: +[country code][number] (e.g., +821012345678)
4593
4590
  // Used for: SMS login, 2FA, notifications
4594
4591
  phone: text2("phone").unique(),
4592
+ // Username (unique, optional)
4593
+ // Used for: display, mention, public profile URL
4594
+ username: text2("username").unique(),
4595
4595
  // Authentication
4596
4596
  // Bcrypt password hash ($2b$10$[salt][hash], 60 chars)
4597
4597
  // Nullable to support OAuth-only accounts
@@ -4629,8 +4629,10 @@ var init_users = __esm({
4629
4629
  sql`${table.email} IS NOT NULL OR ${table.phone} IS NOT NULL`
4630
4630
  ),
4631
4631
  // Indexes for query optimization
4632
+ index2("users_public_id_idx").on(table.publicId),
4632
4633
  index2("users_email_idx").on(table.email),
4633
4634
  index2("users_phone_idx").on(table.phone),
4635
+ index2("users_username_idx").on(table.username),
4634
4636
  index2("users_status_idx").on(table.status),
4635
4637
  index2("users_role_id_idx").on(table.roleId)
4636
4638
  ]
@@ -4655,8 +4657,8 @@ var init_user_profiles = __esm({
4655
4657
  // Foreign key to users table
4656
4658
  userId: foreignKey2("user", () => users.id).unique(),
4657
4659
  // Display Information
4658
- // Display name shown in UI (required)
4659
- displayName: text3("display_name").notNull(),
4660
+ // Display name shown in UI (optional)
4661
+ displayName: text3("display_name"),
4660
4662
  // First name (optional)
4661
4663
  firstName: text3("first_name"),
4662
4664
  // Last name (optional)
@@ -5297,6 +5299,27 @@ var init_user_permissions = __esm({
5297
5299
  }
5298
5300
  });
5299
5301
 
5302
+ // src/server/entities/auth-metadata.ts
5303
+ import { text as text10, timestamp } from "drizzle-orm/pg-core";
5304
+ var authMetadata;
5305
+ var init_auth_metadata = __esm({
5306
+ "src/server/entities/auth-metadata.ts"() {
5307
+ "use strict";
5308
+ init_schema4();
5309
+ authMetadata = authSchema.table(
5310
+ "auth_metadata",
5311
+ {
5312
+ // Metadata key (primary key)
5313
+ key: text10("key").primaryKey(),
5314
+ // Metadata value
5315
+ value: text10("value").notNull(),
5316
+ // Last updated timestamp
5317
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5318
+ }
5319
+ );
5320
+ }
5321
+ });
5322
+
5300
5323
  // src/server/entities/index.ts
5301
5324
  var init_entities = __esm({
5302
5325
  "src/server/entities/index.ts"() {
@@ -5312,6 +5335,7 @@ var init_entities = __esm({
5312
5335
  init_permissions();
5313
5336
  init_role_permissions();
5314
5337
  init_user_permissions();
5338
+ init_auth_metadata();
5315
5339
  }
5316
5340
  });
5317
5341
 
@@ -5349,6 +5373,22 @@ var init_users_repository = __esm({
5349
5373
  const result = await this.readDb.select().from(users).where(eq(users.phone, phone)).limit(1);
5350
5374
  return result[0] ?? null;
5351
5375
  }
5376
+ /**
5377
+ * 사용자명으로 사용자 조회
5378
+ * Read replica 사용
5379
+ */
5380
+ async findByUsername(username) {
5381
+ const result = await this.readDb.select().from(users).where(eq(users.username, username)).limit(1);
5382
+ return result[0] ?? null;
5383
+ }
5384
+ /**
5385
+ * Public ID(UUID)로 사용자 조회
5386
+ * Read replica 사용
5387
+ */
5388
+ async findByPublicId(publicId) {
5389
+ const result = await this.readDb.select().from(users).where(eq(users.publicId, publicId)).limit(1);
5390
+ return result[0] ?? null;
5391
+ }
5352
5392
  /**
5353
5393
  * 이메일 또는 전화번호로 사용자 조회
5354
5394
  * Read replica 사용
@@ -5361,6 +5401,28 @@ var init_users_repository = __esm({
5361
5401
  }
5362
5402
  return null;
5363
5403
  }
5404
+ /**
5405
+ * ID로 사용자 + Role 조회 (leftJoin)
5406
+ * Read replica 사용
5407
+ *
5408
+ * roleId가 null인 유저는 role: null 반환
5409
+ */
5410
+ async findByIdWithRole(id11) {
5411
+ const result = await this.readDb.select({
5412
+ user: users,
5413
+ roleName: roles.name,
5414
+ roleDisplayName: roles.displayName,
5415
+ rolePriority: roles.priority
5416
+ }).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id11)).limit(1);
5417
+ const row = result[0];
5418
+ if (!row) {
5419
+ return null;
5420
+ }
5421
+ return {
5422
+ user: row.user,
5423
+ role: row.roleName ? { name: row.roleName, displayName: row.roleDisplayName, priority: row.rolePriority } : null
5424
+ };
5425
+ }
5364
5426
  /**
5365
5427
  * 사용자 생성
5366
5428
  * Write primary 사용
@@ -5467,7 +5529,9 @@ var init_users_repository = __esm({
5467
5529
  async fetchMinimalUserData(userId) {
5468
5530
  const user = await this.readDb.select({
5469
5531
  id: users.id,
5532
+ publicId: users.publicId,
5470
5533
  email: users.email,
5534
+ username: users.username,
5471
5535
  emailVerifiedAt: users.emailVerifiedAt,
5472
5536
  phoneVerifiedAt: users.phoneVerifiedAt
5473
5537
  }).from(users).where(eq(users.id, userId)).limit(1).then((rows) => rows[0] ?? null);
@@ -5476,7 +5540,9 @@ var init_users_repository = __esm({
5476
5540
  }
5477
5541
  return {
5478
5542
  userId: user.id,
5543
+ publicId: user.publicId,
5479
5544
  email: user.email,
5545
+ username: user.username,
5480
5546
  isEmailVerified: !!user.emailVerifiedAt,
5481
5547
  isPhoneVerified: !!user.phoneVerifiedAt
5482
5548
  };
@@ -5491,7 +5557,9 @@ var init_users_repository = __esm({
5491
5557
  async fetchFullUserData(userId) {
5492
5558
  const user = await this.readDb.select({
5493
5559
  id: users.id,
5560
+ publicId: users.publicId,
5494
5561
  email: users.email,
5562
+ username: users.username,
5495
5563
  emailVerifiedAt: users.emailVerifiedAt,
5496
5564
  phoneVerifiedAt: users.phoneVerifiedAt,
5497
5565
  lastLoginAt: users.lastLoginAt,
@@ -5503,7 +5571,9 @@ var init_users_repository = __esm({
5503
5571
  }
5504
5572
  return {
5505
5573
  userId: user.id,
5574
+ publicId: user.publicId,
5506
5575
  email: user.email,
5576
+ username: user.username,
5507
5577
  isEmailVerified: !!user.emailVerifiedAt,
5508
5578
  isPhoneVerified: !!user.phoneVerifiedAt,
5509
5579
  lastLoginAt: user.lastLoginAt,
@@ -6087,6 +6157,13 @@ var init_user_profiles_repository = __esm({
6087
6157
  const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id, id11)).limit(1);
6088
6158
  return result[0] ?? null;
6089
6159
  }
6160
+ /**
6161
+ * User ID로 locale만 조회 (경량)
6162
+ */
6163
+ async findLocaleByUserId(userId) {
6164
+ const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1);
6165
+ return result[0]?.locale || "en";
6166
+ }
6090
6167
  /**
6091
6168
  * User ID로 프로필 조회
6092
6169
  */
@@ -6260,16 +6337,16 @@ var init_invitations_repository = __esm({
6260
6337
  /**
6261
6338
  * 초대 상태 업데이트
6262
6339
  */
6263
- async updateStatus(id11, status, timestamp) {
6340
+ async updateStatus(id11, status, timestamp2) {
6264
6341
  const updates = {
6265
6342
  status,
6266
6343
  updatedAt: /* @__PURE__ */ new Date()
6267
6344
  };
6268
- if (timestamp) {
6345
+ if (timestamp2) {
6269
6346
  if (status === "accepted") {
6270
- updates.acceptedAt = timestamp;
6347
+ updates.acceptedAt = timestamp2;
6271
6348
  } else if (status === "cancelled") {
6272
- updates.cancelledAt = timestamp;
6349
+ updates.cancelledAt = timestamp2;
6273
6350
  }
6274
6351
  }
6275
6352
  const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id, id11)).returning();
@@ -6413,6 +6490,133 @@ var init_invitations_repository = __esm({
6413
6490
  }
6414
6491
  });
6415
6492
 
6493
+ // src/server/repositories/social-accounts.repository.ts
6494
+ import { eq as eq10, and as and7 } from "drizzle-orm";
6495
+ import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
6496
+ var SocialAccountsRepository, socialAccountsRepository;
6497
+ var init_social_accounts_repository = __esm({
6498
+ "src/server/repositories/social-accounts.repository.ts"() {
6499
+ "use strict";
6500
+ init_entities();
6501
+ SocialAccountsRepository = class extends BaseRepository10 {
6502
+ /**
6503
+ * provider와 providerUserId로 소셜 계정 조회
6504
+ * Read replica 사용
6505
+ */
6506
+ async findByProviderAndProviderId(provider, providerUserId) {
6507
+ const result = await this.readDb.select().from(userSocialAccounts).where(
6508
+ and7(
6509
+ eq10(userSocialAccounts.provider, provider),
6510
+ eq10(userSocialAccounts.providerUserId, providerUserId)
6511
+ )
6512
+ ).limit(1);
6513
+ return result[0] ?? null;
6514
+ }
6515
+ /**
6516
+ * userId로 모든 소셜 계정 조회
6517
+ * Read replica 사용
6518
+ */
6519
+ async findByUserId(userId) {
6520
+ return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
6521
+ }
6522
+ /**
6523
+ * userId와 provider로 소셜 계정 조회
6524
+ * Read replica 사용
6525
+ */
6526
+ async findByUserIdAndProvider(userId, provider) {
6527
+ const result = await this.readDb.select().from(userSocialAccounts).where(
6528
+ and7(
6529
+ eq10(userSocialAccounts.userId, userId),
6530
+ eq10(userSocialAccounts.provider, provider)
6531
+ )
6532
+ ).limit(1);
6533
+ return result[0] ?? null;
6534
+ }
6535
+ /**
6536
+ * 소셜 계정 생성
6537
+ * Write primary 사용
6538
+ */
6539
+ async create(data) {
6540
+ return await this._create(userSocialAccounts, {
6541
+ ...data,
6542
+ createdAt: /* @__PURE__ */ new Date(),
6543
+ updatedAt: /* @__PURE__ */ new Date()
6544
+ });
6545
+ }
6546
+ /**
6547
+ * 토큰 정보 업데이트
6548
+ * Write primary 사용
6549
+ */
6550
+ async updateTokens(id11, data) {
6551
+ const result = await this.db.update(userSocialAccounts).set({
6552
+ ...data,
6553
+ updatedAt: /* @__PURE__ */ new Date()
6554
+ }).where(eq10(userSocialAccounts.id, id11)).returning();
6555
+ return result[0] ?? null;
6556
+ }
6557
+ /**
6558
+ * 소셜 계정 삭제
6559
+ * Write primary 사용
6560
+ */
6561
+ async deleteById(id11) {
6562
+ const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
6563
+ return result[0] ?? null;
6564
+ }
6565
+ /**
6566
+ * userId와 provider로 소셜 계정 삭제
6567
+ * Write primary 사용
6568
+ */
6569
+ async deleteByUserIdAndProvider(userId, provider) {
6570
+ const result = await this.db.delete(userSocialAccounts).where(
6571
+ and7(
6572
+ eq10(userSocialAccounts.userId, userId),
6573
+ eq10(userSocialAccounts.provider, provider)
6574
+ )
6575
+ ).returning();
6576
+ return result[0] ?? null;
6577
+ }
6578
+ };
6579
+ socialAccountsRepository = new SocialAccountsRepository();
6580
+ }
6581
+ });
6582
+
6583
+ // src/server/repositories/auth-metadata.repository.ts
6584
+ import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
6585
+ import { eq as eq11 } from "drizzle-orm";
6586
+ var AuthMetadataRepository, authMetadataRepository;
6587
+ var init_auth_metadata_repository = __esm({
6588
+ "src/server/repositories/auth-metadata.repository.ts"() {
6589
+ "use strict";
6590
+ init_auth_metadata();
6591
+ AuthMetadataRepository = class extends BaseRepository11 {
6592
+ /**
6593
+ * 키로 값 조회
6594
+ */
6595
+ async get(key) {
6596
+ const result = await this.readDb.select().from(authMetadata).where(eq11(authMetadata.key, key)).limit(1);
6597
+ return result[0]?.value ?? null;
6598
+ }
6599
+ /**
6600
+ * 키-값 저장 (upsert)
6601
+ */
6602
+ async set(key, value) {
6603
+ await this.db.insert(authMetadata).values({
6604
+ key,
6605
+ value,
6606
+ updatedAt: /* @__PURE__ */ new Date()
6607
+ }).onConflictDoUpdate({
6608
+ target: authMetadata.key,
6609
+ set: {
6610
+ value,
6611
+ updatedAt: /* @__PURE__ */ new Date()
6612
+ }
6613
+ });
6614
+ }
6615
+ };
6616
+ authMetadataRepository = new AuthMetadataRepository();
6617
+ }
6618
+ });
6619
+
6416
6620
  // src/server/repositories/index.ts
6417
6621
  var init_repositories = __esm({
6418
6622
  "src/server/repositories/index.ts"() {
@@ -6426,6 +6630,8 @@ var init_repositories = __esm({
6426
6630
  init_user_permissions_repository();
6427
6631
  init_user_profiles_repository();
6428
6632
  init_invitations_repository();
6633
+ init_social_accounts_repository();
6634
+ init_auth_metadata_repository();
6429
6635
  }
6430
6636
  });
6431
6637
 
@@ -6552,7 +6758,7 @@ var init_role_service = __esm({
6552
6758
  import "@spfn/auth/config";
6553
6759
 
6554
6760
  // src/server/routes/index.ts
6555
- import { defineRouter as defineRouter4 } from "@spfn/core/route";
6761
+ import { defineRouter as defineRouter5 } from "@spfn/core/route";
6556
6762
 
6557
6763
  // src/server/routes/auth/index.ts
6558
6764
  init_schema3();
@@ -6675,12 +6881,24 @@ function getAuth(c) {
6675
6881
  }
6676
6882
  return c.get("auth");
6677
6883
  }
6884
+ function getOptionalAuth(c) {
6885
+ if ("raw" in c && c.raw) {
6886
+ return c.raw.get("auth");
6887
+ }
6888
+ return c.get("auth");
6889
+ }
6678
6890
  function getUser(c) {
6679
6891
  return getAuth(c).user;
6680
6892
  }
6681
6893
  function getUserId(c) {
6682
6894
  return getAuth(c).userId;
6683
6895
  }
6896
+ function getRole(c) {
6897
+ return getAuth(c).role;
6898
+ }
6899
+ function getLocale(c) {
6900
+ return getAuth(c).locale;
6901
+ }
6684
6902
  function getKeyId(c) {
6685
6903
  return getAuth(c).keyId;
6686
6904
  }
@@ -6690,7 +6908,7 @@ init_types();
6690
6908
 
6691
6909
  // src/server/services/auth.service.ts
6692
6910
  init_repositories();
6693
- import { ValidationError } from "@spfn/core/errors";
6911
+ import { ValidationError as ValidationError2 } from "@spfn/core/errors";
6694
6912
  import {
6695
6913
  InvalidCredentialsError,
6696
6914
  AccountDisabledError,
@@ -6701,9 +6919,10 @@ import {
6701
6919
  } from "@spfn/auth/errors";
6702
6920
 
6703
6921
  // src/server/services/verification.service.ts
6704
- import { env as env5 } from "@spfn/auth/config";
6922
+ import { env as env3 } from "@spfn/auth/config";
6705
6923
  import { InvalidVerificationCodeError } from "@spfn/auth/errors";
6706
6924
  import jwt2 from "jsonwebtoken";
6925
+ import { sendEmail, sendSMS } from "@spfn/notification/server";
6707
6926
 
6708
6927
  // src/server/logger.ts
6709
6928
  import { logger as rootLogger } from "@spfn/core/logger";
@@ -6713,8 +6932,10 @@ var authLogger = {
6713
6932
  interceptor: {
6714
6933
  general: rootLogger.child("@spfn/auth:interceptor:general"),
6715
6934
  login: rootLogger.child("@spfn/auth:interceptor:login"),
6716
- keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation")
6935
+ keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
6936
+ oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
6717
6937
  },
6938
+ session: rootLogger.child("@spfn/auth:session"),
6718
6939
  service: rootLogger.child("@spfn/auth:service"),
6719
6940
  setup: rootLogger.child("@spfn/auth:setup"),
6720
6941
  email: rootLogger.child("@spfn/auth:email"),
@@ -6723,421 +6944,6 @@ var authLogger = {
6723
6944
 
6724
6945
  // src/server/services/verification.service.ts
6725
6946
  init_repositories();
6726
-
6727
- // src/server/services/sms/provider.ts
6728
- var currentProvider = null;
6729
- var fallbackProvider = {
6730
- name: "fallback",
6731
- sendSMS: async (params) => {
6732
- authLogger.sms.debug("DEV MODE - SMS not actually sent", {
6733
- phone: params.phone,
6734
- message: params.message,
6735
- purpose: params.purpose || "N/A"
6736
- });
6737
- return {
6738
- success: true,
6739
- messageId: "dev-mode-no-actual-sms"
6740
- };
6741
- }
6742
- };
6743
- function registerSMSProvider(provider) {
6744
- currentProvider = provider;
6745
- authLogger.sms.info("Registered SMS provider", { name: provider.name });
6746
- }
6747
- function getSMSProvider() {
6748
- return currentProvider || fallbackProvider;
6749
- }
6750
- async function sendSMS(params) {
6751
- const provider = getSMSProvider();
6752
- return await provider.sendSMS(params);
6753
- }
6754
-
6755
- // src/server/services/sms/aws-sns.provider.ts
6756
- import { env as env3 } from "@spfn/auth/config";
6757
- function isValidE164Phone(phone) {
6758
- const e164Regex = /^\+[1-9]\d{1,14}$/;
6759
- return e164Regex.test(phone);
6760
- }
6761
- function createAWSSNSProvider() {
6762
- try {
6763
- const { SNSClient, PublishCommand } = __require("@aws-sdk/client-sns");
6764
- return {
6765
- name: "aws-sns",
6766
- sendSMS: async (params) => {
6767
- const { phone, message, purpose } = params;
6768
- if (!isValidE164Phone(phone)) {
6769
- return {
6770
- success: false,
6771
- error: "Invalid phone number format. Must be E.164 format (e.g., +821012345678)"
6772
- };
6773
- }
6774
- if (!env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID) {
6775
- return {
6776
- success: false,
6777
- error: "AWS SNS credentials not configured. Set SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID environment variable."
6778
- };
6779
- }
6780
- try {
6781
- const config = {
6782
- region: env3.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
6783
- };
6784
- if (env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID && env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY) {
6785
- config.credentials = {
6786
- accessKeyId: env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID,
6787
- secretAccessKey: env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY
6788
- };
6789
- }
6790
- const client = new SNSClient(config);
6791
- const command = new PublishCommand({
6792
- PhoneNumber: phone,
6793
- Message: message,
6794
- MessageAttributes: {
6795
- "AWS.SNS.SMS.SMSType": {
6796
- DataType: "String",
6797
- StringValue: "Transactional"
6798
- // For OTP codes
6799
- },
6800
- ...env3.SPFN_AUTH_AWS_SNS_SENDER_ID && {
6801
- "AWS.SNS.SMS.SenderID": {
6802
- DataType: "String",
6803
- StringValue: env3.SPFN_AUTH_AWS_SNS_SENDER_ID
6804
- }
6805
- }
6806
- }
6807
- });
6808
- const response = await client.send(command);
6809
- authLogger.sms.info("SMS sent via AWS SNS", {
6810
- phone,
6811
- messageId: response.MessageId,
6812
- purpose: purpose || "N/A"
6813
- });
6814
- return {
6815
- success: true,
6816
- messageId: response.MessageId
6817
- };
6818
- } catch (error) {
6819
- const err = error;
6820
- authLogger.sms.error("Failed to send SMS via AWS SNS", {
6821
- phone,
6822
- error: err.message
6823
- });
6824
- return {
6825
- success: false,
6826
- error: err.message || "Failed to send SMS via AWS SNS"
6827
- };
6828
- }
6829
- }
6830
- };
6831
- } catch (error) {
6832
- return null;
6833
- }
6834
- }
6835
- var awsSNSProvider = createAWSSNSProvider();
6836
-
6837
- // src/server/services/sms/index.ts
6838
- if (awsSNSProvider) {
6839
- registerSMSProvider(awsSNSProvider);
6840
- }
6841
-
6842
- // src/server/services/email/provider.ts
6843
- var currentProvider2 = null;
6844
- var fallbackProvider2 = {
6845
- name: "fallback",
6846
- sendEmail: async (params) => {
6847
- authLogger.email.debug("DEV MODE - Email not actually sent", {
6848
- to: params.to,
6849
- subject: params.subject,
6850
- purpose: params.purpose || "N/A",
6851
- textPreview: params.text?.substring(0, 100) || "N/A"
6852
- });
6853
- return {
6854
- success: true,
6855
- messageId: "dev-mode-no-actual-email"
6856
- };
6857
- }
6858
- };
6859
- function registerEmailProvider(provider) {
6860
- currentProvider2 = provider;
6861
- authLogger.email.info("Registered email provider", { name: provider.name });
6862
- }
6863
- function getEmailProvider() {
6864
- return currentProvider2 || fallbackProvider2;
6865
- }
6866
- async function sendEmail(params) {
6867
- const provider = getEmailProvider();
6868
- return await provider.sendEmail(params);
6869
- }
6870
-
6871
- // src/server/services/email/aws-ses.provider.ts
6872
- import { env as env4 } from "@spfn/auth/config";
6873
- function isValidEmail(email) {
6874
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6875
- return emailRegex.test(email);
6876
- }
6877
- function createAWSSESProvider() {
6878
- try {
6879
- const { SESClient, SendEmailCommand } = __require("@aws-sdk/client-ses");
6880
- return {
6881
- name: "aws-ses",
6882
- sendEmail: async (params) => {
6883
- const { to, subject, text: text10, html, purpose } = params;
6884
- if (!isValidEmail(to)) {
6885
- return {
6886
- success: false,
6887
- error: "Invalid email address format"
6888
- };
6889
- }
6890
- if (!env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID) {
6891
- authLogger.email.warn("AWS SES credentials not configured", {
6892
- hint: "Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable"
6893
- });
6894
- return {
6895
- success: false,
6896
- error: "AWS SES credentials not configured. Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable."
6897
- };
6898
- }
6899
- if (!env4.SPFN_AUTH_AWS_SES_FROM_EMAIL) {
6900
- authLogger.email.warn("AWS SES sender email not configured", {
6901
- hint: "Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable"
6902
- });
6903
- return {
6904
- success: false,
6905
- error: "AWS SES sender email not configured. Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable."
6906
- };
6907
- }
6908
- try {
6909
- const config = {
6910
- region: env4.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
6911
- };
6912
- if (env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID && env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY) {
6913
- config.credentials = {
6914
- accessKeyId: env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID,
6915
- secretAccessKey: env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY
6916
- };
6917
- }
6918
- const client = new SESClient(config);
6919
- const body = {};
6920
- if (text10) {
6921
- body.Text = {
6922
- Charset: "UTF-8",
6923
- Data: text10
6924
- };
6925
- }
6926
- if (html) {
6927
- body.Html = {
6928
- Charset: "UTF-8",
6929
- Data: html
6930
- };
6931
- }
6932
- const command = new SendEmailCommand({
6933
- Source: env4.SPFN_AUTH_AWS_SES_FROM_EMAIL,
6934
- Destination: {
6935
- ToAddresses: [to]
6936
- },
6937
- Message: {
6938
- Subject: {
6939
- Charset: "UTF-8",
6940
- Data: subject
6941
- },
6942
- Body: body
6943
- }
6944
- });
6945
- const response = await client.send(command);
6946
- authLogger.email.info("Email sent via AWS SES", {
6947
- to,
6948
- messageId: response.MessageId,
6949
- purpose: purpose || "N/A"
6950
- });
6951
- return {
6952
- success: true,
6953
- messageId: response.MessageId
6954
- };
6955
- } catch (error) {
6956
- const err = error;
6957
- authLogger.email.error("Failed to send email via AWS SES", {
6958
- to,
6959
- error: err.message
6960
- });
6961
- return {
6962
- success: false,
6963
- error: err.message || "Failed to send email via AWS SES"
6964
- };
6965
- }
6966
- }
6967
- };
6968
- } catch (error) {
6969
- authLogger.email.debug("@aws-sdk/client-ses not available, AWS SES provider disabled", {
6970
- error: error instanceof Error ? error.message : String(error)
6971
- });
6972
- return null;
6973
- }
6974
- }
6975
- var awsSESProvider = createAWSSESProvider();
6976
-
6977
- // src/server/services/email/index.ts
6978
- if (awsSESProvider) {
6979
- registerEmailProvider(awsSESProvider);
6980
- } else {
6981
- authLogger.email.warn("No email provider registered, using fallback (dev mode). Emails will NOT be sent. Install @aws-sdk/client-ses to enable AWS SES.");
6982
- }
6983
-
6984
- // src/server/services/email/templates/verification-code.ts
6985
- function getSubject(purpose) {
6986
- switch (purpose) {
6987
- case "registration":
6988
- return "Verify your email address";
6989
- case "login":
6990
- return "Your login verification code";
6991
- case "password_reset":
6992
- return "Reset your password";
6993
- default:
6994
- return "Your verification code";
6995
- }
6996
- }
6997
- function getPurposeText(purpose) {
6998
- switch (purpose) {
6999
- case "registration":
7000
- return "complete your registration";
7001
- case "login":
7002
- return "verify your identity";
7003
- case "password_reset":
7004
- return "reset your password";
7005
- default:
7006
- return "verify your identity";
7007
- }
7008
- }
7009
- function generateText(params) {
7010
- const { code, expiresInMinutes = 5 } = params;
7011
- return `Your verification code is: ${code}
7012
-
7013
- This code will expire in ${expiresInMinutes} minutes.
7014
-
7015
- If you didn't request this code, please ignore this email.`;
7016
- }
7017
- function generateHTML(params) {
7018
- const { code, purpose, expiresInMinutes = 5, appName } = params;
7019
- const purposeText = getPurposeText(purpose);
7020
- return `<!DOCTYPE html>
7021
- <html>
7022
- <head>
7023
- <meta charset="utf-8">
7024
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7025
- <title>Verification Code</title>
7026
- </head>
7027
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f5f5f5;">
7028
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
7029
- <h1 style="color: white; margin: 0; font-size: 24px;">${appName ? appName : "Verification Code"}</h1>
7030
- </div>
7031
- <div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
7032
- <p style="margin-bottom: 20px; font-size: 16px;">
7033
- Please use the following verification code to ${purposeText}:
7034
- </p>
7035
- <div style="background: #f8f9fa; padding: 25px; border-radius: 8px; text-align: center; margin: 25px 0; border: 2px dashed #dee2e6;">
7036
- <span style="font-size: 36px; font-weight: bold; letter-spacing: 10px; color: #333; font-family: 'Courier New', monospace;">${code}</span>
7037
- </div>
7038
- <p style="color: #666; font-size: 14px; margin-top: 20px; text-align: center;">
7039
- <strong>This code will expire in ${expiresInMinutes} minutes.</strong>
7040
- </p>
7041
- <hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
7042
- <p style="color: #999; font-size: 12px; text-align: center; margin: 0;">
7043
- If you didn't request this code, please ignore this email.
7044
- </p>
7045
- </div>
7046
- <div style="text-align: center; padding: 20px; color: #999; font-size: 11px;">
7047
- <p style="margin: 0;">This is an automated message. Please do not reply.</p>
7048
- </div>
7049
- </body>
7050
- </html>`;
7051
- }
7052
- function verificationCodeTemplate(params) {
7053
- return {
7054
- subject: getSubject(params.purpose),
7055
- text: generateText(params),
7056
- html: generateHTML(params)
7057
- };
7058
- }
7059
-
7060
- // src/server/services/email/templates/registry.ts
7061
- var customTemplates = {};
7062
- function registerEmailTemplates(templates) {
7063
- customTemplates = { ...customTemplates, ...templates };
7064
- authLogger.email.info("Registered custom email templates", {
7065
- templates: Object.keys(templates)
7066
- });
7067
- }
7068
- function getVerificationCodeTemplate(params) {
7069
- if (customTemplates.verificationCode) {
7070
- return customTemplates.verificationCode(params);
7071
- }
7072
- return verificationCodeTemplate(params);
7073
- }
7074
- function getWelcomeTemplate(params) {
7075
- if (customTemplates.welcome) {
7076
- return customTemplates.welcome(params);
7077
- }
7078
- return {
7079
- subject: params.appName ? `Welcome to ${params.appName}!` : "Welcome!",
7080
- text: `Welcome! Your account has been created successfully.`,
7081
- html: `
7082
- <!DOCTYPE html>
7083
- <html>
7084
- <head><meta charset="utf-8"></head>
7085
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7086
- <h1>Welcome${params.appName ? ` to ${params.appName}` : ""}!</h1>
7087
- <p>Your account has been created successfully.</p>
7088
- </body>
7089
- </html>`
7090
- };
7091
- }
7092
- function getPasswordResetTemplate(params) {
7093
- if (customTemplates.passwordReset) {
7094
- return customTemplates.passwordReset(params);
7095
- }
7096
- const expires = params.expiresInMinutes || 30;
7097
- return {
7098
- subject: "Reset your password",
7099
- text: `Click this link to reset your password: ${params.resetLink}
7100
-
7101
- This link will expire in ${expires} minutes.`,
7102
- html: `
7103
- <!DOCTYPE html>
7104
- <html>
7105
- <head><meta charset="utf-8"></head>
7106
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7107
- <h1>Reset Your Password</h1>
7108
- <p>Click the button below to reset your password:</p>
7109
- <a href="${params.resetLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Reset Password</a>
7110
- <p style="color: #666; margin-top: 20px;">This link will expire in ${expires} minutes.</p>
7111
- </body>
7112
- </html>`
7113
- };
7114
- }
7115
- function getInvitationTemplate(params) {
7116
- if (customTemplates.invitation) {
7117
- return customTemplates.invitation(params);
7118
- }
7119
- const appName = params.appName || "our platform";
7120
- const inviterText = params.inviterName ? `${params.inviterName} has invited you` : "You have been invited";
7121
- const roleText = params.roleName ? ` as ${params.roleName}` : "";
7122
- return {
7123
- subject: `You're invited to join ${appName}`,
7124
- text: `${inviterText} to join ${appName}${roleText}.
7125
-
7126
- Click here to accept: ${params.inviteLink}`,
7127
- html: `
7128
- <!DOCTYPE html>
7129
- <html>
7130
- <head><meta charset="utf-8"></head>
7131
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7132
- <h1>You're Invited!</h1>
7133
- <p>${inviterText} to join <strong>${appName}</strong>${roleText}.</p>
7134
- <a href="${params.inviteLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Accept Invitation</a>
7135
- </body>
7136
- </html>`
7137
- };
7138
- }
7139
-
7140
- // src/server/services/verification.service.ts
7141
6947
  var VERIFICATION_TOKEN_EXPIRY = "15m";
7142
6948
  var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
7143
6949
  var MAX_VERIFICATION_ATTEMPTS = 5;
@@ -7181,7 +6987,7 @@ async function markCodeAsUsed(codeId) {
7181
6987
  await verificationCodesRepository.markAsUsed(codeId);
7182
6988
  }
7183
6989
  function createVerificationToken(payload) {
7184
- return jwt2.sign(payload, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6990
+ return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7185
6991
  expiresIn: VERIFICATION_TOKEN_EXPIRY,
7186
6992
  issuer: "spfn-auth",
7187
6993
  audience: "spfn-client"
@@ -7189,7 +6995,7 @@ function createVerificationToken(payload) {
7189
6995
  }
7190
6996
  function validateVerificationToken(token) {
7191
6997
  try {
7192
- const decoded = jwt2.verify(token, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6998
+ const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7193
6999
  issuer: "spfn-auth",
7194
7000
  audience: "spfn-client"
7195
7001
  });
@@ -7203,17 +7009,14 @@ function validateVerificationToken(token) {
7203
7009
  }
7204
7010
  }
7205
7011
  async function sendVerificationEmail(email, code, purpose) {
7206
- const { subject, text: text10, html } = getVerificationCodeTemplate({
7207
- code,
7208
- purpose,
7209
- expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7210
- });
7211
7012
  const result = await sendEmail({
7212
7013
  to: email,
7213
- subject,
7214
- text: text10,
7215
- html,
7216
- purpose
7014
+ template: "verification-code",
7015
+ data: {
7016
+ code,
7017
+ purpose,
7018
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7019
+ }
7217
7020
  });
7218
7021
  if (!result.success) {
7219
7022
  authLogger.email.error("Failed to send verification email", {
@@ -7224,11 +7027,13 @@ async function sendVerificationEmail(email, code, purpose) {
7224
7027
  }
7225
7028
  }
7226
7029
  async function sendVerificationSMS(phone, code, purpose) {
7227
- const message = `Your verification code is: ${code}`;
7228
7030
  const result = await sendSMS({
7229
- phone,
7230
- message,
7231
- purpose
7031
+ to: phone,
7032
+ template: "verification-code",
7033
+ data: {
7034
+ code,
7035
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7036
+ }
7232
7037
  });
7233
7038
  if (!result.success) {
7234
7039
  authLogger.sms.error("Failed to send verification SMS", {
@@ -7329,6 +7134,9 @@ async function revokeKeyService(params) {
7329
7134
 
7330
7135
  // src/server/services/user.service.ts
7331
7136
  init_repositories();
7137
+ import { ValidationError } from "@spfn/core/errors";
7138
+ import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
7139
+ import { env as env4 } from "@spfn/auth/config";
7332
7140
  async function getUserByIdService(userId) {
7333
7141
  return await usersRepository.findById(userId);
7334
7142
  }
@@ -7344,6 +7152,108 @@ async function updateLastLoginService(userId) {
7344
7152
  async function updateUserService(userId, updates) {
7345
7153
  await usersRepository.updateById(userId, updates);
7346
7154
  }
7155
+ function getReservedUsernames() {
7156
+ const raw = env4.SPFN_AUTH_RESERVED_USERNAMES ?? "";
7157
+ if (!raw) {
7158
+ return /* @__PURE__ */ new Set();
7159
+ }
7160
+ return new Set(
7161
+ raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
7162
+ );
7163
+ }
7164
+ function isReservedUsername(username) {
7165
+ return getReservedUsernames().has(username.toLowerCase());
7166
+ }
7167
+ function validateUsernameLength(username) {
7168
+ const min = env4.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
7169
+ const max = env4.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
7170
+ if (username.length < min) {
7171
+ throw new ValidationError({
7172
+ message: `Username must be at least ${min} characters`,
7173
+ details: { minLength: min, actual: username.length }
7174
+ });
7175
+ }
7176
+ if (username.length > max) {
7177
+ throw new ValidationError({
7178
+ message: `Username must be at most ${max} characters`,
7179
+ details: { maxLength: max, actual: username.length }
7180
+ });
7181
+ }
7182
+ }
7183
+ async function checkUsernameAvailableService(username) {
7184
+ validateUsernameLength(username);
7185
+ if (isReservedUsername(username)) {
7186
+ return false;
7187
+ }
7188
+ const existing = await usersRepository.findByUsername(username);
7189
+ return !existing;
7190
+ }
7191
+ async function updateUsernameService(userId, username) {
7192
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7193
+ if (username !== null) {
7194
+ validateUsernameLength(username);
7195
+ if (isReservedUsername(username)) {
7196
+ throw new ReservedUsernameError({ username });
7197
+ }
7198
+ const existing = await usersRepository.findByUsername(username);
7199
+ if (existing && existing.id !== userIdNum) {
7200
+ throw new UsernameAlreadyTakenError({ username });
7201
+ }
7202
+ }
7203
+ return await usersRepository.updateById(userIdNum, { username });
7204
+ }
7205
+
7206
+ // src/server/events/index.ts
7207
+ init_esm();
7208
+ import { defineEvent } from "@spfn/core/event";
7209
+ var AuthProviderSchema = Type.Union([
7210
+ Type.Literal("email"),
7211
+ Type.Literal("phone"),
7212
+ Type.Literal("google")
7213
+ ]);
7214
+ var authLoginEvent = defineEvent(
7215
+ "auth.login",
7216
+ Type.Object({
7217
+ userId: Type.String(),
7218
+ provider: AuthProviderSchema,
7219
+ email: Type.Optional(Type.String()),
7220
+ phone: Type.Optional(Type.String())
7221
+ })
7222
+ );
7223
+ var authRegisterEvent = defineEvent(
7224
+ "auth.register",
7225
+ Type.Object({
7226
+ userId: Type.String(),
7227
+ provider: AuthProviderSchema,
7228
+ email: Type.Optional(Type.String()),
7229
+ phone: Type.Optional(Type.String()),
7230
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
7231
+ })
7232
+ );
7233
+ var invitationCreatedEvent = defineEvent(
7234
+ "auth.invitation.created",
7235
+ Type.Object({
7236
+ invitationId: Type.String(),
7237
+ email: Type.String(),
7238
+ token: Type.String(),
7239
+ roleId: Type.Number(),
7240
+ invitedBy: Type.String(),
7241
+ expiresAt: Type.String(),
7242
+ isResend: Type.Boolean(),
7243
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
7244
+ })
7245
+ );
7246
+ var invitationAcceptedEvent = defineEvent(
7247
+ "auth.invitation.accepted",
7248
+ Type.Object({
7249
+ invitationId: Type.String(),
7250
+ email: Type.String(),
7251
+ userId: Type.String(),
7252
+ roleId: Type.Number(),
7253
+ invitedBy: Type.String(),
7254
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
7255
+ })
7256
+ );
7347
7257
 
7348
7258
  // src/server/services/auth.service.ts
7349
7259
  async function checkAccountExistsService(params) {
@@ -7360,7 +7270,7 @@ async function checkAccountExistsService(params) {
7360
7270
  identifierType = "phone";
7361
7271
  user = await usersRepository.findByPhone(phone);
7362
7272
  } else {
7363
- throw new ValidationError({ message: "Either email or phone must be provided" });
7273
+ throw new ValidationError2({ message: "Either email or phone must be provided" });
7364
7274
  }
7365
7275
  return {
7366
7276
  exists: !!user,
@@ -7369,7 +7279,7 @@ async function checkAccountExistsService(params) {
7369
7279
  };
7370
7280
  }
7371
7281
  async function registerService(params) {
7372
- const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm } = params;
7282
+ const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
7373
7283
  const tokenPayload = validateVerificationToken(verificationToken);
7374
7284
  if (!tokenPayload) {
7375
7285
  throw new InvalidVerificationTokenError();
@@ -7411,17 +7321,26 @@ async function registerService(params) {
7411
7321
  fingerprint,
7412
7322
  algorithm
7413
7323
  });
7414
- return {
7324
+ const result = {
7415
7325
  userId: String(newUser.id),
7326
+ publicId: newUser.publicId,
7416
7327
  email: newUser.email || void 0,
7417
7328
  phone: newUser.phone || void 0
7418
7329
  };
7330
+ await authRegisterEvent.emit({
7331
+ userId: result.userId,
7332
+ provider: email ? "email" : "phone",
7333
+ email: result.email,
7334
+ phone: result.phone,
7335
+ metadata
7336
+ });
7337
+ return result;
7419
7338
  }
7420
7339
  async function loginService(params) {
7421
7340
  const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
7422
7341
  const user = await usersRepository.findByEmailOrPhone(email, phone);
7423
7342
  if (!email && !phone) {
7424
- throw new ValidationError({ message: "Either email or phone must be provided" });
7343
+ throw new ValidationError2({ message: "Either email or phone must be provided" });
7425
7344
  }
7426
7345
  if (!user || !user.passwordHash) {
7427
7346
  throw new InvalidCredentialsError();
@@ -7448,12 +7367,20 @@ async function loginService(params) {
7448
7367
  algorithm
7449
7368
  });
7450
7369
  await updateLastLoginService(user.id);
7451
- return {
7370
+ const result = {
7452
7371
  userId: String(user.id),
7372
+ publicId: user.publicId,
7453
7373
  email: user.email || void 0,
7454
7374
  phone: user.phone || void 0,
7455
7375
  passwordChangeRequired: user.passwordChangeRequired
7456
7376
  };
7377
+ await authLoginEvent.emit({
7378
+ userId: result.userId,
7379
+ provider: email ? "email" : "phone",
7380
+ email: result.email,
7381
+ phone: result.phone
7382
+ });
7383
+ return result;
7457
7384
  }
7458
7385
  async function logoutService(params) {
7459
7386
  const { userId, keyId } = params;
@@ -7471,12 +7398,12 @@ async function changePasswordService(params) {
7471
7398
  } else {
7472
7399
  const user = await usersRepository.findById(userId);
7473
7400
  if (!user) {
7474
- throw new ValidationError({ message: "User not found" });
7401
+ throw new ValidationError2({ message: "User not found" });
7475
7402
  }
7476
7403
  passwordHash = user.passwordHash;
7477
7404
  }
7478
7405
  if (!passwordHash) {
7479
- throw new ValidationError({ message: "No password set for this account" });
7406
+ throw new ValidationError2({ message: "No password set for this account" });
7480
7407
  }
7481
7408
  const isValid = await verifyPassword(currentPassword, passwordHash);
7482
7409
  if (!isValid) {
@@ -7489,14 +7416,17 @@ async function changePasswordService(params) {
7489
7416
  // src/server/services/rbac.service.ts
7490
7417
  init_repositories();
7491
7418
  init_rbac();
7419
+ import { createHash } from "crypto";
7492
7420
 
7493
7421
  // src/server/lib/config.ts
7494
- import { env as env6 } from "@spfn/auth/config";
7422
+ import { env as env5 } from "@spfn/auth/config";
7495
7423
  var COOKIE_NAMES = {
7496
7424
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
7497
7425
  SESSION: "spfn_session",
7498
7426
  /** Current key ID (for key rotation) */
7499
- SESSION_KEY_ID: "spfn_session_key_id"
7427
+ SESSION_KEY_ID: "spfn_session_key_id",
7428
+ /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
7429
+ OAUTH_PENDING: "spfn_oauth_pending"
7500
7430
  };
7501
7431
  function parseDuration(duration) {
7502
7432
  if (typeof duration === "number") {
@@ -7541,7 +7471,7 @@ function getSessionTtl(override) {
7541
7471
  if (globalConfig.sessionTtl !== void 0) {
7542
7472
  return parseDuration(globalConfig.sessionTtl);
7543
7473
  }
7544
- const envTtl = env6.SPFN_AUTH_SESSION_TTL;
7474
+ const envTtl = env5.SPFN_AUTH_SESSION_TTL;
7545
7475
  if (envTtl) {
7546
7476
  return parseDuration(envTtl);
7547
7477
  }
@@ -7549,6 +7479,33 @@ function getSessionTtl(override) {
7549
7479
  }
7550
7480
 
7551
7481
  // src/server/services/rbac.service.ts
7482
+ var RBAC_HASH_KEY = "rbac_config_hash";
7483
+ function computeConfigHash(allRoles, allPermissions, allMappings) {
7484
+ const payload = JSON.stringify({
7485
+ roles: allRoles.map((r) => ({ name: r.name, displayName: r.displayName, description: r.description, priority: r.priority, isSystem: r.isSystem, isBuiltin: r.isBuiltin })).sort((a, b) => a.name.localeCompare(b.name)),
7486
+ permissions: allPermissions.map((p) => ({ name: p.name, displayName: p.displayName, description: p.description, category: p.category, isSystem: p.isSystem, isBuiltin: p.isBuiltin })).sort((a, b) => a.name.localeCompare(b.name)),
7487
+ mappings: Object.keys(allMappings).sort().reduce((acc, key) => {
7488
+ acc[key] = [...allMappings[key]].sort();
7489
+ return acc;
7490
+ }, {})
7491
+ });
7492
+ return createHash("sha256").update(payload).digest("hex");
7493
+ }
7494
+ function collectMappings(options) {
7495
+ const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
7496
+ if (options.rolePermissions) {
7497
+ for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
7498
+ if (allMappings[roleName]) {
7499
+ allMappings[roleName] = [
7500
+ .../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
7501
+ ];
7502
+ } else {
7503
+ allMappings[roleName] = permNames;
7504
+ }
7505
+ }
7506
+ }
7507
+ return allMappings;
7508
+ }
7552
7509
  async function initializeAuth(options = {}) {
7553
7510
  authLogger.service.info("\u{1F510} Initializing RBAC system...");
7554
7511
  if (options.sessionTtl !== void 0) {
@@ -7561,100 +7518,100 @@ async function initializeAuth(options = {}) {
7561
7518
  ...Object.values(BUILTIN_ROLES),
7562
7519
  ...options.roles || []
7563
7520
  ];
7564
- for (const roleConfig of allRoles) {
7565
- await upsertRole(roleConfig);
7566
- }
7567
7521
  const allPermissions = [
7568
7522
  ...Object.values(BUILTIN_PERMISSIONS),
7569
7523
  ...options.permissions || []
7570
7524
  ];
7571
- for (const permConfig of allPermissions) {
7572
- await upsertPermission(permConfig);
7573
- }
7574
- const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
7575
- if (options.rolePermissions) {
7576
- for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
7577
- if (allMappings[roleName]) {
7578
- allMappings[roleName] = [
7579
- .../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
7580
- ];
7581
- } else {
7582
- allMappings[roleName] = permNames;
7583
- }
7584
- }
7585
- }
7586
- for (const [roleName, permNames] of Object.entries(allMappings)) {
7587
- await assignPermissionsToRole(roleName, permNames);
7525
+ const allMappings = collectMappings(options);
7526
+ const configHash = computeConfigHash(allRoles, allPermissions, allMappings);
7527
+ const storedHash = await authMetadataRepository.get(RBAC_HASH_KEY);
7528
+ if (storedHash === configHash) {
7529
+ authLogger.service.info("\u2705 RBAC config unchanged, skipping initialization");
7530
+ return;
7588
7531
  }
7532
+ authLogger.service.info("\u{1F504} RBAC config changed, applying updates...");
7533
+ const existingRoles = await rolesRepository.findAll();
7534
+ const existingPermissions = await permissionsRepository.findAll();
7535
+ const rolesByName = new Map(existingRoles.map((r) => [r.name, r]));
7536
+ const permsByName = new Map(existingPermissions.map((p) => [p.name, p]));
7537
+ await syncRoles(allRoles, rolesByName);
7538
+ await syncPermissions(allPermissions, permsByName);
7539
+ const updatedRoles = await rolesRepository.findAll();
7540
+ const updatedPermissions = await permissionsRepository.findAll();
7541
+ const updatedRolesByName = new Map(updatedRoles.map((r) => [r.name, r]));
7542
+ const updatedPermsByName = new Map(updatedPermissions.map((p) => [p.name, p]));
7543
+ await syncMappings(allMappings, updatedRolesByName, updatedPermsByName);
7544
+ await authMetadataRepository.set(RBAC_HASH_KEY, configHash);
7589
7545
  authLogger.service.info("\u2705 RBAC initialization complete");
7590
7546
  authLogger.service.info(`\u{1F4CA} Roles: ${allRoles.length}, Permissions: ${allPermissions.length}`);
7591
7547
  authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
7592
7548
  }
7593
- async function upsertRole(config) {
7594
- const existing = await rolesRepository.findByName(config.name);
7595
- if (!existing) {
7596
- await rolesRepository.create({
7597
- name: config.name,
7598
- displayName: config.displayName,
7599
- description: config.description || null,
7600
- priority: config.priority ?? 10,
7601
- isSystem: config.isSystem ?? false,
7602
- isBuiltin: config.isBuiltin ?? false,
7603
- isActive: true
7604
- });
7605
- authLogger.service.info(` \u2705 Created role: ${config.name}`);
7606
- } else {
7607
- const updateData = {
7608
- displayName: config.displayName,
7609
- description: config.description || null
7610
- };
7611
- if (!existing.isBuiltin) {
7612
- updateData.priority = config.priority ?? existing.priority;
7549
+ async function syncRoles(configs, existingByName) {
7550
+ for (const config of configs) {
7551
+ const existing = existingByName.get(config.name);
7552
+ if (!existing) {
7553
+ await rolesRepository.create({
7554
+ name: config.name,
7555
+ displayName: config.displayName,
7556
+ description: config.description || null,
7557
+ priority: config.priority ?? 10,
7558
+ isSystem: config.isSystem ?? false,
7559
+ isBuiltin: config.isBuiltin ?? false,
7560
+ isActive: true
7561
+ });
7562
+ authLogger.service.info(` \u2705 Created role: ${config.name}`);
7563
+ } else {
7564
+ const updateData = {
7565
+ displayName: config.displayName,
7566
+ description: config.description || null
7567
+ };
7568
+ if (!existing.isBuiltin) {
7569
+ updateData.priority = config.priority ?? existing.priority;
7570
+ }
7571
+ await rolesRepository.updateById(existing.id, updateData);
7613
7572
  }
7614
- await rolesRepository.updateById(existing.id, updateData);
7615
- }
7616
- }
7617
- async function upsertPermission(config) {
7618
- const existing = await permissionsRepository.findByName(config.name);
7619
- if (!existing) {
7620
- await permissionsRepository.create({
7621
- name: config.name,
7622
- displayName: config.displayName,
7623
- description: config.description || null,
7624
- category: config.category || null,
7625
- isSystem: config.isSystem ?? false,
7626
- isBuiltin: config.isBuiltin ?? false,
7627
- isActive: true,
7628
- metadata: null
7629
- });
7630
- authLogger.service.info(` \u2705 Created permission: ${config.name}`);
7631
- } else {
7632
- await permissionsRepository.updateById(existing.id, {
7633
- displayName: config.displayName,
7634
- description: config.description || null,
7635
- category: config.category || null
7636
- });
7637
7573
  }
7638
7574
  }
7639
- async function assignPermissionsToRole(roleName, permissionNames) {
7640
- const role = await rolesRepository.findByName(roleName);
7641
- if (!role) {
7642
- authLogger.service.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
7643
- return;
7644
- }
7645
- const perms = await permissionsRepository.findByNames(permissionNames);
7646
- if (perms.length === 0) {
7647
- authLogger.service.warn(` \u26A0\uFE0F No permissions found for role: ${roleName}`);
7648
- return;
7575
+ async function syncPermissions(configs, existingByName) {
7576
+ for (const config of configs) {
7577
+ const existing = existingByName.get(config.name);
7578
+ if (!existing) {
7579
+ await permissionsRepository.create({
7580
+ name: config.name,
7581
+ displayName: config.displayName,
7582
+ description: config.description || null,
7583
+ category: config.category || null,
7584
+ isSystem: config.isSystem ?? false,
7585
+ isBuiltin: config.isBuiltin ?? false,
7586
+ isActive: true,
7587
+ metadata: null
7588
+ });
7589
+ authLogger.service.info(` \u2705 Created permission: ${config.name}`);
7590
+ } else {
7591
+ await permissionsRepository.updateById(existing.id, {
7592
+ displayName: config.displayName,
7593
+ description: config.description || null,
7594
+ category: config.category || null
7595
+ });
7596
+ }
7649
7597
  }
7650
- const existingMappings = await rolePermissionsRepository.findByRoleId(role.id);
7651
- const existingPermIds = new Set(existingMappings.map((m) => m.permissionId));
7652
- const newMappings = perms.filter((perm) => !existingPermIds.has(perm.id)).map((perm) => ({
7653
- roleId: role.id,
7654
- permissionId: perm.id
7655
- }));
7656
- if (newMappings.length > 0) {
7657
- await rolePermissionsRepository.createMany(newMappings);
7598
+ }
7599
+ async function syncMappings(allMappings, rolesByName, permsByName) {
7600
+ for (const [roleName, permNames] of Object.entries(allMappings)) {
7601
+ const role = rolesByName.get(roleName);
7602
+ if (!role) {
7603
+ authLogger.service.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
7604
+ continue;
7605
+ }
7606
+ const existingMappings = await rolePermissionsRepository.findByRoleId(role.id);
7607
+ const existingPermIds = new Set(existingMappings.map((m) => m.permissionId));
7608
+ const newMappings = permNames.map((name) => permsByName.get(name)).filter((perm) => perm != null).filter((perm) => !existingPermIds.has(perm.id)).map((perm) => ({
7609
+ roleId: role.id,
7610
+ permissionId: perm.id
7611
+ }));
7612
+ if (newMappings.length > 0) {
7613
+ await rolePermissionsRepository.createMany(newMappings);
7614
+ }
7658
7615
  }
7659
7616
  }
7660
7617
 
@@ -7740,7 +7697,7 @@ function calculateExpiresAt(days = 7) {
7740
7697
  return expiresAt;
7741
7698
  }
7742
7699
  async function createInvitation(params) {
7743
- const { email, roleId, invitedBy, expiresInDays = 7, metadata } = params;
7700
+ const { email, roleId, invitedBy, expiresInDays = 7, expiresAt: expiresAtParam, metadata } = params;
7744
7701
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7745
7702
  if (!emailRegex.test(email)) {
7746
7703
  throw new Error("Invalid email format");
@@ -7762,7 +7719,7 @@ async function createInvitation(params) {
7762
7719
  throw new Error(`User with id ${invitedBy} not found`);
7763
7720
  }
7764
7721
  const token = generateInvitationToken();
7765
- const expiresAt = calculateExpiresAt(expiresInDays);
7722
+ const expiresAt = expiresAtParam ?? calculateExpiresAt(expiresInDays);
7766
7723
  const invitation = await invitationsRepository.create({
7767
7724
  email,
7768
7725
  token,
@@ -7772,7 +7729,16 @@ async function createInvitation(params) {
7772
7729
  expiresAt,
7773
7730
  metadata: metadata || null
7774
7731
  });
7775
- console.log(`[Auth] \u2705 Created invitation: ${email} as ${role.name} (expires: ${expiresAt.toISOString()})`);
7732
+ await invitationCreatedEvent.emit({
7733
+ invitationId: String(invitation.id),
7734
+ email,
7735
+ token,
7736
+ roleId,
7737
+ invitedBy: String(invitedBy),
7738
+ expiresAt: expiresAt.toISOString(),
7739
+ isResend: false,
7740
+ metadata
7741
+ });
7776
7742
  return invitation;
7777
7743
  }
7778
7744
  async function getInvitationByToken(token) {
@@ -7836,7 +7802,14 @@ async function acceptInvitation(params) {
7836
7802
  "accepted",
7837
7803
  /* @__PURE__ */ new Date()
7838
7804
  );
7839
- console.log(`[Auth] \u2705 Invitation accepted: ${invitation.email} as ${role.name}`);
7805
+ await invitationAcceptedEvent.emit({
7806
+ invitationId: String(invitation.id),
7807
+ email: invitation.email,
7808
+ userId: String(newUser.id),
7809
+ roleId: Number(invitation.roleId),
7810
+ invitedBy: String(invitation.invitedBy),
7811
+ metadata: invitation.metadata
7812
+ });
7840
7813
  return {
7841
7814
  userId: newUser.id,
7842
7815
  email: newUser.email,
@@ -7881,7 +7854,16 @@ async function resendInvitation(id11, expiresInDays = 7) {
7881
7854
  if (!updated) {
7882
7855
  throw new Error("Failed to update invitation");
7883
7856
  }
7884
- console.log(`[Auth] \u{1F4E7} Invitation resent: ${invitation.email} (new expiry: ${newExpiresAt.toISOString()})`);
7857
+ await invitationCreatedEvent.emit({
7858
+ invitationId: String(invitation.id),
7859
+ email: invitation.email,
7860
+ token: invitation.token,
7861
+ roleId: Number(invitation.roleId),
7862
+ invitedBy: String(invitation.invitedBy),
7863
+ expiresAt: newExpiresAt.toISOString(),
7864
+ isResend: true,
7865
+ metadata: invitation.metadata
7866
+ });
7885
7867
  return updated;
7886
7868
  }
7887
7869
 
@@ -7895,6 +7877,7 @@ async function getAuthSessionService(userId) {
7895
7877
  ]);
7896
7878
  return {
7897
7879
  userId: user.userId,
7880
+ publicId: user.publicId,
7898
7881
  email: user.email,
7899
7882
  emailVerified: user.isEmailVerified,
7900
7883
  phoneVerified: user.isPhoneVerified,
@@ -7902,6 +7885,38 @@ async function getAuthSessionService(userId) {
7902
7885
  };
7903
7886
  }
7904
7887
 
7888
+ // src/server/lib/one-time-token.ts
7889
+ import { SSETokenManager } from "@spfn/core/event/sse";
7890
+ var manager = null;
7891
+ function initOneTimeTokenManager(config) {
7892
+ if (manager) {
7893
+ manager.destroy();
7894
+ }
7895
+ manager = new SSETokenManager({
7896
+ ttl: config?.ttl
7897
+ });
7898
+ }
7899
+ function getOneTimeTokenManager() {
7900
+ if (!manager) {
7901
+ throw new Error(
7902
+ "OneTimeTokenManager not initialized. Ensure createAuthLifecycle() is configured in your server config."
7903
+ );
7904
+ }
7905
+ return manager;
7906
+ }
7907
+
7908
+ // src/server/services/one-time-token.service.ts
7909
+ async function issueOneTimeTokenService(userId) {
7910
+ const manager2 = getOneTimeTokenManager();
7911
+ const token = await manager2.issue(userId);
7912
+ const expiresAt = new Date(Date.now() + 3e4).toISOString();
7913
+ return { token, expiresAt };
7914
+ }
7915
+ async function verifyOneTimeTokenService(token) {
7916
+ const manager2 = getOneTimeTokenManager();
7917
+ return await manager2.verify(token);
7918
+ }
7919
+
7905
7920
  // src/server/services/user-profile.service.ts
7906
7921
  init_repositories();
7907
7922
  async function getUserProfileService(userId) {
@@ -7912,7 +7927,9 @@ async function getUserProfileService(userId) {
7912
7927
  ]);
7913
7928
  return {
7914
7929
  userId: user.userId,
7930
+ publicId: user.publicId,
7915
7931
  email: user.email,
7932
+ username: user.username,
7916
7933
  emailVerified: user.isEmailVerified,
7917
7934
  phoneVerified: user.isPhoneVerified,
7918
7935
  lastLoginAt: user.lastLoginAt,
@@ -7921,6 +7938,12 @@ async function getUserProfileService(userId) {
7921
7938
  profile
7922
7939
  };
7923
7940
  }
7941
+ async function updateLocaleService(userId, locale) {
7942
+ const userIdNum = Number(userId);
7943
+ const normalized = locale.trim() || "en";
7944
+ await userProfilesRepository.upsertByUserId(userIdNum, { locale: normalized });
7945
+ return { locale: normalized };
7946
+ }
7924
7947
  function emptyToNull(value) {
7925
7948
  if (value === "") {
7926
7949
  return null;
@@ -7931,7 +7954,7 @@ async function updateUserProfileService(userId, params) {
7931
7954
  const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7932
7955
  const updateData = {};
7933
7956
  if (params.displayName !== void 0) {
7934
- updateData.displayName = emptyToNull(params.displayName) || "User";
7957
+ updateData.displayName = emptyToNull(params.displayName);
7935
7958
  }
7936
7959
  if (params.firstName !== void 0) {
7937
7960
  updateData.firstName = emptyToNull(params.firstName);
@@ -7972,15 +7995,355 @@ async function updateUserProfileService(userId, params) {
7972
7995
  if (params.metadata !== void 0) {
7973
7996
  updateData.metadata = params.metadata;
7974
7997
  }
7975
- const existing = await userProfilesRepository.findByUserId(userIdNum);
7976
- if (!existing && !updateData.displayName) {
7977
- updateData.displayName = "User";
7978
- }
7979
7998
  await userProfilesRepository.upsertByUserId(userIdNum, updateData);
7980
7999
  const profile = await userProfilesRepository.fetchProfileData(userIdNum);
7981
8000
  return profile;
7982
8001
  }
7983
8002
 
8003
+ // src/server/services/oauth.service.ts
8004
+ init_repositories();
8005
+ import { env as env8 } from "@spfn/auth/config";
8006
+ import { ValidationError as ValidationError3 } from "@spfn/core/errors";
8007
+
8008
+ // src/server/lib/oauth/google.ts
8009
+ import { env as env6 } from "@spfn/auth/config";
8010
+ var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
8011
+ var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
8012
+ var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
8013
+ function isGoogleOAuthEnabled() {
8014
+ return !!(env6.SPFN_AUTH_GOOGLE_CLIENT_ID && env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
8015
+ }
8016
+ function getGoogleOAuthConfig() {
8017
+ const clientId = env6.SPFN_AUTH_GOOGLE_CLIENT_ID;
8018
+ const clientSecret = env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
8019
+ if (!clientId || !clientSecret) {
8020
+ throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
8021
+ }
8022
+ const baseUrl = env6.NEXT_PUBLIC_SPFN_API_URL || env6.SPFN_API_URL;
8023
+ const redirectUri = env6.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
8024
+ return {
8025
+ clientId,
8026
+ clientSecret,
8027
+ redirectUri
8028
+ };
8029
+ }
8030
+ function getDefaultScopes() {
8031
+ const envScopes = env6.SPFN_AUTH_GOOGLE_SCOPES;
8032
+ if (envScopes) {
8033
+ return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
8034
+ }
8035
+ return ["email", "profile"];
8036
+ }
8037
+ function getGoogleAuthUrl(state, scopes) {
8038
+ const resolvedScopes = scopes ?? getDefaultScopes();
8039
+ const config = getGoogleOAuthConfig();
8040
+ const params = new URLSearchParams({
8041
+ client_id: config.clientId,
8042
+ redirect_uri: config.redirectUri,
8043
+ response_type: "code",
8044
+ scope: resolvedScopes.join(" "),
8045
+ state,
8046
+ access_type: "offline",
8047
+ // refresh_token 받기 위해
8048
+ prompt: "consent"
8049
+ // 매번 동의 화면 표시 (refresh_token 보장)
8050
+ });
8051
+ return `${GOOGLE_AUTH_URL}?${params.toString()}`;
8052
+ }
8053
+ async function exchangeCodeForTokens(code) {
8054
+ const config = getGoogleOAuthConfig();
8055
+ const response = await fetch(GOOGLE_TOKEN_URL, {
8056
+ method: "POST",
8057
+ headers: {
8058
+ "Content-Type": "application/x-www-form-urlencoded"
8059
+ },
8060
+ body: new URLSearchParams({
8061
+ client_id: config.clientId,
8062
+ client_secret: config.clientSecret,
8063
+ redirect_uri: config.redirectUri,
8064
+ grant_type: "authorization_code",
8065
+ code
8066
+ })
8067
+ });
8068
+ if (!response.ok) {
8069
+ const error = await response.text();
8070
+ throw new Error(`Failed to exchange code for tokens: ${error}`);
8071
+ }
8072
+ return response.json();
8073
+ }
8074
+ async function getGoogleUserInfo(accessToken) {
8075
+ const response = await fetch(GOOGLE_USERINFO_URL, {
8076
+ headers: {
8077
+ Authorization: `Bearer ${accessToken}`
8078
+ }
8079
+ });
8080
+ if (!response.ok) {
8081
+ const error = await response.text();
8082
+ throw new Error(`Failed to get user info: ${error}`);
8083
+ }
8084
+ return response.json();
8085
+ }
8086
+ async function refreshAccessToken(refreshToken) {
8087
+ const config = getGoogleOAuthConfig();
8088
+ const response = await fetch(GOOGLE_TOKEN_URL, {
8089
+ method: "POST",
8090
+ headers: {
8091
+ "Content-Type": "application/x-www-form-urlencoded"
8092
+ },
8093
+ body: new URLSearchParams({
8094
+ client_id: config.clientId,
8095
+ client_secret: config.clientSecret,
8096
+ refresh_token: refreshToken,
8097
+ grant_type: "refresh_token"
8098
+ })
8099
+ });
8100
+ if (!response.ok) {
8101
+ const error = await response.text();
8102
+ throw new Error(`Failed to refresh access token: ${error}`);
8103
+ }
8104
+ return response.json();
8105
+ }
8106
+
8107
+ // src/server/lib/oauth/state.ts
8108
+ import * as jose from "jose";
8109
+ import { env as env7 } from "@spfn/auth/config";
8110
+ async function getStateKey() {
8111
+ const secret = env7.SPFN_AUTH_SESSION_SECRET;
8112
+ const encoder = new TextEncoder();
8113
+ const data = encoder.encode(`oauth-state:${secret}`);
8114
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
8115
+ return new Uint8Array(hashBuffer);
8116
+ }
8117
+ function generateNonce() {
8118
+ const array = new Uint8Array(16);
8119
+ crypto.getRandomValues(array);
8120
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
8121
+ }
8122
+ async function createOAuthState(params) {
8123
+ const key = await getStateKey();
8124
+ const state = {
8125
+ returnUrl: params.returnUrl,
8126
+ nonce: generateNonce(),
8127
+ provider: params.provider,
8128
+ publicKey: params.publicKey,
8129
+ keyId: params.keyId,
8130
+ fingerprint: params.fingerprint,
8131
+ algorithm: params.algorithm,
8132
+ metadata: params.metadata
8133
+ };
8134
+ const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
8135
+ return encodeURIComponent(jwe);
8136
+ }
8137
+ async function verifyOAuthState(encryptedState) {
8138
+ const key = await getStateKey();
8139
+ const jwe = decodeURIComponent(encryptedState);
8140
+ const { payload } = await jose.jwtDecrypt(jwe, key);
8141
+ return payload.state;
8142
+ }
8143
+
8144
+ // src/server/services/oauth.service.ts
8145
+ async function oauthStartService(params) {
8146
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
8147
+ if (provider === "google") {
8148
+ if (!isGoogleOAuthEnabled()) {
8149
+ throw new ValidationError3({
8150
+ message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
8151
+ });
8152
+ }
8153
+ const state = await createOAuthState({
8154
+ provider: "google",
8155
+ returnUrl,
8156
+ publicKey,
8157
+ keyId,
8158
+ fingerprint,
8159
+ algorithm,
8160
+ metadata
8161
+ });
8162
+ const authUrl = getGoogleAuthUrl(state);
8163
+ return { authUrl };
8164
+ }
8165
+ throw new ValidationError3({
8166
+ message: `Unsupported OAuth provider: ${provider}`
8167
+ });
8168
+ }
8169
+ async function oauthCallbackService(params) {
8170
+ const { provider, code, state } = params;
8171
+ const stateData = await verifyOAuthState(state);
8172
+ if (stateData.provider !== provider) {
8173
+ throw new ValidationError3({
8174
+ message: "OAuth state provider mismatch"
8175
+ });
8176
+ }
8177
+ if (provider === "google") {
8178
+ return handleGoogleCallback(code, stateData);
8179
+ }
8180
+ throw new ValidationError3({
8181
+ message: `Unsupported OAuth provider: ${provider}`
8182
+ });
8183
+ }
8184
+ async function handleGoogleCallback(code, stateData) {
8185
+ const tokens = await exchangeCodeForTokens(code);
8186
+ const googleUser = await getGoogleUserInfo(tokens.access_token);
8187
+ const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
8188
+ "google",
8189
+ googleUser.id
8190
+ );
8191
+ let userId;
8192
+ let isNewUser = false;
8193
+ if (existingSocialAccount) {
8194
+ userId = existingSocialAccount.userId;
8195
+ await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
8196
+ accessToken: tokens.access_token,
8197
+ refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
8198
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8199
+ });
8200
+ } else {
8201
+ const result = await createOrLinkUser(googleUser, tokens);
8202
+ userId = result.userId;
8203
+ isNewUser = result.isNewUser;
8204
+ }
8205
+ await registerPublicKeyService({
8206
+ userId,
8207
+ keyId: stateData.keyId,
8208
+ publicKey: stateData.publicKey,
8209
+ fingerprint: stateData.fingerprint,
8210
+ algorithm: stateData.algorithm
8211
+ });
8212
+ await updateLastLoginService(userId);
8213
+ const appUrl = env8.NEXT_PUBLIC_SPFN_APP_URL || env8.SPFN_APP_URL;
8214
+ const callbackPath = env8.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
8215
+ const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
8216
+ const redirectUrl = buildRedirectUrl(callbackUrl, {
8217
+ userId: String(userId),
8218
+ keyId: stateData.keyId,
8219
+ returnUrl: stateData.returnUrl,
8220
+ isNewUser: String(isNewUser)
8221
+ });
8222
+ const user = await usersRepository.findById(userId);
8223
+ const eventPayload = {
8224
+ userId: String(userId),
8225
+ provider: "google",
8226
+ email: user?.email || void 0,
8227
+ phone: user?.phone || void 0,
8228
+ metadata: stateData.metadata
8229
+ };
8230
+ if (isNewUser) {
8231
+ await authRegisterEvent.emit(eventPayload);
8232
+ } else {
8233
+ await authLoginEvent.emit(eventPayload);
8234
+ }
8235
+ return {
8236
+ redirectUrl,
8237
+ userId: String(userId),
8238
+ keyId: stateData.keyId,
8239
+ isNewUser
8240
+ };
8241
+ }
8242
+ async function createOrLinkUser(googleUser, tokens) {
8243
+ const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
8244
+ let userId;
8245
+ let isNewUser = false;
8246
+ if (existingUser) {
8247
+ if (!googleUser.verified_email) {
8248
+ throw new ValidationError3({
8249
+ message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
8250
+ });
8251
+ }
8252
+ userId = existingUser.id;
8253
+ if (!existingUser.emailVerifiedAt) {
8254
+ await usersRepository.updateById(existingUser.id, {
8255
+ emailVerifiedAt: /* @__PURE__ */ new Date()
8256
+ });
8257
+ }
8258
+ } else {
8259
+ const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
8260
+ const userRole = await getRoleByName3("user");
8261
+ if (!userRole) {
8262
+ throw new Error("Default user role not found. Run initializeAuth() first.");
8263
+ }
8264
+ const newUser = await usersRepository.create({
8265
+ email: googleUser.verified_email ? googleUser.email : null,
8266
+ phone: null,
8267
+ passwordHash: null,
8268
+ // OAuth 사용자는 비밀번호 없음
8269
+ passwordChangeRequired: false,
8270
+ roleId: userRole.id,
8271
+ status: "active",
8272
+ emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
8273
+ });
8274
+ userId = newUser.id;
8275
+ isNewUser = true;
8276
+ }
8277
+ await socialAccountsRepository.create({
8278
+ userId,
8279
+ provider: "google",
8280
+ providerUserId: googleUser.id,
8281
+ providerEmail: googleUser.email,
8282
+ accessToken: tokens.access_token,
8283
+ refreshToken: tokens.refresh_token ?? null,
8284
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8285
+ });
8286
+ return { userId, isNewUser };
8287
+ }
8288
+ function buildRedirectUrl(baseUrl, params) {
8289
+ const url = new URL(baseUrl, "http://placeholder");
8290
+ for (const [key, value] of Object.entries(params)) {
8291
+ url.searchParams.set(key, value);
8292
+ }
8293
+ if (baseUrl.startsWith("http")) {
8294
+ return url.toString();
8295
+ }
8296
+ return `${url.pathname}${url.search}`;
8297
+ }
8298
+ function buildOAuthErrorUrl(error) {
8299
+ const errorUrl = env8.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
8300
+ return errorUrl.replace("{error}", encodeURIComponent(error));
8301
+ }
8302
+ function isOAuthProviderEnabled(provider) {
8303
+ switch (provider) {
8304
+ case "google":
8305
+ return isGoogleOAuthEnabled();
8306
+ case "github":
8307
+ case "kakao":
8308
+ case "naver":
8309
+ return false;
8310
+ default:
8311
+ return false;
8312
+ }
8313
+ }
8314
+ function getEnabledOAuthProviders() {
8315
+ const providers = [];
8316
+ if (isGoogleOAuthEnabled()) {
8317
+ providers.push("google");
8318
+ }
8319
+ return providers;
8320
+ }
8321
+ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
8322
+ async function getGoogleAccessToken(userId) {
8323
+ const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
8324
+ if (!account) {
8325
+ throw new ValidationError3({
8326
+ message: "No Google account linked. User must sign in with Google first."
8327
+ });
8328
+ }
8329
+ const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
8330
+ if (!isExpired && account.accessToken) {
8331
+ return account.accessToken;
8332
+ }
8333
+ if (!account.refreshToken) {
8334
+ throw new ValidationError3({
8335
+ message: "Google refresh token not available. User must re-authenticate with Google."
8336
+ });
8337
+ }
8338
+ const tokens = await refreshAccessToken(account.refreshToken);
8339
+ await socialAccountsRepository.updateTokens(account.id, {
8340
+ accessToken: tokens.access_token,
8341
+ refreshToken: tokens.refresh_token ?? account.refreshToken,
8342
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8343
+ });
8344
+ return tokens.access_token;
8345
+ }
8346
+
7984
8347
  // src/server/routes/auth/index.ts
7985
8348
  init_esm();
7986
8349
  import { Transactional } from "@spfn/core/db";
@@ -8031,7 +8394,10 @@ var register = route.post("/_auth/register").input({
8031
8394
  verificationToken: Type.String({
8032
8395
  description: "Verification token obtained from /verify-code endpoint"
8033
8396
  }),
8034
- password: PasswordSchema
8397
+ password: PasswordSchema,
8398
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
8399
+ description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
8400
+ }))
8035
8401
  }, {
8036
8402
  minProperties: 3,
8037
8403
  // email/phone + verificationToken + password
@@ -8124,6 +8490,10 @@ var getAuthSession = route.get("/_auth/session").handler(async (c) => {
8124
8490
  const { userId } = getAuth(c);
8125
8491
  return await getAuthSessionService(userId);
8126
8492
  });
8493
+ var issueOneTimeToken = route.post("/_auth/tokens").handler(async (c) => {
8494
+ const { userId } = getAuth(c);
8495
+ return await issueOneTimeTokenService(userId);
8496
+ });
8127
8497
  var authRouter = defineRouter({
8128
8498
  checkAccountExists,
8129
8499
  sendVerificationCode,
@@ -8133,7 +8503,8 @@ var authRouter = defineRouter({
8133
8503
  logout,
8134
8504
  rotateKey,
8135
8505
  changePassword,
8136
- getAuthSession
8506
+ getAuthSession,
8507
+ issueOneTimeToken
8137
8508
  });
8138
8509
 
8139
8510
  // src/server/routes/invitations/index.ts
@@ -8142,7 +8513,7 @@ import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
8142
8513
  // src/server/middleware/authenticate.ts
8143
8514
  import { defineMiddleware } from "@spfn/core/route";
8144
8515
  import { UnauthorizedError } from "@spfn/core/errors";
8145
- import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2 } from "@spfn/auth/server";
8516
+ import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2, userProfilesRepository as userProfilesRepository2 } from "@spfn/auth/server";
8146
8517
  import {
8147
8518
  InvalidTokenError,
8148
8519
  TokenExpiredError,
@@ -8189,10 +8560,14 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
8189
8560
  }
8190
8561
  throw new UnauthorizedError({ message: "Authentication failed" });
8191
8562
  }
8192
- const user = await usersRepository2.findById(keyRecord.userId);
8193
- if (!user) {
8563
+ const [result, locale] = await Promise.all([
8564
+ usersRepository2.findByIdWithRole(keyRecord.userId),
8565
+ userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
8566
+ ]);
8567
+ if (!result) {
8194
8568
  throw new UnauthorizedError({ message: "User not found" });
8195
8569
  }
8570
+ const { user, role } = result;
8196
8571
  if (user.status !== "active") {
8197
8572
  throw new AccountDisabledError2({ status: user.status });
8198
8573
  }
@@ -8200,7 +8575,9 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
8200
8575
  c.set("auth", {
8201
8576
  user,
8202
8577
  userId: String(user.id),
8203
- keyId
8578
+ keyId,
8579
+ role: role?.name ?? null,
8580
+ locale
8204
8581
  });
8205
8582
  const method = c.req.method;
8206
8583
  const path = c.req.path;
@@ -8215,6 +8592,55 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
8215
8592
  });
8216
8593
  await next();
8217
8594
  });
8595
+ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
8596
+ const authHeader = c.req.header("Authorization");
8597
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
8598
+ await next();
8599
+ return;
8600
+ }
8601
+ const token = authHeader.substring(7);
8602
+ try {
8603
+ const decoded = decodeToken2(token);
8604
+ if (!decoded || !decoded.keyId) {
8605
+ await next();
8606
+ return;
8607
+ }
8608
+ const keyId = decoded.keyId;
8609
+ const keyRecord = await keysRepository2.findActiveByKeyId(keyId);
8610
+ if (!keyRecord) {
8611
+ await next();
8612
+ return;
8613
+ }
8614
+ if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
8615
+ await next();
8616
+ return;
8617
+ }
8618
+ verifyClientToken2(
8619
+ token,
8620
+ keyRecord.publicKey,
8621
+ keyRecord.algorithm
8622
+ );
8623
+ const [result, locale] = await Promise.all([
8624
+ usersRepository2.findByIdWithRole(keyRecord.userId),
8625
+ userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
8626
+ ]);
8627
+ if (!result || result.user.status !== "active") {
8628
+ await next();
8629
+ return;
8630
+ }
8631
+ const { user, role } = result;
8632
+ keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
8633
+ c.set("auth", {
8634
+ user,
8635
+ userId: String(user.id),
8636
+ keyId,
8637
+ role: role?.name ?? null,
8638
+ locale
8639
+ });
8640
+ } catch {
8641
+ }
8642
+ await next();
8643
+ }, { skips: ["auth"] });
8218
8644
 
8219
8645
  // src/server/middleware/require-permission.ts
8220
8646
  import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
@@ -8280,7 +8706,7 @@ var requireAnyPermission = defineMiddleware2(
8280
8706
 
8281
8707
  // src/server/middleware/require-role.ts
8282
8708
  import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
8283
- import { getAuth as getAuth3, hasAnyRole as hasAnyRole2, authLogger as authLogger4 } from "@spfn/auth/server";
8709
+ import { getAuth as getAuth3, authLogger as authLogger4 } from "@spfn/auth/server";
8284
8710
  import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
8285
8711
  import { InsufficientRoleError } from "@spfn/auth/errors";
8286
8712
  var requireRole = defineMiddleware3(
@@ -8294,11 +8720,11 @@ var requireRole = defineMiddleware3(
8294
8720
  });
8295
8721
  throw new ForbiddenError2({ message: "Authentication required" });
8296
8722
  }
8297
- const { userId } = auth;
8298
- const allowed = await hasAnyRole2(userId, roleNames);
8299
- if (!allowed) {
8723
+ const { userId, role: userRole } = auth;
8724
+ if (!userRole || !roleNames.includes(userRole)) {
8300
8725
  authLogger4.middleware.warn("Role check failed", {
8301
8726
  userId,
8727
+ userRole,
8302
8728
  requiredRoles: roleNames,
8303
8729
  path: c.req.path
8304
8730
  });
@@ -8306,6 +8732,7 @@ var requireRole = defineMiddleware3(
8306
8732
  }
8307
8733
  authLogger4.middleware.debug("Role check passed", {
8308
8734
  userId,
8735
+ userRole,
8309
8736
  roles: roleNames
8310
8737
  });
8311
8738
  await next();
@@ -8314,7 +8741,7 @@ var requireRole = defineMiddleware3(
8314
8741
 
8315
8742
  // src/server/middleware/role-guard.ts
8316
8743
  import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
8317
- import { getAuth as getAuth4, getUserRole as getUserRole2, authLogger as authLogger5 } from "@spfn/auth/server";
8744
+ import { getAuth as getAuth4, authLogger as authLogger5 } from "@spfn/auth/server";
8318
8745
  import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
8319
8746
  import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
8320
8747
  var roleGuard = defineMiddleware4(
@@ -8331,8 +8758,7 @@ var roleGuard = defineMiddleware4(
8331
8758
  });
8332
8759
  throw new ForbiddenError3({ message: "Authentication required" });
8333
8760
  }
8334
- const { userId } = auth;
8335
- const userRole = await getUserRole2(userId);
8761
+ const { userId, role: userRole } = auth;
8336
8762
  if (deny && deny.length > 0) {
8337
8763
  if (userRole && deny.includes(userRole)) {
8338
8764
  authLogger5.middleware.warn("Role guard denied", {
@@ -8365,6 +8791,47 @@ var roleGuard = defineMiddleware4(
8365
8791
  }
8366
8792
  );
8367
8793
 
8794
+ // src/server/middleware/one-time-token-auth.ts
8795
+ import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
8796
+ import { UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
8797
+ import { usersRepository as usersRepository3, userProfilesRepository as userProfilesRepository3 } from "@spfn/auth/server";
8798
+ var oneTimeTokenAuth = defineMiddleware5("oneTimeTokenAuth", async (c, next) => {
8799
+ const token = c.req.query("token") ?? extractOTTHeader(c.req.header("Authorization"));
8800
+ if (!token) {
8801
+ throw new UnauthorizedError2({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
8802
+ }
8803
+ const userId = await verifyOneTimeTokenService(token);
8804
+ if (!userId) {
8805
+ throw new UnauthorizedError2({ message: "Invalid or expired one-time token" });
8806
+ }
8807
+ const [result, locale] = await Promise.all([
8808
+ usersRepository3.findByIdWithRole(Number(userId)),
8809
+ userProfilesRepository3.findLocaleByUserId(Number(userId))
8810
+ ]);
8811
+ if (!result) {
8812
+ throw new UnauthorizedError2({ message: "User not found" });
8813
+ }
8814
+ const { user, role } = result;
8815
+ if (user.status !== "active") {
8816
+ throw new UnauthorizedError2({ message: "Account is not active" });
8817
+ }
8818
+ c.set("auth", {
8819
+ user,
8820
+ userId: String(user.id),
8821
+ keyId: "",
8822
+ // No key involved in OTT auth
8823
+ role: role?.name ?? null,
8824
+ locale
8825
+ });
8826
+ await next();
8827
+ }, { skips: ["auth"] });
8828
+ function extractOTTHeader(header) {
8829
+ if (!header || !header.startsWith("OTT ")) {
8830
+ return null;
8831
+ }
8832
+ return header.substring(4);
8833
+ }
8834
+
8368
8835
  // src/server/routes/invitations/index.ts
8369
8836
  init_types();
8370
8837
  init_esm();
@@ -8440,6 +8907,10 @@ var createInvitation2 = route2.post("/_auth/invitations").input({
8440
8907
  maximum: 30,
8441
8908
  description: "Days until invitation expires (default: 7)"
8442
8909
  })),
8910
+ expiresAt: Type.Optional(Type.String({
8911
+ format: "date-time",
8912
+ description: "Exact expiration timestamp (ISO 8601). Takes precedence over expiresInDays."
8913
+ })),
8443
8914
  metadata: Type.Optional(Type.Any({
8444
8915
  description: "Custom metadata (welcome message, department, etc.)"
8445
8916
  }))
@@ -8452,6 +8923,7 @@ var createInvitation2 = route2.post("/_auth/invitations").input({
8452
8923
  roleId: body.roleId,
8453
8924
  invitedBy: Number(userId),
8454
8925
  expiresInDays: body.expiresInDays,
8926
+ expiresAt: body.expiresAt ? new Date(body.expiresAt) : void 0,
8455
8927
  metadata: body.metadata
8456
8928
  });
8457
8929
  const baseUrl = process.env.SPFN_API_URL || "http://localhost:8790";
@@ -8586,17 +9058,298 @@ var updateUserProfile = route3.patch("/_auth/users/profile").input({
8586
9058
  const { body } = await c.data();
8587
9059
  return await updateUserProfileService(userId, body);
8588
9060
  });
9061
+ var checkUsername = route3.get("/_auth/users/username/check").input({
9062
+ query: Type.Object({
9063
+ username: Type.String({ minLength: 1 })
9064
+ })
9065
+ }).handler(async (c) => {
9066
+ const { query } = await c.data();
9067
+ return { available: await checkUsernameAvailableService(query.username) };
9068
+ });
9069
+ var updateUsername = route3.patch("/_auth/users/username").input({
9070
+ body: Type.Object({
9071
+ username: Type.Union([
9072
+ Type.String({ minLength: 1 }),
9073
+ Type.Null()
9074
+ ], { description: "New username or null to clear" })
9075
+ })
9076
+ }).handler(async (c) => {
9077
+ const { userId } = getAuth(c);
9078
+ const { body } = await c.data();
9079
+ return await updateUsernameService(userId, body.username);
9080
+ });
9081
+ var updateLocale = route3.patch("/_auth/users/locale").input({
9082
+ body: Type.Object({
9083
+ locale: Type.String({ minLength: 1, description: "Locale code (e.g., en, ko, ja)" })
9084
+ })
9085
+ }).handler(async (c) => {
9086
+ const { userId } = getAuth(c);
9087
+ const { body } = await c.data();
9088
+ return await updateLocaleService(userId, body.locale);
9089
+ });
8589
9090
  var userRouter = defineRouter3({
8590
9091
  getUserProfile,
8591
- updateUserProfile
9092
+ updateUserProfile,
9093
+ checkUsername,
9094
+ updateUsername,
9095
+ updateLocale
9096
+ });
9097
+
9098
+ // src/server/routes/oauth/index.ts
9099
+ init_esm();
9100
+ init_types();
9101
+ import { Transactional as Transactional2 } from "@spfn/core/db";
9102
+ import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
9103
+ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
9104
+ query: Type.Object({
9105
+ state: Type.String({
9106
+ description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
9107
+ })
9108
+ })
9109
+ }).skip(["auth"]).handler(async (c) => {
9110
+ const { query } = await c.data();
9111
+ if (!isGoogleOAuthEnabled()) {
9112
+ return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
9113
+ }
9114
+ const authUrl = getGoogleAuthUrl(query.state);
9115
+ return c.redirect(authUrl);
9116
+ });
9117
+ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
9118
+ query: Type.Object({
9119
+ code: Type.Optional(Type.String({
9120
+ description: "Authorization code from Google"
9121
+ })),
9122
+ state: Type.Optional(Type.String({
9123
+ description: "OAuth state parameter"
9124
+ })),
9125
+ error: Type.Optional(Type.String({
9126
+ description: "Error code from Google"
9127
+ })),
9128
+ error_description: Type.Optional(Type.String({
9129
+ description: "Error description from Google"
9130
+ }))
9131
+ })
9132
+ }).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
9133
+ const { query } = await c.data();
9134
+ if (query.error) {
9135
+ const errorMessage = query.error_description || query.error;
9136
+ return c.redirect(buildOAuthErrorUrl(errorMessage));
9137
+ }
9138
+ if (!query.code || !query.state) {
9139
+ return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
9140
+ }
9141
+ try {
9142
+ const result = await oauthCallbackService({
9143
+ provider: "google",
9144
+ code: query.code,
9145
+ state: query.state
9146
+ });
9147
+ return c.redirect(result.redirectUrl);
9148
+ } catch (err) {
9149
+ const message = err instanceof Error ? err.message : "OAuth callback failed";
9150
+ return c.redirect(buildOAuthErrorUrl(message));
9151
+ }
9152
+ });
9153
+ var oauthStart = route4.post("/_auth/oauth/start").input({
9154
+ body: Type.Object({
9155
+ provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
9156
+ description: "OAuth provider (google, github, kakao, naver)"
9157
+ }),
9158
+ returnUrl: Type.String({
9159
+ description: "URL to redirect after OAuth success"
9160
+ }),
9161
+ publicKey: Type.String({
9162
+ description: "Client public key (Base64 DER)"
9163
+ }),
9164
+ keyId: Type.String({
9165
+ description: "Key identifier (UUID)"
9166
+ }),
9167
+ fingerprint: Type.String({
9168
+ description: "Key fingerprint (SHA-256 hex)"
9169
+ }),
9170
+ algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
9171
+ description: "Key algorithm (ES256 or RS256)"
9172
+ }),
9173
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
9174
+ description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
9175
+ }))
9176
+ })
9177
+ }).skip(["auth"]).handler(async (c) => {
9178
+ const { body } = await c.data();
9179
+ const result = await oauthStartService(body);
9180
+ return result;
9181
+ });
9182
+ var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
9183
+ return {
9184
+ providers: getEnabledOAuthProviders()
9185
+ };
9186
+ });
9187
+ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
9188
+ body: Type.Object({
9189
+ returnUrl: Type.Optional(Type.String({
9190
+ description: "URL to redirect after OAuth success"
9191
+ })),
9192
+ state: Type.Optional(Type.String({
9193
+ description: "Encrypted OAuth state (injected by interceptor)"
9194
+ }))
9195
+ })
9196
+ }).skip(["auth"]).handler(async (c) => {
9197
+ const { body } = await c.data();
9198
+ if (!isGoogleOAuthEnabled()) {
9199
+ throw new Error("Google OAuth is not configured");
9200
+ }
9201
+ if (!body.state) {
9202
+ throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
9203
+ }
9204
+ return { authUrl: getGoogleAuthUrl(body.state) };
9205
+ });
9206
+ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
9207
+ body: Type.Object({
9208
+ userId: Type.String({ description: "User ID from OAuth callback" }),
9209
+ keyId: Type.String({ description: "Key ID from OAuth state" }),
9210
+ returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
9211
+ })
9212
+ }).skip(["auth"]).handler(async (c) => {
9213
+ const { body } = await c.data();
9214
+ return {
9215
+ success: true,
9216
+ userId: body.userId,
9217
+ keyId: body.keyId,
9218
+ returnUrl: body.returnUrl || "/"
9219
+ };
9220
+ });
9221
+ var oauthRouter = defineRouter4({
9222
+ oauthGoogleStart,
9223
+ oauthGoogleCallback,
9224
+ oauthStart,
9225
+ oauthProviders,
9226
+ getGoogleOAuthUrl,
9227
+ oauthFinalize
9228
+ });
9229
+
9230
+ // src/server/routes/admin/index.ts
9231
+ init_esm();
9232
+ import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
9233
+ import { route as route5 } from "@spfn/core/route";
9234
+ var listRoles = route5.get("/_auth/admin/roles").input({
9235
+ query: Type.Object({
9236
+ includeInactive: Type.Optional(Type.Boolean({
9237
+ description: "Include inactive roles (default: false)"
9238
+ }))
9239
+ })
9240
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9241
+ const { query } = await c.data();
9242
+ const roles2 = await getAllRoles(query.includeInactive ?? false);
9243
+ return { roles: roles2 };
9244
+ });
9245
+ var createAdminRole = route5.post("/_auth/admin/roles").input({
9246
+ body: Type.Object({
9247
+ name: Type.String({ description: "Unique role name (slug)" }),
9248
+ displayName: Type.String({ description: "Human-readable role name" }),
9249
+ description: Type.Optional(Type.String({ description: "Role description" })),
9250
+ priority: Type.Optional(Type.Number({ description: "Role priority (default: 10)" })),
9251
+ permissionIds: Type.Optional(Type.Array(
9252
+ Type.Number({ description: "Permission ID" }),
9253
+ { description: "Permission IDs to assign" }
9254
+ ))
9255
+ })
9256
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9257
+ const { body } = await c.data();
9258
+ const role = await createRole({
9259
+ name: body.name,
9260
+ displayName: body.displayName,
9261
+ description: body.description,
9262
+ priority: body.priority,
9263
+ permissionIds: body.permissionIds
9264
+ });
9265
+ return { role };
9266
+ });
9267
+ var updateAdminRole = route5.patch("/_auth/admin/roles/:id").input({
9268
+ params: Type.Object({
9269
+ id: Type.Number({ description: "Role ID" })
9270
+ }),
9271
+ body: Type.Object({
9272
+ displayName: Type.Optional(Type.String({ description: "Human-readable role name" })),
9273
+ description: Type.Optional(Type.String({ description: "Role description" })),
9274
+ priority: Type.Optional(Type.Number({ description: "Role priority" })),
9275
+ isActive: Type.Optional(Type.Boolean({ description: "Active status" }))
9276
+ })
9277
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9278
+ const { params, body } = await c.data();
9279
+ const role = await updateRole(params.id, body);
9280
+ return { role };
9281
+ });
9282
+ var deleteAdminRole = route5.delete("/_auth/admin/roles/:id").input({
9283
+ params: Type.Object({
9284
+ id: Type.Number({ description: "Role ID" })
9285
+ })
9286
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9287
+ const { params } = await c.data();
9288
+ await deleteRole(params.id);
9289
+ return c.noContent();
9290
+ });
9291
+ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
9292
+ params: Type.Object({
9293
+ userId: Type.Number({ description: "User ID" })
9294
+ }),
9295
+ body: Type.Object({
9296
+ roleId: Type.Number({ description: "New role ID to assign" })
9297
+ })
9298
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9299
+ const { params, body } = await c.data();
9300
+ const auth = getAuth(c);
9301
+ if (params.userId === Number(auth.userId)) {
9302
+ throw new ForbiddenError4({ message: "Cannot change your own role" });
9303
+ }
9304
+ const targetRole = await getUserRole(params.userId);
9305
+ if (targetRole === "superadmin") {
9306
+ throw new ForbiddenError4({ message: "Cannot modify superadmin role" });
9307
+ }
9308
+ await updateUserService(params.userId, { roleId: body.roleId });
9309
+ return { userId: params.userId, roleId: body.roleId };
8592
9310
  });
8593
9311
 
8594
9312
  // src/server/routes/index.ts
8595
- var mainAuthRouter = defineRouter4({
8596
- // Flatten all routes at root level
8597
- ...authRouter.routes,
8598
- ...invitationRouter.routes,
8599
- ...userRouter.routes
9313
+ var mainAuthRouter = defineRouter5({
9314
+ // Auth routes
9315
+ checkAccountExists,
9316
+ sendVerificationCode,
9317
+ verifyCode,
9318
+ register,
9319
+ login,
9320
+ logout,
9321
+ rotateKey,
9322
+ changePassword,
9323
+ getAuthSession,
9324
+ // One-Time Token routes
9325
+ issueOneTimeToken,
9326
+ // OAuth routes
9327
+ oauthGoogleStart,
9328
+ oauthGoogleCallback,
9329
+ oauthStart,
9330
+ oauthProviders,
9331
+ getGoogleOAuthUrl,
9332
+ oauthFinalize,
9333
+ // Invitation routes
9334
+ getInvitation,
9335
+ acceptInvitation: acceptInvitation2,
9336
+ createInvitation: createInvitation2,
9337
+ listInvitations: listInvitations2,
9338
+ cancelInvitation: cancelInvitation2,
9339
+ resendInvitation: resendInvitation2,
9340
+ deleteInvitation: deleteInvitation2,
9341
+ // User routes
9342
+ getUserProfile,
9343
+ updateUserProfile,
9344
+ checkUsername,
9345
+ updateUsername,
9346
+ updateLocale,
9347
+ // Admin routes (superadmin only)
9348
+ listRoles,
9349
+ createAdminRole,
9350
+ updateAdminRole,
9351
+ deleteAdminRole,
9352
+ updateUserRole
8600
9353
  });
8601
9354
 
8602
9355
  // src/server.ts
@@ -8706,36 +9459,60 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
8706
9459
  }
8707
9460
 
8708
9461
  // src/server/lib/session.ts
8709
- import * as jose from "jose";
8710
- import { env as env7 } from "@spfn/auth/config";
9462
+ import * as jose2 from "jose";
9463
+ import { env as env9 } from "@spfn/auth/config";
8711
9464
  import { env as coreEnv } from "@spfn/core/config";
8712
9465
  async function getSessionSecretKey() {
8713
- const secret = env7.SPFN_AUTH_SESSION_SECRET;
9466
+ const secret = env9.SPFN_AUTH_SESSION_SECRET;
8714
9467
  const encoder = new TextEncoder();
8715
9468
  const data = encoder.encode(secret);
8716
9469
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
8717
9470
  return new Uint8Array(hashBuffer);
8718
9471
  }
9472
+ async function getSecretFingerprint() {
9473
+ const key = await getSessionSecretKey();
9474
+ const hash = await crypto.subtle.digest("SHA-256", key.buffer);
9475
+ const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
9476
+ return hex.slice(0, 8);
9477
+ }
8719
9478
  async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
8720
9479
  const secret = await getSessionSecretKey();
8721
- return await new jose.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
9480
+ const result = await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
9481
+ if (coreEnv.NODE_ENV !== "production") {
9482
+ const fingerprint = await getSecretFingerprint();
9483
+ authLogger.session.debug(`Sealed session`, {
9484
+ secretFingerprint: fingerprint,
9485
+ resultLength: result.length,
9486
+ resultPrefix: result.slice(0, 20)
9487
+ });
9488
+ }
9489
+ return result;
8722
9490
  }
8723
9491
  async function unsealSession(jwt4) {
8724
9492
  try {
8725
9493
  const secret = await getSessionSecretKey();
8726
- const { payload } = await jose.jwtDecrypt(jwt4, secret, {
9494
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
8727
9495
  issuer: "spfn-auth",
8728
9496
  audience: "spfn-client"
8729
9497
  });
8730
9498
  return payload.data;
8731
9499
  } catch (err) {
8732
- if (err instanceof jose.errors.JWTExpired) {
9500
+ if (err instanceof jose2.errors.JWTExpired) {
8733
9501
  throw new Error("Session expired");
8734
9502
  }
8735
- if (err instanceof jose.errors.JWEDecryptionFailed) {
9503
+ if (err instanceof jose2.errors.JWEDecryptionFailed) {
9504
+ if (coreEnv.NODE_ENV !== "production") {
9505
+ const fingerprint = await getSecretFingerprint();
9506
+ authLogger.session.warn(`JWE decryption failed`, {
9507
+ secretFingerprint: fingerprint,
9508
+ jwtLength: jwt4.length,
9509
+ jwtPrefix: jwt4.slice(0, 20),
9510
+ jwtSuffix: jwt4.slice(-10)
9511
+ });
9512
+ }
8736
9513
  throw new Error("Invalid session");
8737
9514
  }
8738
- if (err instanceof jose.errors.JWTClaimValidationFailed) {
9515
+ if (err instanceof jose2.errors.JWTClaimValidationFailed) {
8739
9516
  throw new Error("Session validation failed");
8740
9517
  }
8741
9518
  throw new Error("Failed to unseal session");
@@ -8744,7 +9521,7 @@ async function unsealSession(jwt4) {
8744
9521
  async function getSessionInfo(jwt4) {
8745
9522
  const secret = await getSessionSecretKey();
8746
9523
  try {
8747
- const { payload } = await jose.jwtDecrypt(jwt4, secret);
9524
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret);
8748
9525
  return {
8749
9526
  issuedAt: new Date(payload.iat * 1e3),
8750
9527
  expiresAt: new Date(payload.exp * 1e3),
@@ -8753,7 +9530,7 @@ async function getSessionInfo(jwt4) {
8753
9530
  };
8754
9531
  } catch (err) {
8755
9532
  if (coreEnv.NODE_ENV !== "production") {
8756
- console.warn("[Session] Failed to get session info:", err instanceof Error ? err.message : "Unknown error");
9533
+ authLogger.session.warn("Failed to get session info:", err instanceof Error ? err.message : "Unknown error");
8757
9534
  }
8758
9535
  return null;
8759
9536
  }
@@ -8768,14 +9545,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
8768
9545
  }
8769
9546
 
8770
9547
  // src/server/setup.ts
8771
- import { env as env8 } from "@spfn/auth/config";
9548
+ import { env as env10 } from "@spfn/auth/config";
8772
9549
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
8773
9550
  init_repositories();
8774
9551
  function parseAdminAccounts() {
8775
9552
  const accounts = [];
8776
- if (env8.SPFN_AUTH_ADMIN_ACCOUNTS) {
9553
+ if (env10.SPFN_AUTH_ADMIN_ACCOUNTS) {
8777
9554
  try {
8778
- const accountsJson = env8.SPFN_AUTH_ADMIN_ACCOUNTS;
9555
+ const accountsJson = env10.SPFN_AUTH_ADMIN_ACCOUNTS;
8779
9556
  const parsed = JSON.parse(accountsJson);
8780
9557
  if (!Array.isArray(parsed)) {
8781
9558
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -8802,11 +9579,11 @@ function parseAdminAccounts() {
8802
9579
  return accounts;
8803
9580
  }
8804
9581
  }
8805
- const adminEmails = env8.SPFN_AUTH_ADMIN_EMAILS;
9582
+ const adminEmails = env10.SPFN_AUTH_ADMIN_EMAILS;
8806
9583
  if (adminEmails) {
8807
9584
  const emails = adminEmails.split(",").map((s) => s.trim());
8808
- const passwords = (env8.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8809
- const roles2 = (env8.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
9585
+ const passwords = (env10.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
9586
+ const roles2 = (env10.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
8810
9587
  if (passwords.length !== emails.length) {
8811
9588
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
8812
9589
  return accounts;
@@ -8828,8 +9605,8 @@ function parseAdminAccounts() {
8828
9605
  }
8829
9606
  return accounts;
8830
9607
  }
8831
- const adminEmail = env8.SPFN_AUTH_ADMIN_EMAIL;
8832
- const adminPassword = env8.SPFN_AUTH_ADMIN_PASSWORD;
9608
+ const adminEmail = env10.SPFN_AUTH_ADMIN_EMAIL;
9609
+ const adminPassword = env10.SPFN_AUTH_ADMIN_PASSWORD;
8833
9610
  if (adminEmail && adminPassword) {
8834
9611
  accounts.push({
8835
9612
  email: adminEmail,
@@ -8899,14 +9676,18 @@ function createAuthLifecycle(options = {}) {
8899
9676
  * Performs:
8900
9677
  * 1. Ensures admin account exists (creates if missing)
8901
9678
  * 2. Initializes RBAC system with built-in + custom roles/permissions
9679
+ * 3. Initializes one-time token manager
8902
9680
  */
8903
9681
  afterInfrastructure: async () => {
8904
9682
  await initializeAuth(options);
8905
9683
  await ensureAdminExists();
9684
+ initOneTimeTokenManager(options.oneTimeToken);
8906
9685
  }
8907
9686
  };
8908
9687
  }
8909
9688
  export {
9689
+ AuthMetadataRepository,
9690
+ AuthProviderSchema,
8910
9691
  COOKIE_NAMES,
8911
9692
  EmailSchema,
8912
9693
  INVITATION_STATUSES,
@@ -8919,6 +9700,7 @@ export {
8919
9700
  RolePermissionsRepository,
8920
9701
  RolesRepository,
8921
9702
  SOCIAL_PROVIDERS,
9703
+ SocialAccountsRepository,
8922
9704
  TargetTypeSchema,
8923
9705
  USER_STATUSES,
8924
9706
  UserPermissionsRepository,
@@ -8931,19 +9713,27 @@ export {
8931
9713
  acceptInvitation,
8932
9714
  addPermissionToRole,
8933
9715
  authLogger,
9716
+ authLoginEvent,
9717
+ authMetadata,
9718
+ authMetadataRepository,
9719
+ authRegisterEvent,
8934
9720
  mainAuthRouter as authRouter,
8935
9721
  authSchema,
8936
9722
  authenticate,
9723
+ buildOAuthErrorUrl,
8937
9724
  cancelInvitation,
8938
9725
  changePasswordService,
8939
9726
  checkAccountExistsService,
9727
+ checkUsernameAvailableService,
8940
9728
  configureAuth,
8941
9729
  createAuthLifecycle,
8942
9730
  createInvitation,
9731
+ createOAuthState,
8943
9732
  createRole,
8944
9733
  decodeToken,
8945
9734
  deleteInvitation,
8946
9735
  deleteRole,
9736
+ exchangeCodeForTokens,
8947
9737
  expireOldInvitations,
8948
9738
  generateClientToken,
8949
9739
  generateKeyPair,
@@ -8954,12 +9744,19 @@ export {
8954
9744
  getAuth,
8955
9745
  getAuthConfig,
8956
9746
  getAuthSessionService,
9747
+ getEnabledOAuthProviders,
9748
+ getGoogleAccessToken,
9749
+ getGoogleAuthUrl,
9750
+ getGoogleOAuthConfig,
9751
+ getGoogleUserInfo,
8957
9752
  getInvitationByToken,
8958
- getInvitationTemplate,
8959
9753
  getInvitationWithDetails,
8960
9754
  getKeyId,
8961
9755
  getKeySize,
8962
- getPasswordResetTemplate,
9756
+ getLocale,
9757
+ getOneTimeTokenManager,
9758
+ getOptionalAuth,
9759
+ getRole,
8963
9760
  getRoleByName,
8964
9761
  getRolePermissions,
8965
9762
  getSessionInfo,
@@ -8972,27 +9769,33 @@ export {
8972
9769
  getUserPermissions,
8973
9770
  getUserProfileService,
8974
9771
  getUserRole,
8975
- getVerificationCodeTemplate,
8976
- getWelcomeTemplate,
8977
9772
  hasAllPermissions,
8978
9773
  hasAnyPermission,
8979
9774
  hasAnyRole,
8980
9775
  hasPermission,
8981
9776
  hasRole,
8982
9777
  hashPassword,
9778
+ initOneTimeTokenManager,
8983
9779
  initializeAuth,
9780
+ invitationAcceptedEvent,
9781
+ invitationCreatedEvent,
8984
9782
  invitationsRepository,
9783
+ isGoogleOAuthEnabled,
9784
+ isOAuthProviderEnabled,
9785
+ issueOneTimeTokenService,
8985
9786
  keysRepository,
8986
9787
  listInvitations,
8987
9788
  loginService,
8988
9789
  logoutService,
9790
+ oauthCallbackService,
9791
+ oauthStartService,
9792
+ oneTimeTokenAuth,
9793
+ optionalAuth,
8989
9794
  parseDuration,
8990
9795
  permissions,
8991
9796
  permissionsRepository,
8992
- registerEmailProvider,
8993
- registerEmailTemplates,
9797
+ refreshAccessToken,
8994
9798
  registerPublicKeyService,
8995
- registerSMSProvider,
8996
9799
  registerService,
8997
9800
  removePermissionFromRole,
8998
9801
  requireAnyPermission,
@@ -9007,17 +9810,18 @@ export {
9007
9810
  rolesRepository,
9008
9811
  rotateKeyService,
9009
9812
  sealSession,
9010
- sendEmail,
9011
- sendSMS,
9012
9813
  sendVerificationCodeService,
9013
9814
  setRolePermissions,
9014
9815
  shouldRefreshSession,
9015
9816
  shouldRotateKey,
9817
+ socialAccountsRepository,
9016
9818
  unsealSession,
9017
9819
  updateLastLoginService,
9820
+ updateLocaleService,
9018
9821
  updateRole,
9019
9822
  updateUserProfileService,
9020
9823
  updateUserService,
9824
+ updateUsernameService,
9021
9825
  userInvitations,
9022
9826
  userPermissions,
9023
9827
  userPermissionsRepository,
@@ -9034,6 +9838,8 @@ export {
9034
9838
  verifyClientToken,
9035
9839
  verifyCodeService,
9036
9840
  verifyKeyFingerprint,
9841
+ verifyOAuthState,
9842
+ verifyOneTimeTokenService,
9037
9843
  verifyPassword,
9038
9844
  verifyToken
9039
9845
  };