@spfn/auth 0.2.0-beta.11 → 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
@@ -6407,6 +6407,96 @@ var init_invitations_repository = __esm({
6407
6407
  }
6408
6408
  });
6409
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
+
6410
6500
  // src/server/repositories/index.ts
6411
6501
  var init_repositories = __esm({
6412
6502
  "src/server/repositories/index.ts"() {
@@ -6420,6 +6510,7 @@ var init_repositories = __esm({
6420
6510
  init_user_permissions_repository();
6421
6511
  init_user_profiles_repository();
6422
6512
  init_invitations_repository();
6513
+ init_social_accounts_repository();
6423
6514
  }
6424
6515
  });
6425
6516
 
@@ -6546,7 +6637,7 @@ var init_role_service = __esm({
6546
6637
  import "@spfn/auth/config";
6547
6638
 
6548
6639
  // src/server/routes/index.ts
6549
- import { defineRouter as defineRouter4 } from "@spfn/core/route";
6640
+ import { defineRouter as defineRouter5 } from "@spfn/core/route";
6550
6641
 
6551
6642
  // src/server/routes/auth/index.ts
6552
6643
  init_schema3();
@@ -7075,7 +7166,9 @@ var COOKIE_NAMES = {
7075
7166
  /** Encrypted session data (userId, privateKey, keyId, algorithm) */
7076
7167
  SESSION: "spfn_session",
7077
7168
  /** Current key ID (for key rotation) */
7078
- 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"
7079
7172
  };
7080
7173
  function parseDuration(duration) {
7081
7174
  if (typeof duration === "number") {
@@ -7560,6 +7653,301 @@ async function updateUserProfileService(userId, params) {
7560
7653
  return profile;
7561
7654
  }
7562
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
+
7563
7951
  // src/server/routes/auth/index.ts
7564
7952
  init_esm();
7565
7953
  import { Transactional } from "@spfn/core/db";
@@ -8170,8 +8558,135 @@ var userRouter = defineRouter3({
8170
8558
  updateUserProfile
8171
8559
  });
8172
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
+
8173
8688
  // src/server/routes/index.ts
8174
- var mainAuthRouter = defineRouter4({
8689
+ var mainAuthRouter = defineRouter5({
8175
8690
  // Auth routes
8176
8691
  checkAccountExists,
8177
8692
  sendVerificationCode,
@@ -8182,6 +8697,13 @@ var mainAuthRouter = defineRouter4({
8182
8697
  rotateKey,
8183
8698
  changePassword,
8184
8699
  getAuthSession,
8700
+ // OAuth routes
8701
+ oauthGoogleStart,
8702
+ oauthGoogleCallback,
8703
+ oauthStart,
8704
+ oauthProviders,
8705
+ getGoogleOAuthUrl,
8706
+ oauthFinalize,
8185
8707
  // Invitation routes
8186
8708
  getInvitation,
8187
8709
  acceptInvitation: acceptInvitation2,
@@ -8302,11 +8824,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
8302
8824
  }
8303
8825
 
8304
8826
  // src/server/lib/session.ts
8305
- import * as jose from "jose";
8306
- import { env as env5 } from "@spfn/auth/config";
8827
+ import * as jose2 from "jose";
8828
+ import { env as env8 } from "@spfn/auth/config";
8307
8829
  import { env as coreEnv } from "@spfn/core/config";
8308
8830
  async function getSessionSecretKey() {
8309
- const secret = env5.SPFN_AUTH_SESSION_SECRET;
8831
+ const secret = env8.SPFN_AUTH_SESSION_SECRET;
8310
8832
  const encoder = new TextEncoder();
8311
8833
  const data = encoder.encode(secret);
8312
8834
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -8314,24 +8836,24 @@ async function getSessionSecretKey() {
8314
8836
  }
8315
8837
  async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
8316
8838
  const secret = await getSessionSecretKey();
8317
- 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);
8318
8840
  }
8319
8841
  async function unsealSession(jwt4) {
8320
8842
  try {
8321
8843
  const secret = await getSessionSecretKey();
8322
- const { payload } = await jose.jwtDecrypt(jwt4, secret, {
8844
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
8323
8845
  issuer: "spfn-auth",
8324
8846
  audience: "spfn-client"
8325
8847
  });
8326
8848
  return payload.data;
8327
8849
  } catch (err) {
8328
- if (err instanceof jose.errors.JWTExpired) {
8850
+ if (err instanceof jose2.errors.JWTExpired) {
8329
8851
  throw new Error("Session expired");
8330
8852
  }
8331
- if (err instanceof jose.errors.JWEDecryptionFailed) {
8853
+ if (err instanceof jose2.errors.JWEDecryptionFailed) {
8332
8854
  throw new Error("Invalid session");
8333
8855
  }
8334
- if (err instanceof jose.errors.JWTClaimValidationFailed) {
8856
+ if (err instanceof jose2.errors.JWTClaimValidationFailed) {
8335
8857
  throw new Error("Session validation failed");
8336
8858
  }
8337
8859
  throw new Error("Failed to unseal session");
@@ -8340,7 +8862,7 @@ async function unsealSession(jwt4) {
8340
8862
  async function getSessionInfo(jwt4) {
8341
8863
  const secret = await getSessionSecretKey();
8342
8864
  try {
8343
- const { payload } = await jose.jwtDecrypt(jwt4, secret);
8865
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret);
8344
8866
  return {
8345
8867
  issuedAt: new Date(payload.iat * 1e3),
8346
8868
  expiresAt: new Date(payload.exp * 1e3),
@@ -8364,14 +8886,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
8364
8886
  }
8365
8887
 
8366
8888
  // src/server/setup.ts
8367
- import { env as env6 } from "@spfn/auth/config";
8889
+ import { env as env9 } from "@spfn/auth/config";
8368
8890
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
8369
8891
  init_repositories();
8370
8892
  function parseAdminAccounts() {
8371
8893
  const accounts = [];
8372
- if (env6.SPFN_AUTH_ADMIN_ACCOUNTS) {
8894
+ if (env9.SPFN_AUTH_ADMIN_ACCOUNTS) {
8373
8895
  try {
8374
- const accountsJson = env6.SPFN_AUTH_ADMIN_ACCOUNTS;
8896
+ const accountsJson = env9.SPFN_AUTH_ADMIN_ACCOUNTS;
8375
8897
  const parsed = JSON.parse(accountsJson);
8376
8898
  if (!Array.isArray(parsed)) {
8377
8899
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -8398,11 +8920,11 @@ function parseAdminAccounts() {
8398
8920
  return accounts;
8399
8921
  }
8400
8922
  }
8401
- const adminEmails = env6.SPFN_AUTH_ADMIN_EMAILS;
8923
+ const adminEmails = env9.SPFN_AUTH_ADMIN_EMAILS;
8402
8924
  if (adminEmails) {
8403
8925
  const emails = adminEmails.split(",").map((s) => s.trim());
8404
- const passwords = (env6.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8405
- const roles2 = (env6.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());
8406
8928
  if (passwords.length !== emails.length) {
8407
8929
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
8408
8930
  return accounts;
@@ -8424,8 +8946,8 @@ function parseAdminAccounts() {
8424
8946
  }
8425
8947
  return accounts;
8426
8948
  }
8427
- const adminEmail = env6.SPFN_AUTH_ADMIN_EMAIL;
8428
- const adminPassword = env6.SPFN_AUTH_ADMIN_PASSWORD;
8949
+ const adminEmail = env9.SPFN_AUTH_ADMIN_EMAIL;
8950
+ const adminPassword = env9.SPFN_AUTH_ADMIN_PASSWORD;
8429
8951
  if (adminEmail && adminPassword) {
8430
8952
  accounts.push({
8431
8953
  email: adminEmail,
@@ -8515,6 +9037,7 @@ export {
8515
9037
  RolePermissionsRepository,
8516
9038
  RolesRepository,
8517
9039
  SOCIAL_PROVIDERS,
9040
+ SocialAccountsRepository,
8518
9041
  TargetTypeSchema,
8519
9042
  USER_STATUSES,
8520
9043
  UserPermissionsRepository,
@@ -8530,16 +9053,19 @@ export {
8530
9053
  mainAuthRouter as authRouter,
8531
9054
  authSchema,
8532
9055
  authenticate,
9056
+ buildOAuthErrorUrl,
8533
9057
  cancelInvitation,
8534
9058
  changePasswordService,
8535
9059
  checkAccountExistsService,
8536
9060
  configureAuth,
8537
9061
  createAuthLifecycle,
8538
9062
  createInvitation,
9063
+ createOAuthState,
8539
9064
  createRole,
8540
9065
  decodeToken,
8541
9066
  deleteInvitation,
8542
9067
  deleteRole,
9068
+ exchangeCodeForTokens,
8543
9069
  expireOldInvitations,
8544
9070
  generateClientToken,
8545
9071
  generateKeyPair,
@@ -8550,6 +9076,10 @@ export {
8550
9076
  getAuth,
8551
9077
  getAuthConfig,
8552
9078
  getAuthSessionService,
9079
+ getEnabledOAuthProviders,
9080
+ getGoogleAuthUrl,
9081
+ getGoogleOAuthConfig,
9082
+ getGoogleUserInfo,
8553
9083
  getInvitationByToken,
8554
9084
  getInvitationWithDetails,
8555
9085
  getKeyId,
@@ -8574,13 +9104,18 @@ export {
8574
9104
  hashPassword,
8575
9105
  initializeAuth,
8576
9106
  invitationsRepository,
9107
+ isGoogleOAuthEnabled,
9108
+ isOAuthProviderEnabled,
8577
9109
  keysRepository,
8578
9110
  listInvitations,
8579
9111
  loginService,
8580
9112
  logoutService,
9113
+ oauthCallbackService,
9114
+ oauthStartService,
8581
9115
  parseDuration,
8582
9116
  permissions,
8583
9117
  permissionsRepository,
9118
+ refreshAccessToken,
8584
9119
  registerPublicKeyService,
8585
9120
  registerService,
8586
9121
  removePermissionFromRole,
@@ -8600,6 +9135,7 @@ export {
8600
9135
  setRolePermissions,
8601
9136
  shouldRefreshSession,
8602
9137
  shouldRotateKey,
9138
+ socialAccountsRepository,
8603
9139
  unsealSession,
8604
9140
  updateLastLoginService,
8605
9141
  updateRole,
@@ -8621,6 +9157,7 @@ export {
8621
9157
  verifyClientToken,
8622
9158
  verifyCodeService,
8623
9159
  verifyKeyFingerprint,
9160
+ verifyOAuthState,
8624
9161
  verifyPassword,
8625
9162
  verifyToken
8626
9163
  };