@spfn/auth 0.2.0-beta.2 → 0.2.0-beta.21

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,
@@ -6132,6 +6126,23 @@ var init_user_profiles_repository = __esm({
6132
6126
  const result = await this.db.delete(userProfiles).where(eq8(userProfiles.userId, userId)).returning();
6133
6127
  return result[0] ?? null;
6134
6128
  }
6129
+ /**
6130
+ * 프로필 Upsert (by User ID)
6131
+ *
6132
+ * 프로필이 없으면 생성, 있으면 업데이트
6133
+ * 새로 생성 시 displayName은 필수 (없으면 'User'로 설정)
6134
+ */
6135
+ async upsertByUserId(userId, data) {
6136
+ const existing = await this.findByUserId(userId);
6137
+ if (existing) {
6138
+ return await this.updateByUserId(userId, data);
6139
+ }
6140
+ return await this.create({
6141
+ userId,
6142
+ displayName: data.displayName || "User",
6143
+ ...data
6144
+ });
6145
+ }
6135
6146
  /**
6136
6147
  * User ID로 프로필 데이터 조회 (formatted)
6137
6148
  *
@@ -6151,6 +6162,7 @@ var init_user_profiles_repository = __esm({
6151
6162
  location: userProfiles.location,
6152
6163
  company: userProfiles.company,
6153
6164
  jobTitle: userProfiles.jobTitle,
6165
+ metadata: userProfiles.metadata,
6154
6166
  createdAt: userProfiles.createdAt,
6155
6167
  updatedAt: userProfiles.updatedAt
6156
6168
  }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
@@ -6170,6 +6182,7 @@ var init_user_profiles_repository = __esm({
6170
6182
  location: profile.location,
6171
6183
  company: profile.company,
6172
6184
  jobTitle: profile.jobTitle,
6185
+ metadata: profile.metadata,
6173
6186
  createdAt: profile.createdAt,
6174
6187
  updatedAt: profile.updatedAt
6175
6188
  };
@@ -6394,6 +6407,96 @@ var init_invitations_repository = __esm({
6394
6407
  }
6395
6408
  });
6396
6409
 
6410
+ // src/server/repositories/social-accounts.repository.ts
6411
+ import { eq as eq10, and as and7 } from "drizzle-orm";
6412
+ import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
6413
+ var SocialAccountsRepository, socialAccountsRepository;
6414
+ var init_social_accounts_repository = __esm({
6415
+ "src/server/repositories/social-accounts.repository.ts"() {
6416
+ "use strict";
6417
+ init_entities();
6418
+ SocialAccountsRepository = class extends BaseRepository10 {
6419
+ /**
6420
+ * provider와 providerUserId로 소셜 계정 조회
6421
+ * Read replica 사용
6422
+ */
6423
+ async findByProviderAndProviderId(provider, providerUserId) {
6424
+ const result = await this.readDb.select().from(userSocialAccounts).where(
6425
+ and7(
6426
+ eq10(userSocialAccounts.provider, provider),
6427
+ eq10(userSocialAccounts.providerUserId, providerUserId)
6428
+ )
6429
+ ).limit(1);
6430
+ return result[0] ?? null;
6431
+ }
6432
+ /**
6433
+ * userId로 모든 소셜 계정 조회
6434
+ * Read replica 사용
6435
+ */
6436
+ async findByUserId(userId) {
6437
+ return await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
6438
+ }
6439
+ /**
6440
+ * userId와 provider로 소셜 계정 조회
6441
+ * Read replica 사용
6442
+ */
6443
+ async findByUserIdAndProvider(userId, provider) {
6444
+ const result = await this.readDb.select().from(userSocialAccounts).where(
6445
+ and7(
6446
+ eq10(userSocialAccounts.userId, userId),
6447
+ eq10(userSocialAccounts.provider, provider)
6448
+ )
6449
+ ).limit(1);
6450
+ return result[0] ?? null;
6451
+ }
6452
+ /**
6453
+ * 소셜 계정 생성
6454
+ * Write primary 사용
6455
+ */
6456
+ async create(data) {
6457
+ return await this._create(userSocialAccounts, {
6458
+ ...data,
6459
+ createdAt: /* @__PURE__ */ new Date(),
6460
+ updatedAt: /* @__PURE__ */ new Date()
6461
+ });
6462
+ }
6463
+ /**
6464
+ * 토큰 정보 업데이트
6465
+ * Write primary 사용
6466
+ */
6467
+ async updateTokens(id11, data) {
6468
+ const result = await this.db.update(userSocialAccounts).set({
6469
+ ...data,
6470
+ updatedAt: /* @__PURE__ */ new Date()
6471
+ }).where(eq10(userSocialAccounts.id, id11)).returning();
6472
+ return result[0] ?? null;
6473
+ }
6474
+ /**
6475
+ * 소셜 계정 삭제
6476
+ * Write primary 사용
6477
+ */
6478
+ async deleteById(id11) {
6479
+ const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id11)).returning();
6480
+ return result[0] ?? null;
6481
+ }
6482
+ /**
6483
+ * userId와 provider로 소셜 계정 삭제
6484
+ * Write primary 사용
6485
+ */
6486
+ async deleteByUserIdAndProvider(userId, provider) {
6487
+ const result = await this.db.delete(userSocialAccounts).where(
6488
+ and7(
6489
+ eq10(userSocialAccounts.userId, userId),
6490
+ eq10(userSocialAccounts.provider, provider)
6491
+ )
6492
+ ).returning();
6493
+ return result[0] ?? null;
6494
+ }
6495
+ };
6496
+ socialAccountsRepository = new SocialAccountsRepository();
6497
+ }
6498
+ });
6499
+
6397
6500
  // src/server/repositories/index.ts
6398
6501
  var init_repositories = __esm({
6399
6502
  "src/server/repositories/index.ts"() {
@@ -6407,6 +6510,7 @@ var init_repositories = __esm({
6407
6510
  init_user_permissions_repository();
6408
6511
  init_user_profiles_repository();
6409
6512
  init_invitations_repository();
6513
+ init_social_accounts_repository();
6410
6514
  }
6411
6515
  });
6412
6516
 
@@ -6533,7 +6637,7 @@ var init_role_service = __esm({
6533
6637
  import "@spfn/auth/config";
6534
6638
 
6535
6639
  // src/server/routes/index.ts
6536
- import { defineRouter as defineRouter4 } from "@spfn/core/route";
6640
+ import { defineRouter as defineRouter5 } from "@spfn/core/route";
6537
6641
 
6538
6642
  // src/server/routes/auth/index.ts
6539
6643
  init_schema3();
@@ -6682,9 +6786,10 @@ import {
6682
6786
  } from "@spfn/auth/errors";
6683
6787
 
6684
6788
  // src/server/services/verification.service.ts
6685
- import { env as env5 } from "@spfn/auth/config";
6789
+ import { env as env3 } from "@spfn/auth/config";
6686
6790
  import { InvalidVerificationCodeError } from "@spfn/auth/errors";
6687
6791
  import jwt2 from "jsonwebtoken";
6792
+ import { sendEmail, sendSMS } from "@spfn/notification/server";
6688
6793
 
6689
6794
  // src/server/logger.ts
6690
6795
  import { logger as rootLogger } from "@spfn/core/logger";
@@ -6694,7 +6799,8 @@ var authLogger = {
6694
6799
  interceptor: {
6695
6800
  general: rootLogger.child("@spfn/auth:interceptor:general"),
6696
6801
  login: rootLogger.child("@spfn/auth:interceptor:login"),
6697
- keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation")
6802
+ keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
6803
+ oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
6698
6804
  },
6699
6805
  service: rootLogger.child("@spfn/auth:service"),
6700
6806
  setup: rootLogger.child("@spfn/auth:setup"),
@@ -6704,410 +6810,6 @@ var authLogger = {
6704
6810
 
6705
6811
  // src/server/services/verification.service.ts
6706
6812
  init_repositories();
6707
-
6708
- // src/server/services/sms/provider.ts
6709
- var currentProvider = null;
6710
- var fallbackProvider = {
6711
- name: "fallback",
6712
- sendSMS: async (params) => {
6713
- authLogger.sms.debug("DEV MODE - SMS not actually sent", {
6714
- phone: params.phone,
6715
- message: params.message,
6716
- purpose: params.purpose || "N/A"
6717
- });
6718
- return {
6719
- success: true,
6720
- messageId: "dev-mode-no-actual-sms"
6721
- };
6722
- }
6723
- };
6724
- function registerSMSProvider(provider) {
6725
- currentProvider = provider;
6726
- authLogger.sms.info("Registered SMS provider", { name: provider.name });
6727
- }
6728
- function getSMSProvider() {
6729
- return currentProvider || fallbackProvider;
6730
- }
6731
- async function sendSMS(params) {
6732
- const provider = getSMSProvider();
6733
- return await provider.sendSMS(params);
6734
- }
6735
-
6736
- // src/server/services/sms/aws-sns.provider.ts
6737
- import { env as env3 } from "@spfn/auth/config";
6738
- function isValidE164Phone(phone) {
6739
- const e164Regex = /^\+[1-9]\d{1,14}$/;
6740
- return e164Regex.test(phone);
6741
- }
6742
- function createAWSSNSProvider() {
6743
- try {
6744
- const { SNSClient, PublishCommand } = __require("@aws-sdk/client-sns");
6745
- return {
6746
- name: "aws-sns",
6747
- sendSMS: async (params) => {
6748
- const { phone, message, purpose } = params;
6749
- if (!isValidE164Phone(phone)) {
6750
- return {
6751
- success: false,
6752
- error: "Invalid phone number format. Must be E.164 format (e.g., +821012345678)"
6753
- };
6754
- }
6755
- if (!env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID) {
6756
- return {
6757
- success: false,
6758
- error: "AWS SNS credentials not configured. Set SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID environment variable."
6759
- };
6760
- }
6761
- try {
6762
- const config = {
6763
- region: env3.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
6764
- };
6765
- if (env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID && env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY) {
6766
- config.credentials = {
6767
- accessKeyId: env3.SPFN_AUTH_AWS_SNS_ACCESS_KEY_ID,
6768
- secretAccessKey: env3.SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY
6769
- };
6770
- }
6771
- const client = new SNSClient(config);
6772
- const command = new PublishCommand({
6773
- PhoneNumber: phone,
6774
- Message: message,
6775
- MessageAttributes: {
6776
- "AWS.SNS.SMS.SMSType": {
6777
- DataType: "String",
6778
- StringValue: "Transactional"
6779
- // For OTP codes
6780
- },
6781
- ...env3.SPFN_AUTH_AWS_SNS_SENDER_ID && {
6782
- "AWS.SNS.SMS.SenderID": {
6783
- DataType: "String",
6784
- StringValue: env3.SPFN_AUTH_AWS_SNS_SENDER_ID
6785
- }
6786
- }
6787
- }
6788
- });
6789
- const response = await client.send(command);
6790
- authLogger.sms.info("SMS sent via AWS SNS", {
6791
- phone,
6792
- messageId: response.MessageId,
6793
- purpose: purpose || "N/A"
6794
- });
6795
- return {
6796
- success: true,
6797
- messageId: response.MessageId
6798
- };
6799
- } catch (error) {
6800
- const err = error;
6801
- authLogger.sms.error("Failed to send SMS via AWS SNS", {
6802
- phone,
6803
- error: err.message
6804
- });
6805
- return {
6806
- success: false,
6807
- error: err.message || "Failed to send SMS via AWS SNS"
6808
- };
6809
- }
6810
- }
6811
- };
6812
- } catch (error) {
6813
- return null;
6814
- }
6815
- }
6816
- var awsSNSProvider = createAWSSNSProvider();
6817
-
6818
- // src/server/services/sms/index.ts
6819
- if (awsSNSProvider) {
6820
- registerSMSProvider(awsSNSProvider);
6821
- }
6822
-
6823
- // src/server/services/email/provider.ts
6824
- var currentProvider2 = null;
6825
- var fallbackProvider2 = {
6826
- name: "fallback",
6827
- sendEmail: async (params) => {
6828
- authLogger.email.debug("DEV MODE - Email not actually sent", {
6829
- to: params.to,
6830
- subject: params.subject,
6831
- purpose: params.purpose || "N/A",
6832
- textPreview: params.text?.substring(0, 100) || "N/A"
6833
- });
6834
- return {
6835
- success: true,
6836
- messageId: "dev-mode-no-actual-email"
6837
- };
6838
- }
6839
- };
6840
- function registerEmailProvider(provider) {
6841
- currentProvider2 = provider;
6842
- authLogger.email.info("Registered email provider", { name: provider.name });
6843
- }
6844
- function getEmailProvider() {
6845
- return currentProvider2 || fallbackProvider2;
6846
- }
6847
- async function sendEmail(params) {
6848
- const provider = getEmailProvider();
6849
- return await provider.sendEmail(params);
6850
- }
6851
-
6852
- // src/server/services/email/aws-ses.provider.ts
6853
- import { env as env4 } from "@spfn/auth/config";
6854
- function isValidEmail(email) {
6855
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6856
- return emailRegex.test(email);
6857
- }
6858
- function createAWSSESProvider() {
6859
- try {
6860
- const { SESClient, SendEmailCommand } = __require("@aws-sdk/client-ses");
6861
- return {
6862
- name: "aws-ses",
6863
- sendEmail: async (params) => {
6864
- const { to, subject, text: text10, html, purpose } = params;
6865
- if (!isValidEmail(to)) {
6866
- return {
6867
- success: false,
6868
- error: "Invalid email address format"
6869
- };
6870
- }
6871
- if (!env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID) {
6872
- return {
6873
- success: false,
6874
- error: "AWS SES credentials not configured. Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable."
6875
- };
6876
- }
6877
- if (!env4.SPFN_AUTH_AWS_SES_FROM_EMAIL) {
6878
- return {
6879
- success: false,
6880
- error: "AWS SES sender email not configured. Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable."
6881
- };
6882
- }
6883
- try {
6884
- const config = {
6885
- region: env4.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
6886
- };
6887
- if (env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID && env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY) {
6888
- config.credentials = {
6889
- accessKeyId: env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID,
6890
- secretAccessKey: env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY
6891
- };
6892
- }
6893
- const client = new SESClient(config);
6894
- const body = {};
6895
- if (text10) {
6896
- body.Text = {
6897
- Charset: "UTF-8",
6898
- Data: text10
6899
- };
6900
- }
6901
- if (html) {
6902
- body.Html = {
6903
- Charset: "UTF-8",
6904
- Data: html
6905
- };
6906
- }
6907
- const command = new SendEmailCommand({
6908
- Source: env4.SPFN_AUTH_AWS_SES_FROM_EMAIL,
6909
- Destination: {
6910
- ToAddresses: [to]
6911
- },
6912
- Message: {
6913
- Subject: {
6914
- Charset: "UTF-8",
6915
- Data: subject
6916
- },
6917
- Body: body
6918
- }
6919
- });
6920
- const response = await client.send(command);
6921
- authLogger.email.info("Email sent via AWS SES", {
6922
- to,
6923
- messageId: response.MessageId,
6924
- purpose: purpose || "N/A"
6925
- });
6926
- return {
6927
- success: true,
6928
- messageId: response.MessageId
6929
- };
6930
- } catch (error) {
6931
- const err = error;
6932
- authLogger.email.error("Failed to send email via AWS SES", {
6933
- to,
6934
- error: err.message
6935
- });
6936
- return {
6937
- success: false,
6938
- error: err.message || "Failed to send email via AWS SES"
6939
- };
6940
- }
6941
- }
6942
- };
6943
- } catch (error) {
6944
- return null;
6945
- }
6946
- }
6947
- var awsSESProvider = createAWSSESProvider();
6948
-
6949
- // src/server/services/email/index.ts
6950
- if (awsSESProvider) {
6951
- registerEmailProvider(awsSESProvider);
6952
- }
6953
-
6954
- // src/server/services/email/templates/verification-code.ts
6955
- function getSubject(purpose) {
6956
- switch (purpose) {
6957
- case "registration":
6958
- return "Verify your email address";
6959
- case "login":
6960
- return "Your login verification code";
6961
- case "password_reset":
6962
- return "Reset your password";
6963
- default:
6964
- return "Your verification code";
6965
- }
6966
- }
6967
- function getPurposeText(purpose) {
6968
- switch (purpose) {
6969
- case "registration":
6970
- return "complete your registration";
6971
- case "login":
6972
- return "verify your identity";
6973
- case "password_reset":
6974
- return "reset your password";
6975
- default:
6976
- return "verify your identity";
6977
- }
6978
- }
6979
- function generateText(params) {
6980
- const { code, expiresInMinutes = 5 } = params;
6981
- return `Your verification code is: ${code}
6982
-
6983
- This code will expire in ${expiresInMinutes} minutes.
6984
-
6985
- If you didn't request this code, please ignore this email.`;
6986
- }
6987
- function generateHTML(params) {
6988
- const { code, purpose, expiresInMinutes = 5, appName } = params;
6989
- const purposeText = getPurposeText(purpose);
6990
- return `<!DOCTYPE html>
6991
- <html>
6992
- <head>
6993
- <meta charset="utf-8">
6994
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6995
- <title>Verification Code</title>
6996
- </head>
6997
- <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;">
6998
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
6999
- <h1 style="color: white; margin: 0; font-size: 24px;">${appName ? appName : "Verification Code"}</h1>
7000
- </div>
7001
- <div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
7002
- <p style="margin-bottom: 20px; font-size: 16px;">
7003
- Please use the following verification code to ${purposeText}:
7004
- </p>
7005
- <div style="background: #f8f9fa; padding: 25px; border-radius: 8px; text-align: center; margin: 25px 0; border: 2px dashed #dee2e6;">
7006
- <span style="font-size: 36px; font-weight: bold; letter-spacing: 10px; color: #333; font-family: 'Courier New', monospace;">${code}</span>
7007
- </div>
7008
- <p style="color: #666; font-size: 14px; margin-top: 20px; text-align: center;">
7009
- <strong>This code will expire in ${expiresInMinutes} minutes.</strong>
7010
- </p>
7011
- <hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
7012
- <p style="color: #999; font-size: 12px; text-align: center; margin: 0;">
7013
- If you didn't request this code, please ignore this email.
7014
- </p>
7015
- </div>
7016
- <div style="text-align: center; padding: 20px; color: #999; font-size: 11px;">
7017
- <p style="margin: 0;">This is an automated message. Please do not reply.</p>
7018
- </div>
7019
- </body>
7020
- </html>`;
7021
- }
7022
- function verificationCodeTemplate(params) {
7023
- return {
7024
- subject: getSubject(params.purpose),
7025
- text: generateText(params),
7026
- html: generateHTML(params)
7027
- };
7028
- }
7029
-
7030
- // src/server/services/email/templates/registry.ts
7031
- var customTemplates = {};
7032
- function registerEmailTemplates(templates) {
7033
- customTemplates = { ...customTemplates, ...templates };
7034
- authLogger.email.info("Registered custom email templates", {
7035
- templates: Object.keys(templates)
7036
- });
7037
- }
7038
- function getVerificationCodeTemplate(params) {
7039
- if (customTemplates.verificationCode) {
7040
- return customTemplates.verificationCode(params);
7041
- }
7042
- return verificationCodeTemplate(params);
7043
- }
7044
- function getWelcomeTemplate(params) {
7045
- if (customTemplates.welcome) {
7046
- return customTemplates.welcome(params);
7047
- }
7048
- return {
7049
- subject: params.appName ? `Welcome to ${params.appName}!` : "Welcome!",
7050
- text: `Welcome! Your account has been created successfully.`,
7051
- html: `
7052
- <!DOCTYPE html>
7053
- <html>
7054
- <head><meta charset="utf-8"></head>
7055
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7056
- <h1>Welcome${params.appName ? ` to ${params.appName}` : ""}!</h1>
7057
- <p>Your account has been created successfully.</p>
7058
- </body>
7059
- </html>`
7060
- };
7061
- }
7062
- function getPasswordResetTemplate(params) {
7063
- if (customTemplates.passwordReset) {
7064
- return customTemplates.passwordReset(params);
7065
- }
7066
- const expires = params.expiresInMinutes || 30;
7067
- return {
7068
- subject: "Reset your password",
7069
- text: `Click this link to reset your password: ${params.resetLink}
7070
-
7071
- This link will expire in ${expires} minutes.`,
7072
- html: `
7073
- <!DOCTYPE html>
7074
- <html>
7075
- <head><meta charset="utf-8"></head>
7076
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7077
- <h1>Reset Your Password</h1>
7078
- <p>Click the button below to reset your password:</p>
7079
- <a href="${params.resetLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Reset Password</a>
7080
- <p style="color: #666; margin-top: 20px;">This link will expire in ${expires} minutes.</p>
7081
- </body>
7082
- </html>`
7083
- };
7084
- }
7085
- function getInvitationTemplate(params) {
7086
- if (customTemplates.invitation) {
7087
- return customTemplates.invitation(params);
7088
- }
7089
- const appName = params.appName || "our platform";
7090
- const inviterText = params.inviterName ? `${params.inviterName} has invited you` : "You have been invited";
7091
- const roleText = params.roleName ? ` as ${params.roleName}` : "";
7092
- return {
7093
- subject: `You're invited to join ${appName}`,
7094
- text: `${inviterText} to join ${appName}${roleText}.
7095
-
7096
- Click here to accept: ${params.inviteLink}`,
7097
- html: `
7098
- <!DOCTYPE html>
7099
- <html>
7100
- <head><meta charset="utf-8"></head>
7101
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7102
- <h1>You're Invited!</h1>
7103
- <p>${inviterText} to join <strong>${appName}</strong>${roleText}.</p>
7104
- <a href="${params.inviteLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Accept Invitation</a>
7105
- </body>
7106
- </html>`
7107
- };
7108
- }
7109
-
7110
- // src/server/services/verification.service.ts
7111
6813
  var VERIFICATION_TOKEN_EXPIRY = "15m";
7112
6814
  var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
7113
6815
  var MAX_VERIFICATION_ATTEMPTS = 5;
@@ -7151,7 +6853,7 @@ async function markCodeAsUsed(codeId) {
7151
6853
  await verificationCodesRepository.markAsUsed(codeId);
7152
6854
  }
7153
6855
  function createVerificationToken(payload) {
7154
- return jwt2.sign(payload, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6856
+ return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7155
6857
  expiresIn: VERIFICATION_TOKEN_EXPIRY,
7156
6858
  issuer: "spfn-auth",
7157
6859
  audience: "spfn-client"
@@ -7159,7 +6861,7 @@ function createVerificationToken(payload) {
7159
6861
  }
7160
6862
  function validateVerificationToken(token) {
7161
6863
  try {
7162
- const decoded = jwt2.verify(token, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6864
+ const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7163
6865
  issuer: "spfn-auth",
7164
6866
  audience: "spfn-client"
7165
6867
  });
@@ -7173,17 +6875,14 @@ function validateVerificationToken(token) {
7173
6875
  }
7174
6876
  }
7175
6877
  async function sendVerificationEmail(email, code, purpose) {
7176
- const { subject, text: text10, html } = getVerificationCodeTemplate({
7177
- code,
7178
- purpose,
7179
- expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7180
- });
7181
6878
  const result = await sendEmail({
7182
6879
  to: email,
7183
- subject,
7184
- text: text10,
7185
- html,
7186
- purpose
6880
+ template: "verification-code",
6881
+ data: {
6882
+ code,
6883
+ purpose,
6884
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
6885
+ }
7187
6886
  });
7188
6887
  if (!result.success) {
7189
6888
  authLogger.email.error("Failed to send verification email", {
@@ -7194,11 +6893,13 @@ async function sendVerificationEmail(email, code, purpose) {
7194
6893
  }
7195
6894
  }
7196
6895
  async function sendVerificationSMS(phone, code, purpose) {
7197
- const message = `Your verification code is: ${code}`;
7198
6896
  const result = await sendSMS({
7199
- phone,
7200
- message,
7201
- purpose
6897
+ to: phone,
6898
+ template: "verification-code",
6899
+ data: {
6900
+ code,
6901
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
6902
+ }
7202
6903
  });
7203
6904
  if (!result.success) {
7204
6905
  authLogger.sms.error("Failed to send verification SMS", {
@@ -7315,6 +7016,33 @@ async function updateUserService(userId, updates) {
7315
7016
  await usersRepository.updateById(userId, updates);
7316
7017
  }
7317
7018
 
7019
+ // src/server/events/index.ts
7020
+ init_esm();
7021
+ import { defineEvent } from "@spfn/core/event";
7022
+ var AuthProviderSchema = Type.Union([
7023
+ Type.Literal("email"),
7024
+ Type.Literal("phone"),
7025
+ Type.Literal("google")
7026
+ ]);
7027
+ var authLoginEvent = defineEvent(
7028
+ "auth.login",
7029
+ Type.Object({
7030
+ userId: Type.String(),
7031
+ provider: AuthProviderSchema,
7032
+ email: Type.Optional(Type.String()),
7033
+ phone: Type.Optional(Type.String())
7034
+ })
7035
+ );
7036
+ var authRegisterEvent = defineEvent(
7037
+ "auth.register",
7038
+ Type.Object({
7039
+ userId: Type.String(),
7040
+ provider: AuthProviderSchema,
7041
+ email: Type.Optional(Type.String()),
7042
+ phone: Type.Optional(Type.String())
7043
+ })
7044
+ );
7045
+
7318
7046
  // src/server/services/auth.service.ts
7319
7047
  async function checkAccountExistsService(params) {
7320
7048
  const { email, phone } = params;
@@ -7381,11 +7109,18 @@ async function registerService(params) {
7381
7109
  fingerprint,
7382
7110
  algorithm
7383
7111
  });
7384
- return {
7112
+ const result = {
7385
7113
  userId: String(newUser.id),
7386
7114
  email: newUser.email || void 0,
7387
7115
  phone: newUser.phone || void 0
7388
7116
  };
7117
+ await authRegisterEvent.emit({
7118
+ userId: result.userId,
7119
+ provider: email ? "email" : "phone",
7120
+ email: result.email,
7121
+ phone: result.phone
7122
+ });
7123
+ return result;
7389
7124
  }
7390
7125
  async function loginService(params) {
7391
7126
  const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
@@ -7418,12 +7153,19 @@ async function loginService(params) {
7418
7153
  algorithm
7419
7154
  });
7420
7155
  await updateLastLoginService(user.id);
7421
- return {
7156
+ const result = {
7422
7157
  userId: String(user.id),
7423
7158
  email: user.email || void 0,
7424
7159
  phone: user.phone || void 0,
7425
7160
  passwordChangeRequired: user.passwordChangeRequired
7426
7161
  };
7162
+ await authLoginEvent.emit({
7163
+ userId: result.userId,
7164
+ provider: email ? "email" : "phone",
7165
+ email: result.email,
7166
+ phone: result.phone
7167
+ });
7168
+ return result;
7427
7169
  }
7428
7170
  async function logoutService(params) {
7429
7171
  const { userId, keyId } = params;
@@ -7461,12 +7203,14 @@ init_repositories();
7461
7203
  init_rbac();
7462
7204
 
7463
7205
  // src/server/lib/config.ts
7464
- import { env as env6 } from "@spfn/auth/config";
7206
+ import { env as env4 } from "@spfn/auth/config";
7465
7207
  var COOKIE_NAMES = {
7466
7208
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
7467
7209
  SESSION: "spfn_session",
7468
7210
  /** Current key ID (for key rotation) */
7469
- SESSION_KEY_ID: "spfn_session_key_id"
7211
+ SESSION_KEY_ID: "spfn_session_key_id",
7212
+ /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
7213
+ OAUTH_PENDING: "spfn_oauth_pending"
7470
7214
  };
7471
7215
  function parseDuration(duration) {
7472
7216
  if (typeof duration === "number") {
@@ -7511,7 +7255,7 @@ function getSessionTtl(override) {
7511
7255
  if (globalConfig.sessionTtl !== void 0) {
7512
7256
  return parseDuration(globalConfig.sessionTtl);
7513
7257
  }
7514
- const envTtl = env6.SPFN_AUTH_SESSION_TTL;
7258
+ const envTtl = env4.SPFN_AUTH_SESSION_TTL;
7515
7259
  if (envTtl) {
7516
7260
  return parseDuration(envTtl);
7517
7261
  }
@@ -7673,14 +7417,18 @@ async function hasAllPermissions(userId, permissionNames) {
7673
7417
  const perms = await getUserPermissions(userId);
7674
7418
  return permissionNames.every((p) => perms.includes(p));
7675
7419
  }
7676
- async function hasRole(userId, roleName) {
7420
+ async function getUserRole(userId) {
7677
7421
  const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7678
7422
  const user = await usersRepository.findById(userIdNum);
7679
7423
  if (!user || !user.roleId) {
7680
- return false;
7424
+ return null;
7681
7425
  }
7682
7426
  const role = await rolesRepository.findById(user.roleId);
7683
- return role?.name === roleName;
7427
+ return role?.name || null;
7428
+ }
7429
+ async function hasRole(userId, roleName) {
7430
+ const role = await getUserRole(userId);
7431
+ return role === roleName;
7684
7432
  }
7685
7433
  async function hasAnyRole(userId, roleNames) {
7686
7434
  for (const roleName of roleNames) {
@@ -7887,6 +7635,406 @@ async function getUserProfileService(userId) {
7887
7635
  profile
7888
7636
  };
7889
7637
  }
7638
+ function emptyToNull(value) {
7639
+ if (value === "") {
7640
+ return null;
7641
+ }
7642
+ return value;
7643
+ }
7644
+ async function updateUserProfileService(userId, params) {
7645
+ const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
7646
+ const updateData = {};
7647
+ if (params.displayName !== void 0) {
7648
+ updateData.displayName = emptyToNull(params.displayName) || "User";
7649
+ }
7650
+ if (params.firstName !== void 0) {
7651
+ updateData.firstName = emptyToNull(params.firstName);
7652
+ }
7653
+ if (params.lastName !== void 0) {
7654
+ updateData.lastName = emptyToNull(params.lastName);
7655
+ }
7656
+ if (params.avatarUrl !== void 0) {
7657
+ updateData.avatarUrl = emptyToNull(params.avatarUrl);
7658
+ }
7659
+ if (params.bio !== void 0) {
7660
+ updateData.bio = emptyToNull(params.bio);
7661
+ }
7662
+ if (params.locale !== void 0) {
7663
+ updateData.locale = emptyToNull(params.locale) || "en";
7664
+ }
7665
+ if (params.timezone !== void 0) {
7666
+ updateData.timezone = emptyToNull(params.timezone) || "UTC";
7667
+ }
7668
+ if (params.dateOfBirth !== void 0) {
7669
+ updateData.dateOfBirth = emptyToNull(params.dateOfBirth);
7670
+ }
7671
+ if (params.gender !== void 0) {
7672
+ updateData.gender = emptyToNull(params.gender);
7673
+ }
7674
+ if (params.website !== void 0) {
7675
+ updateData.website = emptyToNull(params.website);
7676
+ }
7677
+ if (params.location !== void 0) {
7678
+ updateData.location = emptyToNull(params.location);
7679
+ }
7680
+ if (params.company !== void 0) {
7681
+ updateData.company = emptyToNull(params.company);
7682
+ }
7683
+ if (params.jobTitle !== void 0) {
7684
+ updateData.jobTitle = emptyToNull(params.jobTitle);
7685
+ }
7686
+ if (params.metadata !== void 0) {
7687
+ updateData.metadata = params.metadata;
7688
+ }
7689
+ const existing = await userProfilesRepository.findByUserId(userIdNum);
7690
+ if (!existing && !updateData.displayName) {
7691
+ updateData.displayName = "User";
7692
+ }
7693
+ await userProfilesRepository.upsertByUserId(userIdNum, updateData);
7694
+ const profile = await userProfilesRepository.fetchProfileData(userIdNum);
7695
+ return profile;
7696
+ }
7697
+
7698
+ // src/server/services/oauth.service.ts
7699
+ init_repositories();
7700
+ import { env as env7 } from "@spfn/auth/config";
7701
+ import { ValidationError as ValidationError2 } from "@spfn/core/errors";
7702
+
7703
+ // src/server/lib/oauth/google.ts
7704
+ import { env as env5 } from "@spfn/auth/config";
7705
+ var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
7706
+ var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
7707
+ var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
7708
+ function isGoogleOAuthEnabled() {
7709
+ return !!(env5.SPFN_AUTH_GOOGLE_CLIENT_ID && env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
7710
+ }
7711
+ function getGoogleOAuthConfig() {
7712
+ const clientId = env5.SPFN_AUTH_GOOGLE_CLIENT_ID;
7713
+ const clientSecret = env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
7714
+ if (!clientId || !clientSecret) {
7715
+ throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
7716
+ }
7717
+ const baseUrl = env5.NEXT_PUBLIC_SPFN_API_URL || env5.SPFN_API_URL;
7718
+ const redirectUri = env5.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
7719
+ return {
7720
+ clientId,
7721
+ clientSecret,
7722
+ redirectUri
7723
+ };
7724
+ }
7725
+ function getDefaultScopes() {
7726
+ const envScopes = env5.SPFN_AUTH_GOOGLE_SCOPES;
7727
+ if (envScopes) {
7728
+ return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
7729
+ }
7730
+ return ["email", "profile"];
7731
+ }
7732
+ function getGoogleAuthUrl(state, scopes) {
7733
+ const resolvedScopes = scopes ?? getDefaultScopes();
7734
+ const config = getGoogleOAuthConfig();
7735
+ const params = new URLSearchParams({
7736
+ client_id: config.clientId,
7737
+ redirect_uri: config.redirectUri,
7738
+ response_type: "code",
7739
+ scope: resolvedScopes.join(" "),
7740
+ state,
7741
+ access_type: "offline",
7742
+ // refresh_token 받기 위해
7743
+ prompt: "consent"
7744
+ // 매번 동의 화면 표시 (refresh_token 보장)
7745
+ });
7746
+ return `${GOOGLE_AUTH_URL}?${params.toString()}`;
7747
+ }
7748
+ async function exchangeCodeForTokens(code) {
7749
+ const config = getGoogleOAuthConfig();
7750
+ const response = await fetch(GOOGLE_TOKEN_URL, {
7751
+ method: "POST",
7752
+ headers: {
7753
+ "Content-Type": "application/x-www-form-urlencoded"
7754
+ },
7755
+ body: new URLSearchParams({
7756
+ client_id: config.clientId,
7757
+ client_secret: config.clientSecret,
7758
+ redirect_uri: config.redirectUri,
7759
+ grant_type: "authorization_code",
7760
+ code
7761
+ })
7762
+ });
7763
+ if (!response.ok) {
7764
+ const error = await response.text();
7765
+ throw new Error(`Failed to exchange code for tokens: ${error}`);
7766
+ }
7767
+ return response.json();
7768
+ }
7769
+ async function getGoogleUserInfo(accessToken) {
7770
+ const response = await fetch(GOOGLE_USERINFO_URL, {
7771
+ headers: {
7772
+ Authorization: `Bearer ${accessToken}`
7773
+ }
7774
+ });
7775
+ if (!response.ok) {
7776
+ const error = await response.text();
7777
+ throw new Error(`Failed to get user info: ${error}`);
7778
+ }
7779
+ return response.json();
7780
+ }
7781
+ async function refreshAccessToken(refreshToken) {
7782
+ const config = getGoogleOAuthConfig();
7783
+ const response = await fetch(GOOGLE_TOKEN_URL, {
7784
+ method: "POST",
7785
+ headers: {
7786
+ "Content-Type": "application/x-www-form-urlencoded"
7787
+ },
7788
+ body: new URLSearchParams({
7789
+ client_id: config.clientId,
7790
+ client_secret: config.clientSecret,
7791
+ refresh_token: refreshToken,
7792
+ grant_type: "refresh_token"
7793
+ })
7794
+ });
7795
+ if (!response.ok) {
7796
+ const error = await response.text();
7797
+ throw new Error(`Failed to refresh access token: ${error}`);
7798
+ }
7799
+ return response.json();
7800
+ }
7801
+
7802
+ // src/server/lib/oauth/state.ts
7803
+ import * as jose from "jose";
7804
+ import { env as env6 } from "@spfn/auth/config";
7805
+ async function getStateKey() {
7806
+ const secret = env6.SPFN_AUTH_SESSION_SECRET;
7807
+ const encoder = new TextEncoder();
7808
+ const data = encoder.encode(`oauth-state:${secret}`);
7809
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
7810
+ return new Uint8Array(hashBuffer);
7811
+ }
7812
+ function generateNonce() {
7813
+ const array = new Uint8Array(16);
7814
+ crypto.getRandomValues(array);
7815
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
7816
+ }
7817
+ async function createOAuthState(params) {
7818
+ const key = await getStateKey();
7819
+ const state = {
7820
+ returnUrl: params.returnUrl,
7821
+ nonce: generateNonce(),
7822
+ provider: params.provider,
7823
+ publicKey: params.publicKey,
7824
+ keyId: params.keyId,
7825
+ fingerprint: params.fingerprint,
7826
+ algorithm: params.algorithm
7827
+ };
7828
+ const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
7829
+ return encodeURIComponent(jwe);
7830
+ }
7831
+ async function verifyOAuthState(encryptedState) {
7832
+ const key = await getStateKey();
7833
+ const jwe = decodeURIComponent(encryptedState);
7834
+ const { payload } = await jose.jwtDecrypt(jwe, key);
7835
+ return payload.state;
7836
+ }
7837
+
7838
+ // src/server/services/oauth.service.ts
7839
+ async function oauthStartService(params) {
7840
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm } = params;
7841
+ if (provider === "google") {
7842
+ if (!isGoogleOAuthEnabled()) {
7843
+ throw new ValidationError2({
7844
+ message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
7845
+ });
7846
+ }
7847
+ const state = await createOAuthState({
7848
+ provider: "google",
7849
+ returnUrl,
7850
+ publicKey,
7851
+ keyId,
7852
+ fingerprint,
7853
+ algorithm
7854
+ });
7855
+ const authUrl = getGoogleAuthUrl(state);
7856
+ return { authUrl };
7857
+ }
7858
+ throw new ValidationError2({
7859
+ message: `Unsupported OAuth provider: ${provider}`
7860
+ });
7861
+ }
7862
+ async function oauthCallbackService(params) {
7863
+ const { provider, code, state } = params;
7864
+ const stateData = await verifyOAuthState(state);
7865
+ if (stateData.provider !== provider) {
7866
+ throw new ValidationError2({
7867
+ message: "OAuth state provider mismatch"
7868
+ });
7869
+ }
7870
+ if (provider === "google") {
7871
+ return handleGoogleCallback(code, stateData);
7872
+ }
7873
+ throw new ValidationError2({
7874
+ message: `Unsupported OAuth provider: ${provider}`
7875
+ });
7876
+ }
7877
+ async function handleGoogleCallback(code, stateData) {
7878
+ const tokens = await exchangeCodeForTokens(code);
7879
+ const googleUser = await getGoogleUserInfo(tokens.access_token);
7880
+ const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
7881
+ "google",
7882
+ googleUser.id
7883
+ );
7884
+ let userId;
7885
+ let isNewUser = false;
7886
+ if (existingSocialAccount) {
7887
+ userId = existingSocialAccount.userId;
7888
+ await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
7889
+ accessToken: tokens.access_token,
7890
+ refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
7891
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
7892
+ });
7893
+ } else {
7894
+ const result = await createOrLinkUser(googleUser, tokens);
7895
+ userId = result.userId;
7896
+ isNewUser = result.isNewUser;
7897
+ }
7898
+ await registerPublicKeyService({
7899
+ userId,
7900
+ keyId: stateData.keyId,
7901
+ publicKey: stateData.publicKey,
7902
+ fingerprint: stateData.fingerprint,
7903
+ algorithm: stateData.algorithm
7904
+ });
7905
+ await updateLastLoginService(userId);
7906
+ const appUrl = env7.NEXT_PUBLIC_SPFN_APP_URL || env7.SPFN_APP_URL;
7907
+ const callbackPath = env7.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
7908
+ const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
7909
+ const redirectUrl = buildRedirectUrl(callbackUrl, {
7910
+ userId: String(userId),
7911
+ keyId: stateData.keyId,
7912
+ returnUrl: stateData.returnUrl,
7913
+ isNewUser: String(isNewUser)
7914
+ });
7915
+ const user = await usersRepository.findById(userId);
7916
+ const eventPayload = {
7917
+ userId: String(userId),
7918
+ provider: "google",
7919
+ email: user?.email || void 0,
7920
+ phone: user?.phone || void 0
7921
+ };
7922
+ if (isNewUser) {
7923
+ await authRegisterEvent.emit(eventPayload);
7924
+ } else {
7925
+ await authLoginEvent.emit(eventPayload);
7926
+ }
7927
+ return {
7928
+ redirectUrl,
7929
+ userId: String(userId),
7930
+ keyId: stateData.keyId,
7931
+ isNewUser
7932
+ };
7933
+ }
7934
+ async function createOrLinkUser(googleUser, tokens) {
7935
+ const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
7936
+ let userId;
7937
+ let isNewUser = false;
7938
+ if (existingUser) {
7939
+ if (!googleUser.verified_email) {
7940
+ throw new ValidationError2({
7941
+ message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
7942
+ });
7943
+ }
7944
+ userId = existingUser.id;
7945
+ if (!existingUser.emailVerifiedAt) {
7946
+ await usersRepository.updateById(existingUser.id, {
7947
+ emailVerifiedAt: /* @__PURE__ */ new Date()
7948
+ });
7949
+ }
7950
+ } else {
7951
+ const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
7952
+ const userRole = await getRoleByName3("user");
7953
+ if (!userRole) {
7954
+ throw new Error("Default user role not found. Run initializeAuth() first.");
7955
+ }
7956
+ const newUser = await usersRepository.create({
7957
+ email: googleUser.verified_email ? googleUser.email : null,
7958
+ phone: null,
7959
+ passwordHash: null,
7960
+ // OAuth 사용자는 비밀번호 없음
7961
+ passwordChangeRequired: false,
7962
+ roleId: userRole.id,
7963
+ status: "active",
7964
+ emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
7965
+ });
7966
+ userId = newUser.id;
7967
+ isNewUser = true;
7968
+ }
7969
+ await socialAccountsRepository.create({
7970
+ userId,
7971
+ provider: "google",
7972
+ providerUserId: googleUser.id,
7973
+ providerEmail: googleUser.email,
7974
+ accessToken: tokens.access_token,
7975
+ refreshToken: tokens.refresh_token ?? null,
7976
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
7977
+ });
7978
+ return { userId, isNewUser };
7979
+ }
7980
+ function buildRedirectUrl(baseUrl, params) {
7981
+ const url = new URL(baseUrl, "http://placeholder");
7982
+ for (const [key, value] of Object.entries(params)) {
7983
+ url.searchParams.set(key, value);
7984
+ }
7985
+ if (baseUrl.startsWith("http")) {
7986
+ return url.toString();
7987
+ }
7988
+ return `${url.pathname}${url.search}`;
7989
+ }
7990
+ function buildOAuthErrorUrl(error) {
7991
+ const errorUrl = env7.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
7992
+ return errorUrl.replace("{error}", encodeURIComponent(error));
7993
+ }
7994
+ function isOAuthProviderEnabled(provider) {
7995
+ switch (provider) {
7996
+ case "google":
7997
+ return isGoogleOAuthEnabled();
7998
+ case "github":
7999
+ case "kakao":
8000
+ case "naver":
8001
+ return false;
8002
+ default:
8003
+ return false;
8004
+ }
8005
+ }
8006
+ function getEnabledOAuthProviders() {
8007
+ const providers = [];
8008
+ if (isGoogleOAuthEnabled()) {
8009
+ providers.push("google");
8010
+ }
8011
+ return providers;
8012
+ }
8013
+ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
8014
+ async function getGoogleAccessToken(userId) {
8015
+ const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
8016
+ if (!account) {
8017
+ throw new ValidationError2({
8018
+ message: "No Google account linked. User must sign in with Google first."
8019
+ });
8020
+ }
8021
+ const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
8022
+ if (!isExpired && account.accessToken) {
8023
+ return account.accessToken;
8024
+ }
8025
+ if (!account.refreshToken) {
8026
+ throw new ValidationError2({
8027
+ message: "Google refresh token not available. User must re-authenticate with Google."
8028
+ });
8029
+ }
8030
+ const tokens = await refreshAccessToken(account.refreshToken);
8031
+ await socialAccountsRepository.updateTokens(account.id, {
8032
+ accessToken: tokens.access_token,
8033
+ refreshToken: tokens.refresh_token ?? account.refreshToken,
8034
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8035
+ });
8036
+ return tokens.access_token;
8037
+ }
7890
8038
 
7891
8039
  // src/server/routes/auth/index.ts
7892
8040
  init_esm();
@@ -7980,9 +8128,7 @@ var login = route.post("/_auth/login").input({
7980
8128
  const { body } = await c.data();
7981
8129
  return await loginService(body);
7982
8130
  });
7983
- var logout = route.post("/_auth/logout").input({
7984
- body: Type.Object({})
7985
- }).handler(async (c) => {
8131
+ var logout = route.post("/_auth/logout").handler(async (c) => {
7986
8132
  const auth = getAuth(c);
7987
8133
  if (!auth) {
7988
8134
  return c.noContent();
@@ -7991,9 +8137,7 @@ var logout = route.post("/_auth/logout").input({
7991
8137
  await logoutService({ userId: Number(userId), keyId });
7992
8138
  return c.noContent();
7993
8139
  });
7994
- var rotateKey = route.post("/_auth/keys/rotate").input({
7995
- body: Type.Object({})
7996
- }).interceptor({
8140
+ var rotateKey = route.post("/_auth/keys/rotate").interceptor({
7997
8141
  body: Type.Object({
7998
8142
  publicKey: Type.String({ description: "New public key" }),
7999
8143
  keyId: Type.String({ description: "New key identifier" }),
@@ -8223,6 +8367,59 @@ var requireRole = defineMiddleware3(
8223
8367
  }
8224
8368
  );
8225
8369
 
8370
+ // src/server/middleware/role-guard.ts
8371
+ import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
8372
+ import { getAuth as getAuth4, getUserRole as getUserRole2, authLogger as authLogger5 } from "@spfn/auth/server";
8373
+ import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
8374
+ import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
8375
+ var roleGuard = defineMiddleware4(
8376
+ "roleGuard",
8377
+ (options) => async (c, next) => {
8378
+ const { allow, deny } = options;
8379
+ if (!allow && !deny) {
8380
+ throw new Error("roleGuard requires at least one of: allow, deny");
8381
+ }
8382
+ const auth = getAuth4(c);
8383
+ if (!auth) {
8384
+ authLogger5.middleware.warn("Role guard failed: not authenticated", {
8385
+ path: c.req.path
8386
+ });
8387
+ throw new ForbiddenError3({ message: "Authentication required" });
8388
+ }
8389
+ const { userId } = auth;
8390
+ const userRole = await getUserRole2(userId);
8391
+ if (deny && deny.length > 0) {
8392
+ if (userRole && deny.includes(userRole)) {
8393
+ authLogger5.middleware.warn("Role guard denied", {
8394
+ userId,
8395
+ userRole,
8396
+ deniedRoles: deny,
8397
+ path: c.req.path
8398
+ });
8399
+ throw new InsufficientRoleError2({ requiredRoles: allow || [] });
8400
+ }
8401
+ }
8402
+ if (allow && allow.length > 0) {
8403
+ if (!userRole || !allow.includes(userRole)) {
8404
+ authLogger5.middleware.warn("Role guard failed: role not allowed", {
8405
+ userId,
8406
+ userRole,
8407
+ allowedRoles: allow,
8408
+ path: c.req.path
8409
+ });
8410
+ throw new InsufficientRoleError2({ requiredRoles: allow });
8411
+ }
8412
+ }
8413
+ authLogger5.middleware.debug("Role guard passed", {
8414
+ userId,
8415
+ userRole,
8416
+ allow,
8417
+ deny
8418
+ });
8419
+ await next();
8420
+ }
8421
+ );
8422
+
8226
8423
  // src/server/routes/invitations/index.ts
8227
8424
  init_types();
8228
8425
  init_esm();
@@ -8416,21 +8613,277 @@ var invitationRouter = defineRouter2({
8416
8613
  });
8417
8614
 
8418
8615
  // src/server/routes/users/index.ts
8616
+ init_esm();
8419
8617
  import { defineRouter as defineRouter3, route as route3 } from "@spfn/core/route";
8420
8618
  var getUserProfile = route3.get("/_auth/users/profile").handler(async (c) => {
8421
8619
  const { userId } = getAuth(c);
8422
8620
  return await getUserProfileService(userId);
8423
8621
  });
8622
+ var updateUserProfile = route3.patch("/_auth/users/profile").input({
8623
+ body: Type.Object({
8624
+ displayName: Type.Optional(Type.String({ description: "Display name shown in UI" })),
8625
+ firstName: Type.Optional(Type.String({ description: "First name" })),
8626
+ lastName: Type.Optional(Type.String({ description: "Last name" })),
8627
+ avatarUrl: Type.Optional(Type.String({ description: "Avatar/profile picture URL" })),
8628
+ bio: Type.Optional(Type.String({ description: "Short bio/description" })),
8629
+ locale: Type.Optional(Type.String({ description: "Locale/language preference (e.g., en, ko)" })),
8630
+ timezone: Type.Optional(Type.String({ description: "Timezone (e.g., Asia/Seoul)" })),
8631
+ dateOfBirth: Type.Optional(Type.String({ description: "Date of birth (YYYY-MM-DD)" })),
8632
+ gender: Type.Optional(Type.String({ description: "Gender" })),
8633
+ website: Type.Optional(Type.String({ description: "Personal or professional website" })),
8634
+ location: Type.Optional(Type.String({ description: "Location (city, country, etc.)" })),
8635
+ company: Type.Optional(Type.String({ description: "Company name" })),
8636
+ jobTitle: Type.Optional(Type.String({ description: "Job title" })),
8637
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Additional metadata" }))
8638
+ })
8639
+ }).handler(async (c) => {
8640
+ const { userId } = getAuth(c);
8641
+ const { body } = await c.data();
8642
+ return await updateUserProfileService(userId, body);
8643
+ });
8424
8644
  var userRouter = defineRouter3({
8425
- getUserProfile
8645
+ getUserProfile,
8646
+ updateUserProfile
8647
+ });
8648
+
8649
+ // src/server/routes/oauth/index.ts
8650
+ init_esm();
8651
+ init_types();
8652
+ import { Transactional as Transactional2 } from "@spfn/core/db";
8653
+ import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
8654
+ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
8655
+ query: Type.Object({
8656
+ state: Type.String({
8657
+ description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
8658
+ })
8659
+ })
8660
+ }).skip(["auth"]).handler(async (c) => {
8661
+ const { query } = await c.data();
8662
+ if (!isGoogleOAuthEnabled()) {
8663
+ return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
8664
+ }
8665
+ const authUrl = getGoogleAuthUrl(query.state);
8666
+ return c.redirect(authUrl);
8667
+ });
8668
+ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
8669
+ query: Type.Object({
8670
+ code: Type.Optional(Type.String({
8671
+ description: "Authorization code from Google"
8672
+ })),
8673
+ state: Type.Optional(Type.String({
8674
+ description: "OAuth state parameter"
8675
+ })),
8676
+ error: Type.Optional(Type.String({
8677
+ description: "Error code from Google"
8678
+ })),
8679
+ error_description: Type.Optional(Type.String({
8680
+ description: "Error description from Google"
8681
+ }))
8682
+ })
8683
+ }).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
8684
+ const { query } = await c.data();
8685
+ if (query.error) {
8686
+ const errorMessage = query.error_description || query.error;
8687
+ return c.redirect(buildOAuthErrorUrl(errorMessage));
8688
+ }
8689
+ if (!query.code || !query.state) {
8690
+ return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
8691
+ }
8692
+ try {
8693
+ const result = await oauthCallbackService({
8694
+ provider: "google",
8695
+ code: query.code,
8696
+ state: query.state
8697
+ });
8698
+ return c.redirect(result.redirectUrl);
8699
+ } catch (err) {
8700
+ const message = err instanceof Error ? err.message : "OAuth callback failed";
8701
+ return c.redirect(buildOAuthErrorUrl(message));
8702
+ }
8703
+ });
8704
+ var oauthStart = route4.post("/_auth/oauth/start").input({
8705
+ body: Type.Object({
8706
+ provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
8707
+ description: "OAuth provider (google, github, kakao, naver)"
8708
+ }),
8709
+ returnUrl: Type.String({
8710
+ description: "URL to redirect after OAuth success"
8711
+ }),
8712
+ publicKey: Type.String({
8713
+ description: "Client public key (Base64 DER)"
8714
+ }),
8715
+ keyId: Type.String({
8716
+ description: "Key identifier (UUID)"
8717
+ }),
8718
+ fingerprint: Type.String({
8719
+ description: "Key fingerprint (SHA-256 hex)"
8720
+ }),
8721
+ algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
8722
+ description: "Key algorithm (ES256 or RS256)"
8723
+ })
8724
+ })
8725
+ }).skip(["auth"]).handler(async (c) => {
8726
+ const { body } = await c.data();
8727
+ const result = await oauthStartService(body);
8728
+ return result;
8729
+ });
8730
+ var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
8731
+ return {
8732
+ providers: getEnabledOAuthProviders()
8733
+ };
8734
+ });
8735
+ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
8736
+ body: Type.Object({
8737
+ returnUrl: Type.Optional(Type.String({
8738
+ description: "URL to redirect after OAuth success"
8739
+ })),
8740
+ state: Type.Optional(Type.String({
8741
+ description: "Encrypted OAuth state (injected by interceptor)"
8742
+ }))
8743
+ })
8744
+ }).skip(["auth"]).handler(async (c) => {
8745
+ const { body } = await c.data();
8746
+ if (!isGoogleOAuthEnabled()) {
8747
+ throw new Error("Google OAuth is not configured");
8748
+ }
8749
+ if (!body.state) {
8750
+ throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
8751
+ }
8752
+ return { authUrl: getGoogleAuthUrl(body.state) };
8753
+ });
8754
+ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
8755
+ body: Type.Object({
8756
+ userId: Type.String({ description: "User ID from OAuth callback" }),
8757
+ keyId: Type.String({ description: "Key ID from OAuth state" }),
8758
+ returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
8759
+ })
8760
+ }).skip(["auth"]).handler(async (c) => {
8761
+ const { body } = await c.data();
8762
+ return {
8763
+ success: true,
8764
+ userId: body.userId,
8765
+ keyId: body.keyId,
8766
+ returnUrl: body.returnUrl || "/"
8767
+ };
8768
+ });
8769
+ var oauthRouter = defineRouter4({
8770
+ oauthGoogleStart,
8771
+ oauthGoogleCallback,
8772
+ oauthStart,
8773
+ oauthProviders,
8774
+ getGoogleOAuthUrl,
8775
+ oauthFinalize
8776
+ });
8777
+
8778
+ // src/server/routes/admin/index.ts
8779
+ init_esm();
8780
+ import { route as route5 } from "@spfn/core/route";
8781
+ var listRoles = route5.get("/_auth/admin/roles").input({
8782
+ query: Type.Object({
8783
+ includeInactive: Type.Optional(Type.Boolean({
8784
+ description: "Include inactive roles (default: false)"
8785
+ }))
8786
+ })
8787
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
8788
+ const { query } = await c.data();
8789
+ const roles2 = await getAllRoles(query.includeInactive ?? false);
8790
+ return { roles: roles2 };
8791
+ });
8792
+ var createAdminRole = route5.post("/_auth/admin/roles").input({
8793
+ body: Type.Object({
8794
+ name: Type.String({ description: "Unique role name (slug)" }),
8795
+ displayName: Type.String({ description: "Human-readable role name" }),
8796
+ description: Type.Optional(Type.String({ description: "Role description" })),
8797
+ priority: Type.Optional(Type.Number({ description: "Role priority (default: 10)" })),
8798
+ permissionIds: Type.Optional(Type.Array(
8799
+ Type.Number({ description: "Permission ID" }),
8800
+ { description: "Permission IDs to assign" }
8801
+ ))
8802
+ })
8803
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
8804
+ const { body } = await c.data();
8805
+ const role = await createRole({
8806
+ name: body.name,
8807
+ displayName: body.displayName,
8808
+ description: body.description,
8809
+ priority: body.priority,
8810
+ permissionIds: body.permissionIds
8811
+ });
8812
+ return { role };
8813
+ });
8814
+ var updateAdminRole = route5.patch("/_auth/admin/roles/:id").input({
8815
+ params: Type.Object({
8816
+ id: Type.Number({ description: "Role ID" })
8817
+ }),
8818
+ body: Type.Object({
8819
+ displayName: Type.Optional(Type.String({ description: "Human-readable role name" })),
8820
+ description: Type.Optional(Type.String({ description: "Role description" })),
8821
+ priority: Type.Optional(Type.Number({ description: "Role priority" })),
8822
+ isActive: Type.Optional(Type.Boolean({ description: "Active status" }))
8823
+ })
8824
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
8825
+ const { params, body } = await c.data();
8826
+ const role = await updateRole(params.id, body);
8827
+ return { role };
8828
+ });
8829
+ var deleteAdminRole = route5.delete("/_auth/admin/roles/:id").input({
8830
+ params: Type.Object({
8831
+ id: Type.Number({ description: "Role ID" })
8832
+ })
8833
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
8834
+ const { params } = await c.data();
8835
+ await deleteRole(params.id);
8836
+ return c.noContent();
8837
+ });
8838
+ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
8839
+ params: Type.Object({
8840
+ userId: Type.Number({ description: "User ID" })
8841
+ }),
8842
+ body: Type.Object({
8843
+ roleId: Type.Number({ description: "New role ID to assign" })
8844
+ })
8845
+ }).use([authenticate, requireRole("superadmin")]).handler(async (c) => {
8846
+ const { params, body } = await c.data();
8847
+ await updateUserService(params.userId, { roleId: body.roleId });
8848
+ return { userId: params.userId, roleId: body.roleId };
8426
8849
  });
8427
8850
 
8428
8851
  // src/server/routes/index.ts
8429
- var mainAuthRouter = defineRouter4({
8430
- // Flatten all routes at root level
8431
- ...authRouter.routes,
8432
- ...invitationRouter.routes,
8433
- ...userRouter.routes
8852
+ var mainAuthRouter = defineRouter5({
8853
+ // Auth routes
8854
+ checkAccountExists,
8855
+ sendVerificationCode,
8856
+ verifyCode,
8857
+ register,
8858
+ login,
8859
+ logout,
8860
+ rotateKey,
8861
+ changePassword,
8862
+ getAuthSession,
8863
+ // OAuth routes
8864
+ oauthGoogleStart,
8865
+ oauthGoogleCallback,
8866
+ oauthStart,
8867
+ oauthProviders,
8868
+ getGoogleOAuthUrl,
8869
+ oauthFinalize,
8870
+ // Invitation routes
8871
+ getInvitation,
8872
+ acceptInvitation: acceptInvitation2,
8873
+ createInvitation: createInvitation2,
8874
+ listInvitations: listInvitations2,
8875
+ cancelInvitation: cancelInvitation2,
8876
+ resendInvitation: resendInvitation2,
8877
+ deleteInvitation: deleteInvitation2,
8878
+ // User routes
8879
+ getUserProfile,
8880
+ updateUserProfile,
8881
+ // Admin routes (superadmin only)
8882
+ listRoles,
8883
+ createAdminRole,
8884
+ updateAdminRole,
8885
+ deleteAdminRole,
8886
+ updateUserRole
8434
8887
  });
8435
8888
 
8436
8889
  // src/server.ts
@@ -8540,11 +8993,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
8540
8993
  }
8541
8994
 
8542
8995
  // src/server/lib/session.ts
8543
- import * as jose from "jose";
8544
- import { env as env7 } from "@spfn/auth/config";
8996
+ import * as jose2 from "jose";
8997
+ import { env as env8 } from "@spfn/auth/config";
8545
8998
  import { env as coreEnv } from "@spfn/core/config";
8546
8999
  async function getSessionSecretKey() {
8547
- const secret = env7.SPFN_AUTH_SESSION_SECRET;
9000
+ const secret = env8.SPFN_AUTH_SESSION_SECRET;
8548
9001
  const encoder = new TextEncoder();
8549
9002
  const data = encoder.encode(secret);
8550
9003
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -8552,24 +9005,24 @@ async function getSessionSecretKey() {
8552
9005
  }
8553
9006
  async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
8554
9007
  const secret = await getSessionSecretKey();
8555
- return await new jose.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
9008
+ return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
8556
9009
  }
8557
9010
  async function unsealSession(jwt4) {
8558
9011
  try {
8559
9012
  const secret = await getSessionSecretKey();
8560
- const { payload } = await jose.jwtDecrypt(jwt4, secret, {
9013
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
8561
9014
  issuer: "spfn-auth",
8562
9015
  audience: "spfn-client"
8563
9016
  });
8564
9017
  return payload.data;
8565
9018
  } catch (err) {
8566
- if (err instanceof jose.errors.JWTExpired) {
9019
+ if (err instanceof jose2.errors.JWTExpired) {
8567
9020
  throw new Error("Session expired");
8568
9021
  }
8569
- if (err instanceof jose.errors.JWEDecryptionFailed) {
9022
+ if (err instanceof jose2.errors.JWEDecryptionFailed) {
8570
9023
  throw new Error("Invalid session");
8571
9024
  }
8572
- if (err instanceof jose.errors.JWTClaimValidationFailed) {
9025
+ if (err instanceof jose2.errors.JWTClaimValidationFailed) {
8573
9026
  throw new Error("Session validation failed");
8574
9027
  }
8575
9028
  throw new Error("Failed to unseal session");
@@ -8578,7 +9031,7 @@ async function unsealSession(jwt4) {
8578
9031
  async function getSessionInfo(jwt4) {
8579
9032
  const secret = await getSessionSecretKey();
8580
9033
  try {
8581
- const { payload } = await jose.jwtDecrypt(jwt4, secret);
9034
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret);
8582
9035
  return {
8583
9036
  issuedAt: new Date(payload.iat * 1e3),
8584
9037
  expiresAt: new Date(payload.exp * 1e3),
@@ -8602,14 +9055,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
8602
9055
  }
8603
9056
 
8604
9057
  // src/server/setup.ts
8605
- import { env as env8 } from "@spfn/auth/config";
9058
+ import { env as env9 } from "@spfn/auth/config";
8606
9059
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
8607
9060
  init_repositories();
8608
9061
  function parseAdminAccounts() {
8609
9062
  const accounts = [];
8610
- if (env8.SPFN_AUTH_ADMIN_ACCOUNTS) {
9063
+ if (env9.SPFN_AUTH_ADMIN_ACCOUNTS) {
8611
9064
  try {
8612
- const accountsJson = env8.SPFN_AUTH_ADMIN_ACCOUNTS;
9065
+ const accountsJson = env9.SPFN_AUTH_ADMIN_ACCOUNTS;
8613
9066
  const parsed = JSON.parse(accountsJson);
8614
9067
  if (!Array.isArray(parsed)) {
8615
9068
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -8636,11 +9089,11 @@ function parseAdminAccounts() {
8636
9089
  return accounts;
8637
9090
  }
8638
9091
  }
8639
- const adminEmails = env8.SPFN_AUTH_ADMIN_EMAILS;
9092
+ const adminEmails = env9.SPFN_AUTH_ADMIN_EMAILS;
8640
9093
  if (adminEmails) {
8641
9094
  const emails = adminEmails.split(",").map((s) => s.trim());
8642
- const passwords = (env8.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8643
- const roles2 = (env8.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
9095
+ const passwords = (env9.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
9096
+ const roles2 = (env9.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
8644
9097
  if (passwords.length !== emails.length) {
8645
9098
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
8646
9099
  return accounts;
@@ -8662,8 +9115,8 @@ function parseAdminAccounts() {
8662
9115
  }
8663
9116
  return accounts;
8664
9117
  }
8665
- const adminEmail = env8.SPFN_AUTH_ADMIN_EMAIL;
8666
- const adminPassword = env8.SPFN_AUTH_ADMIN_PASSWORD;
9118
+ const adminEmail = env9.SPFN_AUTH_ADMIN_EMAIL;
9119
+ const adminPassword = env9.SPFN_AUTH_ADMIN_PASSWORD;
8667
9120
  if (adminEmail && adminPassword) {
8668
9121
  accounts.push({
8669
9122
  email: adminEmail,
@@ -8741,6 +9194,7 @@ function createAuthLifecycle(options = {}) {
8741
9194
  };
8742
9195
  }
8743
9196
  export {
9197
+ AuthProviderSchema,
8744
9198
  COOKIE_NAMES,
8745
9199
  EmailSchema,
8746
9200
  INVITATION_STATUSES,
@@ -8753,6 +9207,7 @@ export {
8753
9207
  RolePermissionsRepository,
8754
9208
  RolesRepository,
8755
9209
  SOCIAL_PROVIDERS,
9210
+ SocialAccountsRepository,
8756
9211
  TargetTypeSchema,
8757
9212
  USER_STATUSES,
8758
9213
  UserPermissionsRepository,
@@ -8765,19 +9220,24 @@ export {
8765
9220
  acceptInvitation,
8766
9221
  addPermissionToRole,
8767
9222
  authLogger,
9223
+ authLoginEvent,
9224
+ authRegisterEvent,
8768
9225
  mainAuthRouter as authRouter,
8769
9226
  authSchema,
8770
9227
  authenticate,
9228
+ buildOAuthErrorUrl,
8771
9229
  cancelInvitation,
8772
9230
  changePasswordService,
8773
9231
  checkAccountExistsService,
8774
9232
  configureAuth,
8775
9233
  createAuthLifecycle,
8776
9234
  createInvitation,
9235
+ createOAuthState,
8777
9236
  createRole,
8778
9237
  decodeToken,
8779
9238
  deleteInvitation,
8780
9239
  deleteRole,
9240
+ exchangeCodeForTokens,
8781
9241
  expireOldInvitations,
8782
9242
  generateClientToken,
8783
9243
  generateKeyPair,
@@ -8788,12 +9248,15 @@ export {
8788
9248
  getAuth,
8789
9249
  getAuthConfig,
8790
9250
  getAuthSessionService,
9251
+ getEnabledOAuthProviders,
9252
+ getGoogleAccessToken,
9253
+ getGoogleAuthUrl,
9254
+ getGoogleOAuthConfig,
9255
+ getGoogleUserInfo,
8791
9256
  getInvitationByToken,
8792
- getInvitationTemplate,
8793
9257
  getInvitationWithDetails,
8794
9258
  getKeyId,
8795
9259
  getKeySize,
8796
- getPasswordResetTemplate,
8797
9260
  getRoleByName,
8798
9261
  getRolePermissions,
8799
9262
  getSessionInfo,
@@ -8805,8 +9268,7 @@ export {
8805
9268
  getUserId,
8806
9269
  getUserPermissions,
8807
9270
  getUserProfileService,
8808
- getVerificationCodeTemplate,
8809
- getWelcomeTemplate,
9271
+ getUserRole,
8810
9272
  hasAllPermissions,
8811
9273
  hasAnyPermission,
8812
9274
  hasAnyRole,
@@ -8815,17 +9277,19 @@ export {
8815
9277
  hashPassword,
8816
9278
  initializeAuth,
8817
9279
  invitationsRepository,
9280
+ isGoogleOAuthEnabled,
9281
+ isOAuthProviderEnabled,
8818
9282
  keysRepository,
8819
9283
  listInvitations,
8820
9284
  loginService,
8821
9285
  logoutService,
9286
+ oauthCallbackService,
9287
+ oauthStartService,
8822
9288
  parseDuration,
8823
9289
  permissions,
8824
9290
  permissionsRepository,
8825
- registerEmailProvider,
8826
- registerEmailTemplates,
9291
+ refreshAccessToken,
8827
9292
  registerPublicKeyService,
8828
- registerSMSProvider,
8829
9293
  registerService,
8830
9294
  removePermissionFromRole,
8831
9295
  requireAnyPermission,
@@ -8833,21 +9297,22 @@ export {
8833
9297
  requireRole,
8834
9298
  resendInvitation,
8835
9299
  revokeKeyService,
9300
+ roleGuard,
8836
9301
  rolePermissions,
8837
9302
  rolePermissionsRepository,
8838
9303
  roles,
8839
9304
  rolesRepository,
8840
9305
  rotateKeyService,
8841
9306
  sealSession,
8842
- sendEmail,
8843
- sendSMS,
8844
9307
  sendVerificationCodeService,
8845
9308
  setRolePermissions,
8846
9309
  shouldRefreshSession,
8847
9310
  shouldRotateKey,
9311
+ socialAccountsRepository,
8848
9312
  unsealSession,
8849
9313
  updateLastLoginService,
8850
9314
  updateRole,
9315
+ updateUserProfileService,
8851
9316
  updateUserService,
8852
9317
  userInvitations,
8853
9318
  userPermissions,
@@ -8865,6 +9330,7 @@ export {
8865
9330
  verifyClientToken,
8866
9331
  verifyCodeService,
8867
9332
  verifyKeyFingerprint,
9333
+ verifyOAuthState,
8868
9334
  verifyPassword,
8869
9335
  verifyToken
8870
9336
  };