@spfn/auth 0.2.0-beta.6 → 0.2.0-beta.60

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,18 +5529,24 @@ 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
- phoneVerifiedAt: users.phoneVerifiedAt
5536
+ phoneVerifiedAt: users.phoneVerifiedAt,
5537
+ passwordHash: users.passwordHash
5473
5538
  }).from(users).where(eq(users.id, userId)).limit(1).then((rows) => rows[0] ?? null);
5474
5539
  if (!user) {
5475
5540
  throw new NotFoundError({ message: "[@spfn/auth] User not found" });
5476
5541
  }
5477
5542
  return {
5478
5543
  userId: user.id,
5544
+ publicId: user.publicId,
5479
5545
  email: user.email,
5546
+ username: user.username,
5480
5547
  isEmailVerified: !!user.emailVerifiedAt,
5481
- isPhoneVerified: !!user.phoneVerifiedAt
5548
+ isPhoneVerified: !!user.phoneVerifiedAt,
5549
+ hasPassword: !!user.passwordHash
5482
5550
  };
5483
5551
  }
5484
5552
  /**
@@ -5491,7 +5559,9 @@ var init_users_repository = __esm({
5491
5559
  async fetchFullUserData(userId) {
5492
5560
  const user = await this.readDb.select({
5493
5561
  id: users.id,
5562
+ publicId: users.publicId,
5494
5563
  email: users.email,
5564
+ username: users.username,
5495
5565
  emailVerifiedAt: users.emailVerifiedAt,
5496
5566
  phoneVerifiedAt: users.phoneVerifiedAt,
5497
5567
  lastLoginAt: users.lastLoginAt,
@@ -5503,7 +5573,9 @@ var init_users_repository = __esm({
5503
5573
  }
5504
5574
  return {
5505
5575
  userId: user.id,
5576
+ publicId: user.publicId,
5506
5577
  email: user.email,
5578
+ username: user.username,
5507
5579
  isEmailVerified: !!user.emailVerifiedAt,
5508
5580
  isPhoneVerified: !!user.phoneVerifiedAt,
5509
5581
  lastLoginAt: user.lastLoginAt,
@@ -6087,6 +6159,13 @@ var init_user_profiles_repository = __esm({
6087
6159
  const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id, id11)).limit(1);
6088
6160
  return result[0] ?? null;
6089
6161
  }
6162
+ /**
6163
+ * User ID로 locale만 조회 (경량)
6164
+ */
6165
+ async findLocaleByUserId(userId) {
6166
+ const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1);
6167
+ return result[0]?.locale || "en";
6168
+ }
6090
6169
  /**
6091
6170
  * User ID로 프로필 조회
6092
6171
  */
@@ -6260,16 +6339,16 @@ var init_invitations_repository = __esm({
6260
6339
  /**
6261
6340
  * 초대 상태 업데이트
6262
6341
  */
6263
- async updateStatus(id11, status, timestamp) {
6342
+ async updateStatus(id11, status, timestamp2) {
6264
6343
  const updates = {
6265
6344
  status,
6266
6345
  updatedAt: /* @__PURE__ */ new Date()
6267
6346
  };
6268
- if (timestamp) {
6347
+ if (timestamp2) {
6269
6348
  if (status === "accepted") {
6270
- updates.acceptedAt = timestamp;
6349
+ updates.acceptedAt = timestamp2;
6271
6350
  } else if (status === "cancelled") {
6272
- updates.cancelledAt = timestamp;
6351
+ updates.cancelledAt = timestamp2;
6273
6352
  }
6274
6353
  }
6275
6354
  const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id, id11)).returning();
@@ -6413,6 +6492,133 @@ var init_invitations_repository = __esm({
6413
6492
  }
6414
6493
  });
6415
6494
 
6495
+ // src/server/repositories/social-accounts.repository.ts
6496
+ import { eq as eq10, and as and7 } from "drizzle-orm";
6497
+ import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
6498
+ var SocialAccountsRepository, socialAccountsRepository;
6499
+ var init_social_accounts_repository = __esm({
6500
+ "src/server/repositories/social-accounts.repository.ts"() {
6501
+ "use strict";
6502
+ init_entities();
6503
+ SocialAccountsRepository = class extends BaseRepository10 {
6504
+ /**
6505
+ * provider와 providerUserId로 소셜 계정 조회
6506
+ * Read replica 사용
6507
+ */
6508
+ async findByProviderAndProviderId(provider, providerUserId) {
6509
+ const result = await this.readDb.select().from(userSocialAccounts).where(
6510
+ and7(
6511
+ eq10(userSocialAccounts.provider, provider),
6512
+ eq10(userSocialAccounts.providerUserId, providerUserId)
6513
+ )
6514
+ ).limit(1);
6515
+ return result[0] ?? null;
6516
+ }
6517
+ /**
6518
+ * userId로 모든 소셜 계정 조회
6519
+ * Read replica 사용
6520
+ */
6521
+ async findByUserId(userId) {
6522
+ return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
6523
+ }
6524
+ /**
6525
+ * userId와 provider로 소셜 계정 조회
6526
+ * Read replica 사용
6527
+ */
6528
+ async findByUserIdAndProvider(userId, provider) {
6529
+ const result = await this.readDb.select().from(userSocialAccounts).where(
6530
+ and7(
6531
+ eq10(userSocialAccounts.userId, userId),
6532
+ eq10(userSocialAccounts.provider, provider)
6533
+ )
6534
+ ).limit(1);
6535
+ return result[0] ?? null;
6536
+ }
6537
+ /**
6538
+ * 소셜 계정 생성
6539
+ * Write primary 사용
6540
+ */
6541
+ async create(data) {
6542
+ return await this._create(userSocialAccounts, {
6543
+ ...data,
6544
+ createdAt: /* @__PURE__ */ new Date(),
6545
+ updatedAt: /* @__PURE__ */ new Date()
6546
+ });
6547
+ }
6548
+ /**
6549
+ * 토큰 정보 업데이트
6550
+ * Write primary 사용
6551
+ */
6552
+ async updateTokens(id11, data) {
6553
+ const result = await this.db.update(userSocialAccounts).set({
6554
+ ...data,
6555
+ updatedAt: /* @__PURE__ */ new Date()
6556
+ }).where(eq10(userSocialAccounts.id, id11)).returning();
6557
+ return result[0] ?? null;
6558
+ }
6559
+ /**
6560
+ * 소셜 계정 삭제
6561
+ * Write primary 사용
6562
+ */
6563
+ async deleteById(id11) {
6564
+ const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
6565
+ return result[0] ?? null;
6566
+ }
6567
+ /**
6568
+ * userId와 provider로 소셜 계정 삭제
6569
+ * Write primary 사용
6570
+ */
6571
+ async deleteByUserIdAndProvider(userId, provider) {
6572
+ const result = await this.db.delete(userSocialAccounts).where(
6573
+ and7(
6574
+ eq10(userSocialAccounts.userId, userId),
6575
+ eq10(userSocialAccounts.provider, provider)
6576
+ )
6577
+ ).returning();
6578
+ return result[0] ?? null;
6579
+ }
6580
+ };
6581
+ socialAccountsRepository = new SocialAccountsRepository();
6582
+ }
6583
+ });
6584
+
6585
+ // src/server/repositories/auth-metadata.repository.ts
6586
+ import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
6587
+ import { eq as eq11 } from "drizzle-orm";
6588
+ var AuthMetadataRepository, authMetadataRepository;
6589
+ var init_auth_metadata_repository = __esm({
6590
+ "src/server/repositories/auth-metadata.repository.ts"() {
6591
+ "use strict";
6592
+ init_auth_metadata();
6593
+ AuthMetadataRepository = class extends BaseRepository11 {
6594
+ /**
6595
+ * 키로 값 조회
6596
+ */
6597
+ async get(key) {
6598
+ const result = await this.readDb.select().from(authMetadata).where(eq11(authMetadata.key, key)).limit(1);
6599
+ return result[0]?.value ?? null;
6600
+ }
6601
+ /**
6602
+ * 키-값 저장 (upsert)
6603
+ */
6604
+ async set(key, value) {
6605
+ await this.db.insert(authMetadata).values({
6606
+ key,
6607
+ value,
6608
+ updatedAt: /* @__PURE__ */ new Date()
6609
+ }).onConflictDoUpdate({
6610
+ target: authMetadata.key,
6611
+ set: {
6612
+ value,
6613
+ updatedAt: /* @__PURE__ */ new Date()
6614
+ }
6615
+ });
6616
+ }
6617
+ };
6618
+ authMetadataRepository = new AuthMetadataRepository();
6619
+ }
6620
+ });
6621
+
6416
6622
  // src/server/repositories/index.ts
6417
6623
  var init_repositories = __esm({
6418
6624
  "src/server/repositories/index.ts"() {
@@ -6426,6 +6632,8 @@ var init_repositories = __esm({
6426
6632
  init_user_permissions_repository();
6427
6633
  init_user_profiles_repository();
6428
6634
  init_invitations_repository();
6635
+ init_social_accounts_repository();
6636
+ init_auth_metadata_repository();
6429
6637
  }
6430
6638
  });
6431
6639
 
@@ -6552,7 +6760,7 @@ var init_role_service = __esm({
6552
6760
  import "@spfn/auth/config";
6553
6761
 
6554
6762
  // src/server/routes/index.ts
6555
- import { defineRouter as defineRouter4 } from "@spfn/core/route";
6763
+ import { defineRouter as defineRouter5 } from "@spfn/core/route";
6556
6764
 
6557
6765
  // src/server/routes/auth/index.ts
6558
6766
  init_schema3();
@@ -6675,12 +6883,24 @@ function getAuth(c) {
6675
6883
  }
6676
6884
  return c.get("auth");
6677
6885
  }
6886
+ function getOptionalAuth(c) {
6887
+ if ("raw" in c && c.raw) {
6888
+ return c.raw.get("auth");
6889
+ }
6890
+ return c.get("auth");
6891
+ }
6678
6892
  function getUser(c) {
6679
6893
  return getAuth(c).user;
6680
6894
  }
6681
6895
  function getUserId(c) {
6682
6896
  return getAuth(c).userId;
6683
6897
  }
6898
+ function getRole(c) {
6899
+ return getAuth(c).role;
6900
+ }
6901
+ function getLocale(c) {
6902
+ return getAuth(c).locale;
6903
+ }
6684
6904
  function getKeyId(c) {
6685
6905
  return getAuth(c).keyId;
6686
6906
  }
@@ -6690,7 +6910,7 @@ init_types();
6690
6910
 
6691
6911
  // src/server/services/auth.service.ts
6692
6912
  init_repositories();
6693
- import { ValidationError } from "@spfn/core/errors";
6913
+ import { ValidationError as ValidationError2 } from "@spfn/core/errors";
6694
6914
  import {
6695
6915
  InvalidCredentialsError,
6696
6916
  AccountDisabledError,
@@ -6701,9 +6921,10 @@ import {
6701
6921
  } from "@spfn/auth/errors";
6702
6922
 
6703
6923
  // src/server/services/verification.service.ts
6704
- import { env as env5 } from "@spfn/auth/config";
6924
+ import { env as env3 } from "@spfn/auth/config";
6705
6925
  import { InvalidVerificationCodeError } from "@spfn/auth/errors";
6706
6926
  import jwt2 from "jsonwebtoken";
6927
+ import { sendEmail, sendSMS } from "@spfn/notification/server";
6707
6928
 
6708
6929
  // src/server/logger.ts
6709
6930
  import { logger as rootLogger } from "@spfn/core/logger";
@@ -6713,8 +6934,10 @@ var authLogger = {
6713
6934
  interceptor: {
6714
6935
  general: rootLogger.child("@spfn/auth:interceptor:general"),
6715
6936
  login: rootLogger.child("@spfn/auth:interceptor:login"),
6716
- keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation")
6937
+ keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
6938
+ oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
6717
6939
  },
6940
+ session: rootLogger.child("@spfn/auth:session"),
6718
6941
  service: rootLogger.child("@spfn/auth:service"),
6719
6942
  setup: rootLogger.child("@spfn/auth:setup"),
6720
6943
  email: rootLogger.child("@spfn/auth:email"),
@@ -6723,410 +6946,6 @@ var authLogger = {
6723
6946
 
6724
6947
  // src/server/services/verification.service.ts
6725
6948
  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
- return {
6892
- success: false,
6893
- error: "AWS SES credentials not configured. Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable."
6894
- };
6895
- }
6896
- if (!env4.SPFN_AUTH_AWS_SES_FROM_EMAIL) {
6897
- return {
6898
- success: false,
6899
- error: "AWS SES sender email not configured. Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable."
6900
- };
6901
- }
6902
- try {
6903
- const config = {
6904
- region: env4.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
6905
- };
6906
- if (env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID && env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY) {
6907
- config.credentials = {
6908
- accessKeyId: env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID,
6909
- secretAccessKey: env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY
6910
- };
6911
- }
6912
- const client = new SESClient(config);
6913
- const body = {};
6914
- if (text10) {
6915
- body.Text = {
6916
- Charset: "UTF-8",
6917
- Data: text10
6918
- };
6919
- }
6920
- if (html) {
6921
- body.Html = {
6922
- Charset: "UTF-8",
6923
- Data: html
6924
- };
6925
- }
6926
- const command = new SendEmailCommand({
6927
- Source: env4.SPFN_AUTH_AWS_SES_FROM_EMAIL,
6928
- Destination: {
6929
- ToAddresses: [to]
6930
- },
6931
- Message: {
6932
- Subject: {
6933
- Charset: "UTF-8",
6934
- Data: subject
6935
- },
6936
- Body: body
6937
- }
6938
- });
6939
- const response = await client.send(command);
6940
- authLogger.email.info("Email sent via AWS SES", {
6941
- to,
6942
- messageId: response.MessageId,
6943
- purpose: purpose || "N/A"
6944
- });
6945
- return {
6946
- success: true,
6947
- messageId: response.MessageId
6948
- };
6949
- } catch (error) {
6950
- const err = error;
6951
- authLogger.email.error("Failed to send email via AWS SES", {
6952
- to,
6953
- error: err.message
6954
- });
6955
- return {
6956
- success: false,
6957
- error: err.message || "Failed to send email via AWS SES"
6958
- };
6959
- }
6960
- }
6961
- };
6962
- } catch (error) {
6963
- return null;
6964
- }
6965
- }
6966
- var awsSESProvider = createAWSSESProvider();
6967
-
6968
- // src/server/services/email/index.ts
6969
- if (awsSESProvider) {
6970
- registerEmailProvider(awsSESProvider);
6971
- }
6972
-
6973
- // src/server/services/email/templates/verification-code.ts
6974
- function getSubject(purpose) {
6975
- switch (purpose) {
6976
- case "registration":
6977
- return "Verify your email address";
6978
- case "login":
6979
- return "Your login verification code";
6980
- case "password_reset":
6981
- return "Reset your password";
6982
- default:
6983
- return "Your verification code";
6984
- }
6985
- }
6986
- function getPurposeText(purpose) {
6987
- switch (purpose) {
6988
- case "registration":
6989
- return "complete your registration";
6990
- case "login":
6991
- return "verify your identity";
6992
- case "password_reset":
6993
- return "reset your password";
6994
- default:
6995
- return "verify your identity";
6996
- }
6997
- }
6998
- function generateText(params) {
6999
- const { code, expiresInMinutes = 5 } = params;
7000
- return `Your verification code is: ${code}
7001
-
7002
- This code will expire in ${expiresInMinutes} minutes.
7003
-
7004
- If you didn't request this code, please ignore this email.`;
7005
- }
7006
- function generateHTML(params) {
7007
- const { code, purpose, expiresInMinutes = 5, appName } = params;
7008
- const purposeText = getPurposeText(purpose);
7009
- return `<!DOCTYPE html>
7010
- <html>
7011
- <head>
7012
- <meta charset="utf-8">
7013
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7014
- <title>Verification Code</title>
7015
- </head>
7016
- <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;">
7017
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
7018
- <h1 style="color: white; margin: 0; font-size: 24px;">${appName ? appName : "Verification Code"}</h1>
7019
- </div>
7020
- <div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
7021
- <p style="margin-bottom: 20px; font-size: 16px;">
7022
- Please use the following verification code to ${purposeText}:
7023
- </p>
7024
- <div style="background: #f8f9fa; padding: 25px; border-radius: 8px; text-align: center; margin: 25px 0; border: 2px dashed #dee2e6;">
7025
- <span style="font-size: 36px; font-weight: bold; letter-spacing: 10px; color: #333; font-family: 'Courier New', monospace;">${code}</span>
7026
- </div>
7027
- <p style="color: #666; font-size: 14px; margin-top: 20px; text-align: center;">
7028
- <strong>This code will expire in ${expiresInMinutes} minutes.</strong>
7029
- </p>
7030
- <hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
7031
- <p style="color: #999; font-size: 12px; text-align: center; margin: 0;">
7032
- If you didn't request this code, please ignore this email.
7033
- </p>
7034
- </div>
7035
- <div style="text-align: center; padding: 20px; color: #999; font-size: 11px;">
7036
- <p style="margin: 0;">This is an automated message. Please do not reply.</p>
7037
- </div>
7038
- </body>
7039
- </html>`;
7040
- }
7041
- function verificationCodeTemplate(params) {
7042
- return {
7043
- subject: getSubject(params.purpose),
7044
- text: generateText(params),
7045
- html: generateHTML(params)
7046
- };
7047
- }
7048
-
7049
- // src/server/services/email/templates/registry.ts
7050
- var customTemplates = {};
7051
- function registerEmailTemplates(templates) {
7052
- customTemplates = { ...customTemplates, ...templates };
7053
- authLogger.email.info("Registered custom email templates", {
7054
- templates: Object.keys(templates)
7055
- });
7056
- }
7057
- function getVerificationCodeTemplate(params) {
7058
- if (customTemplates.verificationCode) {
7059
- return customTemplates.verificationCode(params);
7060
- }
7061
- return verificationCodeTemplate(params);
7062
- }
7063
- function getWelcomeTemplate(params) {
7064
- if (customTemplates.welcome) {
7065
- return customTemplates.welcome(params);
7066
- }
7067
- return {
7068
- subject: params.appName ? `Welcome to ${params.appName}!` : "Welcome!",
7069
- text: `Welcome! Your account has been created successfully.`,
7070
- html: `
7071
- <!DOCTYPE html>
7072
- <html>
7073
- <head><meta charset="utf-8"></head>
7074
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7075
- <h1>Welcome${params.appName ? ` to ${params.appName}` : ""}!</h1>
7076
- <p>Your account has been created successfully.</p>
7077
- </body>
7078
- </html>`
7079
- };
7080
- }
7081
- function getPasswordResetTemplate(params) {
7082
- if (customTemplates.passwordReset) {
7083
- return customTemplates.passwordReset(params);
7084
- }
7085
- const expires = params.expiresInMinutes || 30;
7086
- return {
7087
- subject: "Reset your password",
7088
- text: `Click this link to reset your password: ${params.resetLink}
7089
-
7090
- This link will expire in ${expires} minutes.`,
7091
- html: `
7092
- <!DOCTYPE html>
7093
- <html>
7094
- <head><meta charset="utf-8"></head>
7095
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7096
- <h1>Reset Your Password</h1>
7097
- <p>Click the button below to reset your password:</p>
7098
- <a href="${params.resetLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Reset Password</a>
7099
- <p style="color: #666; margin-top: 20px;">This link will expire in ${expires} minutes.</p>
7100
- </body>
7101
- </html>`
7102
- };
7103
- }
7104
- function getInvitationTemplate(params) {
7105
- if (customTemplates.invitation) {
7106
- return customTemplates.invitation(params);
7107
- }
7108
- const appName = params.appName || "our platform";
7109
- const inviterText = params.inviterName ? `${params.inviterName} has invited you` : "You have been invited";
7110
- const roleText = params.roleName ? ` as ${params.roleName}` : "";
7111
- return {
7112
- subject: `You're invited to join ${appName}`,
7113
- text: `${inviterText} to join ${appName}${roleText}.
7114
-
7115
- Click here to accept: ${params.inviteLink}`,
7116
- html: `
7117
- <!DOCTYPE html>
7118
- <html>
7119
- <head><meta charset="utf-8"></head>
7120
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7121
- <h1>You're Invited!</h1>
7122
- <p>${inviterText} to join <strong>${appName}</strong>${roleText}.</p>
7123
- <a href="${params.inviteLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Accept Invitation</a>
7124
- </body>
7125
- </html>`
7126
- };
7127
- }
7128
-
7129
- // src/server/services/verification.service.ts
7130
6949
  var VERIFICATION_TOKEN_EXPIRY = "15m";
7131
6950
  var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
7132
6951
  var MAX_VERIFICATION_ATTEMPTS = 5;
@@ -7170,7 +6989,7 @@ async function markCodeAsUsed(codeId) {
7170
6989
  await verificationCodesRepository.markAsUsed(codeId);
7171
6990
  }
7172
6991
  function createVerificationToken(payload) {
7173
- return jwt2.sign(payload, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6992
+ return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7174
6993
  expiresIn: VERIFICATION_TOKEN_EXPIRY,
7175
6994
  issuer: "spfn-auth",
7176
6995
  audience: "spfn-client"
@@ -7178,7 +6997,7 @@ function createVerificationToken(payload) {
7178
6997
  }
7179
6998
  function validateVerificationToken(token) {
7180
6999
  try {
7181
- const decoded = jwt2.verify(token, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7000
+ const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7182
7001
  issuer: "spfn-auth",
7183
7002
  audience: "spfn-client"
7184
7003
  });
@@ -7192,17 +7011,14 @@ function validateVerificationToken(token) {
7192
7011
  }
7193
7012
  }
7194
7013
  async function sendVerificationEmail(email, code, purpose) {
7195
- const { subject, text: text10, html } = getVerificationCodeTemplate({
7196
- code,
7197
- purpose,
7198
- expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7199
- });
7200
7014
  const result = await sendEmail({
7201
7015
  to: email,
7202
- subject,
7203
- text: text10,
7204
- html,
7205
- purpose
7016
+ template: "verification-code",
7017
+ data: {
7018
+ code,
7019
+ purpose,
7020
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7021
+ }
7206
7022
  });
7207
7023
  if (!result.success) {
7208
7024
  authLogger.email.error("Failed to send verification email", {
@@ -7213,11 +7029,13 @@ async function sendVerificationEmail(email, code, purpose) {
7213
7029
  }
7214
7030
  }
7215
7031
  async function sendVerificationSMS(phone, code, purpose) {
7216
- const message = `Your verification code is: ${code}`;
7217
7032
  const result = await sendSMS({
7218
- phone,
7219
- message,
7220
- purpose
7033
+ to: phone,
7034
+ template: "verification-code",
7035
+ data: {
7036
+ code,
7037
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7038
+ }
7221
7039
  });
7222
7040
  if (!result.success) {
7223
7041
  authLogger.sms.error("Failed to send verification SMS", {
@@ -7270,6 +7088,10 @@ function getKeyExpiryDate() {
7270
7088
  }
7271
7089
  async function registerPublicKeyService(params) {
7272
7090
  const { userId, keyId, publicKey, fingerprint, algorithm = "ES256" } = params;
7091
+ const existing = await keysRepository.findActiveByKeyId(keyId);
7092
+ if (existing) {
7093
+ return;
7094
+ }
7273
7095
  const isValidFingerprint = verifyKeyFingerprint(publicKey, fingerprint);
7274
7096
  if (!isValidFingerprint) {
7275
7097
  throw new InvalidKeyFingerprintError();
@@ -7318,6 +7140,9 @@ async function revokeKeyService(params) {
7318
7140
 
7319
7141
  // src/server/services/user.service.ts
7320
7142
  init_repositories();
7143
+ import { ValidationError } from "@spfn/core/errors";
7144
+ import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
7145
+ import { env as env4 } from "@spfn/auth/config";
7321
7146
  async function getUserByIdService(userId) {
7322
7147
  return await usersRepository.findById(userId);
7323
7148
  }
@@ -7333,6 +7158,108 @@ async function updateLastLoginService(userId) {
7333
7158
  async function updateUserService(userId, updates) {
7334
7159
  await usersRepository.updateById(userId, updates);
7335
7160
  }
7161
+ function getReservedUsernames() {
7162
+ const raw = env4.SPFN_AUTH_RESERVED_USERNAMES ?? "";
7163
+ if (!raw) {
7164
+ return /* @__PURE__ */ new Set();
7165
+ }
7166
+ return new Set(
7167
+ raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
7168
+ );
7169
+ }
7170
+ function isReservedUsername(username) {
7171
+ return getReservedUsernames().has(username.toLowerCase());
7172
+ }
7173
+ function validateUsernameLength(username) {
7174
+ const min = env4.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
7175
+ const max = env4.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
7176
+ if (username.length < min) {
7177
+ throw new ValidationError({
7178
+ message: `Username must be at least ${min} characters`,
7179
+ details: { minLength: min, actual: username.length }
7180
+ });
7181
+ }
7182
+ if (username.length > max) {
7183
+ throw new ValidationError({
7184
+ message: `Username must be at most ${max} characters`,
7185
+ details: { maxLength: max, actual: username.length }
7186
+ });
7187
+ }
7188
+ }
7189
+ async function checkUsernameAvailableService(username) {
7190
+ validateUsernameLength(username);
7191
+ if (isReservedUsername(username)) {
7192
+ return false;
7193
+ }
7194
+ const existing = await usersRepository.findByUsername(username);
7195
+ return !existing;
7196
+ }
7197
+ async function updateUsernameService(userId, username) {
7198
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7199
+ if (username !== null) {
7200
+ validateUsernameLength(username);
7201
+ if (isReservedUsername(username)) {
7202
+ throw new ReservedUsernameError({ username });
7203
+ }
7204
+ const existing = await usersRepository.findByUsername(username);
7205
+ if (existing && existing.id !== userIdNum) {
7206
+ throw new UsernameAlreadyTakenError({ username });
7207
+ }
7208
+ }
7209
+ return await usersRepository.updateById(userIdNum, { username });
7210
+ }
7211
+
7212
+ // src/server/events/index.ts
7213
+ init_esm();
7214
+ import { defineEvent } from "@spfn/core/event";
7215
+ var AuthProviderSchema = Type.Union([
7216
+ Type.Literal("email"),
7217
+ Type.Literal("phone"),
7218
+ Type.Literal("google")
7219
+ ]);
7220
+ var authLoginEvent = defineEvent(
7221
+ "auth.login",
7222
+ Type.Object({
7223
+ userId: Type.String(),
7224
+ provider: AuthProviderSchema,
7225
+ email: Type.Optional(Type.String()),
7226
+ phone: Type.Optional(Type.String())
7227
+ })
7228
+ );
7229
+ var authRegisterEvent = defineEvent(
7230
+ "auth.register",
7231
+ Type.Object({
7232
+ userId: Type.String(),
7233
+ provider: AuthProviderSchema,
7234
+ email: Type.Optional(Type.String()),
7235
+ phone: Type.Optional(Type.String()),
7236
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
7237
+ })
7238
+ );
7239
+ var invitationCreatedEvent = defineEvent(
7240
+ "auth.invitation.created",
7241
+ Type.Object({
7242
+ invitationId: Type.String(),
7243
+ email: Type.String(),
7244
+ token: Type.String(),
7245
+ roleId: Type.Number(),
7246
+ invitedBy: Type.String(),
7247
+ expiresAt: Type.String(),
7248
+ isResend: Type.Boolean(),
7249
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
7250
+ })
7251
+ );
7252
+ var invitationAcceptedEvent = defineEvent(
7253
+ "auth.invitation.accepted",
7254
+ Type.Object({
7255
+ invitationId: Type.String(),
7256
+ email: Type.String(),
7257
+ userId: Type.String(),
7258
+ roleId: Type.Number(),
7259
+ invitedBy: Type.String(),
7260
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
7261
+ })
7262
+ );
7336
7263
 
7337
7264
  // src/server/services/auth.service.ts
7338
7265
  async function checkAccountExistsService(params) {
@@ -7349,7 +7276,7 @@ async function checkAccountExistsService(params) {
7349
7276
  identifierType = "phone";
7350
7277
  user = await usersRepository.findByPhone(phone);
7351
7278
  } else {
7352
- throw new ValidationError({ message: "Either email or phone must be provided" });
7279
+ throw new ValidationError2({ message: "Either email or phone must be provided" });
7353
7280
  }
7354
7281
  return {
7355
7282
  exists: !!user,
@@ -7358,7 +7285,7 @@ async function checkAccountExistsService(params) {
7358
7285
  };
7359
7286
  }
7360
7287
  async function registerService(params) {
7361
- const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm } = params;
7288
+ const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
7362
7289
  const tokenPayload = validateVerificationToken(verificationToken);
7363
7290
  if (!tokenPayload) {
7364
7291
  throw new InvalidVerificationTokenError();
@@ -7400,17 +7327,26 @@ async function registerService(params) {
7400
7327
  fingerprint,
7401
7328
  algorithm
7402
7329
  });
7403
- return {
7330
+ const result = {
7404
7331
  userId: String(newUser.id),
7332
+ publicId: newUser.publicId,
7405
7333
  email: newUser.email || void 0,
7406
7334
  phone: newUser.phone || void 0
7407
7335
  };
7336
+ await authRegisterEvent.emit({
7337
+ userId: result.userId,
7338
+ provider: email ? "email" : "phone",
7339
+ email: result.email,
7340
+ phone: result.phone,
7341
+ metadata
7342
+ });
7343
+ return result;
7408
7344
  }
7409
7345
  async function loginService(params) {
7410
7346
  const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
7411
7347
  const user = await usersRepository.findByEmailOrPhone(email, phone);
7412
7348
  if (!email && !phone) {
7413
- throw new ValidationError({ message: "Either email or phone must be provided" });
7349
+ throw new ValidationError2({ message: "Either email or phone must be provided" });
7414
7350
  }
7415
7351
  if (!user || !user.passwordHash) {
7416
7352
  throw new InvalidCredentialsError();
@@ -7437,12 +7373,20 @@ async function loginService(params) {
7437
7373
  algorithm
7438
7374
  });
7439
7375
  await updateLastLoginService(user.id);
7440
- return {
7376
+ const result = {
7441
7377
  userId: String(user.id),
7378
+ publicId: user.publicId,
7442
7379
  email: user.email || void 0,
7443
7380
  phone: user.phone || void 0,
7444
7381
  passwordChangeRequired: user.passwordChangeRequired
7445
7382
  };
7383
+ await authLoginEvent.emit({
7384
+ userId: result.userId,
7385
+ provider: email ? "email" : "phone",
7386
+ email: result.email,
7387
+ phone: result.phone
7388
+ });
7389
+ return result;
7446
7390
  }
7447
7391
  async function logoutService(params) {
7448
7392
  const { userId, keyId } = params;
@@ -7460,16 +7404,18 @@ async function changePasswordService(params) {
7460
7404
  } else {
7461
7405
  const user = await usersRepository.findById(userId);
7462
7406
  if (!user) {
7463
- throw new ValidationError({ message: "User not found" });
7407
+ throw new ValidationError2({ message: "User not found" });
7464
7408
  }
7465
7409
  passwordHash = user.passwordHash;
7466
7410
  }
7467
- if (!passwordHash) {
7468
- throw new ValidationError({ message: "No password set for this account" });
7469
- }
7470
- const isValid = await verifyPassword(currentPassword, passwordHash);
7471
- if (!isValid) {
7472
- throw new InvalidCredentialsError({ message: "Current password is incorrect" });
7411
+ if (passwordHash) {
7412
+ if (!currentPassword) {
7413
+ throw new ValidationError2({ message: "Current password is required" });
7414
+ }
7415
+ const isValid = await verifyPassword(currentPassword, passwordHash);
7416
+ if (!isValid) {
7417
+ throw new InvalidCredentialsError({ message: "Current password is incorrect" });
7418
+ }
7473
7419
  }
7474
7420
  const newPasswordHash = await hashPassword(newPassword);
7475
7421
  await usersRepository.updatePassword(userId, newPasswordHash, true);
@@ -7478,14 +7424,27 @@ async function changePasswordService(params) {
7478
7424
  // src/server/services/rbac.service.ts
7479
7425
  init_repositories();
7480
7426
  init_rbac();
7427
+ import { createHash } from "crypto";
7481
7428
 
7482
7429
  // src/server/lib/config.ts
7483
- import { env as env6 } from "@spfn/auth/config";
7430
+ import { env as env5 } from "@spfn/auth/config";
7431
+ function getCookieSuffix() {
7432
+ const port = process.env.PORT;
7433
+ return port ? `_${port}` : "";
7434
+ }
7484
7435
  var COOKIE_NAMES = {
7485
7436
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
7486
- SESSION: "spfn_session",
7437
+ get SESSION() {
7438
+ return `spfn_session${getCookieSuffix()}`;
7439
+ },
7487
7440
  /** Current key ID (for key rotation) */
7488
- SESSION_KEY_ID: "spfn_session_key_id"
7441
+ get SESSION_KEY_ID() {
7442
+ return `spfn_session_key_id${getCookieSuffix()}`;
7443
+ },
7444
+ /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
7445
+ get OAUTH_PENDING() {
7446
+ return `spfn_oauth_pending${getCookieSuffix()}`;
7447
+ }
7489
7448
  };
7490
7449
  function parseDuration(duration) {
7491
7450
  if (typeof duration === "number") {
@@ -7530,7 +7489,7 @@ function getSessionTtl(override) {
7530
7489
  if (globalConfig.sessionTtl !== void 0) {
7531
7490
  return parseDuration(globalConfig.sessionTtl);
7532
7491
  }
7533
- const envTtl = env6.SPFN_AUTH_SESSION_TTL;
7492
+ const envTtl = env5.SPFN_AUTH_SESSION_TTL;
7534
7493
  if (envTtl) {
7535
7494
  return parseDuration(envTtl);
7536
7495
  }
@@ -7538,28 +7497,19 @@ function getSessionTtl(override) {
7538
7497
  }
7539
7498
 
7540
7499
  // src/server/services/rbac.service.ts
7541
- async function initializeAuth(options = {}) {
7542
- authLogger.service.info("\u{1F510} Initializing RBAC system...");
7543
- if (options.sessionTtl !== void 0) {
7544
- configureAuth({
7545
- sessionTtl: options.sessionTtl
7546
- });
7547
- authLogger.service.info(`\u23F1\uFE0F Session TTL: ${options.sessionTtl}`);
7548
- }
7549
- const allRoles = [
7550
- ...Object.values(BUILTIN_ROLES),
7551
- ...options.roles || []
7552
- ];
7553
- for (const roleConfig of allRoles) {
7554
- await upsertRole(roleConfig);
7555
- }
7556
- const allPermissions = [
7557
- ...Object.values(BUILTIN_PERMISSIONS),
7558
- ...options.permissions || []
7559
- ];
7560
- for (const permConfig of allPermissions) {
7561
- await upsertPermission(permConfig);
7562
- }
7500
+ var RBAC_HASH_KEY = "rbac_config_hash";
7501
+ function computeConfigHash(allRoles, allPermissions, allMappings) {
7502
+ const payload = JSON.stringify({
7503
+ 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)),
7504
+ 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)),
7505
+ mappings: Object.keys(allMappings).sort().reduce((acc, key) => {
7506
+ acc[key] = [...allMappings[key]].sort();
7507
+ return acc;
7508
+ }, {})
7509
+ });
7510
+ return createHash("sha256").update(payload).digest("hex");
7511
+ }
7512
+ function collectMappings(options) {
7563
7513
  const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
7564
7514
  if (options.rolePermissions) {
7565
7515
  for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
@@ -7572,78 +7522,114 @@ async function initializeAuth(options = {}) {
7572
7522
  }
7573
7523
  }
7574
7524
  }
7575
- for (const [roleName, permNames] of Object.entries(allMappings)) {
7576
- await assignPermissionsToRole(roleName, permNames);
7525
+ return allMappings;
7526
+ }
7527
+ async function initializeAuth(options = {}) {
7528
+ authLogger.service.info("\u{1F510} Initializing RBAC system...");
7529
+ if (options.sessionTtl !== void 0) {
7530
+ configureAuth({
7531
+ sessionTtl: options.sessionTtl
7532
+ });
7533
+ authLogger.service.info(`\u23F1\uFE0F Session TTL: ${options.sessionTtl}`);
7534
+ }
7535
+ const allRoles = [
7536
+ ...Object.values(BUILTIN_ROLES),
7537
+ ...options.roles || []
7538
+ ];
7539
+ const allPermissions = [
7540
+ ...Object.values(BUILTIN_PERMISSIONS),
7541
+ ...options.permissions || []
7542
+ ];
7543
+ const allMappings = collectMappings(options);
7544
+ const configHash = computeConfigHash(allRoles, allPermissions, allMappings);
7545
+ const storedHash = await authMetadataRepository.get(RBAC_HASH_KEY);
7546
+ if (storedHash === configHash) {
7547
+ authLogger.service.info("\u2705 RBAC config unchanged, skipping initialization");
7548
+ return;
7577
7549
  }
7550
+ authLogger.service.info("\u{1F504} RBAC config changed, applying updates...");
7551
+ const existingRoles = await rolesRepository.findAll();
7552
+ const existingPermissions = await permissionsRepository.findAll();
7553
+ const rolesByName = new Map(existingRoles.map((r) => [r.name, r]));
7554
+ const permsByName = new Map(existingPermissions.map((p) => [p.name, p]));
7555
+ await syncRoles(allRoles, rolesByName);
7556
+ await syncPermissions(allPermissions, permsByName);
7557
+ const updatedRoles = await rolesRepository.findAll();
7558
+ const updatedPermissions = await permissionsRepository.findAll();
7559
+ const updatedRolesByName = new Map(updatedRoles.map((r) => [r.name, r]));
7560
+ const updatedPermsByName = new Map(updatedPermissions.map((p) => [p.name, p]));
7561
+ await syncMappings(allMappings, updatedRolesByName, updatedPermsByName);
7562
+ await authMetadataRepository.set(RBAC_HASH_KEY, configHash);
7578
7563
  authLogger.service.info("\u2705 RBAC initialization complete");
7579
7564
  authLogger.service.info(`\u{1F4CA} Roles: ${allRoles.length}, Permissions: ${allPermissions.length}`);
7580
7565
  authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
7581
7566
  }
7582
- async function upsertRole(config) {
7583
- const existing = await rolesRepository.findByName(config.name);
7584
- if (!existing) {
7585
- await rolesRepository.create({
7586
- name: config.name,
7587
- displayName: config.displayName,
7588
- description: config.description || null,
7589
- priority: config.priority ?? 10,
7590
- isSystem: config.isSystem ?? false,
7591
- isBuiltin: config.isBuiltin ?? false,
7592
- isActive: true
7593
- });
7594
- authLogger.service.info(` \u2705 Created role: ${config.name}`);
7595
- } else {
7596
- const updateData = {
7597
- displayName: config.displayName,
7598
- description: config.description || null
7599
- };
7600
- if (!existing.isBuiltin) {
7601
- updateData.priority = config.priority ?? existing.priority;
7567
+ async function syncRoles(configs, existingByName) {
7568
+ for (const config of configs) {
7569
+ const existing = existingByName.get(config.name);
7570
+ if (!existing) {
7571
+ await rolesRepository.create({
7572
+ name: config.name,
7573
+ displayName: config.displayName,
7574
+ description: config.description || null,
7575
+ priority: config.priority ?? 10,
7576
+ isSystem: config.isSystem ?? false,
7577
+ isBuiltin: config.isBuiltin ?? false,
7578
+ isActive: true
7579
+ });
7580
+ authLogger.service.info(` \u2705 Created role: ${config.name}`);
7581
+ } else {
7582
+ const updateData = {
7583
+ displayName: config.displayName,
7584
+ description: config.description || null
7585
+ };
7586
+ if (!existing.isBuiltin) {
7587
+ updateData.priority = config.priority ?? existing.priority;
7588
+ }
7589
+ await rolesRepository.updateById(existing.id, updateData);
7602
7590
  }
7603
- await rolesRepository.updateById(existing.id, updateData);
7604
- }
7605
- }
7606
- async function upsertPermission(config) {
7607
- const existing = await permissionsRepository.findByName(config.name);
7608
- if (!existing) {
7609
- await permissionsRepository.create({
7610
- name: config.name,
7611
- displayName: config.displayName,
7612
- description: config.description || null,
7613
- category: config.category || null,
7614
- isSystem: config.isSystem ?? false,
7615
- isBuiltin: config.isBuiltin ?? false,
7616
- isActive: true,
7617
- metadata: null
7618
- });
7619
- authLogger.service.info(` \u2705 Created permission: ${config.name}`);
7620
- } else {
7621
- await permissionsRepository.updateById(existing.id, {
7622
- displayName: config.displayName,
7623
- description: config.description || null,
7624
- category: config.category || null
7625
- });
7626
7591
  }
7627
7592
  }
7628
- async function assignPermissionsToRole(roleName, permissionNames) {
7629
- const role = await rolesRepository.findByName(roleName);
7630
- if (!role) {
7631
- authLogger.service.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
7632
- return;
7633
- }
7634
- const perms = await permissionsRepository.findByNames(permissionNames);
7635
- if (perms.length === 0) {
7636
- authLogger.service.warn(` \u26A0\uFE0F No permissions found for role: ${roleName}`);
7637
- return;
7593
+ async function syncPermissions(configs, existingByName) {
7594
+ for (const config of configs) {
7595
+ const existing = existingByName.get(config.name);
7596
+ if (!existing) {
7597
+ await permissionsRepository.create({
7598
+ name: config.name,
7599
+ displayName: config.displayName,
7600
+ description: config.description || null,
7601
+ category: config.category || null,
7602
+ isSystem: config.isSystem ?? false,
7603
+ isBuiltin: config.isBuiltin ?? false,
7604
+ isActive: true,
7605
+ metadata: null
7606
+ });
7607
+ authLogger.service.info(` \u2705 Created permission: ${config.name}`);
7608
+ } else {
7609
+ await permissionsRepository.updateById(existing.id, {
7610
+ displayName: config.displayName,
7611
+ description: config.description || null,
7612
+ category: config.category || null
7613
+ });
7614
+ }
7638
7615
  }
7639
- const existingMappings = await rolePermissionsRepository.findByRoleId(role.id);
7640
- const existingPermIds = new Set(existingMappings.map((m) => m.permissionId));
7641
- const newMappings = perms.filter((perm) => !existingPermIds.has(perm.id)).map((perm) => ({
7642
- roleId: role.id,
7643
- permissionId: perm.id
7644
- }));
7645
- if (newMappings.length > 0) {
7646
- await rolePermissionsRepository.createMany(newMappings);
7616
+ }
7617
+ async function syncMappings(allMappings, rolesByName, permsByName) {
7618
+ for (const [roleName, permNames] of Object.entries(allMappings)) {
7619
+ const role = rolesByName.get(roleName);
7620
+ if (!role) {
7621
+ authLogger.service.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
7622
+ continue;
7623
+ }
7624
+ const existingMappings = await rolePermissionsRepository.findByRoleId(role.id);
7625
+ const existingPermIds = new Set(existingMappings.map((m) => m.permissionId));
7626
+ const newMappings = permNames.map((name) => permsByName.get(name)).filter((perm) => perm != null).filter((perm) => !existingPermIds.has(perm.id)).map((perm) => ({
7627
+ roleId: role.id,
7628
+ permissionId: perm.id
7629
+ }));
7630
+ if (newMappings.length > 0) {
7631
+ await rolePermissionsRepository.createMany(newMappings);
7632
+ }
7647
7633
  }
7648
7634
  }
7649
7635
 
@@ -7729,7 +7715,7 @@ function calculateExpiresAt(days = 7) {
7729
7715
  return expiresAt;
7730
7716
  }
7731
7717
  async function createInvitation(params) {
7732
- const { email, roleId, invitedBy, expiresInDays = 7, metadata } = params;
7718
+ const { email, roleId, invitedBy, expiresInDays = 7, expiresAt: expiresAtParam, metadata } = params;
7733
7719
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7734
7720
  if (!emailRegex.test(email)) {
7735
7721
  throw new Error("Invalid email format");
@@ -7751,7 +7737,7 @@ async function createInvitation(params) {
7751
7737
  throw new Error(`User with id ${invitedBy} not found`);
7752
7738
  }
7753
7739
  const token = generateInvitationToken();
7754
- const expiresAt = calculateExpiresAt(expiresInDays);
7740
+ const expiresAt = expiresAtParam ?? calculateExpiresAt(expiresInDays);
7755
7741
  const invitation = await invitationsRepository.create({
7756
7742
  email,
7757
7743
  token,
@@ -7761,7 +7747,16 @@ async function createInvitation(params) {
7761
7747
  expiresAt,
7762
7748
  metadata: metadata || null
7763
7749
  });
7764
- console.log(`[Auth] \u2705 Created invitation: ${email} as ${role.name} (expires: ${expiresAt.toISOString()})`);
7750
+ await invitationCreatedEvent.emit({
7751
+ invitationId: String(invitation.id),
7752
+ email,
7753
+ token,
7754
+ roleId,
7755
+ invitedBy: String(invitedBy),
7756
+ expiresAt: expiresAt.toISOString(),
7757
+ isResend: false,
7758
+ metadata
7759
+ });
7765
7760
  return invitation;
7766
7761
  }
7767
7762
  async function getInvitationByToken(token) {
@@ -7825,7 +7820,14 @@ async function acceptInvitation(params) {
7825
7820
  "accepted",
7826
7821
  /* @__PURE__ */ new Date()
7827
7822
  );
7828
- console.log(`[Auth] \u2705 Invitation accepted: ${invitation.email} as ${role.name}`);
7823
+ await invitationAcceptedEvent.emit({
7824
+ invitationId: String(invitation.id),
7825
+ email: invitation.email,
7826
+ userId: String(newUser.id),
7827
+ roleId: Number(invitation.roleId),
7828
+ invitedBy: String(invitation.invitedBy),
7829
+ metadata: invitation.metadata
7830
+ });
7829
7831
  return {
7830
7832
  userId: newUser.id,
7831
7833
  email: newUser.email,
@@ -7870,7 +7872,16 @@ async function resendInvitation(id11, expiresInDays = 7) {
7870
7872
  if (!updated) {
7871
7873
  throw new Error("Failed to update invitation");
7872
7874
  }
7873
- console.log(`[Auth] \u{1F4E7} Invitation resent: ${invitation.email} (new expiry: ${newExpiresAt.toISOString()})`);
7875
+ await invitationCreatedEvent.emit({
7876
+ invitationId: String(invitation.id),
7877
+ email: invitation.email,
7878
+ token: invitation.token,
7879
+ roleId: Number(invitation.roleId),
7880
+ invitedBy: String(invitation.invitedBy),
7881
+ expiresAt: newExpiresAt.toISOString(),
7882
+ isResend: true,
7883
+ metadata: invitation.metadata
7884
+ });
7874
7885
  return updated;
7875
7886
  }
7876
7887
 
@@ -7884,13 +7895,48 @@ async function getAuthSessionService(userId) {
7884
7895
  ]);
7885
7896
  return {
7886
7897
  userId: user.userId,
7898
+ publicId: user.publicId,
7887
7899
  email: user.email,
7888
7900
  emailVerified: user.isEmailVerified,
7889
7901
  phoneVerified: user.isPhoneVerified,
7902
+ hasPassword: user.hasPassword,
7890
7903
  ...roleAndPerms
7891
7904
  };
7892
7905
  }
7893
7906
 
7907
+ // src/server/lib/one-time-token.ts
7908
+ import { SSETokenManager } from "@spfn/core/event/sse";
7909
+ var manager = null;
7910
+ function initOneTimeTokenManager(config) {
7911
+ if (manager) {
7912
+ manager.destroy();
7913
+ }
7914
+ manager = new SSETokenManager({
7915
+ ttl: config?.ttl,
7916
+ store: config?.store
7917
+ });
7918
+ }
7919
+ function getOneTimeTokenManager() {
7920
+ if (!manager) {
7921
+ throw new Error(
7922
+ "OneTimeTokenManager not initialized. Ensure createAuthLifecycle() is configured in your server config."
7923
+ );
7924
+ }
7925
+ return manager;
7926
+ }
7927
+
7928
+ // src/server/services/one-time-token.service.ts
7929
+ async function issueOneTimeTokenService(userId) {
7930
+ const manager2 = getOneTimeTokenManager();
7931
+ const token = await manager2.issue(userId);
7932
+ const expiresAt = new Date(Date.now() + 3e4).toISOString();
7933
+ return { token, expiresAt };
7934
+ }
7935
+ async function verifyOneTimeTokenService(token) {
7936
+ const manager2 = getOneTimeTokenManager();
7937
+ return await manager2.verify(token);
7938
+ }
7939
+
7894
7940
  // src/server/services/user-profile.service.ts
7895
7941
  init_repositories();
7896
7942
  async function getUserProfileService(userId) {
@@ -7901,7 +7947,9 @@ async function getUserProfileService(userId) {
7901
7947
  ]);
7902
7948
  return {
7903
7949
  userId: user.userId,
7950
+ publicId: user.publicId,
7904
7951
  email: user.email,
7952
+ username: user.username,
7905
7953
  emailVerified: user.isEmailVerified,
7906
7954
  phoneVerified: user.isPhoneVerified,
7907
7955
  lastLoginAt: user.lastLoginAt,
@@ -7910,6 +7958,12 @@ async function getUserProfileService(userId) {
7910
7958
  profile
7911
7959
  };
7912
7960
  }
7961
+ async function updateLocaleService(userId, locale) {
7962
+ const userIdNum = Number(userId);
7963
+ const normalized = locale.trim() || "en";
7964
+ await userProfilesRepository.upsertByUserId(userIdNum, { locale: normalized });
7965
+ return { locale: normalized };
7966
+ }
7913
7967
  function emptyToNull(value) {
7914
7968
  if (value === "") {
7915
7969
  return null;
@@ -7920,7 +7974,7 @@ async function updateUserProfileService(userId, params) {
7920
7974
  const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7921
7975
  const updateData = {};
7922
7976
  if (params.displayName !== void 0) {
7923
- updateData.displayName = emptyToNull(params.displayName) || "User";
7977
+ updateData.displayName = emptyToNull(params.displayName);
7924
7978
  }
7925
7979
  if (params.firstName !== void 0) {
7926
7980
  updateData.firstName = emptyToNull(params.firstName);
@@ -7961,15 +8015,355 @@ async function updateUserProfileService(userId, params) {
7961
8015
  if (params.metadata !== void 0) {
7962
8016
  updateData.metadata = params.metadata;
7963
8017
  }
7964
- const existing = await userProfilesRepository.findByUserId(userIdNum);
7965
- if (!existing && !updateData.displayName) {
7966
- updateData.displayName = "User";
7967
- }
7968
8018
  await userProfilesRepository.upsertByUserId(userIdNum, updateData);
7969
8019
  const profile = await userProfilesRepository.fetchProfileData(userIdNum);
7970
8020
  return profile;
7971
8021
  }
7972
8022
 
8023
+ // src/server/services/oauth.service.ts
8024
+ init_repositories();
8025
+ import { env as env8 } from "@spfn/auth/config";
8026
+ import { ValidationError as ValidationError3 } from "@spfn/core/errors";
8027
+
8028
+ // src/server/lib/oauth/google.ts
8029
+ import { env as env6 } from "@spfn/auth/config";
8030
+ var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
8031
+ var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
8032
+ var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
8033
+ function isGoogleOAuthEnabled() {
8034
+ return !!(env6.SPFN_AUTH_GOOGLE_CLIENT_ID && env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
8035
+ }
8036
+ function getGoogleOAuthConfig() {
8037
+ const clientId = env6.SPFN_AUTH_GOOGLE_CLIENT_ID;
8038
+ const clientSecret = env6.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
8039
+ if (!clientId || !clientSecret) {
8040
+ throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
8041
+ }
8042
+ const baseUrl = env6.NEXT_PUBLIC_SPFN_API_URL || env6.SPFN_API_URL;
8043
+ const redirectUri = env6.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
8044
+ return {
8045
+ clientId,
8046
+ clientSecret,
8047
+ redirectUri
8048
+ };
8049
+ }
8050
+ function getDefaultScopes() {
8051
+ const envScopes = env6.SPFN_AUTH_GOOGLE_SCOPES;
8052
+ if (envScopes) {
8053
+ return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
8054
+ }
8055
+ return ["email", "profile"];
8056
+ }
8057
+ function getGoogleAuthUrl(state, scopes) {
8058
+ const resolvedScopes = scopes ?? getDefaultScopes();
8059
+ const config = getGoogleOAuthConfig();
8060
+ const params = new URLSearchParams({
8061
+ client_id: config.clientId,
8062
+ redirect_uri: config.redirectUri,
8063
+ response_type: "code",
8064
+ scope: resolvedScopes.join(" "),
8065
+ state,
8066
+ access_type: "offline",
8067
+ // refresh_token 받기 위해
8068
+ prompt: "consent"
8069
+ // 매번 동의 화면 표시 (refresh_token 보장)
8070
+ });
8071
+ return `${GOOGLE_AUTH_URL}?${params.toString()}`;
8072
+ }
8073
+ async function exchangeCodeForTokens(code) {
8074
+ const config = getGoogleOAuthConfig();
8075
+ const response = await fetch(GOOGLE_TOKEN_URL, {
8076
+ method: "POST",
8077
+ headers: {
8078
+ "Content-Type": "application/x-www-form-urlencoded"
8079
+ },
8080
+ body: new URLSearchParams({
8081
+ client_id: config.clientId,
8082
+ client_secret: config.clientSecret,
8083
+ redirect_uri: config.redirectUri,
8084
+ grant_type: "authorization_code",
8085
+ code
8086
+ })
8087
+ });
8088
+ if (!response.ok) {
8089
+ const error = await response.text();
8090
+ throw new Error(`Failed to exchange code for tokens: ${error}`);
8091
+ }
8092
+ return response.json();
8093
+ }
8094
+ async function getGoogleUserInfo(accessToken) {
8095
+ const response = await fetch(GOOGLE_USERINFO_URL, {
8096
+ headers: {
8097
+ Authorization: `Bearer ${accessToken}`
8098
+ }
8099
+ });
8100
+ if (!response.ok) {
8101
+ const error = await response.text();
8102
+ throw new Error(`Failed to get user info: ${error}`);
8103
+ }
8104
+ return response.json();
8105
+ }
8106
+ async function refreshAccessToken(refreshToken) {
8107
+ const config = getGoogleOAuthConfig();
8108
+ const response = await fetch(GOOGLE_TOKEN_URL, {
8109
+ method: "POST",
8110
+ headers: {
8111
+ "Content-Type": "application/x-www-form-urlencoded"
8112
+ },
8113
+ body: new URLSearchParams({
8114
+ client_id: config.clientId,
8115
+ client_secret: config.clientSecret,
8116
+ refresh_token: refreshToken,
8117
+ grant_type: "refresh_token"
8118
+ })
8119
+ });
8120
+ if (!response.ok) {
8121
+ const error = await response.text();
8122
+ throw new Error(`Failed to refresh access token: ${error}`);
8123
+ }
8124
+ return response.json();
8125
+ }
8126
+
8127
+ // src/server/lib/oauth/state.ts
8128
+ import * as jose from "jose";
8129
+ import { env as env7 } from "@spfn/auth/config";
8130
+ async function getStateKey() {
8131
+ const secret = env7.SPFN_AUTH_SESSION_SECRET;
8132
+ const encoder = new TextEncoder();
8133
+ const data = encoder.encode(`oauth-state:${secret}`);
8134
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
8135
+ return new Uint8Array(hashBuffer);
8136
+ }
8137
+ function generateNonce() {
8138
+ const array = new Uint8Array(16);
8139
+ crypto.getRandomValues(array);
8140
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
8141
+ }
8142
+ async function createOAuthState(params) {
8143
+ const key = await getStateKey();
8144
+ const state = {
8145
+ returnUrl: params.returnUrl,
8146
+ nonce: generateNonce(),
8147
+ provider: params.provider,
8148
+ publicKey: params.publicKey,
8149
+ keyId: params.keyId,
8150
+ fingerprint: params.fingerprint,
8151
+ algorithm: params.algorithm,
8152
+ metadata: params.metadata
8153
+ };
8154
+ const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
8155
+ return encodeURIComponent(jwe);
8156
+ }
8157
+ async function verifyOAuthState(encryptedState) {
8158
+ const key = await getStateKey();
8159
+ const jwe = decodeURIComponent(encryptedState);
8160
+ const { payload } = await jose.jwtDecrypt(jwe, key);
8161
+ return payload.state;
8162
+ }
8163
+
8164
+ // src/server/services/oauth.service.ts
8165
+ async function oauthStartService(params) {
8166
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
8167
+ if (provider === "google") {
8168
+ if (!isGoogleOAuthEnabled()) {
8169
+ throw new ValidationError3({
8170
+ message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
8171
+ });
8172
+ }
8173
+ const state = await createOAuthState({
8174
+ provider: "google",
8175
+ returnUrl,
8176
+ publicKey,
8177
+ keyId,
8178
+ fingerprint,
8179
+ algorithm,
8180
+ metadata
8181
+ });
8182
+ const authUrl = getGoogleAuthUrl(state);
8183
+ return { authUrl };
8184
+ }
8185
+ throw new ValidationError3({
8186
+ message: `Unsupported OAuth provider: ${provider}`
8187
+ });
8188
+ }
8189
+ async function oauthCallbackService(params) {
8190
+ const { provider, code, state } = params;
8191
+ const stateData = await verifyOAuthState(state);
8192
+ if (stateData.provider !== provider) {
8193
+ throw new ValidationError3({
8194
+ message: "OAuth state provider mismatch"
8195
+ });
8196
+ }
8197
+ if (provider === "google") {
8198
+ return handleGoogleCallback(code, stateData);
8199
+ }
8200
+ throw new ValidationError3({
8201
+ message: `Unsupported OAuth provider: ${provider}`
8202
+ });
8203
+ }
8204
+ async function handleGoogleCallback(code, stateData) {
8205
+ const tokens = await exchangeCodeForTokens(code);
8206
+ const googleUser = await getGoogleUserInfo(tokens.access_token);
8207
+ const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
8208
+ "google",
8209
+ googleUser.id
8210
+ );
8211
+ let userId;
8212
+ let isNewUser = false;
8213
+ if (existingSocialAccount) {
8214
+ userId = existingSocialAccount.userId;
8215
+ await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
8216
+ accessToken: tokens.access_token,
8217
+ refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
8218
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8219
+ });
8220
+ } else {
8221
+ const result = await createOrLinkUser(googleUser, tokens);
8222
+ userId = result.userId;
8223
+ isNewUser = result.isNewUser;
8224
+ }
8225
+ await registerPublicKeyService({
8226
+ userId,
8227
+ keyId: stateData.keyId,
8228
+ publicKey: stateData.publicKey,
8229
+ fingerprint: stateData.fingerprint,
8230
+ algorithm: stateData.algorithm
8231
+ });
8232
+ await updateLastLoginService(userId);
8233
+ const appUrl = env8.NEXT_PUBLIC_SPFN_APP_URL || env8.SPFN_APP_URL;
8234
+ const callbackPath = env8.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
8235
+ const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
8236
+ const redirectUrl = buildRedirectUrl(callbackUrl, {
8237
+ userId: String(userId),
8238
+ keyId: stateData.keyId,
8239
+ returnUrl: stateData.returnUrl,
8240
+ isNewUser: String(isNewUser)
8241
+ });
8242
+ const user = await usersRepository.findById(userId);
8243
+ const eventPayload = {
8244
+ userId: String(userId),
8245
+ provider: "google",
8246
+ email: user?.email || void 0,
8247
+ phone: user?.phone || void 0,
8248
+ metadata: stateData.metadata
8249
+ };
8250
+ if (isNewUser) {
8251
+ await authRegisterEvent.emit(eventPayload);
8252
+ } else {
8253
+ await authLoginEvent.emit(eventPayload);
8254
+ }
8255
+ return {
8256
+ redirectUrl,
8257
+ userId: String(userId),
8258
+ keyId: stateData.keyId,
8259
+ isNewUser
8260
+ };
8261
+ }
8262
+ async function createOrLinkUser(googleUser, tokens) {
8263
+ const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
8264
+ let userId;
8265
+ let isNewUser = false;
8266
+ if (existingUser) {
8267
+ if (!googleUser.verified_email) {
8268
+ throw new ValidationError3({
8269
+ message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
8270
+ });
8271
+ }
8272
+ userId = existingUser.id;
8273
+ if (!existingUser.emailVerifiedAt) {
8274
+ await usersRepository.updateById(existingUser.id, {
8275
+ emailVerifiedAt: /* @__PURE__ */ new Date()
8276
+ });
8277
+ }
8278
+ } else {
8279
+ const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
8280
+ const userRole = await getRoleByName3("user");
8281
+ if (!userRole) {
8282
+ throw new Error("Default user role not found. Run initializeAuth() first.");
8283
+ }
8284
+ const newUser = await usersRepository.create({
8285
+ email: googleUser.verified_email ? googleUser.email : null,
8286
+ phone: null,
8287
+ passwordHash: null,
8288
+ // OAuth 사용자는 비밀번호 없음
8289
+ passwordChangeRequired: false,
8290
+ roleId: userRole.id,
8291
+ status: "active",
8292
+ emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
8293
+ });
8294
+ userId = newUser.id;
8295
+ isNewUser = true;
8296
+ }
8297
+ await socialAccountsRepository.create({
8298
+ userId,
8299
+ provider: "google",
8300
+ providerUserId: googleUser.id,
8301
+ providerEmail: googleUser.email,
8302
+ accessToken: tokens.access_token,
8303
+ refreshToken: tokens.refresh_token ?? null,
8304
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8305
+ });
8306
+ return { userId, isNewUser };
8307
+ }
8308
+ function buildRedirectUrl(baseUrl, params) {
8309
+ const url = new URL(baseUrl, "http://placeholder");
8310
+ for (const [key, value] of Object.entries(params)) {
8311
+ url.searchParams.set(key, value);
8312
+ }
8313
+ if (baseUrl.startsWith("http")) {
8314
+ return url.toString();
8315
+ }
8316
+ return `${url.pathname}${url.search}`;
8317
+ }
8318
+ function buildOAuthErrorUrl(error) {
8319
+ const errorUrl = env8.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
8320
+ return errorUrl.replace("{error}", encodeURIComponent(error));
8321
+ }
8322
+ function isOAuthProviderEnabled(provider) {
8323
+ switch (provider) {
8324
+ case "google":
8325
+ return isGoogleOAuthEnabled();
8326
+ case "github":
8327
+ case "kakao":
8328
+ case "naver":
8329
+ return false;
8330
+ default:
8331
+ return false;
8332
+ }
8333
+ }
8334
+ function getEnabledOAuthProviders() {
8335
+ const providers = [];
8336
+ if (isGoogleOAuthEnabled()) {
8337
+ providers.push("google");
8338
+ }
8339
+ return providers;
8340
+ }
8341
+ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
8342
+ async function getGoogleAccessToken(userId) {
8343
+ const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
8344
+ if (!account) {
8345
+ throw new ValidationError3({
8346
+ message: "No Google account linked. User must sign in with Google first."
8347
+ });
8348
+ }
8349
+ const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
8350
+ if (!isExpired && account.accessToken) {
8351
+ return account.accessToken;
8352
+ }
8353
+ if (!account.refreshToken) {
8354
+ throw new ValidationError3({
8355
+ message: "Google refresh token not available. User must re-authenticate with Google."
8356
+ });
8357
+ }
8358
+ const tokens = await refreshAccessToken(account.refreshToken);
8359
+ await socialAccountsRepository.updateTokens(account.id, {
8360
+ accessToken: tokens.access_token,
8361
+ refreshToken: tokens.refresh_token ?? account.refreshToken,
8362
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8363
+ });
8364
+ return tokens.access_token;
8365
+ }
8366
+
7973
8367
  // src/server/routes/auth/index.ts
7974
8368
  init_esm();
7975
8369
  import { Transactional } from "@spfn/core/db";
@@ -8020,7 +8414,10 @@ var register = route.post("/_auth/register").input({
8020
8414
  verificationToken: Type.String({
8021
8415
  description: "Verification token obtained from /verify-code endpoint"
8022
8416
  }),
8023
- password: PasswordSchema
8417
+ password: PasswordSchema,
8418
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
8419
+ description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
8420
+ }))
8024
8421
  }, {
8025
8422
  minProperties: 3,
8026
8423
  // email/phone + verificationToken + password
@@ -8062,9 +8459,7 @@ var login = route.post("/_auth/login").input({
8062
8459
  const { body } = await c.data();
8063
8460
  return await loginService(body);
8064
8461
  });
8065
- var logout = route.post("/_auth/logout").input({
8066
- body: Type.Object({})
8067
- }).handler(async (c) => {
8462
+ var logout = route.post("/_auth/logout").handler(async (c) => {
8068
8463
  const auth = getAuth(c);
8069
8464
  if (!auth) {
8070
8465
  return c.noContent();
@@ -8073,9 +8468,7 @@ var logout = route.post("/_auth/logout").input({
8073
8468
  await logoutService({ userId: Number(userId), keyId });
8074
8469
  return c.noContent();
8075
8470
  });
8076
- var rotateKey = route.post("/_auth/keys/rotate").input({
8077
- body: Type.Object({})
8078
- }).interceptor({
8471
+ var rotateKey = route.post("/_auth/keys/rotate").interceptor({
8079
8472
  body: Type.Object({
8080
8473
  publicKey: Type.String({ description: "New public key" }),
8081
8474
  keyId: Type.String({ description: "New key identifier" }),
@@ -8096,10 +8489,10 @@ var rotateKey = route.post("/_auth/keys/rotate").input({
8096
8489
  });
8097
8490
  var changePassword = route.put("/_auth/password").input({
8098
8491
  body: Type.Object({
8099
- currentPassword: Type.String({
8492
+ currentPassword: Type.Optional(Type.String({
8100
8493
  minLength: 1,
8101
- description: "Current password for verification"
8102
- }),
8494
+ description: "Current password for verification (required when changing existing password)"
8495
+ })),
8103
8496
  newPassword: PasswordSchema
8104
8497
  })
8105
8498
  }).handler(async (c) => {
@@ -8117,6 +8510,10 @@ var getAuthSession = route.get("/_auth/session").handler(async (c) => {
8117
8510
  const { userId } = getAuth(c);
8118
8511
  return await getAuthSessionService(userId);
8119
8512
  });
8513
+ var issueOneTimeToken = route.post("/_auth/tokens").handler(async (c) => {
8514
+ const { userId } = getAuth(c);
8515
+ return await issueOneTimeTokenService(userId);
8516
+ });
8120
8517
  var authRouter = defineRouter({
8121
8518
  checkAccountExists,
8122
8519
  sendVerificationCode,
@@ -8126,7 +8523,8 @@ var authRouter = defineRouter({
8126
8523
  logout,
8127
8524
  rotateKey,
8128
8525
  changePassword,
8129
- getAuthSession
8526
+ getAuthSession,
8527
+ issueOneTimeToken
8130
8528
  });
8131
8529
 
8132
8530
  // src/server/routes/invitations/index.ts
@@ -8135,7 +8533,7 @@ import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
8135
8533
  // src/server/middleware/authenticate.ts
8136
8534
  import { defineMiddleware } from "@spfn/core/route";
8137
8535
  import { UnauthorizedError } from "@spfn/core/errors";
8138
- import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2 } from "@spfn/auth/server";
8536
+ import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2, userProfilesRepository as userProfilesRepository2 } from "@spfn/auth/server";
8139
8537
  import {
8140
8538
  InvalidTokenError,
8141
8539
  TokenExpiredError,
@@ -8182,10 +8580,14 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
8182
8580
  }
8183
8581
  throw new UnauthorizedError({ message: "Authentication failed" });
8184
8582
  }
8185
- const user = await usersRepository2.findById(keyRecord.userId);
8186
- if (!user) {
8583
+ const [result, locale] = await Promise.all([
8584
+ usersRepository2.findByIdWithRole(keyRecord.userId),
8585
+ userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
8586
+ ]);
8587
+ if (!result) {
8187
8588
  throw new UnauthorizedError({ message: "User not found" });
8188
8589
  }
8590
+ const { user, role } = result;
8189
8591
  if (user.status !== "active") {
8190
8592
  throw new AccountDisabledError2({ status: user.status });
8191
8593
  }
@@ -8193,7 +8595,9 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
8193
8595
  c.set("auth", {
8194
8596
  user,
8195
8597
  userId: String(user.id),
8196
- keyId
8598
+ keyId,
8599
+ role: role?.name ?? null,
8600
+ locale
8197
8601
  });
8198
8602
  const method = c.req.method;
8199
8603
  const path = c.req.path;
@@ -8208,6 +8612,55 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
8208
8612
  });
8209
8613
  await next();
8210
8614
  });
8615
+ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
8616
+ const authHeader = c.req.header("Authorization");
8617
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
8618
+ await next();
8619
+ return;
8620
+ }
8621
+ const token = authHeader.substring(7);
8622
+ try {
8623
+ const decoded = decodeToken2(token);
8624
+ if (!decoded || !decoded.keyId) {
8625
+ await next();
8626
+ return;
8627
+ }
8628
+ const keyId = decoded.keyId;
8629
+ const keyRecord = await keysRepository2.findActiveByKeyId(keyId);
8630
+ if (!keyRecord) {
8631
+ await next();
8632
+ return;
8633
+ }
8634
+ if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
8635
+ await next();
8636
+ return;
8637
+ }
8638
+ verifyClientToken2(
8639
+ token,
8640
+ keyRecord.publicKey,
8641
+ keyRecord.algorithm
8642
+ );
8643
+ const [result, locale] = await Promise.all([
8644
+ usersRepository2.findByIdWithRole(keyRecord.userId),
8645
+ userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
8646
+ ]);
8647
+ if (!result || result.user.status !== "active") {
8648
+ await next();
8649
+ return;
8650
+ }
8651
+ const { user, role } = result;
8652
+ keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
8653
+ c.set("auth", {
8654
+ user,
8655
+ userId: String(user.id),
8656
+ keyId,
8657
+ role: role?.name ?? null,
8658
+ locale
8659
+ });
8660
+ } catch {
8661
+ }
8662
+ await next();
8663
+ }, { skips: ["auth"] });
8211
8664
 
8212
8665
  // src/server/middleware/require-permission.ts
8213
8666
  import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
@@ -8273,7 +8726,7 @@ var requireAnyPermission = defineMiddleware2(
8273
8726
 
8274
8727
  // src/server/middleware/require-role.ts
8275
8728
  import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
8276
- import { getAuth as getAuth3, hasAnyRole as hasAnyRole2, authLogger as authLogger4 } from "@spfn/auth/server";
8729
+ import { getAuth as getAuth3, authLogger as authLogger4 } from "@spfn/auth/server";
8277
8730
  import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
8278
8731
  import { InsufficientRoleError } from "@spfn/auth/errors";
8279
8732
  var requireRole = defineMiddleware3(
@@ -8287,11 +8740,11 @@ var requireRole = defineMiddleware3(
8287
8740
  });
8288
8741
  throw new ForbiddenError2({ message: "Authentication required" });
8289
8742
  }
8290
- const { userId } = auth;
8291
- const allowed = await hasAnyRole2(userId, roleNames);
8292
- if (!allowed) {
8743
+ const { userId, role: userRole } = auth;
8744
+ if (!userRole || !roleNames.includes(userRole)) {
8293
8745
  authLogger4.middleware.warn("Role check failed", {
8294
8746
  userId,
8747
+ userRole,
8295
8748
  requiredRoles: roleNames,
8296
8749
  path: c.req.path
8297
8750
  });
@@ -8299,6 +8752,7 @@ var requireRole = defineMiddleware3(
8299
8752
  }
8300
8753
  authLogger4.middleware.debug("Role check passed", {
8301
8754
  userId,
8755
+ userRole,
8302
8756
  roles: roleNames
8303
8757
  });
8304
8758
  await next();
@@ -8307,7 +8761,7 @@ var requireRole = defineMiddleware3(
8307
8761
 
8308
8762
  // src/server/middleware/role-guard.ts
8309
8763
  import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
8310
- import { getAuth as getAuth4, getUserRole as getUserRole2, authLogger as authLogger5 } from "@spfn/auth/server";
8764
+ import { getAuth as getAuth4, authLogger as authLogger5 } from "@spfn/auth/server";
8311
8765
  import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
8312
8766
  import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
8313
8767
  var roleGuard = defineMiddleware4(
@@ -8324,8 +8778,7 @@ var roleGuard = defineMiddleware4(
8324
8778
  });
8325
8779
  throw new ForbiddenError3({ message: "Authentication required" });
8326
8780
  }
8327
- const { userId } = auth;
8328
- const userRole = await getUserRole2(userId);
8781
+ const { userId, role: userRole } = auth;
8329
8782
  if (deny && deny.length > 0) {
8330
8783
  if (userRole && deny.includes(userRole)) {
8331
8784
  authLogger5.middleware.warn("Role guard denied", {
@@ -8358,6 +8811,47 @@ var roleGuard = defineMiddleware4(
8358
8811
  }
8359
8812
  );
8360
8813
 
8814
+ // src/server/middleware/one-time-token-auth.ts
8815
+ import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
8816
+ import { UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
8817
+ import { usersRepository as usersRepository3, userProfilesRepository as userProfilesRepository3 } from "@spfn/auth/server";
8818
+ var oneTimeTokenAuth = defineMiddleware5("oneTimeTokenAuth", async (c, next) => {
8819
+ const token = c.req.query("token") ?? extractOTTHeader(c.req.header("Authorization"));
8820
+ if (!token) {
8821
+ throw new UnauthorizedError2({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
8822
+ }
8823
+ const userId = await verifyOneTimeTokenService(token);
8824
+ if (!userId) {
8825
+ throw new UnauthorizedError2({ message: "Invalid or expired one-time token" });
8826
+ }
8827
+ const [result, locale] = await Promise.all([
8828
+ usersRepository3.findByIdWithRole(Number(userId)),
8829
+ userProfilesRepository3.findLocaleByUserId(Number(userId))
8830
+ ]);
8831
+ if (!result) {
8832
+ throw new UnauthorizedError2({ message: "User not found" });
8833
+ }
8834
+ const { user, role } = result;
8835
+ if (user.status !== "active") {
8836
+ throw new UnauthorizedError2({ message: "Account is not active" });
8837
+ }
8838
+ c.set("auth", {
8839
+ user,
8840
+ userId: String(user.id),
8841
+ keyId: "",
8842
+ // No key involved in OTT auth
8843
+ role: role?.name ?? null,
8844
+ locale
8845
+ });
8846
+ await next();
8847
+ }, { skips: ["auth"] });
8848
+ function extractOTTHeader(header) {
8849
+ if (!header || !header.startsWith("OTT ")) {
8850
+ return null;
8851
+ }
8852
+ return header.substring(4);
8853
+ }
8854
+
8361
8855
  // src/server/routes/invitations/index.ts
8362
8856
  init_types();
8363
8857
  init_esm();
@@ -8433,6 +8927,10 @@ var createInvitation2 = route2.post("/_auth/invitations").input({
8433
8927
  maximum: 30,
8434
8928
  description: "Days until invitation expires (default: 7)"
8435
8929
  })),
8930
+ expiresAt: Type.Optional(Type.String({
8931
+ format: "date-time",
8932
+ description: "Exact expiration timestamp (ISO 8601). Takes precedence over expiresInDays."
8933
+ })),
8436
8934
  metadata: Type.Optional(Type.Any({
8437
8935
  description: "Custom metadata (welcome message, department, etc.)"
8438
8936
  }))
@@ -8445,6 +8943,7 @@ var createInvitation2 = route2.post("/_auth/invitations").input({
8445
8943
  roleId: body.roleId,
8446
8944
  invitedBy: Number(userId),
8447
8945
  expiresInDays: body.expiresInDays,
8946
+ expiresAt: body.expiresAt ? new Date(body.expiresAt) : void 0,
8448
8947
  metadata: body.metadata
8449
8948
  });
8450
8949
  const baseUrl = process.env.SPFN_API_URL || "http://localhost:8790";
@@ -8579,17 +9078,312 @@ var updateUserProfile = route3.patch("/_auth/users/profile").input({
8579
9078
  const { body } = await c.data();
8580
9079
  return await updateUserProfileService(userId, body);
8581
9080
  });
9081
+ var checkUsername = route3.get("/_auth/users/username/check").input({
9082
+ query: Type.Object({
9083
+ username: Type.String({ minLength: 1 })
9084
+ })
9085
+ }).handler(async (c) => {
9086
+ const { query } = await c.data();
9087
+ return { available: await checkUsernameAvailableService(query.username) };
9088
+ });
9089
+ var updateUsername = route3.patch("/_auth/users/username").input({
9090
+ body: Type.Object({
9091
+ username: Type.Union([
9092
+ Type.String({ minLength: 1 }),
9093
+ Type.Null()
9094
+ ], { description: "New username or null to clear" })
9095
+ })
9096
+ }).handler(async (c) => {
9097
+ const { userId } = getAuth(c);
9098
+ const { body } = await c.data();
9099
+ return await updateUsernameService(userId, body.username);
9100
+ });
9101
+ var updateLocale = route3.patch("/_auth/users/locale").input({
9102
+ body: Type.Object({
9103
+ locale: Type.String({ minLength: 1, description: "Locale code (e.g., en, ko, ja)" })
9104
+ })
9105
+ }).handler(async (c) => {
9106
+ const { userId } = getAuth(c);
9107
+ const { body } = await c.data();
9108
+ return await updateLocaleService(userId, body.locale);
9109
+ });
8582
9110
  var userRouter = defineRouter3({
8583
9111
  getUserProfile,
8584
- updateUserProfile
9112
+ updateUserProfile,
9113
+ checkUsername,
9114
+ updateUsername,
9115
+ updateLocale
9116
+ });
9117
+
9118
+ // src/server/routes/oauth/index.ts
9119
+ init_esm();
9120
+ init_types();
9121
+ import { Transactional as Transactional2 } from "@spfn/core/db";
9122
+ import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
9123
+ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
9124
+ query: Type.Object({
9125
+ state: Type.String({
9126
+ description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
9127
+ })
9128
+ })
9129
+ }).skip(["auth"]).handler(async (c) => {
9130
+ const { query } = await c.data();
9131
+ if (!isGoogleOAuthEnabled()) {
9132
+ return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
9133
+ }
9134
+ const authUrl = getGoogleAuthUrl(query.state);
9135
+ return c.redirect(authUrl);
9136
+ });
9137
+ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
9138
+ query: Type.Object({
9139
+ code: Type.Optional(Type.String({
9140
+ description: "Authorization code from Google"
9141
+ })),
9142
+ state: Type.Optional(Type.String({
9143
+ description: "OAuth state parameter"
9144
+ })),
9145
+ error: Type.Optional(Type.String({
9146
+ description: "Error code from Google"
9147
+ })),
9148
+ error_description: Type.Optional(Type.String({
9149
+ description: "Error description from Google"
9150
+ }))
9151
+ })
9152
+ }).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
9153
+ const { query } = await c.data();
9154
+ if (query.error) {
9155
+ const errorMessage = query.error_description || query.error;
9156
+ return c.redirect(buildOAuthErrorUrl(errorMessage));
9157
+ }
9158
+ if (!query.code || !query.state) {
9159
+ return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
9160
+ }
9161
+ try {
9162
+ const result = await oauthCallbackService({
9163
+ provider: "google",
9164
+ code: query.code,
9165
+ state: query.state
9166
+ });
9167
+ return c.redirect(result.redirectUrl);
9168
+ } catch (err) {
9169
+ const message = err instanceof Error ? err.message : "OAuth callback failed";
9170
+ return c.redirect(buildOAuthErrorUrl(message));
9171
+ }
9172
+ });
9173
+ var oauthStart = route4.post("/_auth/oauth/start").input({
9174
+ body: Type.Object({
9175
+ provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
9176
+ description: "OAuth provider (google, github, kakao, naver)"
9177
+ }),
9178
+ returnUrl: Type.String({
9179
+ description: "URL to redirect after OAuth success"
9180
+ }),
9181
+ publicKey: Type.String({
9182
+ description: "Client public key (Base64 DER)"
9183
+ }),
9184
+ keyId: Type.String({
9185
+ description: "Key identifier (UUID)"
9186
+ }),
9187
+ fingerprint: Type.String({
9188
+ description: "Key fingerprint (SHA-256 hex)"
9189
+ }),
9190
+ algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
9191
+ description: "Key algorithm (ES256 or RS256)"
9192
+ }),
9193
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
9194
+ description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
9195
+ }))
9196
+ })
9197
+ }).skip(["auth"]).handler(async (c) => {
9198
+ const { body } = await c.data();
9199
+ const result = await oauthStartService(body);
9200
+ return result;
9201
+ });
9202
+ var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
9203
+ return {
9204
+ providers: getEnabledOAuthProviders()
9205
+ };
9206
+ });
9207
+ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
9208
+ body: Type.Object({
9209
+ returnUrl: Type.Optional(Type.String({
9210
+ description: "URL to redirect after OAuth success"
9211
+ })),
9212
+ state: Type.Optional(Type.String({
9213
+ description: "Encrypted OAuth state (injected by interceptor)"
9214
+ }))
9215
+ })
9216
+ }).skip(["auth"]).handler(async (c) => {
9217
+ const { body } = await c.data();
9218
+ if (!isGoogleOAuthEnabled()) {
9219
+ throw new Error("Google OAuth is not configured");
9220
+ }
9221
+ if (!body.state) {
9222
+ throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
9223
+ }
9224
+ return { authUrl: getGoogleAuthUrl(body.state) };
9225
+ });
9226
+ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
9227
+ body: Type.Object({
9228
+ userId: Type.String({ description: "User ID from OAuth callback" }),
9229
+ keyId: Type.String({ description: "Key ID from OAuth state" }),
9230
+ returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
9231
+ })
9232
+ }).skip(["auth"]).handler(async (c) => {
9233
+ const { body } = await c.data();
9234
+ return {
9235
+ success: true,
9236
+ userId: body.userId,
9237
+ keyId: body.keyId,
9238
+ returnUrl: body.returnUrl || "/"
9239
+ };
9240
+ });
9241
+ var oauthRouter = defineRouter4({
9242
+ oauthGoogleStart,
9243
+ oauthGoogleCallback,
9244
+ oauthStart,
9245
+ oauthProviders,
9246
+ getGoogleOAuthUrl,
9247
+ oauthFinalize
9248
+ });
9249
+
9250
+ // src/server/routes/admin/index.ts
9251
+ init_repositories();
9252
+ init_esm();
9253
+ import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
9254
+ import { route as route5 } from "@spfn/core/route";
9255
+ var listRoles = route5.get("/_auth/admin/roles").input({
9256
+ query: Type.Object({
9257
+ includeInactive: Type.Optional(Type.Boolean({
9258
+ description: "Include inactive roles (default: false)"
9259
+ }))
9260
+ })
9261
+ }).use([authenticate, requireRole("admin", "superadmin")]).handler(async (c) => {
9262
+ const { query } = await c.data();
9263
+ const roles2 = await getAllRoles(query.includeInactive ?? false);
9264
+ return { roles: roles2 };
9265
+ });
9266
+ var createAdminRole = route5.post("/_auth/admin/roles").input({
9267
+ body: Type.Object({
9268
+ name: Type.String({ description: "Unique role name (slug)" }),
9269
+ displayName: Type.String({ description: "Human-readable role name" }),
9270
+ description: Type.Optional(Type.String({ description: "Role description" })),
9271
+ priority: Type.Optional(Type.Number({ description: "Role priority (default: 10)" })),
9272
+ permissionIds: Type.Optional(Type.Array(
9273
+ Type.Number({ description: "Permission ID" }),
9274
+ { description: "Permission IDs to assign" }
9275
+ ))
9276
+ })
9277
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9278
+ const { body } = await c.data();
9279
+ const role = await createRole({
9280
+ name: body.name,
9281
+ displayName: body.displayName,
9282
+ description: body.description,
9283
+ priority: body.priority,
9284
+ permissionIds: body.permissionIds
9285
+ });
9286
+ return { role };
9287
+ });
9288
+ var updateAdminRole = route5.patch("/_auth/admin/roles/:id").input({
9289
+ params: Type.Object({
9290
+ id: Type.Number({ description: "Role ID" })
9291
+ }),
9292
+ body: Type.Object({
9293
+ displayName: Type.Optional(Type.String({ description: "Human-readable role name" })),
9294
+ description: Type.Optional(Type.String({ description: "Role description" })),
9295
+ priority: Type.Optional(Type.Number({ description: "Role priority" })),
9296
+ isActive: Type.Optional(Type.Boolean({ description: "Active status" }))
9297
+ })
9298
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9299
+ const { params, body } = await c.data();
9300
+ const role = await updateRole(params.id, body);
9301
+ return { role };
9302
+ });
9303
+ var deleteAdminRole = route5.delete("/_auth/admin/roles/:id").input({
9304
+ params: Type.Object({
9305
+ id: Type.Number({ description: "Role ID" })
9306
+ })
9307
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
9308
+ const { params } = await c.data();
9309
+ await deleteRole(params.id);
9310
+ return c.noContent();
9311
+ });
9312
+ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
9313
+ params: Type.Object({
9314
+ userId: Type.Number({ description: "User ID" })
9315
+ }),
9316
+ body: Type.Object({
9317
+ roleId: Type.Number({ description: "New role ID to assign" })
9318
+ })
9319
+ }).use([authenticate, requireRole("admin", "superadmin")]).handler(async (c) => {
9320
+ const { params, body } = await c.data();
9321
+ const auth = getAuth(c);
9322
+ const callerRole = await getUserRole(auth.userId);
9323
+ if (params.userId === Number(auth.userId)) {
9324
+ throw new ForbiddenError4({ message: "Cannot change your own role" });
9325
+ }
9326
+ const targetRole = await getUserRole(params.userId);
9327
+ if (targetRole === "superadmin") {
9328
+ throw new ForbiddenError4({ message: "Cannot modify superadmin role" });
9329
+ }
9330
+ if (callerRole !== "superadmin") {
9331
+ const newRole = await rolesRepository.findById(body.roleId);
9332
+ if (newRole?.name === "superadmin") {
9333
+ throw new ForbiddenError4({ message: "Only superadmin can assign superadmin role" });
9334
+ }
9335
+ if (newRole?.name === "admin") {
9336
+ const canPromote = await hasPermission(auth.userId, "admin:promote");
9337
+ if (!canPromote) {
9338
+ throw new ForbiddenError4({ message: "admin:promote permission required to assign admin role" });
9339
+ }
9340
+ }
9341
+ }
9342
+ await updateUserService(params.userId, { roleId: body.roleId });
9343
+ return { userId: params.userId, roleId: body.roleId };
8585
9344
  });
8586
9345
 
8587
9346
  // src/server/routes/index.ts
8588
- var mainAuthRouter = defineRouter4({
8589
- // Flatten all routes at root level
8590
- ...authRouter.routes,
8591
- ...invitationRouter.routes,
8592
- ...userRouter.routes
9347
+ var mainAuthRouter = defineRouter5({
9348
+ // Auth routes
9349
+ checkAccountExists,
9350
+ sendVerificationCode,
9351
+ verifyCode,
9352
+ register,
9353
+ login,
9354
+ logout,
9355
+ rotateKey,
9356
+ changePassword,
9357
+ getAuthSession,
9358
+ // One-Time Token routes
9359
+ issueOneTimeToken,
9360
+ // OAuth routes
9361
+ oauthGoogleStart,
9362
+ oauthGoogleCallback,
9363
+ oauthStart,
9364
+ oauthProviders,
9365
+ getGoogleOAuthUrl,
9366
+ oauthFinalize,
9367
+ // Invitation routes
9368
+ getInvitation,
9369
+ acceptInvitation: acceptInvitation2,
9370
+ createInvitation: createInvitation2,
9371
+ listInvitations: listInvitations2,
9372
+ cancelInvitation: cancelInvitation2,
9373
+ resendInvitation: resendInvitation2,
9374
+ deleteInvitation: deleteInvitation2,
9375
+ // User routes
9376
+ getUserProfile,
9377
+ updateUserProfile,
9378
+ checkUsername,
9379
+ updateUsername,
9380
+ updateLocale,
9381
+ // Admin routes (superadmin only)
9382
+ listRoles,
9383
+ createAdminRole,
9384
+ updateAdminRole,
9385
+ deleteAdminRole,
9386
+ updateUserRole
8593
9387
  });
8594
9388
 
8595
9389
  // src/server.ts
@@ -8699,36 +9493,60 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
8699
9493
  }
8700
9494
 
8701
9495
  // src/server/lib/session.ts
8702
- import * as jose from "jose";
8703
- import { env as env7 } from "@spfn/auth/config";
9496
+ import * as jose2 from "jose";
9497
+ import { env as env9 } from "@spfn/auth/config";
8704
9498
  import { env as coreEnv } from "@spfn/core/config";
8705
9499
  async function getSessionSecretKey() {
8706
- const secret = env7.SPFN_AUTH_SESSION_SECRET;
9500
+ const secret = env9.SPFN_AUTH_SESSION_SECRET;
8707
9501
  const encoder = new TextEncoder();
8708
9502
  const data = encoder.encode(secret);
8709
9503
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
8710
9504
  return new Uint8Array(hashBuffer);
8711
9505
  }
9506
+ async function getSecretFingerprint() {
9507
+ const key = await getSessionSecretKey();
9508
+ const hash = await crypto.subtle.digest("SHA-256", key.buffer);
9509
+ const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
9510
+ return hex.slice(0, 8);
9511
+ }
8712
9512
  async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
8713
9513
  const secret = await getSessionSecretKey();
8714
- return await new jose.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
9514
+ const result = await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
9515
+ if (coreEnv.NODE_ENV !== "production") {
9516
+ const fingerprint = await getSecretFingerprint();
9517
+ authLogger.session.debug(`Sealed session`, {
9518
+ secretFingerprint: fingerprint,
9519
+ resultLength: result.length,
9520
+ resultPrefix: result.slice(0, 20)
9521
+ });
9522
+ }
9523
+ return result;
8715
9524
  }
8716
9525
  async function unsealSession(jwt4) {
8717
9526
  try {
8718
9527
  const secret = await getSessionSecretKey();
8719
- const { payload } = await jose.jwtDecrypt(jwt4, secret, {
9528
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
8720
9529
  issuer: "spfn-auth",
8721
9530
  audience: "spfn-client"
8722
9531
  });
8723
9532
  return payload.data;
8724
9533
  } catch (err) {
8725
- if (err instanceof jose.errors.JWTExpired) {
9534
+ if (err instanceof jose2.errors.JWTExpired) {
8726
9535
  throw new Error("Session expired");
8727
9536
  }
8728
- if (err instanceof jose.errors.JWEDecryptionFailed) {
9537
+ if (err instanceof jose2.errors.JWEDecryptionFailed) {
9538
+ if (coreEnv.NODE_ENV !== "production") {
9539
+ const fingerprint = await getSecretFingerprint();
9540
+ authLogger.session.warn(`JWE decryption failed`, {
9541
+ secretFingerprint: fingerprint,
9542
+ jwtLength: jwt4.length,
9543
+ jwtPrefix: jwt4.slice(0, 20),
9544
+ jwtSuffix: jwt4.slice(-10)
9545
+ });
9546
+ }
8729
9547
  throw new Error("Invalid session");
8730
9548
  }
8731
- if (err instanceof jose.errors.JWTClaimValidationFailed) {
9549
+ if (err instanceof jose2.errors.JWTClaimValidationFailed) {
8732
9550
  throw new Error("Session validation failed");
8733
9551
  }
8734
9552
  throw new Error("Failed to unseal session");
@@ -8737,7 +9555,7 @@ async function unsealSession(jwt4) {
8737
9555
  async function getSessionInfo(jwt4) {
8738
9556
  const secret = await getSessionSecretKey();
8739
9557
  try {
8740
- const { payload } = await jose.jwtDecrypt(jwt4, secret);
9558
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret);
8741
9559
  return {
8742
9560
  issuedAt: new Date(payload.iat * 1e3),
8743
9561
  expiresAt: new Date(payload.exp * 1e3),
@@ -8746,7 +9564,7 @@ async function getSessionInfo(jwt4) {
8746
9564
  };
8747
9565
  } catch (err) {
8748
9566
  if (coreEnv.NODE_ENV !== "production") {
8749
- console.warn("[Session] Failed to get session info:", err instanceof Error ? err.message : "Unknown error");
9567
+ authLogger.session.warn("Failed to get session info:", err instanceof Error ? err.message : "Unknown error");
8750
9568
  }
8751
9569
  return null;
8752
9570
  }
@@ -8761,14 +9579,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
8761
9579
  }
8762
9580
 
8763
9581
  // src/server/setup.ts
8764
- import { env as env8 } from "@spfn/auth/config";
9582
+ import { env as env10 } from "@spfn/auth/config";
8765
9583
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
8766
9584
  init_repositories();
8767
9585
  function parseAdminAccounts() {
8768
9586
  const accounts = [];
8769
- if (env8.SPFN_AUTH_ADMIN_ACCOUNTS) {
9587
+ if (env10.SPFN_AUTH_ADMIN_ACCOUNTS) {
8770
9588
  try {
8771
- const accountsJson = env8.SPFN_AUTH_ADMIN_ACCOUNTS;
9589
+ const accountsJson = env10.SPFN_AUTH_ADMIN_ACCOUNTS;
8772
9590
  const parsed = JSON.parse(accountsJson);
8773
9591
  if (!Array.isArray(parsed)) {
8774
9592
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -8795,11 +9613,11 @@ function parseAdminAccounts() {
8795
9613
  return accounts;
8796
9614
  }
8797
9615
  }
8798
- const adminEmails = env8.SPFN_AUTH_ADMIN_EMAILS;
9616
+ const adminEmails = env10.SPFN_AUTH_ADMIN_EMAILS;
8799
9617
  if (adminEmails) {
8800
9618
  const emails = adminEmails.split(",").map((s) => s.trim());
8801
- const passwords = (env8.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8802
- const roles2 = (env8.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
9619
+ const passwords = (env10.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
9620
+ const roles2 = (env10.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
8803
9621
  if (passwords.length !== emails.length) {
8804
9622
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
8805
9623
  return accounts;
@@ -8821,8 +9639,8 @@ function parseAdminAccounts() {
8821
9639
  }
8822
9640
  return accounts;
8823
9641
  }
8824
- const adminEmail = env8.SPFN_AUTH_ADMIN_EMAIL;
8825
- const adminPassword = env8.SPFN_AUTH_ADMIN_PASSWORD;
9642
+ const adminEmail = env10.SPFN_AUTH_ADMIN_EMAIL;
9643
+ const adminPassword = env10.SPFN_AUTH_ADMIN_PASSWORD;
8826
9644
  if (adminEmail && adminPassword) {
8827
9645
  accounts.push({
8828
9646
  email: adminEmail,
@@ -8892,14 +9710,18 @@ function createAuthLifecycle(options = {}) {
8892
9710
  * Performs:
8893
9711
  * 1. Ensures admin account exists (creates if missing)
8894
9712
  * 2. Initializes RBAC system with built-in + custom roles/permissions
9713
+ * 3. Initializes one-time token manager
8895
9714
  */
8896
9715
  afterInfrastructure: async () => {
8897
9716
  await initializeAuth(options);
8898
9717
  await ensureAdminExists();
9718
+ initOneTimeTokenManager(options.oneTimeToken);
8899
9719
  }
8900
9720
  };
8901
9721
  }
8902
9722
  export {
9723
+ AuthMetadataRepository,
9724
+ AuthProviderSchema,
8903
9725
  COOKIE_NAMES,
8904
9726
  EmailSchema,
8905
9727
  INVITATION_STATUSES,
@@ -8912,6 +9734,7 @@ export {
8912
9734
  RolePermissionsRepository,
8913
9735
  RolesRepository,
8914
9736
  SOCIAL_PROVIDERS,
9737
+ SocialAccountsRepository,
8915
9738
  TargetTypeSchema,
8916
9739
  USER_STATUSES,
8917
9740
  UserPermissionsRepository,
@@ -8924,19 +9747,27 @@ export {
8924
9747
  acceptInvitation,
8925
9748
  addPermissionToRole,
8926
9749
  authLogger,
9750
+ authLoginEvent,
9751
+ authMetadata,
9752
+ authMetadataRepository,
9753
+ authRegisterEvent,
8927
9754
  mainAuthRouter as authRouter,
8928
9755
  authSchema,
8929
9756
  authenticate,
9757
+ buildOAuthErrorUrl,
8930
9758
  cancelInvitation,
8931
9759
  changePasswordService,
8932
9760
  checkAccountExistsService,
9761
+ checkUsernameAvailableService,
8933
9762
  configureAuth,
8934
9763
  createAuthLifecycle,
8935
9764
  createInvitation,
9765
+ createOAuthState,
8936
9766
  createRole,
8937
9767
  decodeToken,
8938
9768
  deleteInvitation,
8939
9769
  deleteRole,
9770
+ exchangeCodeForTokens,
8940
9771
  expireOldInvitations,
8941
9772
  generateClientToken,
8942
9773
  generateKeyPair,
@@ -8947,12 +9778,19 @@ export {
8947
9778
  getAuth,
8948
9779
  getAuthConfig,
8949
9780
  getAuthSessionService,
9781
+ getEnabledOAuthProviders,
9782
+ getGoogleAccessToken,
9783
+ getGoogleAuthUrl,
9784
+ getGoogleOAuthConfig,
9785
+ getGoogleUserInfo,
8950
9786
  getInvitationByToken,
8951
- getInvitationTemplate,
8952
9787
  getInvitationWithDetails,
8953
9788
  getKeyId,
8954
9789
  getKeySize,
8955
- getPasswordResetTemplate,
9790
+ getLocale,
9791
+ getOneTimeTokenManager,
9792
+ getOptionalAuth,
9793
+ getRole,
8956
9794
  getRoleByName,
8957
9795
  getRolePermissions,
8958
9796
  getSessionInfo,
@@ -8965,27 +9803,33 @@ export {
8965
9803
  getUserPermissions,
8966
9804
  getUserProfileService,
8967
9805
  getUserRole,
8968
- getVerificationCodeTemplate,
8969
- getWelcomeTemplate,
8970
9806
  hasAllPermissions,
8971
9807
  hasAnyPermission,
8972
9808
  hasAnyRole,
8973
9809
  hasPermission,
8974
9810
  hasRole,
8975
9811
  hashPassword,
9812
+ initOneTimeTokenManager,
8976
9813
  initializeAuth,
9814
+ invitationAcceptedEvent,
9815
+ invitationCreatedEvent,
8977
9816
  invitationsRepository,
9817
+ isGoogleOAuthEnabled,
9818
+ isOAuthProviderEnabled,
9819
+ issueOneTimeTokenService,
8978
9820
  keysRepository,
8979
9821
  listInvitations,
8980
9822
  loginService,
8981
9823
  logoutService,
9824
+ oauthCallbackService,
9825
+ oauthStartService,
9826
+ oneTimeTokenAuth,
9827
+ optionalAuth,
8982
9828
  parseDuration,
8983
9829
  permissions,
8984
9830
  permissionsRepository,
8985
- registerEmailProvider,
8986
- registerEmailTemplates,
9831
+ refreshAccessToken,
8987
9832
  registerPublicKeyService,
8988
- registerSMSProvider,
8989
9833
  registerService,
8990
9834
  removePermissionFromRole,
8991
9835
  requireAnyPermission,
@@ -9000,17 +9844,18 @@ export {
9000
9844
  rolesRepository,
9001
9845
  rotateKeyService,
9002
9846
  sealSession,
9003
- sendEmail,
9004
- sendSMS,
9005
9847
  sendVerificationCodeService,
9006
9848
  setRolePermissions,
9007
9849
  shouldRefreshSession,
9008
9850
  shouldRotateKey,
9851
+ socialAccountsRepository,
9009
9852
  unsealSession,
9010
9853
  updateLastLoginService,
9854
+ updateLocaleService,
9011
9855
  updateRole,
9012
9856
  updateUserProfileService,
9013
9857
  updateUserService,
9858
+ updateUsernameService,
9014
9859
  userInvitations,
9015
9860
  userPermissions,
9016
9861
  userPermissionsRepository,
@@ -9027,6 +9872,8 @@ export {
9027
9872
  verifyClientToken,
9028
9873
  verifyCodeService,
9029
9874
  verifyKeyFingerprint,
9875
+ verifyOAuthState,
9876
+ verifyOneTimeTokenService,
9030
9877
  verifyPassword,
9031
9878
  verifyToken
9032
9879
  };