@syncello/auth 3.5.0 → 3.6.1

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: {
@@ -3612,6 +3650,16 @@ function generateEmailOtp() {
3612
3650
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
3613
3651
  return String(num % 1e6).padStart(6, "0");
3614
3652
  }
3653
+ var EMAIL_CODE_TTL_SECONDS = 300;
3654
+ async function sendEmailChallengeCode(env, database, usersTable, user, emailBranding) {
3655
+ const code = generateEmailOtp();
3656
+ await env.OAUTH_STATES.put(`email_2fa_challenge:${user.id}`, await hashToken(code), {
3657
+ expirationTtl: EMAIL_CODE_TTL_SECONDS
3658
+ });
3659
+ const emailService = new EmailService(env, void 0, emailBranding);
3660
+ const firstName = user.name?.split(" ")[0] || null;
3661
+ await emailService.send2faCodeEmail({ email: user.email, firstName, code }, database, usersTable);
3662
+ }
3615
3663
  async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3616
3664
  const { user2faMethods: user2faMethodsTable, userBackupCodes: userBackupCodesTable } = tables2;
3617
3665
  if (method === "totp") {
@@ -3696,7 +3744,7 @@ var loginRoute = (0, import_zod_openapi3.createRoute)({
3696
3744
  });
3697
3745
  var loginHandler = async (c) => {
3698
3746
  const { email, password, turnstileToken } = c.req.valid("json");
3699
- const { db: database, schema } = getAuthContext(c);
3747
+ const { db: database, schema, emailBranding } = getAuthContext(c);
3700
3748
  const env = c.env;
3701
3749
  const turnstileValid = await verifyTurnstileToken(
3702
3750
  turnstileToken,
@@ -3781,6 +3829,10 @@ var loginHandler = async (c) => {
3781
3829
  };
3782
3830
  await storeChallengeToken(env.OAUTH_STATES, challengeToken, challengePayload);
3783
3831
  setChallengeCookie(c, env, challengeToken);
3832
+ const emailMethod = enabled2faMethods.find((m) => m.method === "email");
3833
+ if (emailMethod && (emailMethod.isPrimary || enabled2faMethods.length === 1)) {
3834
+ await sendEmailChallengeCode(env, database, schema.users, user, emailBranding);
3835
+ }
3784
3836
  return c.json({
3785
3837
  requires2fa: true,
3786
3838
  methods: enabled2faMethods.map((m) => m.method)
@@ -5637,14 +5689,7 @@ var challengeResendHandler = async (c) => {
5637
5689
  logger_default.error("User not found during 2FA resend", { userId: payload.userId });
5638
5690
  return problems.notFound(c, "User not found");
5639
5691
  }
5640
- const code = generateEmailOtp();
5641
- const codeHash = await hashToken(code);
5642
- await c.env.OAUTH_STATES.put(`email_2fa_challenge:${payload.userId}`, codeHash, {
5643
- expirationTtl: 300
5644
- });
5645
- const emailService = new EmailService(c.env, void 0, emailBranding);
5646
- const firstName = user.name?.split(" ")[0] || null;
5647
- await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
5692
+ await sendEmailChallengeCode(c.env, db, schema.users, { id: payload.userId, ...user }, emailBranding);
5648
5693
  logger_default.info("Email 2FA code resent", { userId: payload.userId });
5649
5694
  return c.json({ success: true });
5650
5695
  };