@spfn/auth 0.2.0-beta.80 → 0.2.0-beta.81

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
@@ -6977,6 +6977,38 @@ var init_schema5 = __esm({
6977
6977
  })
6978
6978
  },
6979
6979
  // ============================================================================
6980
+ // OAuth Configuration - GitHub
6981
+ // ============================================================================
6982
+ SPFN_AUTH_GITHUB_CLIENT_ID: {
6983
+ ...envString({
6984
+ description: "GitHub OAuth app client ID. When set, GitHub OAuth routes are automatically enabled.",
6985
+ required: false,
6986
+ examples: ["Iv1.abc123def456"]
6987
+ })
6988
+ },
6989
+ SPFN_AUTH_GITHUB_CLIENT_SECRET: {
6990
+ ...envString({
6991
+ description: "GitHub OAuth app client secret.",
6992
+ required: false,
6993
+ sensitive: true,
6994
+ examples: ["your-github-client-secret"]
6995
+ })
6996
+ },
6997
+ SPFN_AUTH_GITHUB_SCOPES: {
6998
+ ...envString({
6999
+ description: 'Comma-separated GitHub OAuth scopes. Defaults to "read:user,user:email".',
7000
+ required: false,
7001
+ examples: ["read:user,user:email"]
7002
+ })
7003
+ },
7004
+ SPFN_AUTH_GITHUB_REDIRECT_URI: {
7005
+ ...envString({
7006
+ description: "GitHub OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/github/callback.",
7007
+ required: false,
7008
+ examples: ["https://app.example.com/_auth/oauth/github/callback"]
7009
+ })
7010
+ },
7011
+ // ============================================================================
6980
7012
  // Native Social Login (mobile/web id_token verification)
6981
7013
  //
6982
7014
  // 네이티브 SDK가 받은 id_token을 서버가 JWKS로 검증하는 경로 전용 설정.
@@ -9346,7 +9378,7 @@ async function updateUserProfileService(userId, params) {
9346
9378
  // src/server/services/oauth.service.ts
9347
9379
  init_repositories();
9348
9380
  import { env as env11 } from "@spfn/auth/config";
9349
- import { ValidationError as ValidationError8 } from "@spfn/core/errors";
9381
+ import { ValidationError as ValidationError9 } from "@spfn/core/errors";
9350
9382
  import { AccountDisabledError as AccountDisabledError2, AccountPendingDeletionError as AccountPendingDeletionError2 } from "@spfn/auth/errors";
9351
9383
 
9352
9384
  // src/server/lib/oauth/google.ts
@@ -9667,9 +9699,142 @@ var appleProvider = {
9667
9699
  };
9668
9700
  registerOAuthProvider(appleProvider);
9669
9701
 
9670
- // src/server/lib/oauth/kakao-provider.ts
9702
+ // src/server/lib/oauth/github-provider.ts
9671
9703
  init_config();
9672
9704
  import { ValidationError as ValidationError6 } from "@spfn/core/errors";
9705
+ var GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize";
9706
+ var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
9707
+ var GITHUB_USERINFO_URL = "https://api.github.com/user";
9708
+ var GITHUB_EMAILS_URL = "https://api.github.com/user/emails";
9709
+ var GITHUB_USER_AGENT = "spfn-auth";
9710
+ var GITHUB_DEFAULT_EXPIRES_IN = 60 * 60 * 24 * 365;
9711
+ function getGithubConfig() {
9712
+ const clientId = env3.SPFN_AUTH_GITHUB_CLIENT_ID;
9713
+ const clientSecret = env3.SPFN_AUTH_GITHUB_CLIENT_SECRET;
9714
+ if (!clientId || !clientSecret) {
9715
+ throw new ValidationError6({
9716
+ message: "GitHub OAuth is not configured. Set SPFN_AUTH_GITHUB_CLIENT_ID and SPFN_AUTH_GITHUB_CLIENT_SECRET."
9717
+ });
9718
+ }
9719
+ const baseUrl = env3.NEXT_PUBLIC_SPFN_APP_URL || env3.SPFN_APP_URL;
9720
+ return {
9721
+ clientId,
9722
+ clientSecret,
9723
+ redirectUri: env3.SPFN_AUTH_GITHUB_REDIRECT_URI || `${baseUrl}/_auth/oauth/github/callback`
9724
+ };
9725
+ }
9726
+ function getGithubScopes() {
9727
+ const configured = env3.SPFN_AUTH_GITHUB_SCOPES;
9728
+ return configured ? configured.split(",").map((scope) => scope.trim()).filter(Boolean) : ["read:user", "user:email"];
9729
+ }
9730
+ async function requestGithubTokens(params) {
9731
+ const response = await fetch(GITHUB_TOKEN_URL, {
9732
+ method: "POST",
9733
+ headers: {
9734
+ "Content-Type": "application/x-www-form-urlencoded",
9735
+ Accept: "application/json"
9736
+ },
9737
+ body: params
9738
+ });
9739
+ if (!response.ok) {
9740
+ throw new Error(`GitHub token request failed with status ${response.status}`);
9741
+ }
9742
+ const body = await response.json();
9743
+ if (typeof body.error === "string") {
9744
+ throw new Error(`GitHub token request failed: ${body.error}`);
9745
+ }
9746
+ if (typeof body.access_token !== "string") {
9747
+ throw new Error("GitHub token response is invalid");
9748
+ }
9749
+ const expiresIn = Number(body.expires_in);
9750
+ return {
9751
+ accessToken: body.access_token,
9752
+ refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
9753
+ expiresIn: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : GITHUB_DEFAULT_EXPIRES_IN
9754
+ };
9755
+ }
9756
+ async function fetchPrimaryEmail(accessToken) {
9757
+ const response = await fetch(GITHUB_EMAILS_URL, {
9758
+ headers: {
9759
+ "Authorization": `Bearer ${accessToken}`,
9760
+ "Accept": "application/vnd.github+json",
9761
+ "User-Agent": GITHUB_USER_AGENT
9762
+ }
9763
+ });
9764
+ if (!response.ok) {
9765
+ return null;
9766
+ }
9767
+ const entries = await response.json();
9768
+ if (!Array.isArray(entries)) {
9769
+ return null;
9770
+ }
9771
+ const primary = entries.find((entry) => entry.primary === true && typeof entry.email === "string") ?? entries.find((entry) => entry.verified === true && typeof entry.email === "string");
9772
+ return primary ? { email: primary.email, verified: primary.verified === true } : null;
9773
+ }
9774
+ var githubProvider = {
9775
+ id: "github",
9776
+ isEnabled() {
9777
+ return !!(env3.SPFN_AUTH_GITHUB_CLIENT_ID && env3.SPFN_AUTH_GITHUB_CLIENT_SECRET);
9778
+ },
9779
+ getAuthUrl(state, scopes) {
9780
+ const config2 = getGithubConfig();
9781
+ const params = new URLSearchParams({
9782
+ client_id: config2.clientId,
9783
+ redirect_uri: config2.redirectUri,
9784
+ state,
9785
+ scope: (scopes ?? getGithubScopes()).join(" ")
9786
+ });
9787
+ return `${GITHUB_AUTH_URL}?${params.toString()}`;
9788
+ },
9789
+ async exchangeCodeForTokens(code) {
9790
+ const config2 = getGithubConfig();
9791
+ return requestGithubTokens(new URLSearchParams({
9792
+ client_id: config2.clientId,
9793
+ client_secret: config2.clientSecret,
9794
+ redirect_uri: config2.redirectUri,
9795
+ code
9796
+ }));
9797
+ },
9798
+ async getUserInfo(accessToken) {
9799
+ const response = await fetch(GITHUB_USERINFO_URL, {
9800
+ headers: {
9801
+ "Authorization": `Bearer ${accessToken}`,
9802
+ "Accept": "application/vnd.github+json",
9803
+ "User-Agent": GITHUB_USER_AGENT
9804
+ }
9805
+ });
9806
+ if (!response.ok) {
9807
+ throw new Error(`GitHub user-info request failed with status ${response.status}`);
9808
+ }
9809
+ const body = await response.json();
9810
+ if (typeof body.id !== "number" && typeof body.id !== "string") {
9811
+ throw new Error("GitHub user-info response is missing the provider user ID");
9812
+ }
9813
+ const primaryEmail = await fetchPrimaryEmail(accessToken);
9814
+ const publicEmail = typeof body.email === "string" ? body.email : null;
9815
+ return {
9816
+ providerUserId: String(body.id),
9817
+ email: primaryEmail?.email ?? publicEmail,
9818
+ emailVerified: primaryEmail?.verified ?? false,
9819
+ name: typeof body.name === "string" ? body.name : typeof body.login === "string" ? body.login : void 0,
9820
+ avatar: typeof body.avatar_url === "string" ? body.avatar_url : void 0
9821
+ };
9822
+ },
9823
+ async refreshTokens(refreshToken) {
9824
+ const config2 = getGithubConfig();
9825
+ return requestGithubTokens(new URLSearchParams({
9826
+ grant_type: "refresh_token",
9827
+ client_id: config2.clientId,
9828
+ client_secret: config2.clientSecret,
9829
+ refresh_token: refreshToken
9830
+ }));
9831
+ }
9832
+ };
9833
+ registerOAuthProvider(githubProvider);
9834
+
9835
+ // src/server/lib/oauth/kakao-provider.ts
9836
+ init_config();
9837
+ import { ValidationError as ValidationError7 } from "@spfn/core/errors";
9673
9838
  var KAKAO_AUTH_URL = "https://kauth.kakao.com/oauth/authorize";
9674
9839
  var KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token";
9675
9840
  var KAKAO_USERINFO_URL = "https://kapi.kakao.com/v2/user/me";
@@ -9677,7 +9842,7 @@ function getKakaoConfig() {
9677
9842
  const clientId = env3.SPFN_AUTH_KAKAO_CLIENT_ID;
9678
9843
  const clientSecret = env3.SPFN_AUTH_KAKAO_CLIENT_SECRET;
9679
9844
  if (!clientId) {
9680
- throw new ValidationError6({
9845
+ throw new ValidationError7({
9681
9846
  message: "Kakao OAuth is not configured. Set SPFN_AUTH_KAKAO_CLIENT_ID."
9682
9847
  });
9683
9848
  }
@@ -9779,7 +9944,7 @@ registerOAuthProvider(kakaoProvider);
9779
9944
 
9780
9945
  // src/server/lib/oauth/naver-provider.ts
9781
9946
  init_config();
9782
- import { ValidationError as ValidationError7 } from "@spfn/core/errors";
9947
+ import { ValidationError as ValidationError8 } from "@spfn/core/errors";
9783
9948
  var NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
9784
9949
  var NAVER_TOKEN_URL = "https://nid.naver.com/oauth2.0/token";
9785
9950
  var NAVER_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
@@ -9787,7 +9952,7 @@ function getNaverConfig() {
9787
9952
  const clientId = env3.SPFN_AUTH_NAVER_CLIENT_ID;
9788
9953
  const clientSecret = env3.SPFN_AUTH_NAVER_CLIENT_SECRET;
9789
9954
  if (!clientId || !clientSecret) {
9790
- throw new ValidationError7({
9955
+ throw new ValidationError8({
9791
9956
  message: "Naver OAuth is not configured. Set SPFN_AUTH_NAVER_CLIENT_ID and SPFN_AUTH_NAVER_CLIENT_SECRET."
9792
9957
  });
9793
9958
  }
@@ -9881,12 +10046,12 @@ registerOAuthProvider(naverProvider);
9881
10046
  function requireEnabledProvider(provider) {
9882
10047
  const oauthProvider = getOAuthProvider(provider);
9883
10048
  if (!oauthProvider) {
9884
- throw new ValidationError8({
10049
+ throw new ValidationError9({
9885
10050
  message: `Unsupported OAuth provider: ${provider}. No provider is registered for this id.`
9886
10051
  });
9887
10052
  }
9888
10053
  if (!oauthProvider.isEnabled()) {
9889
- throw new ValidationError8({
10054
+ throw new ValidationError9({
9890
10055
  message: `OAuth provider '${provider}' is registered but not configured. Check its required environment variables.`
9891
10056
  });
9892
10057
  }
@@ -9894,7 +10059,7 @@ function requireEnabledProvider(provider) {
9894
10059
  }
9895
10060
  function tokenExpiryDate(expiresIn) {
9896
10061
  if (!Number.isFinite(expiresIn)) {
9897
- throw new ValidationError8({
10062
+ throw new ValidationError9({
9898
10063
  message: `Invalid token expiry returned from OAuth provider: ${expiresIn}`
9899
10064
  });
9900
10065
  }
@@ -9920,12 +10085,12 @@ async function oauthCallbackService(params) {
9920
10085
  const stateData = await verifyOAuthState(state);
9921
10086
  const nonceCandidates = typeof expectedNonce === "string" ? [expectedNonce] : expectedNonce ?? [];
9922
10087
  if (!stateData.nonce || !nonceCandidates.includes(stateData.nonce)) {
9923
- throw new ValidationError8({
10088
+ throw new ValidationError9({
9924
10089
  message: "OAuth state validation failed"
9925
10090
  });
9926
10091
  }
9927
10092
  if (stateData.provider !== provider) {
9928
- throw new ValidationError8({
10093
+ throw new ValidationError9({
9929
10094
  message: "OAuth state provider mismatch"
9930
10095
  });
9931
10096
  }
@@ -9991,7 +10156,7 @@ async function oauthCallbackService(params) {
9991
10156
  async function assertActiveForOAuthSession(userId) {
9992
10157
  const user = await usersRepository.findByIdOnPrimary(userId);
9993
10158
  if (!user) {
9994
- throw new ValidationError8({ message: "User not found" });
10159
+ throw new ValidationError9({ message: "User not found" });
9995
10160
  }
9996
10161
  if (user.status === "active") {
9997
10162
  return;
@@ -10008,7 +10173,7 @@ async function createOrLinkUser(provider, identity, tokens, metadata) {
10008
10173
  let isNewUser = false;
10009
10174
  if (existingUser) {
10010
10175
  if (!identity.emailVerified) {
10011
- throw new ValidationError8({
10176
+ throw new ValidationError9({
10012
10177
  message: "Cannot link to existing account with unverified email. Please verify your email with the provider first."
10013
10178
  });
10014
10179
  }
@@ -10079,7 +10244,7 @@ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
10079
10244
  async function getGoogleAccessToken(userId) {
10080
10245
  const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
10081
10246
  if (!account) {
10082
- throw new ValidationError8({
10247
+ throw new ValidationError9({
10083
10248
  message: "No Google account linked. User must sign in with Google first."
10084
10249
  });
10085
10250
  }
@@ -10088,7 +10253,7 @@ async function getGoogleAccessToken(userId) {
10088
10253
  return account.accessToken;
10089
10254
  }
10090
10255
  if (!account.refreshToken) {
10091
- throw new ValidationError8({
10256
+ throw new ValidationError9({
10092
10257
  message: "Google refresh token not available. User must re-authenticate with Google."
10093
10258
  });
10094
10259
  }
@@ -10103,12 +10268,12 @@ async function getGoogleAccessToken(userId) {
10103
10268
 
10104
10269
  // src/server/services/oauth-native.service.ts
10105
10270
  init_repositories();
10106
- import { ValidationError as ValidationError9 } from "@spfn/core/errors";
10271
+ import { ValidationError as ValidationError10 } from "@spfn/core/errors";
10107
10272
  import { runInTransaction as runInTransaction2, onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
10108
10273
  async function oauthNativeService(params) {
10109
10274
  const oauthProvider = getOAuthProvider(params.provider);
10110
10275
  if (!oauthProvider?.verifyNativeIdToken) {
10111
- throw new ValidationError9({
10276
+ throw new ValidationError10({
10112
10277
  message: `Provider '${params.provider}' does not support native id_token sign-in.`
10113
10278
  });
10114
10279
  }
@@ -11145,7 +11310,7 @@ var deleteCookie = (c, name, opt) => {
11145
11310
  // src/server/routes/oauth/index.ts
11146
11311
  init_types();
11147
11312
  import { Transactional as Transactional3 } from "@spfn/core/db";
11148
- import { ValidationError as ValidationError10 } from "@spfn/core/errors";
11313
+ import { ValidationError as ValidationError11 } from "@spfn/core/errors";
11149
11314
  import { rateLimitPolicy as rateLimitPolicy4 } from "@spfn/core/middleware";
11150
11315
  import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
11151
11316
  var providerParams = Type.Object({
@@ -11265,10 +11430,10 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
11265
11430
  }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
11266
11431
  const { body } = await c.data();
11267
11432
  if (!isGoogleOAuthEnabled()) {
11268
- throw new ValidationError10({ message: "Google OAuth is not configured" });
11433
+ throw new ValidationError11({ message: "Google OAuth is not configured" });
11269
11434
  }
11270
11435
  if (!body.state) {
11271
- throw new ValidationError10({
11436
+ throw new ValidationError11({
11272
11437
  message: "OAuth state is required. Ensure the OAuth interceptor is configured."
11273
11438
  });
11274
11439
  }
@@ -11363,7 +11528,7 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
11363
11528
  const { params, body } = await c.data();
11364
11529
  const provider = requireEnabledProvider(params.provider);
11365
11530
  if (!body.state) {
11366
- throw new ValidationError10({
11531
+ throw new ValidationError11({
11367
11532
  message: "OAuth state is required. Ensure the OAuth interceptor is configured."
11368
11533
  });
11369
11534
  }
@@ -12051,6 +12216,7 @@ export {
12051
12216
  getUserPermissions,
12052
12217
  getUserProfileService,
12053
12218
  getUserRole,
12219
+ githubProvider,
12054
12220
  googleProvider,
12055
12221
  hasAllPermissions,
12056
12222
  hasAnyPermission,