@spfn/auth 0.2.0-beta.10 → 0.2.0-beta.12

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
  };
@@ -6413,6 +6407,96 @@ var init_invitations_repository = __esm({
6413
6407
  }
6414
6408
  });
6415
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
+
6416
6500
  // src/server/repositories/index.ts
6417
6501
  var init_repositories = __esm({
6418
6502
  "src/server/repositories/index.ts"() {
@@ -6426,6 +6510,7 @@ var init_repositories = __esm({
6426
6510
  init_user_permissions_repository();
6427
6511
  init_user_profiles_repository();
6428
6512
  init_invitations_repository();
6513
+ init_social_accounts_repository();
6429
6514
  }
6430
6515
  });
6431
6516
 
@@ -6552,7 +6637,7 @@ var init_role_service = __esm({
6552
6637
  import "@spfn/auth/config";
6553
6638
 
6554
6639
  // src/server/routes/index.ts
6555
- import { defineRouter as defineRouter4 } from "@spfn/core/route";
6640
+ import { defineRouter as defineRouter5 } from "@spfn/core/route";
6556
6641
 
6557
6642
  // src/server/routes/auth/index.ts
6558
6643
  init_schema3();
@@ -6701,9 +6786,10 @@ import {
6701
6786
  } from "@spfn/auth/errors";
6702
6787
 
6703
6788
  // src/server/services/verification.service.ts
6704
- import { env as env5 } from "@spfn/auth/config";
6789
+ import { env as env3 } from "@spfn/auth/config";
6705
6790
  import { InvalidVerificationCodeError } from "@spfn/auth/errors";
6706
6791
  import jwt2 from "jsonwebtoken";
6792
+ import { sendEmail, sendSMS } from "@spfn/notification/server";
6707
6793
 
6708
6794
  // src/server/logger.ts
6709
6795
  import { logger as rootLogger } from "@spfn/core/logger";
@@ -6723,425 +6809,6 @@ var authLogger = {
6723
6809
 
6724
6810
  // src/server/services/verification.service.ts
6725
6811
  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
- return {
6879
- name: "aws-ses",
6880
- sendEmail: async (params) => {
6881
- const { to, subject, text: text10, html, purpose } = params;
6882
- if (!isValidEmail(to)) {
6883
- return {
6884
- success: false,
6885
- error: "Invalid email address format"
6886
- };
6887
- }
6888
- if (!env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID) {
6889
- authLogger.email.warn("AWS SES credentials not configured", {
6890
- hint: "Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable"
6891
- });
6892
- return {
6893
- success: false,
6894
- error: "AWS SES credentials not configured. Set SPFN_AUTH_AWS_SES_ACCESS_KEY_ID environment variable."
6895
- };
6896
- }
6897
- if (!env4.SPFN_AUTH_AWS_SES_FROM_EMAIL) {
6898
- authLogger.email.warn("AWS SES sender email not configured", {
6899
- hint: "Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable"
6900
- });
6901
- return {
6902
- success: false,
6903
- error: "AWS SES sender email not configured. Set SPFN_AUTH_AWS_SES_FROM_EMAIL environment variable."
6904
- };
6905
- }
6906
- let SESClient;
6907
- let SendEmailCommand;
6908
- try {
6909
- const ses = await import("@aws-sdk/client-ses");
6910
- SESClient = ses.SESClient;
6911
- SendEmailCommand = ses.SendEmailCommand;
6912
- } catch (error) {
6913
- authLogger.email.warn("@aws-sdk/client-ses not installed", {
6914
- error: error instanceof Error ? error.message : String(error),
6915
- hint: "Run: pnpm add @aws-sdk/client-ses"
6916
- });
6917
- return {
6918
- success: false,
6919
- error: "@aws-sdk/client-ses not installed. Run: pnpm add @aws-sdk/client-ses"
6920
- };
6921
- }
6922
- try {
6923
- const config = {
6924
- region: env4.SPFN_AUTH_AWS_REGION || "ap-northeast-2"
6925
- };
6926
- if (env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID && env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY) {
6927
- config.credentials = {
6928
- accessKeyId: env4.SPFN_AUTH_AWS_SES_ACCESS_KEY_ID,
6929
- secretAccessKey: env4.SPFN_AUTH_AWS_SES_SECRET_ACCESS_KEY
6930
- };
6931
- }
6932
- const client = new SESClient(config);
6933
- const body = {};
6934
- if (text10) {
6935
- body.Text = {
6936
- Charset: "UTF-8",
6937
- Data: text10
6938
- };
6939
- }
6940
- if (html) {
6941
- body.Html = {
6942
- Charset: "UTF-8",
6943
- Data: html
6944
- };
6945
- }
6946
- const command = new SendEmailCommand({
6947
- Source: env4.SPFN_AUTH_AWS_SES_FROM_EMAIL,
6948
- Destination: {
6949
- ToAddresses: [to]
6950
- },
6951
- Message: {
6952
- Subject: {
6953
- Charset: "UTF-8",
6954
- Data: subject
6955
- },
6956
- Body: body
6957
- }
6958
- });
6959
- const response = await client.send(command);
6960
- authLogger.email.info("Email sent via AWS SES", {
6961
- to,
6962
- messageId: response.MessageId,
6963
- purpose: purpose || "N/A"
6964
- });
6965
- return {
6966
- success: true,
6967
- messageId: response.MessageId
6968
- };
6969
- } catch (error) {
6970
- const err = error;
6971
- authLogger.email.error("Failed to send email via AWS SES", {
6972
- to,
6973
- error: err.message
6974
- });
6975
- return {
6976
- success: false,
6977
- error: err.message || "Failed to send email via AWS SES"
6978
- };
6979
- }
6980
- }
6981
- };
6982
- }
6983
- var awsSESProvider = createAWSSESProvider();
6984
-
6985
- // src/server/services/email/index.ts
6986
- registerEmailProvider(awsSESProvider);
6987
-
6988
- // src/server/services/email/templates/verification-code.ts
6989
- function getSubject(purpose) {
6990
- switch (purpose) {
6991
- case "registration":
6992
- return "Verify your email address";
6993
- case "login":
6994
- return "Your login verification code";
6995
- case "password_reset":
6996
- return "Reset your password";
6997
- default:
6998
- return "Your verification code";
6999
- }
7000
- }
7001
- function getPurposeText(purpose) {
7002
- switch (purpose) {
7003
- case "registration":
7004
- return "complete your registration";
7005
- case "login":
7006
- return "verify your identity";
7007
- case "password_reset":
7008
- return "reset your password";
7009
- default:
7010
- return "verify your identity";
7011
- }
7012
- }
7013
- function generateText(params) {
7014
- const { code, expiresInMinutes = 5 } = params;
7015
- return `Your verification code is: ${code}
7016
-
7017
- This code will expire in ${expiresInMinutes} minutes.
7018
-
7019
- If you didn't request this code, please ignore this email.`;
7020
- }
7021
- function generateHTML(params) {
7022
- const { code, purpose, expiresInMinutes = 5, appName } = params;
7023
- const purposeText = getPurposeText(purpose);
7024
- return `<!DOCTYPE html>
7025
- <html>
7026
- <head>
7027
- <meta charset="utf-8">
7028
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7029
- <title>Verification Code</title>
7030
- </head>
7031
- <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;">
7032
- <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
7033
- <h1 style="color: white; margin: 0; font-size: 24px;">${appName ? appName : "Verification Code"}</h1>
7034
- </div>
7035
- <div style="background: #ffffff; padding: 30px; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 10px 10px;">
7036
- <p style="margin-bottom: 20px; font-size: 16px;">
7037
- Please use the following verification code to ${purposeText}:
7038
- </p>
7039
- <div style="background: #f8f9fa; padding: 25px; border-radius: 8px; text-align: center; margin: 25px 0; border: 2px dashed #dee2e6;">
7040
- <span style="font-size: 36px; font-weight: bold; letter-spacing: 10px; color: #333; font-family: 'Courier New', monospace;">${code}</span>
7041
- </div>
7042
- <p style="color: #666; font-size: 14px; margin-top: 20px; text-align: center;">
7043
- <strong>This code will expire in ${expiresInMinutes} minutes.</strong>
7044
- </p>
7045
- <hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
7046
- <p style="color: #999; font-size: 12px; text-align: center; margin: 0;">
7047
- If you didn't request this code, please ignore this email.
7048
- </p>
7049
- </div>
7050
- <div style="text-align: center; padding: 20px; color: #999; font-size: 11px;">
7051
- <p style="margin: 0;">This is an automated message. Please do not reply.</p>
7052
- </div>
7053
- </body>
7054
- </html>`;
7055
- }
7056
- function verificationCodeTemplate(params) {
7057
- return {
7058
- subject: getSubject(params.purpose),
7059
- text: generateText(params),
7060
- html: generateHTML(params)
7061
- };
7062
- }
7063
-
7064
- // src/server/services/email/templates/registry.ts
7065
- var customTemplates = {};
7066
- function registerEmailTemplates(templates) {
7067
- customTemplates = { ...customTemplates, ...templates };
7068
- authLogger.email.info("Registered custom email templates", {
7069
- templates: Object.keys(templates)
7070
- });
7071
- }
7072
- function getVerificationCodeTemplate(params) {
7073
- if (customTemplates.verificationCode) {
7074
- return customTemplates.verificationCode(params);
7075
- }
7076
- return verificationCodeTemplate(params);
7077
- }
7078
- function getWelcomeTemplate(params) {
7079
- if (customTemplates.welcome) {
7080
- return customTemplates.welcome(params);
7081
- }
7082
- return {
7083
- subject: params.appName ? `Welcome to ${params.appName}!` : "Welcome!",
7084
- text: `Welcome! Your account has been created successfully.`,
7085
- html: `
7086
- <!DOCTYPE html>
7087
- <html>
7088
- <head><meta charset="utf-8"></head>
7089
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7090
- <h1>Welcome${params.appName ? ` to ${params.appName}` : ""}!</h1>
7091
- <p>Your account has been created successfully.</p>
7092
- </body>
7093
- </html>`
7094
- };
7095
- }
7096
- function getPasswordResetTemplate(params) {
7097
- if (customTemplates.passwordReset) {
7098
- return customTemplates.passwordReset(params);
7099
- }
7100
- const expires = params.expiresInMinutes || 30;
7101
- return {
7102
- subject: "Reset your password",
7103
- text: `Click this link to reset your password: ${params.resetLink}
7104
-
7105
- This link will expire in ${expires} minutes.`,
7106
- html: `
7107
- <!DOCTYPE html>
7108
- <html>
7109
- <head><meta charset="utf-8"></head>
7110
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7111
- <h1>Reset Your Password</h1>
7112
- <p>Click the button below to reset your password:</p>
7113
- <a href="${params.resetLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Reset Password</a>
7114
- <p style="color: #666; margin-top: 20px;">This link will expire in ${expires} minutes.</p>
7115
- </body>
7116
- </html>`
7117
- };
7118
- }
7119
- function getInvitationTemplate(params) {
7120
- if (customTemplates.invitation) {
7121
- return customTemplates.invitation(params);
7122
- }
7123
- const appName = params.appName || "our platform";
7124
- const inviterText = params.inviterName ? `${params.inviterName} has invited you` : "You have been invited";
7125
- const roleText = params.roleName ? ` as ${params.roleName}` : "";
7126
- return {
7127
- subject: `You're invited to join ${appName}`,
7128
- text: `${inviterText} to join ${appName}${roleText}.
7129
-
7130
- Click here to accept: ${params.inviteLink}`,
7131
- html: `
7132
- <!DOCTYPE html>
7133
- <html>
7134
- <head><meta charset="utf-8"></head>
7135
- <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 20px;">
7136
- <h1>You're Invited!</h1>
7137
- <p>${inviterText} to join <strong>${appName}</strong>${roleText}.</p>
7138
- <a href="${params.inviteLink}" style="display: inline-block; background: #667eea; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Accept Invitation</a>
7139
- </body>
7140
- </html>`
7141
- };
7142
- }
7143
-
7144
- // src/server/services/verification.service.ts
7145
6812
  var VERIFICATION_TOKEN_EXPIRY = "15m";
7146
6813
  var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
7147
6814
  var MAX_VERIFICATION_ATTEMPTS = 5;
@@ -7185,7 +6852,7 @@ async function markCodeAsUsed(codeId) {
7185
6852
  await verificationCodesRepository.markAsUsed(codeId);
7186
6853
  }
7187
6854
  function createVerificationToken(payload) {
7188
- return jwt2.sign(payload, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6855
+ return jwt2.sign(payload, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7189
6856
  expiresIn: VERIFICATION_TOKEN_EXPIRY,
7190
6857
  issuer: "spfn-auth",
7191
6858
  audience: "spfn-client"
@@ -7193,7 +6860,7 @@ function createVerificationToken(payload) {
7193
6860
  }
7194
6861
  function validateVerificationToken(token) {
7195
6862
  try {
7196
- const decoded = jwt2.verify(token, env5.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
6863
+ const decoded = jwt2.verify(token, env3.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
7197
6864
  issuer: "spfn-auth",
7198
6865
  audience: "spfn-client"
7199
6866
  });
@@ -7207,17 +6874,14 @@ function validateVerificationToken(token) {
7207
6874
  }
7208
6875
  }
7209
6876
  async function sendVerificationEmail(email, code, purpose) {
7210
- const { subject, text: text10, html } = getVerificationCodeTemplate({
7211
- code,
7212
- purpose,
7213
- expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
7214
- });
7215
6877
  const result = await sendEmail({
7216
6878
  to: email,
7217
- subject,
7218
- text: text10,
7219
- html,
7220
- purpose
6879
+ template: "verification-code",
6880
+ data: {
6881
+ code,
6882
+ purpose,
6883
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
6884
+ }
7221
6885
  });
7222
6886
  if (!result.success) {
7223
6887
  authLogger.email.error("Failed to send verification email", {
@@ -7228,11 +6892,13 @@ async function sendVerificationEmail(email, code, purpose) {
7228
6892
  }
7229
6893
  }
7230
6894
  async function sendVerificationSMS(phone, code, purpose) {
7231
- const message = `Your verification code is: ${code}`;
7232
6895
  const result = await sendSMS({
7233
- phone,
7234
- message,
7235
- purpose
6896
+ to: phone,
6897
+ template: "verification-code",
6898
+ data: {
6899
+ code,
6900
+ expiresInMinutes: VERIFICATION_CODE_EXPIRY_MINUTES
6901
+ }
7236
6902
  });
7237
6903
  if (!result.success) {
7238
6904
  authLogger.sms.error("Failed to send verification SMS", {
@@ -7495,12 +7161,14 @@ init_repositories();
7495
7161
  init_rbac();
7496
7162
 
7497
7163
  // src/server/lib/config.ts
7498
- import { env as env6 } from "@spfn/auth/config";
7164
+ import { env as env4 } from "@spfn/auth/config";
7499
7165
  var COOKIE_NAMES = {
7500
7166
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
7501
7167
  SESSION: "spfn_session",
7502
7168
  /** Current key ID (for key rotation) */
7503
- SESSION_KEY_ID: "spfn_session_key_id"
7169
+ SESSION_KEY_ID: "spfn_session_key_id",
7170
+ /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
7171
+ OAUTH_PENDING: "spfn_oauth_pending"
7504
7172
  };
7505
7173
  function parseDuration(duration) {
7506
7174
  if (typeof duration === "number") {
@@ -7545,7 +7213,7 @@ function getSessionTtl(override) {
7545
7213
  if (globalConfig.sessionTtl !== void 0) {
7546
7214
  return parseDuration(globalConfig.sessionTtl);
7547
7215
  }
7548
- const envTtl = env6.SPFN_AUTH_SESSION_TTL;
7216
+ const envTtl = env4.SPFN_AUTH_SESSION_TTL;
7549
7217
  if (envTtl) {
7550
7218
  return parseDuration(envTtl);
7551
7219
  }
@@ -7985,6 +7653,301 @@ async function updateUserProfileService(userId, params) {
7985
7653
  return profile;
7986
7654
  }
7987
7655
 
7656
+ // src/server/services/oauth.service.ts
7657
+ init_repositories();
7658
+ import { env as env7 } from "@spfn/auth/config";
7659
+ import { ValidationError as ValidationError2 } from "@spfn/core/errors";
7660
+
7661
+ // src/server/lib/oauth/google.ts
7662
+ import { env as env5 } from "@spfn/auth/config";
7663
+ var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
7664
+ var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
7665
+ var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
7666
+ function isGoogleOAuthEnabled() {
7667
+ return !!(env5.SPFN_AUTH_GOOGLE_CLIENT_ID && env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
7668
+ }
7669
+ function getGoogleOAuthConfig() {
7670
+ const clientId = env5.SPFN_AUTH_GOOGLE_CLIENT_ID;
7671
+ const clientSecret = env5.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
7672
+ if (!clientId || !clientSecret) {
7673
+ throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
7674
+ }
7675
+ const redirectUri = env5.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${env5.SPFN_API_URL}/_auth/oauth/google/callback`;
7676
+ return {
7677
+ clientId,
7678
+ clientSecret,
7679
+ redirectUri
7680
+ };
7681
+ }
7682
+ function getGoogleAuthUrl(state, scopes = ["email", "profile"]) {
7683
+ const config = getGoogleOAuthConfig();
7684
+ const params = new URLSearchParams({
7685
+ client_id: config.clientId,
7686
+ redirect_uri: config.redirectUri,
7687
+ response_type: "code",
7688
+ scope: scopes.join(" "),
7689
+ state,
7690
+ access_type: "offline",
7691
+ // refresh_token 받기 위해
7692
+ prompt: "consent"
7693
+ // 매번 동의 화면 표시 (refresh_token 보장)
7694
+ });
7695
+ return `${GOOGLE_AUTH_URL}?${params.toString()}`;
7696
+ }
7697
+ async function exchangeCodeForTokens(code) {
7698
+ const config = getGoogleOAuthConfig();
7699
+ const response = await fetch(GOOGLE_TOKEN_URL, {
7700
+ method: "POST",
7701
+ headers: {
7702
+ "Content-Type": "application/x-www-form-urlencoded"
7703
+ },
7704
+ body: new URLSearchParams({
7705
+ client_id: config.clientId,
7706
+ client_secret: config.clientSecret,
7707
+ redirect_uri: config.redirectUri,
7708
+ grant_type: "authorization_code",
7709
+ code
7710
+ })
7711
+ });
7712
+ if (!response.ok) {
7713
+ const error = await response.text();
7714
+ throw new Error(`Failed to exchange code for tokens: ${error}`);
7715
+ }
7716
+ return response.json();
7717
+ }
7718
+ async function getGoogleUserInfo(accessToken) {
7719
+ const response = await fetch(GOOGLE_USERINFO_URL, {
7720
+ headers: {
7721
+ Authorization: `Bearer ${accessToken}`
7722
+ }
7723
+ });
7724
+ if (!response.ok) {
7725
+ const error = await response.text();
7726
+ throw new Error(`Failed to get user info: ${error}`);
7727
+ }
7728
+ return response.json();
7729
+ }
7730
+ async function refreshAccessToken(refreshToken) {
7731
+ const config = getGoogleOAuthConfig();
7732
+ const response = await fetch(GOOGLE_TOKEN_URL, {
7733
+ method: "POST",
7734
+ headers: {
7735
+ "Content-Type": "application/x-www-form-urlencoded"
7736
+ },
7737
+ body: new URLSearchParams({
7738
+ client_id: config.clientId,
7739
+ client_secret: config.clientSecret,
7740
+ refresh_token: refreshToken,
7741
+ grant_type: "refresh_token"
7742
+ })
7743
+ });
7744
+ if (!response.ok) {
7745
+ const error = await response.text();
7746
+ throw new Error(`Failed to refresh access token: ${error}`);
7747
+ }
7748
+ return response.json();
7749
+ }
7750
+
7751
+ // src/server/lib/oauth/state.ts
7752
+ import * as jose from "jose";
7753
+ import { env as env6 } from "@spfn/auth/config";
7754
+ async function getStateKey() {
7755
+ const secret = env6.SPFN_AUTH_SESSION_SECRET;
7756
+ const encoder = new TextEncoder();
7757
+ const data = encoder.encode(`oauth-state:${secret}`);
7758
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
7759
+ return new Uint8Array(hashBuffer);
7760
+ }
7761
+ function generateNonce() {
7762
+ const array = new Uint8Array(16);
7763
+ crypto.getRandomValues(array);
7764
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
7765
+ }
7766
+ async function createOAuthState(params) {
7767
+ const key = await getStateKey();
7768
+ const state = {
7769
+ returnUrl: params.returnUrl,
7770
+ nonce: generateNonce(),
7771
+ provider: params.provider,
7772
+ publicKey: params.publicKey,
7773
+ keyId: params.keyId,
7774
+ fingerprint: params.fingerprint,
7775
+ algorithm: params.algorithm
7776
+ };
7777
+ const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
7778
+ return encodeURIComponent(jwe);
7779
+ }
7780
+ async function verifyOAuthState(encryptedState) {
7781
+ const key = await getStateKey();
7782
+ const jwe = decodeURIComponent(encryptedState);
7783
+ const { payload } = await jose.jwtDecrypt(jwe, key);
7784
+ return payload.state;
7785
+ }
7786
+
7787
+ // src/server/services/oauth.service.ts
7788
+ async function oauthStartService(params) {
7789
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm } = params;
7790
+ if (provider === "google") {
7791
+ if (!isGoogleOAuthEnabled()) {
7792
+ throw new ValidationError2({
7793
+ message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
7794
+ });
7795
+ }
7796
+ const state = await createOAuthState({
7797
+ provider: "google",
7798
+ returnUrl,
7799
+ publicKey,
7800
+ keyId,
7801
+ fingerprint,
7802
+ algorithm
7803
+ });
7804
+ const authUrl = getGoogleAuthUrl(state);
7805
+ return { authUrl };
7806
+ }
7807
+ throw new ValidationError2({
7808
+ message: `Unsupported OAuth provider: ${provider}`
7809
+ });
7810
+ }
7811
+ async function oauthCallbackService(params) {
7812
+ const { provider, code, state } = params;
7813
+ const stateData = await verifyOAuthState(state);
7814
+ if (stateData.provider !== provider) {
7815
+ throw new ValidationError2({
7816
+ message: "OAuth state provider mismatch"
7817
+ });
7818
+ }
7819
+ if (provider === "google") {
7820
+ return handleGoogleCallback(code, stateData);
7821
+ }
7822
+ throw new ValidationError2({
7823
+ message: `Unsupported OAuth provider: ${provider}`
7824
+ });
7825
+ }
7826
+ async function handleGoogleCallback(code, stateData) {
7827
+ const tokens = await exchangeCodeForTokens(code);
7828
+ const googleUser = await getGoogleUserInfo(tokens.access_token);
7829
+ const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
7830
+ "google",
7831
+ googleUser.id
7832
+ );
7833
+ let userId;
7834
+ let isNewUser = false;
7835
+ if (existingSocialAccount) {
7836
+ userId = existingSocialAccount.userId;
7837
+ await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
7838
+ accessToken: tokens.access_token,
7839
+ refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
7840
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
7841
+ });
7842
+ } else {
7843
+ const result = await createOrLinkUser(googleUser, tokens);
7844
+ userId = result.userId;
7845
+ isNewUser = result.isNewUser;
7846
+ }
7847
+ await registerPublicKeyService({
7848
+ userId,
7849
+ keyId: stateData.keyId,
7850
+ publicKey: stateData.publicKey,
7851
+ fingerprint: stateData.fingerprint,
7852
+ algorithm: stateData.algorithm
7853
+ });
7854
+ await updateLastLoginService(userId);
7855
+ const appUrl = env7.SPFN_APP_URL;
7856
+ const callbackPath = env7.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
7857
+ const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
7858
+ const redirectUrl = buildRedirectUrl(callbackUrl, {
7859
+ userId: String(userId),
7860
+ keyId: stateData.keyId,
7861
+ returnUrl: stateData.returnUrl,
7862
+ isNewUser: String(isNewUser)
7863
+ });
7864
+ return {
7865
+ redirectUrl,
7866
+ userId: String(userId),
7867
+ keyId: stateData.keyId,
7868
+ isNewUser
7869
+ };
7870
+ }
7871
+ async function createOrLinkUser(googleUser, tokens) {
7872
+ const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
7873
+ let userId;
7874
+ let isNewUser = false;
7875
+ if (existingUser) {
7876
+ if (!googleUser.verified_email) {
7877
+ throw new ValidationError2({
7878
+ message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
7879
+ });
7880
+ }
7881
+ userId = existingUser.id;
7882
+ if (!existingUser.emailVerifiedAt) {
7883
+ await usersRepository.updateById(existingUser.id, {
7884
+ emailVerifiedAt: /* @__PURE__ */ new Date()
7885
+ });
7886
+ }
7887
+ } else {
7888
+ const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
7889
+ const userRole = await getRoleByName3("user");
7890
+ if (!userRole) {
7891
+ throw new Error("Default user role not found. Run initializeAuth() first.");
7892
+ }
7893
+ const newUser = await usersRepository.create({
7894
+ email: googleUser.verified_email ? googleUser.email : null,
7895
+ phone: null,
7896
+ passwordHash: null,
7897
+ // OAuth 사용자는 비밀번호 없음
7898
+ passwordChangeRequired: false,
7899
+ roleId: userRole.id,
7900
+ status: "active",
7901
+ emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
7902
+ });
7903
+ userId = newUser.id;
7904
+ isNewUser = true;
7905
+ }
7906
+ await socialAccountsRepository.create({
7907
+ userId,
7908
+ provider: "google",
7909
+ providerUserId: googleUser.id,
7910
+ providerEmail: googleUser.email,
7911
+ accessToken: tokens.access_token,
7912
+ refreshToken: tokens.refresh_token ?? null,
7913
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
7914
+ });
7915
+ return { userId, isNewUser };
7916
+ }
7917
+ function buildRedirectUrl(baseUrl, params) {
7918
+ const url = new URL(baseUrl, "http://placeholder");
7919
+ for (const [key, value] of Object.entries(params)) {
7920
+ url.searchParams.set(key, value);
7921
+ }
7922
+ if (baseUrl.startsWith("http")) {
7923
+ return url.toString();
7924
+ }
7925
+ return `${url.pathname}${url.search}`;
7926
+ }
7927
+ function buildOAuthErrorUrl(error) {
7928
+ const errorUrl = env7.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
7929
+ return errorUrl.replace("{error}", encodeURIComponent(error));
7930
+ }
7931
+ function isOAuthProviderEnabled(provider) {
7932
+ switch (provider) {
7933
+ case "google":
7934
+ return isGoogleOAuthEnabled();
7935
+ case "github":
7936
+ case "kakao":
7937
+ case "naver":
7938
+ return false;
7939
+ default:
7940
+ return false;
7941
+ }
7942
+ }
7943
+ function getEnabledOAuthProviders() {
7944
+ const providers = [];
7945
+ if (isGoogleOAuthEnabled()) {
7946
+ providers.push("google");
7947
+ }
7948
+ return providers;
7949
+ }
7950
+
7988
7951
  // src/server/routes/auth/index.ts
7989
7952
  init_esm();
7990
7953
  import { Transactional } from "@spfn/core/db";
@@ -8595,12 +8558,163 @@ var userRouter = defineRouter3({
8595
8558
  updateUserProfile
8596
8559
  });
8597
8560
 
8561
+ // src/server/routes/oauth/index.ts
8562
+ init_esm();
8563
+ init_types();
8564
+ import { Transactional as Transactional2 } from "@spfn/core/db";
8565
+ import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
8566
+ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
8567
+ query: Type.Object({
8568
+ state: Type.String({
8569
+ description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
8570
+ })
8571
+ })
8572
+ }).skip(["auth"]).handler(async (c) => {
8573
+ const { query } = await c.data();
8574
+ if (!isGoogleOAuthEnabled()) {
8575
+ return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
8576
+ }
8577
+ const authUrl = getGoogleAuthUrl(query.state);
8578
+ return c.redirect(authUrl);
8579
+ });
8580
+ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
8581
+ query: Type.Object({
8582
+ code: Type.Optional(Type.String({
8583
+ description: "Authorization code from Google"
8584
+ })),
8585
+ state: Type.Optional(Type.String({
8586
+ description: "OAuth state parameter"
8587
+ })),
8588
+ error: Type.Optional(Type.String({
8589
+ description: "Error code from Google"
8590
+ })),
8591
+ error_description: Type.Optional(Type.String({
8592
+ description: "Error description from Google"
8593
+ }))
8594
+ })
8595
+ }).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
8596
+ const { query } = await c.data();
8597
+ if (query.error) {
8598
+ const errorMessage = query.error_description || query.error;
8599
+ return c.redirect(buildOAuthErrorUrl(errorMessage));
8600
+ }
8601
+ if (!query.code || !query.state) {
8602
+ return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
8603
+ }
8604
+ try {
8605
+ const result = await oauthCallbackService({
8606
+ provider: "google",
8607
+ code: query.code,
8608
+ state: query.state
8609
+ });
8610
+ return c.redirect(result.redirectUrl);
8611
+ } catch (err) {
8612
+ const message = err instanceof Error ? err.message : "OAuth callback failed";
8613
+ return c.redirect(buildOAuthErrorUrl(message));
8614
+ }
8615
+ });
8616
+ var oauthStart = route4.post("/_auth/oauth/start").input({
8617
+ body: Type.Object({
8618
+ provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
8619
+ description: "OAuth provider (google, github, kakao, naver)"
8620
+ }),
8621
+ returnUrl: Type.String({
8622
+ description: "URL to redirect after OAuth success"
8623
+ }),
8624
+ publicKey: Type.String({
8625
+ description: "Client public key (Base64 DER)"
8626
+ }),
8627
+ keyId: Type.String({
8628
+ description: "Key identifier (UUID)"
8629
+ }),
8630
+ fingerprint: Type.String({
8631
+ description: "Key fingerprint (SHA-256 hex)"
8632
+ }),
8633
+ algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
8634
+ description: "Key algorithm (ES256 or RS256)"
8635
+ })
8636
+ })
8637
+ }).skip(["auth"]).handler(async (c) => {
8638
+ const { body } = await c.data();
8639
+ const result = await oauthStartService(body);
8640
+ return result;
8641
+ });
8642
+ var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
8643
+ return {
8644
+ providers: getEnabledOAuthProviders()
8645
+ };
8646
+ });
8647
+ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
8648
+ body: Type.Object({
8649
+ returnUrl: Type.Optional(Type.String({
8650
+ description: "URL to redirect after OAuth success"
8651
+ })),
8652
+ state: Type.Optional(Type.String({
8653
+ description: "Encrypted OAuth state (injected by interceptor)"
8654
+ }))
8655
+ })
8656
+ }).skip(["auth"]).handler(async (c) => {
8657
+ const { body } = await c.data();
8658
+ if (!isGoogleOAuthEnabled()) {
8659
+ throw new Error("Google OAuth is not configured");
8660
+ }
8661
+ if (!body.state) {
8662
+ throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
8663
+ }
8664
+ return { authUrl: getGoogleAuthUrl(body.state) };
8665
+ });
8666
+ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
8667
+ body: Type.Object({
8668
+ userId: Type.String({ description: "User ID from OAuth callback" }),
8669
+ keyId: Type.String({ description: "Key ID from OAuth state" }),
8670
+ returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
8671
+ })
8672
+ }).skip(["auth"]).handler(async (c) => {
8673
+ const { body } = await c.data();
8674
+ return {
8675
+ success: true,
8676
+ returnUrl: body.returnUrl || "/"
8677
+ };
8678
+ });
8679
+ var oauthRouter = defineRouter4({
8680
+ oauthGoogleStart,
8681
+ oauthGoogleCallback,
8682
+ oauthStart,
8683
+ oauthProviders,
8684
+ getGoogleOAuthUrl,
8685
+ oauthFinalize
8686
+ });
8687
+
8598
8688
  // src/server/routes/index.ts
8599
- var mainAuthRouter = defineRouter4({
8600
- // Flatten all routes at root level
8601
- ...authRouter.routes,
8602
- ...invitationRouter.routes,
8603
- ...userRouter.routes
8689
+ var mainAuthRouter = defineRouter5({
8690
+ // Auth routes
8691
+ checkAccountExists,
8692
+ sendVerificationCode,
8693
+ verifyCode,
8694
+ register,
8695
+ login,
8696
+ logout,
8697
+ rotateKey,
8698
+ changePassword,
8699
+ getAuthSession,
8700
+ // OAuth routes
8701
+ oauthGoogleStart,
8702
+ oauthGoogleCallback,
8703
+ oauthStart,
8704
+ oauthProviders,
8705
+ getGoogleOAuthUrl,
8706
+ oauthFinalize,
8707
+ // Invitation routes
8708
+ getInvitation,
8709
+ acceptInvitation: acceptInvitation2,
8710
+ createInvitation: createInvitation2,
8711
+ listInvitations: listInvitations2,
8712
+ cancelInvitation: cancelInvitation2,
8713
+ resendInvitation: resendInvitation2,
8714
+ deleteInvitation: deleteInvitation2,
8715
+ // User routes
8716
+ getUserProfile,
8717
+ updateUserProfile
8604
8718
  });
8605
8719
 
8606
8720
  // src/server.ts
@@ -8710,11 +8824,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
8710
8824
  }
8711
8825
 
8712
8826
  // src/server/lib/session.ts
8713
- import * as jose from "jose";
8714
- import { env as env7 } from "@spfn/auth/config";
8827
+ import * as jose2 from "jose";
8828
+ import { env as env8 } from "@spfn/auth/config";
8715
8829
  import { env as coreEnv } from "@spfn/core/config";
8716
8830
  async function getSessionSecretKey() {
8717
- const secret = env7.SPFN_AUTH_SESSION_SECRET;
8831
+ const secret = env8.SPFN_AUTH_SESSION_SECRET;
8718
8832
  const encoder = new TextEncoder();
8719
8833
  const data = encoder.encode(secret);
8720
8834
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -8722,24 +8836,24 @@ async function getSessionSecretKey() {
8722
8836
  }
8723
8837
  async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
8724
8838
  const secret = await getSessionSecretKey();
8725
- return await new jose.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
8839
+ return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
8726
8840
  }
8727
8841
  async function unsealSession(jwt4) {
8728
8842
  try {
8729
8843
  const secret = await getSessionSecretKey();
8730
- const { payload } = await jose.jwtDecrypt(jwt4, secret, {
8844
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
8731
8845
  issuer: "spfn-auth",
8732
8846
  audience: "spfn-client"
8733
8847
  });
8734
8848
  return payload.data;
8735
8849
  } catch (err) {
8736
- if (err instanceof jose.errors.JWTExpired) {
8850
+ if (err instanceof jose2.errors.JWTExpired) {
8737
8851
  throw new Error("Session expired");
8738
8852
  }
8739
- if (err instanceof jose.errors.JWEDecryptionFailed) {
8853
+ if (err instanceof jose2.errors.JWEDecryptionFailed) {
8740
8854
  throw new Error("Invalid session");
8741
8855
  }
8742
- if (err instanceof jose.errors.JWTClaimValidationFailed) {
8856
+ if (err instanceof jose2.errors.JWTClaimValidationFailed) {
8743
8857
  throw new Error("Session validation failed");
8744
8858
  }
8745
8859
  throw new Error("Failed to unseal session");
@@ -8748,7 +8862,7 @@ async function unsealSession(jwt4) {
8748
8862
  async function getSessionInfo(jwt4) {
8749
8863
  const secret = await getSessionSecretKey();
8750
8864
  try {
8751
- const { payload } = await jose.jwtDecrypt(jwt4, secret);
8865
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret);
8752
8866
  return {
8753
8867
  issuedAt: new Date(payload.iat * 1e3),
8754
8868
  expiresAt: new Date(payload.exp * 1e3),
@@ -8772,14 +8886,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
8772
8886
  }
8773
8887
 
8774
8888
  // src/server/setup.ts
8775
- import { env as env8 } from "@spfn/auth/config";
8889
+ import { env as env9 } from "@spfn/auth/config";
8776
8890
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
8777
8891
  init_repositories();
8778
8892
  function parseAdminAccounts() {
8779
8893
  const accounts = [];
8780
- if (env8.SPFN_AUTH_ADMIN_ACCOUNTS) {
8894
+ if (env9.SPFN_AUTH_ADMIN_ACCOUNTS) {
8781
8895
  try {
8782
- const accountsJson = env8.SPFN_AUTH_ADMIN_ACCOUNTS;
8896
+ const accountsJson = env9.SPFN_AUTH_ADMIN_ACCOUNTS;
8783
8897
  const parsed = JSON.parse(accountsJson);
8784
8898
  if (!Array.isArray(parsed)) {
8785
8899
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -8806,11 +8920,11 @@ function parseAdminAccounts() {
8806
8920
  return accounts;
8807
8921
  }
8808
8922
  }
8809
- const adminEmails = env8.SPFN_AUTH_ADMIN_EMAILS;
8923
+ const adminEmails = env9.SPFN_AUTH_ADMIN_EMAILS;
8810
8924
  if (adminEmails) {
8811
8925
  const emails = adminEmails.split(",").map((s) => s.trim());
8812
- const passwords = (env8.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8813
- const roles2 = (env8.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
8926
+ const passwords = (env9.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8927
+ const roles2 = (env9.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
8814
8928
  if (passwords.length !== emails.length) {
8815
8929
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
8816
8930
  return accounts;
@@ -8832,8 +8946,8 @@ function parseAdminAccounts() {
8832
8946
  }
8833
8947
  return accounts;
8834
8948
  }
8835
- const adminEmail = env8.SPFN_AUTH_ADMIN_EMAIL;
8836
- const adminPassword = env8.SPFN_AUTH_ADMIN_PASSWORD;
8949
+ const adminEmail = env9.SPFN_AUTH_ADMIN_EMAIL;
8950
+ const adminPassword = env9.SPFN_AUTH_ADMIN_PASSWORD;
8837
8951
  if (adminEmail && adminPassword) {
8838
8952
  accounts.push({
8839
8953
  email: adminEmail,
@@ -8923,6 +9037,7 @@ export {
8923
9037
  RolePermissionsRepository,
8924
9038
  RolesRepository,
8925
9039
  SOCIAL_PROVIDERS,
9040
+ SocialAccountsRepository,
8926
9041
  TargetTypeSchema,
8927
9042
  USER_STATUSES,
8928
9043
  UserPermissionsRepository,
@@ -8938,16 +9053,19 @@ export {
8938
9053
  mainAuthRouter as authRouter,
8939
9054
  authSchema,
8940
9055
  authenticate,
9056
+ buildOAuthErrorUrl,
8941
9057
  cancelInvitation,
8942
9058
  changePasswordService,
8943
9059
  checkAccountExistsService,
8944
9060
  configureAuth,
8945
9061
  createAuthLifecycle,
8946
9062
  createInvitation,
9063
+ createOAuthState,
8947
9064
  createRole,
8948
9065
  decodeToken,
8949
9066
  deleteInvitation,
8950
9067
  deleteRole,
9068
+ exchangeCodeForTokens,
8951
9069
  expireOldInvitations,
8952
9070
  generateClientToken,
8953
9071
  generateKeyPair,
@@ -8958,12 +9076,14 @@ export {
8958
9076
  getAuth,
8959
9077
  getAuthConfig,
8960
9078
  getAuthSessionService,
9079
+ getEnabledOAuthProviders,
9080
+ getGoogleAuthUrl,
9081
+ getGoogleOAuthConfig,
9082
+ getGoogleUserInfo,
8961
9083
  getInvitationByToken,
8962
- getInvitationTemplate,
8963
9084
  getInvitationWithDetails,
8964
9085
  getKeyId,
8965
9086
  getKeySize,
8966
- getPasswordResetTemplate,
8967
9087
  getRoleByName,
8968
9088
  getRolePermissions,
8969
9089
  getSessionInfo,
@@ -8976,8 +9096,6 @@ export {
8976
9096
  getUserPermissions,
8977
9097
  getUserProfileService,
8978
9098
  getUserRole,
8979
- getVerificationCodeTemplate,
8980
- getWelcomeTemplate,
8981
9099
  hasAllPermissions,
8982
9100
  hasAnyPermission,
8983
9101
  hasAnyRole,
@@ -8986,17 +9104,19 @@ export {
8986
9104
  hashPassword,
8987
9105
  initializeAuth,
8988
9106
  invitationsRepository,
9107
+ isGoogleOAuthEnabled,
9108
+ isOAuthProviderEnabled,
8989
9109
  keysRepository,
8990
9110
  listInvitations,
8991
9111
  loginService,
8992
9112
  logoutService,
9113
+ oauthCallbackService,
9114
+ oauthStartService,
8993
9115
  parseDuration,
8994
9116
  permissions,
8995
9117
  permissionsRepository,
8996
- registerEmailProvider,
8997
- registerEmailTemplates,
9118
+ refreshAccessToken,
8998
9119
  registerPublicKeyService,
8999
- registerSMSProvider,
9000
9120
  registerService,
9001
9121
  removePermissionFromRole,
9002
9122
  requireAnyPermission,
@@ -9011,12 +9131,11 @@ export {
9011
9131
  rolesRepository,
9012
9132
  rotateKeyService,
9013
9133
  sealSession,
9014
- sendEmail,
9015
- sendSMS,
9016
9134
  sendVerificationCodeService,
9017
9135
  setRolePermissions,
9018
9136
  shouldRefreshSession,
9019
9137
  shouldRotateKey,
9138
+ socialAccountsRepository,
9020
9139
  unsealSession,
9021
9140
  updateLastLoginService,
9022
9141
  updateRole,
@@ -9038,6 +9157,7 @@ export {
9038
9157
  verifyClientToken,
9039
9158
  verifyCodeService,
9040
9159
  verifyKeyFingerprint,
9160
+ verifyOAuthState,
9041
9161
  verifyPassword,
9042
9162
  verifyToken
9043
9163
  };