@spfn/auth 0.2.0-beta.61 → 0.2.0-beta.64

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
@@ -4501,7 +4501,7 @@ var init_types = __esm({
4501
4501
  KEY_ALGORITHM = ["ES256", "RS256"];
4502
4502
  INVITATION_STATUSES = ["pending", "accepted", "expired", "cancelled"];
4503
4503
  USER_STATUSES = ["active", "inactive", "suspended"];
4504
- SOCIAL_PROVIDERS = ["google", "github", "kakao", "naver"];
4504
+ SOCIAL_PROVIDERS = ["google", "github", "kakao", "naver", "superself"];
4505
4505
  }
4506
4506
  });
4507
4507
 
@@ -7211,11 +7211,12 @@ async function updateUsernameService(userId, username) {
7211
7211
 
7212
7212
  // src/server/events/index.ts
7213
7213
  init_esm();
7214
+ init_types();
7214
7215
  import { defineEvent } from "@spfn/core/event";
7215
7216
  var AuthProviderSchema = Type.Union([
7216
7217
  Type.Literal("email"),
7217
7218
  Type.Literal("phone"),
7218
- Type.Literal("google")
7219
+ ...SOCIAL_PROVIDERS.map((p) => Type.Literal(p))
7219
7220
  ]);
7220
7221
  var authLoginEvent = defineEvent(
7221
7222
  "auth.login",
@@ -8161,30 +8162,88 @@ async function verifyOAuthState(encryptedState) {
8161
8162
  return payload.state;
8162
8163
  }
8163
8164
 
8165
+ // src/server/lib/oauth/provider.ts
8166
+ var registry = /* @__PURE__ */ new Map();
8167
+ function registerOAuthProvider(provider) {
8168
+ registry.set(provider.id, provider);
8169
+ }
8170
+ function getOAuthProvider(id11) {
8171
+ return registry.get(id11);
8172
+ }
8173
+ function getRegisteredProviders() {
8174
+ return [...registry.values()];
8175
+ }
8176
+
8177
+ // src/server/lib/oauth/google-provider.ts
8178
+ var googleProvider = {
8179
+ id: "google",
8180
+ isEnabled: isGoogleOAuthEnabled,
8181
+ getAuthUrl: getGoogleAuthUrl,
8182
+ async exchangeCodeForTokens(code) {
8183
+ const tokens = await exchangeCodeForTokens(code);
8184
+ return {
8185
+ accessToken: tokens.access_token,
8186
+ refreshToken: tokens.refresh_token,
8187
+ expiresIn: tokens.expires_in
8188
+ };
8189
+ },
8190
+ async getUserInfo(accessToken) {
8191
+ const user = await getGoogleUserInfo(accessToken);
8192
+ return {
8193
+ providerUserId: user.id,
8194
+ email: user.email ?? null,
8195
+ emailVerified: user.verified_email,
8196
+ name: user.name,
8197
+ avatar: user.picture
8198
+ };
8199
+ },
8200
+ async refreshTokens(refreshToken) {
8201
+ const tokens = await refreshAccessToken(refreshToken);
8202
+ return {
8203
+ accessToken: tokens.access_token,
8204
+ refreshToken: tokens.refresh_token,
8205
+ expiresIn: tokens.expires_in
8206
+ };
8207
+ }
8208
+ };
8209
+ registerOAuthProvider(googleProvider);
8210
+
8164
8211
  // src/server/services/oauth.service.ts
8165
- async function oauthStartService(params) {
8166
- const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
8167
- if (provider === "google") {
8168
- if (!isGoogleOAuthEnabled()) {
8169
- throw new ValidationError3({
8170
- message: "Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET."
8171
- });
8172
- }
8173
- const state = await createOAuthState({
8174
- provider: "google",
8175
- returnUrl,
8176
- publicKey,
8177
- keyId,
8178
- fingerprint,
8179
- algorithm,
8180
- metadata
8212
+ function requireEnabledProvider(provider) {
8213
+ const oauthProvider = getOAuthProvider(provider);
8214
+ if (!oauthProvider) {
8215
+ throw new ValidationError3({
8216
+ message: `Unsupported OAuth provider: ${provider}. No provider is registered for this id.`
8181
8217
  });
8182
- const authUrl = getGoogleAuthUrl(state);
8183
- return { authUrl };
8184
8218
  }
8185
- throw new ValidationError3({
8186
- message: `Unsupported OAuth provider: ${provider}`
8219
+ if (!oauthProvider.isEnabled()) {
8220
+ throw new ValidationError3({
8221
+ message: `OAuth provider '${provider}' is registered but not configured. Check its required environment variables.`
8222
+ });
8223
+ }
8224
+ return oauthProvider;
8225
+ }
8226
+ function tokenExpiryDate(expiresIn) {
8227
+ if (!Number.isFinite(expiresIn)) {
8228
+ throw new ValidationError3({
8229
+ message: `Invalid token expiry returned from OAuth provider: ${expiresIn}`
8230
+ });
8231
+ }
8232
+ return new Date(Date.now() + expiresIn * 1e3);
8233
+ }
8234
+ async function oauthStartService(params) {
8235
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
8236
+ const oauthProvider = requireEnabledProvider(provider);
8237
+ const state = await createOAuthState({
8238
+ provider,
8239
+ returnUrl,
8240
+ publicKey,
8241
+ keyId,
8242
+ fingerprint,
8243
+ algorithm,
8244
+ metadata
8187
8245
  });
8246
+ return { authUrl: oauthProvider.getAuthUrl(state) };
8188
8247
  }
8189
8248
  async function oauthCallbackService(params) {
8190
8249
  const { provider, code, state } = params;
@@ -8194,31 +8253,24 @@ async function oauthCallbackService(params) {
8194
8253
  message: "OAuth state provider mismatch"
8195
8254
  });
8196
8255
  }
8197
- if (provider === "google") {
8198
- return handleGoogleCallback(code, stateData);
8199
- }
8200
- throw new ValidationError3({
8201
- message: `Unsupported OAuth provider: ${provider}`
8202
- });
8203
- }
8204
- async function handleGoogleCallback(code, stateData) {
8205
- const tokens = await exchangeCodeForTokens(code);
8206
- const googleUser = await getGoogleUserInfo(tokens.access_token);
8256
+ const oauthProvider = requireEnabledProvider(provider);
8257
+ const tokens = await oauthProvider.exchangeCodeForTokens(code);
8258
+ const identity = await oauthProvider.getUserInfo(tokens.accessToken);
8207
8259
  const existingSocialAccount = await socialAccountsRepository.findByProviderAndProviderId(
8208
- "google",
8209
- googleUser.id
8260
+ provider,
8261
+ identity.providerUserId
8210
8262
  );
8211
8263
  let userId;
8212
8264
  let isNewUser = false;
8213
8265
  if (existingSocialAccount) {
8214
8266
  userId = existingSocialAccount.userId;
8215
8267
  await socialAccountsRepository.updateTokens(existingSocialAccount.id, {
8216
- accessToken: tokens.access_token,
8217
- refreshToken: tokens.refresh_token ?? existingSocialAccount.refreshToken,
8218
- tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8268
+ accessToken: tokens.accessToken,
8269
+ refreshToken: tokens.refreshToken ?? existingSocialAccount.refreshToken,
8270
+ tokenExpiresAt: tokenExpiryDate(tokens.expiresIn)
8219
8271
  });
8220
8272
  } else {
8221
- const result = await createOrLinkUser(googleUser, tokens);
8273
+ const result = await createOrLinkUser(provider, identity, tokens);
8222
8274
  userId = result.userId;
8223
8275
  isNewUser = result.isNewUser;
8224
8276
  }
@@ -8242,7 +8294,7 @@ async function handleGoogleCallback(code, stateData) {
8242
8294
  const user = await usersRepository.findById(userId);
8243
8295
  const eventPayload = {
8244
8296
  userId: String(userId),
8245
- provider: "google",
8297
+ provider,
8246
8298
  email: user?.email || void 0,
8247
8299
  phone: user?.phone || void 0,
8248
8300
  metadata: stateData.metadata
@@ -8259,14 +8311,14 @@ async function handleGoogleCallback(code, stateData) {
8259
8311
  isNewUser
8260
8312
  };
8261
8313
  }
8262
- async function createOrLinkUser(googleUser, tokens) {
8263
- const existingUser = googleUser.email ? await usersRepository.findByEmail(googleUser.email) : null;
8314
+ async function createOrLinkUser(provider, identity, tokens) {
8315
+ const existingUser = identity.email ? await usersRepository.findByEmail(identity.email) : null;
8264
8316
  let userId;
8265
8317
  let isNewUser = false;
8266
8318
  if (existingUser) {
8267
- if (!googleUser.verified_email) {
8319
+ if (!identity.emailVerified) {
8268
8320
  throw new ValidationError3({
8269
- message: "Cannot link to existing account with unverified email. Please verify your email with Google first."
8321
+ message: "Cannot link to existing account with unverified email. Please verify your email with the provider first."
8270
8322
  });
8271
8323
  }
8272
8324
  userId = existingUser.id;
@@ -8282,26 +8334,26 @@ async function createOrLinkUser(googleUser, tokens) {
8282
8334
  throw new Error("Default user role not found. Run initializeAuth() first.");
8283
8335
  }
8284
8336
  const newUser = await usersRepository.create({
8285
- email: googleUser.verified_email ? googleUser.email : null,
8337
+ email: identity.emailVerified ? identity.email : null,
8286
8338
  phone: null,
8287
8339
  passwordHash: null,
8288
8340
  // OAuth 사용자는 비밀번호 없음
8289
8341
  passwordChangeRequired: false,
8290
8342
  roleId: userRole.id,
8291
8343
  status: "active",
8292
- emailVerifiedAt: googleUser.verified_email ? /* @__PURE__ */ new Date() : null
8344
+ emailVerifiedAt: identity.emailVerified ? /* @__PURE__ */ new Date() : null
8293
8345
  });
8294
8346
  userId = newUser.id;
8295
8347
  isNewUser = true;
8296
8348
  }
8297
8349
  await socialAccountsRepository.create({
8298
8350
  userId,
8299
- provider: "google",
8300
- providerUserId: googleUser.id,
8301
- providerEmail: googleUser.email,
8302
- accessToken: tokens.access_token,
8303
- refreshToken: tokens.refresh_token ?? null,
8304
- tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8351
+ provider,
8352
+ providerUserId: identity.providerUserId,
8353
+ providerEmail: identity.email,
8354
+ accessToken: tokens.accessToken,
8355
+ refreshToken: tokens.refreshToken ?? null,
8356
+ tokenExpiresAt: tokenExpiryDate(tokens.expiresIn)
8305
8357
  });
8306
8358
  return { userId, isNewUser };
8307
8359
  }
@@ -8320,23 +8372,10 @@ function buildOAuthErrorUrl(error) {
8320
8372
  return errorUrl.replace("{error}", encodeURIComponent(error));
8321
8373
  }
8322
8374
  function isOAuthProviderEnabled(provider) {
8323
- switch (provider) {
8324
- case "google":
8325
- return isGoogleOAuthEnabled();
8326
- case "github":
8327
- case "kakao":
8328
- case "naver":
8329
- return false;
8330
- default:
8331
- return false;
8332
- }
8375
+ return getOAuthProvider(provider)?.isEnabled() ?? false;
8333
8376
  }
8334
8377
  function getEnabledOAuthProviders() {
8335
- const providers = [];
8336
- if (isGoogleOAuthEnabled()) {
8337
- providers.push("google");
8338
- }
8339
- return providers;
8378
+ return getRegisteredProviders().filter((p) => p.isEnabled()).map((p) => p.id);
8340
8379
  }
8341
8380
  var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
8342
8381
  async function getGoogleAccessToken(userId) {
@@ -8359,7 +8398,7 @@ async function getGoogleAccessToken(userId) {
8359
8398
  await socialAccountsRepository.updateTokens(account.id, {
8360
8399
  accessToken: tokens.access_token,
8361
8400
  refreshToken: tokens.refresh_token ?? account.refreshToken,
8362
- tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
8401
+ tokenExpiresAt: tokenExpiryDate(tokens.expires_in)
8363
8402
  });
8364
8403
  return tokens.access_token;
8365
8404
  }
@@ -9119,7 +9158,13 @@ var userRouter = defineRouter3({
9119
9158
  init_esm();
9120
9159
  init_types();
9121
9160
  import { Transactional as Transactional2 } from "@spfn/core/db";
9161
+ import { ValidationError as ValidationError4 } from "@spfn/core/errors";
9122
9162
  import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
9163
+ var providerParams = Type.Object({
9164
+ provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
9165
+ description: "OAuth provider id (google, github, kakao, naver, superself)"
9166
+ })
9167
+ });
9123
9168
  var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
9124
9169
  query: Type.Object({
9125
9170
  state: Type.String({
@@ -9216,10 +9261,12 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
9216
9261
  }).skip(["auth"]).handler(async (c) => {
9217
9262
  const { body } = await c.data();
9218
9263
  if (!isGoogleOAuthEnabled()) {
9219
- throw new Error("Google OAuth is not configured");
9264
+ throw new ValidationError4({ message: "Google OAuth is not configured" });
9220
9265
  }
9221
9266
  if (!body.state) {
9222
- throw new Error("OAuth state is required. Ensure the OAuth interceptor is configured.");
9267
+ throw new ValidationError4({
9268
+ message: "OAuth state is required. Ensure the OAuth interceptor is configured."
9269
+ });
9223
9270
  }
9224
9271
  return { authUrl: getGoogleAuthUrl(body.state) };
9225
9272
  });
@@ -9238,13 +9285,88 @@ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
9238
9285
  returnUrl: body.returnUrl || "/"
9239
9286
  };
9240
9287
  });
9288
+ var oauthProviderStart = route4.get("/_auth/oauth/:provider").input({
9289
+ params: providerParams,
9290
+ query: Type.Object({
9291
+ state: Type.String({
9292
+ description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
9293
+ })
9294
+ })
9295
+ }).skip(["auth"]).handler(async (c) => {
9296
+ const { params, query } = await c.data();
9297
+ const provider = getOAuthProvider(params.provider);
9298
+ if (!provider?.isEnabled()) {
9299
+ return c.redirect(buildOAuthErrorUrl(`OAuth provider '${params.provider}' is not configured`));
9300
+ }
9301
+ return c.redirect(provider.getAuthUrl(query.state));
9302
+ });
9303
+ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input({
9304
+ params: providerParams,
9305
+ query: Type.Object({
9306
+ code: Type.Optional(Type.String({
9307
+ description: "Authorization code from provider"
9308
+ })),
9309
+ state: Type.Optional(Type.String({
9310
+ description: "OAuth state parameter"
9311
+ })),
9312
+ error: Type.Optional(Type.String({
9313
+ description: "Error code from provider"
9314
+ })),
9315
+ error_description: Type.Optional(Type.String({
9316
+ description: "Error description from provider"
9317
+ }))
9318
+ })
9319
+ }).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
9320
+ const { params, query } = await c.data();
9321
+ if (query.error) {
9322
+ const errorMessage = query.error_description || query.error;
9323
+ return c.redirect(buildOAuthErrorUrl(errorMessage));
9324
+ }
9325
+ if (!query.code || !query.state) {
9326
+ return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
9327
+ }
9328
+ try {
9329
+ const result = await oauthCallbackService({
9330
+ provider: params.provider,
9331
+ code: query.code,
9332
+ state: query.state
9333
+ });
9334
+ return c.redirect(result.redirectUrl);
9335
+ } catch (err) {
9336
+ const message = err instanceof Error ? err.message : "OAuth callback failed";
9337
+ return c.redirect(buildOAuthErrorUrl(message));
9338
+ }
9339
+ });
9340
+ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
9341
+ params: providerParams,
9342
+ body: Type.Object({
9343
+ returnUrl: Type.Optional(Type.String({
9344
+ description: "URL to redirect after OAuth success"
9345
+ })),
9346
+ state: Type.Optional(Type.String({
9347
+ description: "Encrypted OAuth state (injected by interceptor)"
9348
+ }))
9349
+ })
9350
+ }).skip(["auth"]).handler(async (c) => {
9351
+ const { params, body } = await c.data();
9352
+ const provider = requireEnabledProvider(params.provider);
9353
+ if (!body.state) {
9354
+ throw new ValidationError4({
9355
+ message: "OAuth state is required. Ensure the OAuth interceptor is configured."
9356
+ });
9357
+ }
9358
+ return { authUrl: provider.getAuthUrl(body.state) };
9359
+ });
9241
9360
  var oauthRouter = defineRouter4({
9242
9361
  oauthGoogleStart,
9243
9362
  oauthGoogleCallback,
9244
9363
  oauthStart,
9245
9364
  oauthProviders,
9246
9365
  getGoogleOAuthUrl,
9247
- oauthFinalize
9366
+ oauthFinalize,
9367
+ oauthProviderStart,
9368
+ oauthProviderCallback,
9369
+ getProviderOAuthUrl
9248
9370
  });
9249
9371
 
9250
9372
  // src/server/routes/admin/index.ts
@@ -9364,6 +9486,9 @@ var mainAuthRouter = defineRouter5({
9364
9486
  oauthProviders,
9365
9487
  getGoogleOAuthUrl,
9366
9488
  oauthFinalize,
9489
+ oauthProviderStart,
9490
+ oauthProviderCallback,
9491
+ getProviderOAuthUrl,
9367
9492
  // Invitation routes
9368
9493
  getInvitation,
9369
9494
  acceptInvitation: acceptInvitation2,
@@ -9788,8 +9913,10 @@ export {
9788
9913
  getKeyId,
9789
9914
  getKeySize,
9790
9915
  getLocale,
9916
+ getOAuthProvider,
9791
9917
  getOneTimeTokenManager,
9792
9918
  getOptionalAuth,
9919
+ getRegisteredProviders,
9793
9920
  getRole,
9794
9921
  getRoleByName,
9795
9922
  getRolePermissions,
@@ -9803,6 +9930,7 @@ export {
9803
9930
  getUserPermissions,
9804
9931
  getUserProfileService,
9805
9932
  getUserRole,
9933
+ googleProvider,
9806
9934
  hasAllPermissions,
9807
9935
  hasAnyPermission,
9808
9936
  hasAnyRole,
@@ -9829,10 +9957,12 @@ export {
9829
9957
  permissions,
9830
9958
  permissionsRepository,
9831
9959
  refreshAccessToken,
9960
+ registerOAuthProvider,
9832
9961
  registerPublicKeyService,
9833
9962
  registerService,
9834
9963
  removePermissionFromRole,
9835
9964
  requireAnyPermission,
9965
+ requireEnabledProvider,
9836
9966
  requirePermissions,
9837
9967
  requireRole,
9838
9968
  resendInvitation,