@syncello/auth 2.5.1 → 3.2.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.js CHANGED
@@ -507,16 +507,16 @@ function createDeviceToken() {
507
507
  };
508
508
  }
509
509
  async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
510
- const { eq: eq31, and: and14, gt: gt6 } = await import("drizzle-orm");
510
+ const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
511
511
  const [device] = await db.select().from(userTrustedDevicesTable).where(
512
512
  and14(
513
- eq31(userTrustedDevicesTable.userId, userId),
514
- eq31(userTrustedDevicesTable.tokenHash, tokenHash),
513
+ eq33(userTrustedDevicesTable.userId, userId),
514
+ eq33(userTrustedDevicesTable.tokenHash, tokenHash),
515
515
  gt6(userTrustedDevicesTable.expiresAt, Date.now())
516
516
  )
517
517
  ).limit(1);
518
518
  if (device) {
519
- await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq31(userTrustedDevicesTable.id, device.id));
519
+ await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
520
520
  return true;
521
521
  }
522
522
  return false;
@@ -1438,7 +1438,7 @@ var requireVerifiedEmail = createMiddleware4(
1438
1438
  );
1439
1439
 
1440
1440
  // src/routes/index.ts
1441
- import { OpenAPIHono as OpenAPIHono2 } from "@hono/zod-openapi";
1441
+ import { OpenAPIHono as OpenAPIHono3 } from "@hono/zod-openapi";
1442
1442
 
1443
1443
  // src/routes/signup.ts
1444
1444
  import { createRoute } from "@hono/zod-openapi";
@@ -2181,13 +2181,92 @@ var ResendAdapter = class {
2181
2181
  }
2182
2182
  };
2183
2183
 
2184
+ // src/lib/email/adapters/cloudflare-adapter.ts
2185
+ var CloudflareEmailAdapter = class {
2186
+ constructor(binding, options) {
2187
+ this.binding = binding;
2188
+ this.options = options;
2189
+ }
2190
+ binding;
2191
+ options;
2192
+ providerName = "cloudflare";
2193
+ async send(options) {
2194
+ if (this.options.dryRun) {
2195
+ logger_default.info("Cloudflare: Dry-run, email not sent", {
2196
+ provider: this.providerName,
2197
+ to: options.to,
2198
+ from: options.from,
2199
+ subject: options.subject
2200
+ });
2201
+ return { success: true, emailId: "dry-run" };
2202
+ }
2203
+ try {
2204
+ logger_default.info("Cloudflare: Sending email", {
2205
+ provider: this.providerName,
2206
+ to: options.to,
2207
+ subject: options.subject
2208
+ });
2209
+ const result = await this.binding.send({
2210
+ to: options.to,
2211
+ from: options.from,
2212
+ subject: options.subject,
2213
+ html: options.html,
2214
+ text: options.text,
2215
+ headers: options.tags ? Object.fromEntries(
2216
+ Object.entries(options.tags).map(([name, value]) => [`X-Tag-${name}`, value])
2217
+ ) : void 0
2218
+ });
2219
+ logger_default.info("Cloudflare: Email sent successfully", {
2220
+ provider: this.providerName,
2221
+ emailId: result.messageId,
2222
+ to: options.to
2223
+ });
2224
+ return { success: true, emailId: result.messageId };
2225
+ } catch (error) {
2226
+ const message = error instanceof Error ? error.message : "Unknown error";
2227
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
2228
+ const errorText = code ? `${code}: ${message}` : message;
2229
+ if (code === "E_RECIPIENT_SUPPRESSED") {
2230
+ logger_default.warn("Cloudflare: Recipient is on the suppression list", {
2231
+ provider: this.providerName,
2232
+ to: options.to
2233
+ });
2234
+ } else {
2235
+ logger_default.error("Cloudflare send error", {
2236
+ provider: this.providerName,
2237
+ error: errorText,
2238
+ to: options.to,
2239
+ subject: options.subject
2240
+ });
2241
+ }
2242
+ return { success: false, error: errorText };
2243
+ }
2244
+ }
2245
+ };
2246
+
2184
2247
  // src/lib/email/adapters/factory.ts
2185
2248
  function createEmailAdapter(env) {
2186
- logger_default.info("Creating email adapter", { provider: "resend" });
2187
- if (!env.RESEND_API_KEY) {
2188
- throw new Error("RESEND_API_KEY is required");
2249
+ const provider = env.EMAIL_PROVIDER ?? "resend";
2250
+ logger_default.info("Creating email adapter", { provider });
2251
+ switch (provider) {
2252
+ case "resend":
2253
+ if (!env.RESEND_API_KEY) {
2254
+ throw new Error("RESEND_API_KEY is required");
2255
+ }
2256
+ return new ResendAdapter(env.RESEND_API_KEY);
2257
+ case "cloudflare": {
2258
+ if (!env.EMAIL) {
2259
+ throw new Error(
2260
+ 'EMAIL send_email binding is required when EMAIL_PROVIDER=cloudflare - add [[send_email]] name = "EMAIL" to wrangler.toml'
2261
+ );
2262
+ }
2263
+ const environment = env.ENVIRONMENT?.toLowerCase();
2264
+ const dryRun = environment !== "production" && environment !== "staging";
2265
+ return new CloudflareEmailAdapter(env.EMAIL, { dryRun });
2266
+ }
2267
+ default:
2268
+ throw new Error(`Unknown EMAIL_PROVIDER: ${String(provider)}`);
2189
2269
  }
2190
- return new ResendAdapter(env.RESEND_API_KEY);
2191
2270
  }
2192
2271
 
2193
2272
  // src/lib/email/email-service.ts
@@ -2195,6 +2274,9 @@ var EmailService = class {
2195
2274
  adapter;
2196
2275
  env;
2197
2276
  get fromAddress() {
2277
+ if (this.env.EMAIL_FROM) {
2278
+ return this.env.EMAIL_FROM;
2279
+ }
2198
2280
  const appName = this.env.APP_NAME ?? "Your App";
2199
2281
  const appUrl = this.env.APP_URL ?? "";
2200
2282
  let domain = "example.com";
@@ -2816,19 +2898,19 @@ var signupRequestSchema = z.object({
2816
2898
  var loginRequestSchema = z.object({
2817
2899
  email: z.string().email().openapi({ example: "user@example.com" }),
2818
2900
  password: z.string().min(1).openapi({ example: "SecurePass123!" }),
2819
- turnstileToken: z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2901
+ turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2820
2902
  }).openapi("LoginRequest");
2821
2903
  var verifyEmailRequestSchema = z.object({
2822
2904
  token: z.string().min(1).openapi({ example: "verification-token-abc123" })
2823
2905
  }).openapi("VerifyEmailRequest");
2824
2906
  var forgotPasswordRequestSchema = z.object({
2825
2907
  email: z.string().email().openapi({ example: "user@example.com" }),
2826
- turnstileToken: z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2908
+ turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2827
2909
  }).openapi("ForgotPasswordRequest");
2828
2910
  var resetPasswordRequestSchema = z.object({
2829
2911
  token: z.string().min(1).openapi({ example: "reset-token-xyz789" }),
2830
2912
  password: z.string().min(8).openapi({ example: "NewSecurePass456!" }),
2831
- turnstileToken: z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2913
+ turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2832
2914
  }).openapi("ResetPasswordRequest");
2833
2915
  var changePasswordRequestSchema = z.object({
2834
2916
  currentPassword: z.string().min(1).openapi({ example: "OldPassword123!" }),
@@ -3051,7 +3133,8 @@ var signupHandler = async (c) => {
3051
3133
  verificationUrl,
3052
3134
  firstName: newUser.name || void 0
3053
3135
  },
3054
- database
3136
+ database,
3137
+ schema.users
3055
3138
  );
3056
3139
  const fingerprint = await generateFingerprint(c.req.raw);
3057
3140
  const ipAddress = getClientIp(c.req.raw);
@@ -3525,7 +3608,7 @@ var forgotPasswordRoute = createRoute6({
3525
3608
  });
3526
3609
  var forgotPasswordHandler = async (c) => {
3527
3610
  const { email, turnstileToken } = c.req.valid("json");
3528
- const database = c.get("db");
3611
+ const { db: database, schema } = getAuthContext(c);
3529
3612
  const env = c.env;
3530
3613
  const turnstileValid = await verifyTurnstileToken(
3531
3614
  turnstileToken,
@@ -3536,7 +3619,11 @@ var forgotPasswordHandler = async (c) => {
3536
3619
  if (!turnstileValid) {
3537
3620
  return problems.badRequest(c, "Invalid captcha");
3538
3621
  }
3539
- const resetResult = await createPasswordResetToken(database, email);
3622
+ const resetResult = await createPasswordResetToken(
3623
+ database,
3624
+ { users: schema.users, passwordResetTokens: schema.passwordResetTokens },
3625
+ email
3626
+ );
3540
3627
  if (!resetResult) {
3541
3628
  return c.json({ success: true });
3542
3629
  }
@@ -3548,7 +3635,8 @@ var forgotPasswordHandler = async (c) => {
3548
3635
  token: resetResult.token,
3549
3636
  resetUrl
3550
3637
  },
3551
- database
3638
+ database,
3639
+ schema.users
3552
3640
  );
3553
3641
  logSecurityEvent("password_reset_requested", "medium", { userId: resetResult.userId });
3554
3642
  return c.json({ success: true });
@@ -3690,7 +3778,7 @@ var changePasswordHandler = async (c) => {
3690
3778
  const userId = c.get("userId");
3691
3779
  if (!userId) return problems.unauthorized(c, "Not authenticated");
3692
3780
  const { currentPassword, newPassword } = c.req.valid("json");
3693
- const database = c.get("db");
3781
+ const { db: database, schema } = getAuthContext(c);
3694
3782
  const env = c.env;
3695
3783
  const passwordValidation = validatePassword(newPassword);
3696
3784
  if (!passwordValidation.valid) {
@@ -3707,7 +3795,8 @@ var changePasswordHandler = async (c) => {
3707
3795
  newPassword,
3708
3796
  currentSessionId,
3709
3797
  pepper: env.PASSWORD_PEPPER_V1,
3710
- db: database
3798
+ db: database,
3799
+ tables: { users: schema.users, sessions: schema.sessions }
3711
3800
  });
3712
3801
  if (!result.success) {
3713
3802
  return problems.badRequest(c, result.error || "Failed to change password");
@@ -3786,7 +3875,7 @@ var changeEmailHandler = async (c) => {
3786
3875
  const userId = c.get("userId");
3787
3876
  if (!userId) return problems.unauthorized(c, "Not authenticated");
3788
3877
  const { password, newEmail } = c.req.valid("json");
3789
- const database = c.get("db");
3878
+ const { db: database, schema } = getAuthContext(c);
3790
3879
  const env = c.env;
3791
3880
  const appUrl = env.APP_URL || "http://localhost:5173";
3792
3881
  const result = await requestEmailChange({
@@ -3794,7 +3883,8 @@ var changeEmailHandler = async (c) => {
3794
3883
  password,
3795
3884
  newEmail,
3796
3885
  pepper: env.PASSWORD_PEPPER_V1,
3797
- db: database
3886
+ db: database,
3887
+ tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
3798
3888
  });
3799
3889
  if (!result.success) {
3800
3890
  if (result.error === "Incorrect password") {
@@ -3806,7 +3896,8 @@ var changeEmailHandler = async (c) => {
3806
3896
  const confirmUrl = `${appUrl}/confirm-email-change?token=${result.confirmToken}`;
3807
3897
  const confirmResult = await emailService.sendEmailChangeConfirmation(
3808
3898
  { newEmail, confirmUrl },
3809
- database
3899
+ database,
3900
+ schema.users
3810
3901
  );
3811
3902
  if (!confirmResult.success) {
3812
3903
  return problems.badRequest(
@@ -3817,7 +3908,8 @@ var changeEmailHandler = async (c) => {
3817
3908
  const cancelUrl = `${appUrl}/cancel-email-change?token=${result.cancelToken}`;
3818
3909
  await emailService.sendEmailChangeNotification(
3819
3910
  { oldEmail: result.oldEmail, newEmail, cancelUrl },
3820
- database
3911
+ database,
3912
+ schema.users
3821
3913
  );
3822
3914
  logSecurityEvent("email_change_requested", "medium", { userId });
3823
3915
  return c.json({ success: true });
@@ -3858,8 +3950,12 @@ var confirmEmailChangeRoute = createRoute11({
3858
3950
  });
3859
3951
  var confirmEmailChangeHandler = async (c) => {
3860
3952
  const { token } = c.req.valid("json");
3861
- const database = c.get("db");
3862
- const result = await confirmEmailChange({ token, db: database });
3953
+ const { db: database, schema } = getAuthContext(c);
3954
+ const result = await confirmEmailChange({
3955
+ token,
3956
+ db: database,
3957
+ tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
3958
+ });
3863
3959
  if (!result.success) {
3864
3960
  return problems.badRequest(c, result.error || "Invalid or expired token");
3865
3961
  }
@@ -3900,8 +3996,12 @@ var cancelEmailChangeRoute = createRoute12({
3900
3996
  });
3901
3997
  var cancelEmailChangeHandler = async (c) => {
3902
3998
  const { token } = c.req.valid("json");
3903
- const database = c.get("db");
3904
- const result = await cancelEmailChange({ token, db: database });
3999
+ const { db: database, schema } = getAuthContext(c);
4000
+ const result = await cancelEmailChange({
4001
+ token,
4002
+ db: database,
4003
+ tables: { emailChangeTokens: schema.emailChangeTokens }
4004
+ });
3905
4005
  if (!result.success) {
3906
4006
  return problems.badRequest(c, result.error || "Invalid or expired token");
3907
4007
  }
@@ -4040,7 +4140,7 @@ var refreshHandler = async (c) => {
4040
4140
  if (!session) {
4041
4141
  return problems.unauthorized(c, "Invalid or expired refresh token");
4042
4142
  }
4043
- await deleteSession(db, session.id);
4143
+ await deleteSession(db, { sessions: schema.sessions }, session.id);
4044
4144
  const newSessionId = await createSession(
4045
4145
  db,
4046
4146
  { sessions: schema.sessions },
@@ -4131,7 +4231,8 @@ var resendVerificationHandler = async (c) => {
4131
4231
  verificationUrl,
4132
4232
  firstName: user.name || void 0
4133
4233
  },
4134
- db
4234
+ db,
4235
+ schema.users
4135
4236
  );
4136
4237
  logger_default.info("Verification email resent", { userId: user.id, email: user.email });
4137
4238
  return c.json({
@@ -4457,7 +4558,8 @@ var totpVerifyHandler = async (c) => {
4457
4558
  const firstName = user.name?.split(" ")[0] || null;
4458
4559
  await emailService.send2faEnabledEmail(
4459
4560
  { email: user.email, firstName, method: "totp" },
4460
- db
4561
+ db,
4562
+ schema.users
4461
4563
  );
4462
4564
  }
4463
4565
  logSecurityEvent("2fa_totp_enrolled", "high", { userId });
@@ -4514,12 +4616,12 @@ var totpDisableHandler = async (c) => {
4514
4616
  if (remaining.length === 0) {
4515
4617
  await db.delete(schema.userBackupCodes).where(eq22(schema.userBackupCodes.userId, userId));
4516
4618
  await db.delete(schema.userTrustedDevices).where(eq22(schema.userTrustedDevices.userId, userId));
4517
- await invalidateAllUserSessions(db, userId);
4619
+ await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
4518
4620
  const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq22(schema.users.id, userId)).limit(1);
4519
4621
  if (user) {
4520
4622
  const emailService = new EmailService(env);
4521
4623
  const firstName = user.name?.split(" ")[0] || null;
4522
- await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
4624
+ await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
4523
4625
  }
4524
4626
  logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "totp" });
4525
4627
  return c.json({ success: true, sessionInvalidated: true });
@@ -4575,7 +4677,7 @@ var emailSetupHandler = async (c) => {
4575
4677
  await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
4576
4678
  const emailService = new EmailService(env);
4577
4679
  const firstName = user.name?.split(" ")[0] || null;
4578
- await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
4680
+ await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
4579
4681
  logSecurityEvent("2fa_email_setup_initiated", "low", { userId });
4580
4682
  return c.json({ success: true });
4581
4683
  };
@@ -4656,7 +4758,8 @@ var emailVerifyHandler = async (c) => {
4656
4758
  const firstName = user.name?.split(" ")[0] || null;
4657
4759
  await emailService.send2faEnabledEmail(
4658
4760
  { email: user.email, firstName, method: "email" },
4659
- db
4761
+ db,
4762
+ schema.users
4660
4763
  );
4661
4764
  }
4662
4765
  logSecurityEvent("2fa_email_enrolled", "high", { userId });
@@ -4710,7 +4813,8 @@ var emailSendCodeHandler = async (c) => {
4710
4813
  const firstName = user.name?.split(" ")[0] || null;
4711
4814
  const emailResult = await emailService.send2faCodeEmail(
4712
4815
  { email: user.email, firstName, code },
4713
- db
4816
+ db,
4817
+ schema.users
4714
4818
  );
4715
4819
  if (!emailResult.success) {
4716
4820
  return problems.badRequest(
@@ -4772,12 +4876,12 @@ var emailDisableHandler = async (c) => {
4772
4876
  if (remaining.length === 0) {
4773
4877
  await db.delete(schema.userBackupCodes).where(eq26(schema.userBackupCodes.userId, userId));
4774
4878
  await db.delete(schema.userTrustedDevices).where(eq26(schema.userTrustedDevices.userId, userId));
4775
- await invalidateAllUserSessions(db, userId);
4879
+ await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
4776
4880
  const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where(eq26(schema.users.id, userId)).limit(1);
4777
4881
  if (user) {
4778
4882
  const emailService = new EmailService(env);
4779
4883
  const firstName = user.name?.split(" ")[0] || null;
4780
- await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
4884
+ await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
4781
4885
  }
4782
4886
  logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "email" });
4783
4887
  return c.json({ success: true, sessionInvalidated: true });
@@ -5098,7 +5202,7 @@ var challengeResendHandler = async (c) => {
5098
5202
  });
5099
5203
  const emailService = new EmailService(c.env);
5100
5204
  const firstName = user.name?.split(" ")[0] || null;
5101
- await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
5205
+ await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
5102
5206
  logger_default.info("Email 2FA code resent", { userId: payload.userId });
5103
5207
  return c.json({ success: true });
5104
5208
  };
@@ -5107,11 +5211,8 @@ var challengeResendHandler = async (c) => {
5107
5211
  var twoFa = new OpenAPIHono();
5108
5212
  twoFa.use("*", csrf);
5109
5213
  twoFa.use("/status", requireAuth);
5110
- twoFa.openapi(statusRoute, statusHandler);
5111
5214
  twoFa.use("/totp/setup", requireAuth);
5112
- twoFa.openapi(totpSetupRoute, totpSetupHandler);
5113
5215
  twoFa.use("/totp/verify", requireAuth);
5114
- twoFa.openapi(totpVerifyRoute, totpVerifyHandler);
5115
5216
  twoFa.use(
5116
5217
  "/totp/disable",
5117
5218
  requireAuth,
@@ -5123,11 +5224,8 @@ twoFa.use(
5123
5224
  // 5 attempts per 15 minutes
5124
5225
  })
5125
5226
  );
5126
- twoFa.openapi(totpDisableRoute, totpDisableHandler);
5127
5227
  twoFa.use("/email/setup", requireAuth);
5128
- twoFa.openapi(emailSetupRoute, emailSetupHandler);
5129
5228
  twoFa.use("/email/verify", requireAuth);
5130
- twoFa.openapi(emailVerifyRoute, emailVerifyHandler);
5131
5229
  twoFa.use(
5132
5230
  "/email/send-code",
5133
5231
  requireAuth,
@@ -5139,7 +5237,6 @@ twoFa.use(
5139
5237
  // 3 per 5 minutes
5140
5238
  })
5141
5239
  );
5142
- twoFa.openapi(emailSendCodeRoute, emailSendCodeHandler);
5143
5240
  twoFa.use(
5144
5241
  "/email/disable",
5145
5242
  requireAuth,
@@ -5151,11 +5248,8 @@ twoFa.use(
5151
5248
  // 5 attempts per 15 minutes
5152
5249
  })
5153
5250
  );
5154
- twoFa.openapi(emailDisableRoute, emailDisableHandler);
5155
5251
  twoFa.use("/trusted-devices", requireAuth);
5156
- twoFa.openapi(trustedDevicesGetRoute, trustedDevicesGetHandler);
5157
5252
  twoFa.use("/trusted-devices/:id", requireAuth);
5158
- twoFa.openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler);
5159
5253
  twoFa.use(
5160
5254
  "/backup-codes/regenerate",
5161
5255
  requireAuth,
@@ -5167,7 +5261,6 @@ twoFa.use(
5167
5261
  // 3 per hour per user
5168
5262
  })
5169
5263
  );
5170
- twoFa.openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler);
5171
5264
  twoFa.use(
5172
5265
  "/challenge",
5173
5266
  rateLimit({
@@ -5178,7 +5271,6 @@ twoFa.use(
5178
5271
  // 10 per 5 min per IP
5179
5272
  })
5180
5273
  );
5181
- twoFa.openapi(challengeRoute, challengeHandler);
5182
5274
  twoFa.use(
5183
5275
  "/challenge/resend",
5184
5276
  rateLimit({
@@ -5189,40 +5281,423 @@ twoFa.use(
5189
5281
  // 3 per 5 min per IP
5190
5282
  })
5191
5283
  );
5192
- twoFa.openapi(challengeResendRoute, challengeResendHandler);
5193
- var fa_default = twoFa;
5284
+ var twoFaRoutes = twoFa.openapi(statusRoute, statusHandler).openapi(totpSetupRoute, totpSetupHandler).openapi(totpVerifyRoute, totpVerifyHandler).openapi(totpDisableRoute, totpDisableHandler).openapi(emailSetupRoute, emailSetupHandler).openapi(emailVerifyRoute, emailVerifyHandler).openapi(emailSendCodeRoute, emailSendCodeHandler).openapi(emailDisableRoute, emailDisableHandler).openapi(trustedDevicesGetRoute, trustedDevicesGetHandler).openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler).openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler).openapi(challengeRoute, challengeHandler).openapi(challengeResendRoute, challengeResendHandler);
5285
+ var fa_default = twoFaRoutes;
5286
+
5287
+ // src/routes/oauth/index.ts
5288
+ import { OpenAPIHono as OpenAPIHono2 } from "@hono/zod-openapi";
5289
+
5290
+ // src/routes/oauth/authorize.ts
5291
+ import { createRoute as createRoute28 } from "@hono/zod-openapi";
5292
+
5293
+ // src/routes/oauth/providers.ts
5294
+ var PROVIDERS = {
5295
+ google: {
5296
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
5297
+ tokenUrl: "https://oauth2.googleapis.com/token",
5298
+ userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
5299
+ scopes: ["email"],
5300
+ emailExtractor: (data) => data.email || null,
5301
+ userIdExtractor: (data) => data.id || null
5302
+ },
5303
+ microsoft: {
5304
+ authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
5305
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
5306
+ userinfoUrl: "https://graph.microsoft.com/v1.0/me",
5307
+ scopes: ["openid", "email", "User.Read"],
5308
+ emailExtractor: (data) => data.mail || data.userPrincipalName || null,
5309
+ userIdExtractor: (data) => data.id || null
5310
+ }
5311
+ };
5312
+ function getProviderCredentials(provider, env) {
5313
+ if (provider === "google" && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
5314
+ return { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET };
5315
+ }
5316
+ if (provider === "microsoft" && env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) {
5317
+ return { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET };
5318
+ }
5319
+ return null;
5320
+ }
5321
+
5322
+ // src/routes/oauth/schemas.ts
5323
+ import { z as z10 } from "@hono/zod-openapi";
5324
+ var oauthProviderParamSchema = z10.object({
5325
+ provider: z10.enum(["google", "microsoft"]).openapi({ example: "google" })
5326
+ });
5327
+ var oauthAuthorizeQuerySchema = z10.object({
5328
+ redirect: z10.string().optional().openapi({ example: "/dashboard" }),
5329
+ invitationToken: z10.string().optional().openapi({ example: "token123" }),
5330
+ code_challenge: z10.string().min(43).max(128).optional().openapi({ example: "E9Mrozoa2owUednMY..." }),
5331
+ code_challenge_method: z10.enum(["S256"]).optional().openapi({ example: "S256" }),
5332
+ redirect_uri: z10.string().url().optional().openapi({ example: "myapp://oauth-callback" })
5333
+ }).openapi("OAuthAuthorizeQuery");
5334
+ var oauthCallbackQuerySchema = z10.object({
5335
+ code: z10.string().optional(),
5336
+ state: z10.string().optional(),
5337
+ error: z10.string().optional()
5338
+ });
5339
+ var oauthCompleteRequestSchema = z10.object({
5340
+ code: z10.string().uuid("Invalid code format").openapi({ example: "a1b2c3d4-..." })
5341
+ }).openapi("OAuthCompleteRequest");
5342
+ var oauthCompleteResponseSchema = z10.object({
5343
+ message: z10.string().openapi({ example: "Login successful" }),
5344
+ redirect: z10.string().openapi({ example: "/" }),
5345
+ user: z10.object({
5346
+ id: z10.string().uuid(),
5347
+ email: z10.string().email(),
5348
+ emailVerified: z10.boolean()
5349
+ })
5350
+ }).openapi("OAuthCompleteResponse");
5351
+ var errorResponseSchema4 = z10.object({
5352
+ type: z10.string().url(),
5353
+ title: z10.string(),
5354
+ status: z10.number().int(),
5355
+ detail: z10.string().optional()
5356
+ }).openapi("ErrorResponse");
5357
+
5358
+ // src/routes/oauth/authorize.ts
5359
+ var authorizeRoute = createRoute28({
5360
+ method: "get",
5361
+ path: "/{provider}/authorize",
5362
+ tags: ["OAuth"],
5363
+ summary: "Start OAuth login flow",
5364
+ description: "Generates a CSRF state token and redirects the user to the OAuth provider.",
5365
+ request: {
5366
+ params: oauthProviderParamSchema,
5367
+ query: oauthAuthorizeQuerySchema
5368
+ },
5369
+ responses: {
5370
+ 302: { description: "Redirect to OAuth provider or error page" }
5371
+ }
5372
+ });
5373
+ var authorizeHandler = async (c) => {
5374
+ const { provider } = c.req.valid("param");
5375
+ const {
5376
+ redirect: loginRedirect,
5377
+ invitationToken,
5378
+ code_challenge,
5379
+ code_challenge_method,
5380
+ redirect_uri: mobileRedirectUri
5381
+ } = c.req.valid("query");
5382
+ const providerConfig = PROVIDERS[provider];
5383
+ if (!providerConfig) {
5384
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_provider`);
5385
+ }
5386
+ const credentials = getProviderCredentials(provider, c.env);
5387
+ if (!credentials) {
5388
+ logError(new Error(`OAuth credentials not configured for ${provider}`), {
5389
+ context: "oauth_authorize",
5390
+ provider
5391
+ });
5392
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_not_configured`);
5393
+ }
5394
+ if (code_challenge && !code_challenge_method || !code_challenge && code_challenge_method) {
5395
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_pkce`);
5396
+ }
5397
+ const state = crypto.randomUUID();
5398
+ await c.env.OAUTH_STATES.put(
5399
+ `oauth:state:${state}`,
5400
+ JSON.stringify({
5401
+ provider,
5402
+ invitationToken: invitationToken || null,
5403
+ redirect: loginRedirect || null,
5404
+ codeChallenge: code_challenge || null,
5405
+ codeChallengeMethod: code_challenge_method || null,
5406
+ mobileRedirectUri: mobileRedirectUri || null
5407
+ }),
5408
+ { expirationTtl: 600 }
5409
+ // 10 minutes
5410
+ );
5411
+ const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
5412
+ const authUrl = new URL(providerConfig.authorizeUrl);
5413
+ authUrl.searchParams.set("client_id", credentials.clientId);
5414
+ authUrl.searchParams.set("redirect_uri", redirectUri);
5415
+ authUrl.searchParams.set("response_type", "code");
5416
+ if (providerConfig.scopes.length > 0) {
5417
+ authUrl.searchParams.set("scope", providerConfig.scopes.join(" "));
5418
+ }
5419
+ authUrl.searchParams.set("state", state);
5420
+ if (provider === "google") {
5421
+ authUrl.searchParams.set("prompt", "select_account");
5422
+ }
5423
+ logger_default.info("OAuth authorize initiated", { provider, hasPkce: !!code_challenge });
5424
+ return c.redirect(authUrl.toString());
5425
+ };
5426
+
5427
+ // src/routes/oauth/callback.ts
5428
+ import { createRoute as createRoute29 } from "@hono/zod-openapi";
5429
+ import { eq as eq31 } from "drizzle-orm";
5430
+ var callbackRoute = createRoute29({
5431
+ method: "get",
5432
+ path: "/{provider}/callback",
5433
+ tags: ["OAuth"],
5434
+ summary: "OAuth provider callback",
5435
+ description: "Handles the redirect from an OAuth provider. Validates CSRF state, exchanges the code for a token, and either starts a login or signup flow.",
5436
+ request: {
5437
+ params: oauthProviderParamSchema,
5438
+ query: oauthCallbackQuerySchema
5439
+ },
5440
+ responses: {
5441
+ 302: { description: "Redirect to app with login code or signup token" }
5442
+ }
5443
+ });
5444
+ var callbackHandler = async (c) => {
5445
+ const { provider } = c.req.valid("param");
5446
+ const { code, state, error } = c.req.valid("query");
5447
+ const appUrl = c.env.APP_URL;
5448
+ if (error) {
5449
+ logger_default.info("OAuth provider returned error", { provider, error });
5450
+ const errorType = error === "access_denied" || error === "consent_required" ? "oauth_cancelled" : "oauth_failed";
5451
+ return c.redirect(`${appUrl}/login?error=${errorType}`);
5452
+ }
5453
+ if (!code || !state) {
5454
+ logger_default.warn("Missing code or state in OAuth callback", { provider });
5455
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5456
+ }
5457
+ const storedState = await c.env.OAUTH_STATES.get(`oauth:state:${state}`);
5458
+ if (!storedState) {
5459
+ logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
5460
+ return c.redirect(`${appUrl}/login?error=oauth_expired`);
5461
+ }
5462
+ let stateData;
5463
+ try {
5464
+ stateData = JSON.parse(storedState);
5465
+ } catch (err) {
5466
+ logError(err, { context: "oauth_state_parse", provider });
5467
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5468
+ }
5469
+ if (stateData.provider !== provider) {
5470
+ logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
5471
+ return c.redirect(`${appUrl}/login?error=oauth_expired`);
5472
+ }
5473
+ await c.env.OAUTH_STATES.delete(`oauth:state:${state}`);
5474
+ const {
5475
+ provider: _stateProvider,
5476
+ invitationToken,
5477
+ redirect: loginRedirect,
5478
+ codeChallenge,
5479
+ codeChallengeMethod,
5480
+ mobileRedirectUri
5481
+ } = stateData;
5482
+ const codeVerifier = c.req.query("code_verifier");
5483
+ if (codeChallenge && codeChallengeMethod === "S256") {
5484
+ if (!codeVerifier) {
5485
+ logger_default.warn("OAuth PKCE: missing code_verifier", { provider });
5486
+ return c.redirect(`${appUrl}/login?error=oauth_pkce_missing`);
5487
+ }
5488
+ const pkceValid = await verifyCodeChallenge(codeVerifier, codeChallenge);
5489
+ if (!pkceValid) {
5490
+ logger_default.warn("OAuth PKCE: invalid code_verifier", { provider });
5491
+ return c.redirect(`${appUrl}/login?error=oauth_pkce_invalid`);
5492
+ }
5493
+ }
5494
+ const providerConfig = PROVIDERS[provider];
5495
+ if (!providerConfig) {
5496
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5497
+ }
5498
+ const credentials = getProviderCredentials(provider, c.env);
5499
+ if (!credentials) {
5500
+ return c.redirect(`${appUrl}/login?error=oauth_not_configured`);
5501
+ }
5502
+ const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
5503
+ try {
5504
+ const tokenResponse = await fetch(providerConfig.tokenUrl, {
5505
+ method: "POST",
5506
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5507
+ body: new URLSearchParams({
5508
+ grant_type: "authorization_code",
5509
+ code,
5510
+ redirect_uri: redirectUri,
5511
+ client_id: credentials.clientId,
5512
+ client_secret: credentials.clientSecret
5513
+ }),
5514
+ signal: AbortSignal.timeout(1e4)
5515
+ });
5516
+ if (!tokenResponse.ok) {
5517
+ const body = await tokenResponse.text();
5518
+ logError(new Error(`OAuth token exchange failed: ${tokenResponse.status} ${body}`), {
5519
+ context: "oauth_token_exchange",
5520
+ provider
5521
+ });
5522
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5523
+ }
5524
+ const tokenData = await tokenResponse.json();
5525
+ if (!tokenData.access_token) {
5526
+ logError(new Error("No access_token in OAuth response"), {
5527
+ context: "oauth_token_exchange",
5528
+ provider
5529
+ });
5530
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5531
+ }
5532
+ const userinfoResponse = await fetch(providerConfig.userinfoUrl, {
5533
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
5534
+ signal: AbortSignal.timeout(1e4)
5535
+ });
5536
+ if (!userinfoResponse.ok) {
5537
+ logError(new Error(`OAuth userinfo failed: ${userinfoResponse.status}`), {
5538
+ context: "oauth_userinfo",
5539
+ provider
5540
+ });
5541
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5542
+ }
5543
+ const userInfo = await userinfoResponse.json();
5544
+ const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
5545
+ const providerUserId = providerConfig.userIdExtractor(userInfo);
5546
+ if (!email) {
5547
+ logger_default.warn("No email returned from OAuth provider", { provider });
5548
+ return c.redirect(`${appUrl}/login?error=oauth_no_email`);
5549
+ }
5550
+ if (!providerUserId) {
5551
+ logger_default.warn("No user ID returned from OAuth provider", { provider });
5552
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5553
+ }
5554
+ const { db, schema } = getAuthContext(c);
5555
+ const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where(eq31(schema.users.email, email)).limit(1);
5556
+ if (existingUser) {
5557
+ const loginCode = crypto.randomUUID();
5558
+ await c.env.OAUTH_STATES.put(
5559
+ `oauth:login:${loginCode}`,
5560
+ JSON.stringify({
5561
+ userId: existingUser.id,
5562
+ email: existingUser.email,
5563
+ provider,
5564
+ providerUserId,
5565
+ redirect: loginRedirect,
5566
+ invitationToken
5567
+ }),
5568
+ { expirationTtl: 60 }
5569
+ );
5570
+ logSecurityEvent("oauth_login_initiated", "low", {
5571
+ userId: existingUser.id,
5572
+ email: existingUser.email,
5573
+ provider
5574
+ });
5575
+ if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
5576
+ const mobileUrl = new URL(mobileRedirectUri);
5577
+ mobileUrl.searchParams.set("code", loginCode);
5578
+ mobileUrl.searchParams.set("action", "login");
5579
+ return c.redirect(mobileUrl.toString());
5580
+ }
5581
+ return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
5582
+ } else {
5583
+ const signupToken = crypto.randomUUID();
5584
+ await c.env.OAUTH_STATES.put(
5585
+ `oauth:signup:${signupToken}`,
5586
+ JSON.stringify({ email, provider, providerUserId, invitationToken }),
5587
+ { expirationTtl: 600 }
5588
+ );
5589
+ logger_default.info("OAuth signup initiated", { provider, email });
5590
+ if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
5591
+ const mobileUrl = new URL(mobileRedirectUri);
5592
+ mobileUrl.searchParams.set("oauth_token", signupToken);
5593
+ mobileUrl.searchParams.set("email", email);
5594
+ if (invitationToken) {
5595
+ mobileUrl.searchParams.set("invitation", invitationToken);
5596
+ }
5597
+ return c.redirect(mobileUrl.toString());
5598
+ }
5599
+ let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
5600
+ if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
5601
+ return c.redirect(signupUrl);
5602
+ }
5603
+ } catch (err) {
5604
+ logError(err, { context: "oauth_callback", provider });
5605
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5606
+ }
5607
+ };
5608
+
5609
+ // src/routes/oauth/complete.ts
5610
+ import { createRoute as createRoute30 } from "@hono/zod-openapi";
5611
+ import { eq as eq32 } from "drizzle-orm";
5612
+ var completeRoute = createRoute30({
5613
+ method: "post",
5614
+ path: "/complete",
5615
+ tags: ["OAuth"],
5616
+ summary: "Complete OAuth login",
5617
+ description: "Exchanges a one-time login code (from the OAuth callback) for a session cookie. Called by the /auth/callback frontend page.",
5618
+ request: {
5619
+ body: {
5620
+ content: { "application/json": { schema: oauthCompleteRequestSchema } },
5621
+ required: true
5622
+ }
5623
+ },
5624
+ responses: {
5625
+ 200: {
5626
+ description: "Session created successfully",
5627
+ content: { "application/json": { schema: oauthCompleteResponseSchema } }
5628
+ },
5629
+ 401: {
5630
+ description: "Invalid or expired login code",
5631
+ content: { "application/json": { schema: errorResponseSchema4 } }
5632
+ }
5633
+ }
5634
+ });
5635
+ var completeMiddleware = [
5636
+ rateLimit({
5637
+ identifier: (c) => c.req.header("cf-connecting-ip") || "unknown",
5638
+ action: "oauth_complete",
5639
+ maxAttempts: 10,
5640
+ windowMs: 9e5
5641
+ // 15 minutes
5642
+ })
5643
+ ];
5644
+ var completeHandler = async (c) => {
5645
+ const { code } = c.req.valid("json");
5646
+ const stored = await c.env.OAUTH_STATES.get(`oauth:login:${code}`);
5647
+ if (!stored) {
5648
+ return problems.unauthorized(c, "Invalid or expired login code");
5649
+ }
5650
+ await c.env.OAUTH_STATES.delete(`oauth:login:${code}`);
5651
+ const {
5652
+ userId,
5653
+ email,
5654
+ provider,
5655
+ providerUserId,
5656
+ redirect: storedRedirect
5657
+ } = JSON.parse(stored);
5658
+ const { db, schema } = getAuthContext(c);
5659
+ await db.update(schema.users).set({ emailVerified: true }).where(eq32(schema.users.id, userId));
5660
+ await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
5661
+ const fingerprint = await generateFingerprint(c.req.raw);
5662
+ const ipAddress = getClientIp(c.req.raw);
5663
+ const sessionId = await createSession(db, { sessions: schema.sessions }, userId, fingerprint, ipAddress);
5664
+ const redirect = storedRedirect?.startsWith("/") && !storedRedirect.startsWith("//") ? storedRedirect : "/";
5665
+ logger_default.info("OAuth login completed", { userId, provider, sessionId: sessionId.slice(0, 8) });
5666
+ logSecurityEvent("oauth_login_success", "low", {
5667
+ userId,
5668
+ email,
5669
+ provider,
5670
+ ip: c.req.header("cf-connecting-ip")
5671
+ });
5672
+ return c.json(
5673
+ { message: "Login successful", redirect, user: { id: userId, email, emailVerified: true } },
5674
+ 200,
5675
+ { "Set-Cookie": setSessionCookie(sessionId, c.env) }
5676
+ );
5677
+ };
5678
+
5679
+ // src/routes/oauth/index.ts
5680
+ var oauth = new OpenAPIHono2();
5681
+ oauth.use("/complete", ...completeMiddleware);
5682
+ var oauthRoutes = oauth.openapi(authorizeRoute, authorizeHandler).openapi(callbackRoute, callbackHandler).openapi(completeRoute, completeHandler);
5683
+ var oauth_default = oauthRoutes;
5194
5684
 
5195
5685
  // src/routes/index.ts
5196
- var auth = new OpenAPIHono2();
5686
+ var auth = new OpenAPIHono3();
5197
5687
  auth.use("*", csrf);
5198
5688
  auth.use("/signup", ...signupMiddleware);
5199
- auth.openapi(signupRoute, signupHandler);
5200
5689
  auth.use("/login", ...loginMiddleware);
5201
- auth.openapi(loginRoute, loginHandler);
5202
- auth.openapi(logoutRoute, logoutHandler);
5203
5690
  auth.use("/me", ...meMiddleware);
5204
- auth.openapi(meRoute, meHandler);
5205
- auth.openapi(verifyEmailRoute, verifyEmailHandler);
5206
5691
  auth.use("/forgot-password", ...forgotPasswordMiddleware);
5207
- auth.openapi(forgotPasswordRoute, forgotPasswordHandler);
5208
5692
  auth.use("/reset-password", ...resetPasswordMiddleware);
5209
- auth.openapi(resetPasswordRoute, resetPasswordHandler);
5210
5693
  auth.use("/change-password", ...changePasswordMiddleware);
5211
- auth.openapi(changePasswordRoute, changePasswordHandler);
5212
5694
  auth.use("/heartbeat", ...heartbeatMiddleware);
5213
- auth.openapi(heartbeatRoute, heartbeatHandler);
5214
5695
  auth.use("/change-email", ...changeEmailMiddleware);
5215
- auth.openapi(changeEmailRoute, changeEmailHandler);
5216
- auth.openapi(confirmEmailChangeRoute, confirmEmailChangeHandler);
5217
- auth.openapi(cancelEmailChangeRoute, cancelEmailChangeHandler);
5218
5696
  auth.use("/account", ...deleteAccountMiddleware);
5219
- auth.openapi(deleteAccountRoute, deleteAccountHandler);
5220
5697
  auth.use("/refresh", ...refreshMiddleware);
5221
- auth.openapi(refreshRoute, refreshHandler);
5222
5698
  auth.use("/resend-verification", ...resendVerificationMiddleware);
5223
- auth.openapi(resendVerificationRoute, resendVerificationHandler);
5224
- auth.route("/2fa", fa_default);
5225
- var routes_default = auth;
5699
+ var authRoutes = auth.openapi(signupRoute, signupHandler).openapi(loginRoute, loginHandler).openapi(logoutRoute, logoutHandler).openapi(meRoute, meHandler).openapi(verifyEmailRoute, verifyEmailHandler).openapi(forgotPasswordRoute, forgotPasswordHandler).openapi(resetPasswordRoute, resetPasswordHandler).openapi(changePasswordRoute, changePasswordHandler).openapi(heartbeatRoute, heartbeatHandler).openapi(changeEmailRoute, changeEmailHandler).openapi(confirmEmailChangeRoute, confirmEmailChangeHandler).openapi(cancelEmailChangeRoute, cancelEmailChangeHandler).openapi(deleteAccountRoute, deleteAccountHandler).openapi(refreshRoute, refreshHandler).openapi(resendVerificationRoute, resendVerificationHandler).route("/2fa", fa_default).route("/oauth", oauth_default);
5700
+ var routes_default = authRoutes;
5226
5701
 
5227
5702
  // src/lib/email/webhook-verifier.ts
5228
5703
  import { Webhook } from "svix";
@@ -5250,6 +5725,7 @@ function verifyWebhookSignature(payload, signature, secret) {
5250
5725
  export {
5251
5726
  AUTH_DEFAULTS,
5252
5727
  CHALLENGE_TTL_MS,
5728
+ CloudflareEmailAdapter,
5253
5729
  EmailService,
5254
5730
  MAX_CHALLENGE_ATTEMPTS,
5255
5731
  ProblemTypes,