@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/README.md +11 -7
- package/dist/config.d.ts +74 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.js +186 -20
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
10088
|
+
throw new ValidationError9({
|
|
9924
10089
|
message: "OAuth state validation failed"
|
|
9925
10090
|
});
|
|
9926
10091
|
}
|
|
9927
10092
|
if (stateData.provider !== provider) {
|
|
9928
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11433
|
+
throw new ValidationError11({ message: "Google OAuth is not configured" });
|
|
11269
11434
|
}
|
|
11270
11435
|
if (!body.state) {
|
|
11271
|
-
throw new
|
|
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
|
|
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,
|