@syncello/auth 3.4.1 → 3.6.0

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/index.cjs CHANGED
@@ -1341,12 +1341,13 @@ var import_drizzle_orm4 = require("drizzle-orm");
1341
1341
 
1342
1342
  // src/factory.ts
1343
1343
  function createAuth(config) {
1344
- const { db, schema, getEnv, emailBranding } = config;
1344
+ const { db, schema, getEnv, emailBranding, invitations } = config;
1345
1345
  const getContext = (c) => ({
1346
1346
  db,
1347
1347
  schema,
1348
1348
  env: getEnv(c),
1349
- emailBranding
1349
+ emailBranding,
1350
+ invitations
1350
1351
  });
1351
1352
  return {
1352
1353
  db,
@@ -3324,7 +3325,10 @@ var signupRequestSchema = import_zod_openapi.z.object({
3324
3325
  name: import_zod_openapi.z.string().min(1).optional().openapi({ example: "John Doe" }),
3325
3326
  turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
3326
3327
  // OAuth signup completion token (from /signup?oauth_token=...)
3327
- oauthToken: import_zod_openapi.z.string().optional().openapi({ example: "a1b2c3d4-..." })
3328
+ oauthToken: import_zod_openapi.z.string().optional().openapi({ example: "a1b2c3d4-..." }),
3329
+ // App invitation token (from /signup?token=...). When it validates for this
3330
+ // email, the account is created already-verified with no verification email.
3331
+ invitationToken: import_zod_openapi.z.string().optional().openapi({ example: "iIB8tDJJ..." })
3328
3332
  }).openapi("SignupRequest");
3329
3333
  var loginRequestSchema = import_zod_openapi.z.object({
3330
3334
  email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
@@ -3454,8 +3458,8 @@ var signupRoute = (0, import_zod_openapi2.createRoute)({
3454
3458
  }
3455
3459
  });
3456
3460
  var signupHandler = async (c) => {
3457
- const { email, password, name, turnstileToken, oauthToken } = c.req.valid("json");
3458
- const { db: database, schema, emailBranding } = getAuthContext(c);
3461
+ const { email, password, name, turnstileToken, oauthToken, invitationToken } = c.req.valid("json");
3462
+ const { db: database, schema, emailBranding, invitations } = getAuthContext(c);
3459
3463
  const env = c.env;
3460
3464
  if (oauthToken) {
3461
3465
  const stored = await env.OAUTH_STATES.get(`oauth:signup:${oauthToken}`);
@@ -3537,40 +3541,74 @@ var signupHandler = async (c) => {
3537
3541
  return problems.badRequest(c, passwordValidation.error || "Invalid password");
3538
3542
  }
3539
3543
  const hashedPassword = await hashPassword(password, env.PASSWORD_PEPPER_V1);
3544
+ let invitedVerified = false;
3545
+ if (invitationToken && invitations) {
3546
+ const match = await invitations.verify({
3547
+ token: invitationToken,
3548
+ email: email.toLowerCase(),
3549
+ db: database,
3550
+ env
3551
+ });
3552
+ if (!match || match.email.toLowerCase() !== email.toLowerCase()) {
3553
+ return problems.badRequest(
3554
+ c,
3555
+ "This invitation is invalid, expired, or for a different email address"
3556
+ );
3557
+ }
3558
+ invitedVerified = true;
3559
+ }
3540
3560
  const [newUser] = await database.insert(schema.users).values({
3541
3561
  email: email.toLowerCase(),
3542
3562
  name: name || null,
3543
3563
  hashedPassword,
3544
3564
  pepperKid: "v1",
3545
- emailVerified: false,
3565
+ emailVerified: invitedVerified,
3546
3566
  activatedAt: /* @__PURE__ */ new Date()
3547
3567
  }).returning();
3548
- const emailToken = generateSecureToken(32);
3549
- const tokenHash = await hashToken(emailToken);
3550
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3551
- await database.insert(schema.emailVerificationTokens).values({
3552
- tokenHash,
3553
- userId: newUser.id,
3554
- email: newUser.email,
3555
- expiresAt,
3556
- createdAt: Date.now()
3557
- });
3558
- const emailService = new EmailService(env, void 0, emailBranding);
3559
- const verificationUrl = `${env.APP_URL}/verify-email?token=${emailToken}`;
3560
- await emailService.sendVerificationEmail(
3561
- {
3568
+ if (!invitedVerified) {
3569
+ const emailToken = generateSecureToken(32);
3570
+ const tokenHash = await hashToken(emailToken);
3571
+ const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3572
+ await database.insert(schema.emailVerificationTokens).values({
3573
+ tokenHash,
3574
+ userId: newUser.id,
3562
3575
  email: newUser.email,
3563
- token: emailToken,
3564
- verificationUrl,
3565
- firstName: newUser.name || void 0
3566
- },
3567
- database,
3568
- schema.users
3569
- );
3576
+ expiresAt,
3577
+ createdAt: Date.now()
3578
+ });
3579
+ const emailService = new EmailService(env, void 0, emailBranding);
3580
+ const verificationUrl = `${env.APP_URL}/verify-email?token=${emailToken}`;
3581
+ await emailService.sendVerificationEmail(
3582
+ {
3583
+ email: newUser.email,
3584
+ token: emailToken,
3585
+ verificationUrl,
3586
+ firstName: newUser.name || void 0
3587
+ },
3588
+ database,
3589
+ schema.users
3590
+ );
3591
+ }
3592
+ if (invitedVerified && invitationToken && invitations?.onAccepted) {
3593
+ try {
3594
+ await invitations.onAccepted({
3595
+ token: invitationToken,
3596
+ user: { id: newUser.id, email: newUser.email, name: newUser.name },
3597
+ db: database,
3598
+ env
3599
+ });
3600
+ } catch (err) {
3601
+ logError(err, { context: "signup_invitation_onAccepted_failed", userId: newUser.id });
3602
+ }
3603
+ }
3570
3604
  const fingerprint = await generateFingerprint(c.req.raw);
3571
3605
  const ipAddress = getClientIp(c.req.raw);
3572
3606
  const sessionId = await createSession(database, { sessions: schema.sessions }, newUser.id, fingerprint, ipAddress);
3573
- logSecurityEvent("user_signup", "low", { userId: newUser.id, email: newUser.email });
3607
+ logSecurityEvent("user_signup", "low", {
3608
+ userId: newUser.id,
3609
+ email: newUser.email,
3610
+ invited: invitedVerified
3611
+ });
3574
3612
  c.header("Set-Cookie", setSessionCookie(sessionId, c.env), { append: true });
3575
3613
  return c.json({
3576
3614
  user: {
@@ -5738,9 +5776,10 @@ var PROVIDERS = {
5738
5776
  authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
5739
5777
  tokenUrl: "https://oauth2.googleapis.com/token",
5740
5778
  userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
5741
- scopes: ["email"],
5779
+ scopes: ["email", "profile"],
5742
5780
  emailExtractor: (data) => data.email || null,
5743
- userIdExtractor: (data) => data.id || null
5781
+ userIdExtractor: (data) => data.id || null,
5782
+ nameExtractor: (data) => data.name || null
5744
5783
  },
5745
5784
  microsoft: {
5746
5785
  authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
@@ -5748,7 +5787,8 @@ var PROVIDERS = {
5748
5787
  userinfoUrl: "https://graph.microsoft.com/v1.0/me",
5749
5788
  scopes: ["openid", "email", "User.Read"],
5750
5789
  emailExtractor: (data) => data.mail || data.userPrincipalName || null,
5751
- userIdExtractor: (data) => data.id || null
5790
+ userIdExtractor: (data) => data.id || null,
5791
+ nameExtractor: (data) => data.displayName || null
5752
5792
  }
5753
5793
  };
5754
5794
  function getProviderCredentials(provider, env) {
@@ -5985,6 +6025,7 @@ var callbackHandler = async (c) => {
5985
6025
  const userInfo = await userinfoResponse.json();
5986
6026
  const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
5987
6027
  const providerUserId = providerConfig.userIdExtractor(userInfo);
6028
+ const name = providerConfig.nameExtractor(userInfo);
5988
6029
  if (!email) {
5989
6030
  logger_default.warn("No email returned from OAuth provider", { provider });
5990
6031
  return c.redirect(`${appUrl}/login?error=oauth_no_email`);
@@ -5995,53 +6036,49 @@ var callbackHandler = async (c) => {
5995
6036
  }
5996
6037
  const { db, schema } = getAuthContext(c);
5997
6038
  const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.email, email)).limit(1);
5998
- if (existingUser) {
5999
- const loginCode = crypto.randomUUID();
6000
- await c.env.OAUTH_STATES.put(
6001
- `oauth:login:${loginCode}`,
6002
- JSON.stringify({
6003
- userId: existingUser.id,
6004
- email: existingUser.email,
6005
- provider,
6006
- providerUserId,
6007
- redirect: loginRedirect,
6008
- invitationToken
6009
- }),
6010
- { expirationTtl: 60 }
6011
- );
6012
- logSecurityEvent("oauth_login_initiated", "low", {
6013
- userId: existingUser.id,
6014
- email: existingUser.email,
6039
+ let authUser = existingUser;
6040
+ if (!authUser) {
6041
+ const [createdUser] = await db.insert(schema.users).values({
6042
+ email,
6043
+ name: name || null,
6044
+ hashedPassword: null,
6045
+ pepperKid: null,
6046
+ emailVerified: true,
6047
+ // Provider verified the email
6048
+ activatedAt: /* @__PURE__ */ new Date()
6049
+ }).returning({ id: schema.users.id, email: schema.users.email });
6050
+ authUser = createdUser;
6051
+ logSecurityEvent("user_signup_oauth", "low", {
6052
+ userId: createdUser.id,
6053
+ email: createdUser.email,
6015
6054
  provider
6016
6055
  });
6017
- if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
6018
- const mobileUrl = new URL(mobileRedirectUri);
6019
- mobileUrl.searchParams.set("code", loginCode);
6020
- mobileUrl.searchParams.set("action", "login");
6021
- return c.redirect(mobileUrl.toString());
6022
- }
6023
- return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
6024
- } else {
6025
- const signupToken = crypto.randomUUID();
6026
- await c.env.OAUTH_STATES.put(
6027
- `oauth:signup:${signupToken}`,
6028
- JSON.stringify({ email, provider, providerUserId, invitationToken }),
6029
- { expirationTtl: 600 }
6030
- );
6031
- logger_default.info("OAuth signup initiated", { provider, email });
6032
- if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
6033
- const mobileUrl = new URL(mobileRedirectUri);
6034
- mobileUrl.searchParams.set("oauth_token", signupToken);
6035
- mobileUrl.searchParams.set("email", email);
6036
- if (invitationToken) {
6037
- mobileUrl.searchParams.set("invitation", invitationToken);
6038
- }
6039
- return c.redirect(mobileUrl.toString());
6040
- }
6041
- let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
6042
- if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
6043
- return c.redirect(signupUrl);
6044
6056
  }
6057
+ const loginCode = crypto.randomUUID();
6058
+ await c.env.OAUTH_STATES.put(
6059
+ `oauth:login:${loginCode}`,
6060
+ JSON.stringify({
6061
+ userId: authUser.id,
6062
+ email: authUser.email,
6063
+ provider,
6064
+ providerUserId,
6065
+ redirect: loginRedirect,
6066
+ invitationToken
6067
+ }),
6068
+ { expirationTtl: 60 }
6069
+ );
6070
+ logSecurityEvent("oauth_login_initiated", "low", {
6071
+ userId: authUser.id,
6072
+ email: authUser.email,
6073
+ provider
6074
+ });
6075
+ if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
6076
+ const mobileUrl = new URL(mobileRedirectUri);
6077
+ mobileUrl.searchParams.set("code", loginCode);
6078
+ mobileUrl.searchParams.set("action", "login");
6079
+ return c.redirect(mobileUrl.toString());
6080
+ }
6081
+ return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
6045
6082
  } catch (err) {
6046
6083
  logError(err, { context: "oauth_callback", provider });
6047
6084
  return c.redirect(`${appUrl}/login?error=oauth_failed`);