@spfn/auth 0.2.0-beta.11 → 0.2.0-beta.13

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
@@ -1359,7 +1359,7 @@ var init_literal2 = __esm({
1359
1359
  });
1360
1360
 
1361
1361
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/boolean/boolean.mjs
1362
- function Boolean(options) {
1362
+ function Boolean2(options) {
1363
1363
  return CreateType({ [Kind]: "Boolean", type: "boolean" }, options);
1364
1364
  }
1365
1365
  var init_boolean = __esm({
@@ -1441,7 +1441,7 @@ var init_string2 = __esm({
1441
1441
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/template-literal/syntax.mjs
1442
1442
  function* FromUnion(syntax) {
1443
1443
  const trim = syntax.trim().replace(/"|'/g, "");
1444
- 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 (() => {
1445
1445
  const literals = trim.split("|").map((literal) => Literal(literal.trim()));
1446
1446
  return literals.length === 0 ? Never() : literals.length === 1 ? literals[0] : UnionEvaluated(literals);
1447
1447
  })();
@@ -4244,7 +4244,7 @@ __export(type_exports3, {
4244
4244
  AsyncIterator: () => AsyncIterator,
4245
4245
  Awaited: () => Awaited,
4246
4246
  BigInt: () => BigInt,
4247
- Boolean: () => Boolean,
4247
+ Boolean: () => Boolean2,
4248
4248
  Capitalize: () => Capitalize,
4249
4249
  Composite: () => Composite,
4250
4250
  Const: () => Const,
@@ -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,334 @@ 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 getDefaultScopes() {
7683
+ const envScopes = env5.SPFN_AUTH_GOOGLE_SCOPES;
7684
+ if (envScopes) {
7685
+ return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
7686
+ }
7687
+ return ["email", "profile"];
7688
+ }
7689
+ function getGoogleAuthUrl(state, scopes) {
7690
+ const resolvedScopes = scopes ?? getDefaultScopes();
7691
+ const config = getGoogleOAuthConfig();
7692
+ const params = new URLSearchParams({
7693
+ client_id: config.clientId,
7694
+ redirect_uri: config.redirectUri,
7695
+ response_type: "code",
7696
+ scope: resolvedScopes.join(" "),
7697
+ state,
7698
+ access_type: "offline",
7699
+ // refresh_token 받기 위해
7700
+ prompt: "consent"
7701
+ // 매번 동의 화면 표시 (refresh_token 보장)
7702
+ });
7703
+ return `${GOOGLE_AUTH_URL}?${params.toString()}`;
7704
+ }
7705
+ async function exchangeCodeForTokens(code) {
7706
+ const config = getGoogleOAuthConfig();
7707
+ const response = await fetch(GOOGLE_TOKEN_URL, {
7708
+ method: "POST",
7709
+ headers: {
7710
+ "Content-Type": "application/x-www-form-urlencoded"
7711
+ },
7712
+ body: new URLSearchParams({
7713
+ client_id: config.clientId,
7714
+ client_secret: config.clientSecret,
7715
+ redirect_uri: config.redirectUri,
7716
+ grant_type: "authorization_code",
7717
+ code
7718
+ })
7719
+ });
7720
+ if (!response.ok) {
7721
+ const error = await response.text();
7722
+ throw new Error(`Failed to exchange code for tokens: ${error}`);
7723
+ }
7724
+ return response.json();
7725
+ }
7726
+ async function getGoogleUserInfo(accessToken) {
7727
+ const response = await fetch(GOOGLE_USERINFO_URL, {
7728
+ headers: {
7729
+ Authorization: `Bearer ${accessToken}`
7730
+ }
7731
+ });
7732
+ if (!response.ok) {
7733
+ const error = await response.text();
7734
+ throw new Error(`Failed to get user info: ${error}`);
7735
+ }
7736
+ return response.json();
7737
+ }
7738
+ async function refreshAccessToken(refreshToken) {
7739
+ const config = getGoogleOAuthConfig();
7740
+ const response = await fetch(GOOGLE_TOKEN_URL, {
7741
+ method: "POST",
7742
+ headers: {
7743
+ "Content-Type": "application/x-www-form-urlencoded"
7744
+ },
7745
+ body: new URLSearchParams({
7746
+ client_id: config.clientId,
7747
+ client_secret: config.clientSecret,
7748
+ refresh_token: refreshToken,
7749
+ grant_type: "refresh_token"
7750
+ })
7751
+ });
7752
+ if (!response.ok) {
7753
+ const error = await response.text();
7754
+ throw new Error(`Failed to refresh access token: ${error}`);
7755
+ }
7756
+ return response.json();
7757
+ }
7758
+
7759
+ // src/server/lib/oauth/state.ts
7760
+ import * as jose from "jose";
7761
+ import { env as env6 } from "@spfn/auth/config";
7762
+ async function getStateKey() {
7763
+ const secret = env6.SPFN_AUTH_SESSION_SECRET;
7764
+ const encoder = new TextEncoder();
7765
+ const data = encoder.encode(`oauth-state:${secret}`);
7766
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
7767
+ return new Uint8Array(hashBuffer);
7768
+ }
7769
+ function generateNonce() {
7770
+ const array = new Uint8Array(16);
7771
+ crypto.getRandomValues(array);
7772
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
7773
+ }
7774
+ async function createOAuthState(params) {
7775
+ const key = await getStateKey();
7776
+ const state = {
7777
+ returnUrl: params.returnUrl,
7778
+ nonce: generateNonce(),
7779
+ provider: params.provider,
7780
+ publicKey: params.publicKey,
7781
+ keyId: params.keyId,
7782
+ fingerprint: params.fingerprint,
7783
+ algorithm: params.algorithm
7784
+ };
7785
+ const jwe = await new jose.EncryptJWT({ state }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime("10m").encrypt(key);
7786
+ return encodeURIComponent(jwe);
7787
+ }
7788
+ async function verifyOAuthState(encryptedState) {
7789
+ const key = await getStateKey();
7790
+ const jwe = decodeURIComponent(encryptedState);
7791
+ const { payload } = await jose.jwtDecrypt(jwe, key);
7792
+ return payload.state;
7793
+ }
7794
+
7795
+ // src/server/services/oauth.service.ts
7796
+ async function oauthStartService(params) {
7797
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm } = params;
7798
+ if (provider === "google") {
7799
+ if (!isGoogleOAuthEnabled()) {
7800
+ throw new ValidationError2({
7801
+ message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
7802
+ });
7803
+ }
7804
+ const state = await createOAuthState({
7805
+ provider: "google",
7806
+ returnUrl,
7807
+ publicKey,
7808
+ keyId,
7809
+ fingerprint,
7810
+ algorithm
7811
+ });
7812
+ const authUrl = getGoogleAuthUrl(state);
7813
+ return { authUrl };
7814
+ }
7815
+ throw new ValidationError2({
7816
+ message: `Unsupported OAuth provider: ${provider}`
7817
+ });
7818
+ }
7819
+ async function oauthCallbackService(params) {
7820
+ const { provider, code, state } = params;
7821
+ const stateData = await verifyOAuthState(state);
7822
+ if (stateData.provider !== provider) {
7823
+ throw new ValidationError2({
7824
+ message: "OAuth state provider mismatch"
7825
+ });
7826
+ }
7827
+ if (provider === "google") {
7828
+ return handleGoogleCallback(code, stateData);
7829
+ }
7830
+ throw new ValidationError2({
7831
+ message: `Unsupported OAuth provider: ${provider}`
7832
+ });
7833
+ }
7834
+ async function handleGoogleCallback(code, stateData) {
7835
+ const tokens = await exchangeCodeForTokens(code);
7836
+ const googleUser = await getGoogleUserInfo(tokens.access_token);
7837
+ const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
7838
+ "google",
7839
+ googleUser.id
7840
+ );
7841
+ let userId;
7842
+ let isNewUser = false;
7843
+ if (existingSocialAccount) {
7844
+ userId = existingSocialAccount.userId;
7845
+ await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
7846
+ accessToken: tokens.access_token,
7847
+ refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
7848
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
7849
+ });
7850
+ } else {
7851
+ const result = await createOrLinkUser(googleUser, tokens);
7852
+ userId = result.userId;
7853
+ isNewUser = result.isNewUser;
7854
+ }
7855
+ await registerPublicKeyService({
7856
+ userId,
7857
+ keyId: stateData.keyId,
7858
+ publicKey: stateData.publicKey,
7859
+ fingerprint: stateData.fingerprint,
7860
+ algorithm: stateData.algorithm
7861
+ });
7862
+ await updateLastLoginService(userId);
7863
+ const appUrl = env7.SPFN_APP_URL;
7864
+ const callbackPath = env7.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
7865
+ const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
7866
+ const redirectUrl = buildRedirectUrl(callbackUrl, {
7867
+ userId: String(userId),
7868
+ keyId: stateData.keyId,
7869
+ returnUrl: stateData.returnUrl,
7870
+ isNewUser: String(isNewUser)
7871
+ });
7872
+ return {
7873
+ redirectUrl,
7874
+ userId: String(userId),
7875
+ keyId: stateData.keyId,
7876
+ isNewUser
7877
+ };
7878
+ }
7879
+ async function createOrLinkUser(googleUser, tokens) {
7880
+ const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
7881
+ let userId;
7882
+ let isNewUser = false;
7883
+ if (existingUser) {
7884
+ if (!googleUser.verified_email) {
7885
+ throw new ValidationError2({
7886
+ message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
7887
+ });
7888
+ }
7889
+ userId = existingUser.id;
7890
+ if (!existingUser.emailVerifiedAt) {
7891
+ await usersRepository.updateById(existingUser.id, {
7892
+ emailVerifiedAt: /* @__PURE__ */ new Date()
7893
+ });
7894
+ }
7895
+ } else {
7896
+ const { getRoleByName: getRoleByName3 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
7897
+ const userRole = await getRoleByName3("user");
7898
+ if (!userRole) {
7899
+ throw new Error("Default user role not found. Run initializeAuth() first.");
7900
+ }
7901
+ const newUser = await usersRepository.create({
7902
+ email: googleUser.verified_email ? googleUser.email : null,
7903
+ phone: null,
7904
+ passwordHash: null,
7905
+ // OAuth 사용자는 비밀번호 없음
7906
+ passwordChangeRequired: false,
7907
+ roleId: userRole.id,
7908
+ status: "active",
7909
+ emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
7910
+ });
7911
+ userId = newUser.id;
7912
+ isNewUser = true;
7913
+ }
7914
+ await socialAccountsRepository.create({
7915
+ userId,
7916
+ provider: "google",
7917
+ providerUserId: googleUser.id,
7918
+ providerEmail: googleUser.email,
7919
+ accessToken: tokens.access_token,
7920
+ refreshToken: tokens.refresh_token ?? null,
7921
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
7922
+ });
7923
+ return { userId, isNewUser };
7924
+ }
7925
+ function buildRedirectUrl(baseUrl, params) {
7926
+ const url = new URL(baseUrl, "http://placeholder");
7927
+ for (const [key, value] of Object.entries(params)) {
7928
+ url.searchParams.set(key, value);
7929
+ }
7930
+ if (baseUrl.startsWith("http")) {
7931
+ return url.toString();
7932
+ }
7933
+ return `${url.pathname}${url.search}`;
7934
+ }
7935
+ function buildOAuthErrorUrl(error) {
7936
+ const errorUrl = env7.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
7937
+ return errorUrl.replace("{error}", encodeURIComponent(error));
7938
+ }
7939
+ function isOAuthProviderEnabled(provider) {
7940
+ switch (provider) {
7941
+ case "google":
7942
+ return isGoogleOAuthEnabled();
7943
+ case "github":
7944
+ case "kakao":
7945
+ case "naver":
7946
+ return false;
7947
+ default:
7948
+ return false;
7949
+ }
7950
+ }
7951
+ function getEnabledOAuthProviders() {
7952
+ const providers = [];
7953
+ if (isGoogleOAuthEnabled()) {
7954
+ providers.push("google");
7955
+ }
7956
+ return providers;
7957
+ }
7958
+ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
7959
+ async function getGoogleAccessToken(userId) {
7960
+ const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
7961
+ if (!account) {
7962
+ throw new ValidationError2({
7963
+ message: "No Google account linked. User must sign in with Google first."
7964
+ });
7965
+ }
7966
+ const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
7967
+ if (!isExpired && account.accessToken) {
7968
+ return account.accessToken;
7969
+ }
7970
+ if (!account.refreshToken) {
7971
+ throw new ValidationError2({
7972
+ message: "Google refresh token not available. User must re-authenticate with Google."
7973
+ });
7974
+ }
7975
+ const tokens = await refreshAccessToken(account.refreshToken);
7976
+ await socialAccountsRepository.updateTokens(account.id, {
7977
+ accessToken: tokens.access_token,
7978
+ refreshToken: tokens.refresh_token ?? account.refreshToken,
7979
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
7980
+ });
7981
+ return tokens.access_token;
7982
+ }
7983
+
7563
7984
  // src/server/routes/auth/index.ts
7564
7985
  init_esm();
7565
7986
  import { Transactional } from "@spfn/core/db";
@@ -8170,8 +8591,135 @@ var userRouter = defineRouter3({
8170
8591
  updateUserProfile
8171
8592
  });
8172
8593
 
8594
+ // src/server/routes/oauth/index.ts
8595
+ init_esm();
8596
+ init_types();
8597
+ import { Transactional as Transactional2 } from "@spfn/core/db";
8598
+ import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
8599
+ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
8600
+ query: Type.Object({
8601
+ state: Type.String({
8602
+ description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
8603
+ })
8604
+ })
8605
+ }).skip(["auth"]).handler(async (c) => {
8606
+ const { query } = await c.data();
8607
+ if (!isGoogleOAuthEnabled()) {
8608
+ return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
8609
+ }
8610
+ const authUrl = getGoogleAuthUrl(query.state);
8611
+ return c.redirect(authUrl);
8612
+ });
8613
+ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
8614
+ query: Type.Object({
8615
+ code: Type.Optional(Type.String({
8616
+ description: "Authorization code from Google"
8617
+ })),
8618
+ state: Type.Optional(Type.String({
8619
+ description: "OAuth state parameter"
8620
+ })),
8621
+ error: Type.Optional(Type.String({
8622
+ description: "Error code from Google"
8623
+ })),
8624
+ error_description: Type.Optional(Type.String({
8625
+ description: "Error description from Google"
8626
+ }))
8627
+ })
8628
+ }).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
8629
+ const { query } = await c.data();
8630
+ if (query.error) {
8631
+ const errorMessage = query.error_description || query.error;
8632
+ return c.redirect(buildOAuthErrorUrl(errorMessage));
8633
+ }
8634
+ if (!query.code || !query.state) {
8635
+ return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
8636
+ }
8637
+ try {
8638
+ const result = await oauthCallbackService({
8639
+ provider: "google",
8640
+ code: query.code,
8641
+ state: query.state
8642
+ });
8643
+ return c.redirect(result.redirectUrl);
8644
+ } catch (err) {
8645
+ const message = err instanceof Error ? err.message : "OAuth callback failed";
8646
+ return c.redirect(buildOAuthErrorUrl(message));
8647
+ }
8648
+ });
8649
+ var oauthStart = route4.post("/_auth/oauth/start").input({
8650
+ body: Type.Object({
8651
+ provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
8652
+ description: "OAuth provider (google, github, kakao, naver)"
8653
+ }),
8654
+ returnUrl: Type.String({
8655
+ description: "URL to redirect after OAuth success"
8656
+ }),
8657
+ publicKey: Type.String({
8658
+ description: "Client public key (Base64 DER)"
8659
+ }),
8660
+ keyId: Type.String({
8661
+ description: "Key identifier (UUID)"
8662
+ }),
8663
+ fingerprint: Type.String({
8664
+ description: "Key fingerprint (SHA-256 hex)"
8665
+ }),
8666
+ algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
8667
+ description: "Key algorithm (ES256 or RS256)"
8668
+ })
8669
+ })
8670
+ }).skip(["auth"]).handler(async (c) => {
8671
+ const { body } = await c.data();
8672
+ const result = await oauthStartService(body);
8673
+ return result;
8674
+ });
8675
+ var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
8676
+ return {
8677
+ providers: getEnabledOAuthProviders()
8678
+ };
8679
+ });
8680
+ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
8681
+ body: Type.Object({
8682
+ returnUrl: Type.Optional(Type.String({
8683
+ description: "URL to redirect after OAuth success"
8684
+ })),
8685
+ state: Type.Optional(Type.String({
8686
+ description: "Encrypted OAuth state (injected by interceptor)"
8687
+ }))
8688
+ })
8689
+ }).skip(["auth"]).handler(async (c) => {
8690
+ const { body } = await c.data();
8691
+ if (!isGoogleOAuthEnabled()) {
8692
+ throw new Error("Google OAuth is not configured");
8693
+ }
8694
+ if (!body.state) {
8695
+ throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
8696
+ }
8697
+ return { authUrl: getGoogleAuthUrl(body.state) };
8698
+ });
8699
+ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
8700
+ body: Type.Object({
8701
+ userId: Type.String({ description: "User ID from OAuth callback" }),
8702
+ keyId: Type.String({ description: "Key ID from OAuth state" }),
8703
+ returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
8704
+ })
8705
+ }).skip(["auth"]).handler(async (c) => {
8706
+ const { body } = await c.data();
8707
+ return {
8708
+ success: true,
8709
+ returnUrl: body.returnUrl || "/"
8710
+ };
8711
+ });
8712
+ var oauthRouter = defineRouter4({
8713
+ oauthGoogleStart,
8714
+ oauthGoogleCallback,
8715
+ oauthStart,
8716
+ oauthProviders,
8717
+ getGoogleOAuthUrl,
8718
+ oauthFinalize
8719
+ });
8720
+
8173
8721
  // src/server/routes/index.ts
8174
- var mainAuthRouter = defineRouter4({
8722
+ var mainAuthRouter = defineRouter5({
8175
8723
  // Auth routes
8176
8724
  checkAccountExists,
8177
8725
  sendVerificationCode,
@@ -8182,6 +8730,13 @@ var mainAuthRouter = defineRouter4({
8182
8730
  rotateKey,
8183
8731
  changePassword,
8184
8732
  getAuthSession,
8733
+ // OAuth routes
8734
+ oauthGoogleStart,
8735
+ oauthGoogleCallback,
8736
+ oauthStart,
8737
+ oauthProviders,
8738
+ getGoogleOAuthUrl,
8739
+ oauthFinalize,
8185
8740
  // Invitation routes
8186
8741
  getInvitation,
8187
8742
  acceptInvitation: acceptInvitation2,
@@ -8302,11 +8857,11 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
8302
8857
  }
8303
8858
 
8304
8859
  // src/server/lib/session.ts
8305
- import * as jose from "jose";
8306
- import { env as env5 } from "@spfn/auth/config";
8860
+ import * as jose2 from "jose";
8861
+ import { env as env8 } from "@spfn/auth/config";
8307
8862
  import { env as coreEnv } from "@spfn/core/config";
8308
8863
  async function getSessionSecretKey() {
8309
- const secret = env5.SPFN_AUTH_SESSION_SECRET;
8864
+ const secret = env8.SPFN_AUTH_SESSION_SECRET;
8310
8865
  const encoder = new TextEncoder();
8311
8866
  const data = encoder.encode(secret);
8312
8867
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -8314,24 +8869,24 @@ async function getSessionSecretKey() {
8314
8869
  }
8315
8870
  async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
8316
8871
  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);
8872
+ return await new jose2.EncryptJWT({ data }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${ttl}s`).setIssuer("spfn-auth").setAudience("spfn-client").encrypt(secret);
8318
8873
  }
8319
8874
  async function unsealSession(jwt4) {
8320
8875
  try {
8321
8876
  const secret = await getSessionSecretKey();
8322
- const { payload } = await jose.jwtDecrypt(jwt4, secret, {
8877
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret, {
8323
8878
  issuer: "spfn-auth",
8324
8879
  audience: "spfn-client"
8325
8880
  });
8326
8881
  return payload.data;
8327
8882
  } catch (err) {
8328
- if (err instanceof jose.errors.JWTExpired) {
8883
+ if (err instanceof jose2.errors.JWTExpired) {
8329
8884
  throw new Error("Session expired");
8330
8885
  }
8331
- if (err instanceof jose.errors.JWEDecryptionFailed) {
8886
+ if (err instanceof jose2.errors.JWEDecryptionFailed) {
8332
8887
  throw new Error("Invalid session");
8333
8888
  }
8334
- if (err instanceof jose.errors.JWTClaimValidationFailed) {
8889
+ if (err instanceof jose2.errors.JWTClaimValidationFailed) {
8335
8890
  throw new Error("Session validation failed");
8336
8891
  }
8337
8892
  throw new Error("Failed to unseal session");
@@ -8340,7 +8895,7 @@ async function unsealSession(jwt4) {
8340
8895
  async function getSessionInfo(jwt4) {
8341
8896
  const secret = await getSessionSecretKey();
8342
8897
  try {
8343
- const { payload } = await jose.jwtDecrypt(jwt4, secret);
8898
+ const { payload } = await jose2.jwtDecrypt(jwt4, secret);
8344
8899
  return {
8345
8900
  issuedAt: new Date(payload.iat * 1e3),
8346
8901
  expiresAt: new Date(payload.exp * 1e3),
@@ -8364,14 +8919,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
8364
8919
  }
8365
8920
 
8366
8921
  // src/server/setup.ts
8367
- import { env as env6 } from "@spfn/auth/config";
8922
+ import { env as env9 } from "@spfn/auth/config";
8368
8923
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
8369
8924
  init_repositories();
8370
8925
  function parseAdminAccounts() {
8371
8926
  const accounts = [];
8372
- if (env6.SPFN_AUTH_ADMIN_ACCOUNTS) {
8927
+ if (env9.SPFN_AUTH_ADMIN_ACCOUNTS) {
8373
8928
  try {
8374
- const accountsJson = env6.SPFN_AUTH_ADMIN_ACCOUNTS;
8929
+ const accountsJson = env9.SPFN_AUTH_ADMIN_ACCOUNTS;
8375
8930
  const parsed = JSON.parse(accountsJson);
8376
8931
  if (!Array.isArray(parsed)) {
8377
8932
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -8398,11 +8953,11 @@ function parseAdminAccounts() {
8398
8953
  return accounts;
8399
8954
  }
8400
8955
  }
8401
- const adminEmails = env6.SPFN_AUTH_ADMIN_EMAILS;
8956
+ const adminEmails = env9.SPFN_AUTH_ADMIN_EMAILS;
8402
8957
  if (adminEmails) {
8403
8958
  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());
8959
+ const passwords = (env9.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
8960
+ const roles2 = (env9.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
8406
8961
  if (passwords.length !== emails.length) {
8407
8962
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
8408
8963
  return accounts;
@@ -8424,8 +8979,8 @@ function parseAdminAccounts() {
8424
8979
  }
8425
8980
  return accounts;
8426
8981
  }
8427
- const adminEmail = env6.SPFN_AUTH_ADMIN_EMAIL;
8428
- const adminPassword = env6.SPFN_AUTH_ADMIN_PASSWORD;
8982
+ const adminEmail = env9.SPFN_AUTH_ADMIN_EMAIL;
8983
+ const adminPassword = env9.SPFN_AUTH_ADMIN_PASSWORD;
8429
8984
  if (adminEmail && adminPassword) {
8430
8985
  accounts.push({
8431
8986
  email: adminEmail,
@@ -8515,6 +9070,7 @@ export {
8515
9070
  RolePermissionsRepository,
8516
9071
  RolesRepository,
8517
9072
  SOCIAL_PROVIDERS,
9073
+ SocialAccountsRepository,
8518
9074
  TargetTypeSchema,
8519
9075
  USER_STATUSES,
8520
9076
  UserPermissionsRepository,
@@ -8530,16 +9086,19 @@ export {
8530
9086
  mainAuthRouter as authRouter,
8531
9087
  authSchema,
8532
9088
  authenticate,
9089
+ buildOAuthErrorUrl,
8533
9090
  cancelInvitation,
8534
9091
  changePasswordService,
8535
9092
  checkAccountExistsService,
8536
9093
  configureAuth,
8537
9094
  createAuthLifecycle,
8538
9095
  createInvitation,
9096
+ createOAuthState,
8539
9097
  createRole,
8540
9098
  decodeToken,
8541
9099
  deleteInvitation,
8542
9100
  deleteRole,
9101
+ exchangeCodeForTokens,
8543
9102
  expireOldInvitations,
8544
9103
  generateClientToken,
8545
9104
  generateKeyPair,
@@ -8550,6 +9109,11 @@ export {
8550
9109
  getAuth,
8551
9110
  getAuthConfig,
8552
9111
  getAuthSessionService,
9112
+ getEnabledOAuthProviders,
9113
+ getGoogleAccessToken,
9114
+ getGoogleAuthUrl,
9115
+ getGoogleOAuthConfig,
9116
+ getGoogleUserInfo,
8553
9117
  getInvitationByToken,
8554
9118
  getInvitationWithDetails,
8555
9119
  getKeyId,
@@ -8574,13 +9138,18 @@ export {
8574
9138
  hashPassword,
8575
9139
  initializeAuth,
8576
9140
  invitationsRepository,
9141
+ isGoogleOAuthEnabled,
9142
+ isOAuthProviderEnabled,
8577
9143
  keysRepository,
8578
9144
  listInvitations,
8579
9145
  loginService,
8580
9146
  logoutService,
9147
+ oauthCallbackService,
9148
+ oauthStartService,
8581
9149
  parseDuration,
8582
9150
  permissions,
8583
9151
  permissionsRepository,
9152
+ refreshAccessToken,
8584
9153
  registerPublicKeyService,
8585
9154
  registerService,
8586
9155
  removePermissionFromRole,
@@ -8600,6 +9169,7 @@ export {
8600
9169
  setRolePermissions,
8601
9170
  shouldRefreshSession,
8602
9171
  shouldRotateKey,
9172
+ socialAccountsRepository,
8603
9173
  unsealSession,
8604
9174
  updateLastLoginService,
8605
9175
  updateRole,
@@ -8621,6 +9191,7 @@ export {
8621
9191
  verifyClientToken,
8622
9192
  verifyCodeService,
8623
9193
  verifyKeyFingerprint,
9194
+ verifyOAuthState,
8624
9195
  verifyPassword,
8625
9196
  verifyToken
8626
9197
  };