@syncello/auth 3.1.0 → 3.2.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
@@ -32,6 +32,7 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AUTH_DEFAULTS: () => AUTH_DEFAULTS,
34
34
  CHALLENGE_TTL_MS: () => CHALLENGE_TTL_MS,
35
+ CloudflareEmailAdapter: () => CloudflareEmailAdapter,
35
36
  EmailService: () => EmailService,
36
37
  MAX_CHALLENGE_ATTEMPTS: () => MAX_CHALLENGE_ATTEMPTS,
37
38
  ProblemTypes: () => ProblemTypes,
@@ -2572,13 +2573,93 @@ var ResendAdapter = class {
2572
2573
  }
2573
2574
  };
2574
2575
 
2576
+ // src/lib/email/adapters/cloudflare-adapter.ts
2577
+ var CloudflareEmailAdapter = class {
2578
+ constructor(binding, options) {
2579
+ this.binding = binding;
2580
+ this.options = options;
2581
+ }
2582
+ binding;
2583
+ options;
2584
+ providerName = "cloudflare";
2585
+ async send(options) {
2586
+ if (this.options.dryRun) {
2587
+ logger_default.info("Cloudflare: Dry-run, email not sent", {
2588
+ provider: this.providerName,
2589
+ to: options.to,
2590
+ from: options.from,
2591
+ subject: options.subject
2592
+ });
2593
+ return { success: true, emailId: "dry-run" };
2594
+ }
2595
+ try {
2596
+ logger_default.info("Cloudflare: Sending email", {
2597
+ provider: this.providerName,
2598
+ to: options.to,
2599
+ subject: options.subject
2600
+ });
2601
+ const result = await this.binding.send({
2602
+ to: options.to,
2603
+ from: options.from,
2604
+ subject: options.subject,
2605
+ html: options.html,
2606
+ text: options.text,
2607
+ headers: options.tags ? Object.fromEntries(
2608
+ Object.entries(options.tags).map(([name, value]) => [`X-Tag-${name}`, value])
2609
+ ) : void 0
2610
+ });
2611
+ logger_default.info("Cloudflare: Email sent successfully", {
2612
+ provider: this.providerName,
2613
+ emailId: result.messageId,
2614
+ to: options.to
2615
+ });
2616
+ return { success: true, emailId: result.messageId };
2617
+ } catch (error) {
2618
+ const message = error instanceof Error ? error.message : "Unknown error";
2619
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
2620
+ const errorText = code ? `${code}: ${message}` : message;
2621
+ if (code === "E_RECIPIENT_SUPPRESSED") {
2622
+ logger_default.warn("Cloudflare: Recipient is on the suppression list", {
2623
+ provider: this.providerName,
2624
+ to: options.to
2625
+ });
2626
+ } else {
2627
+ logger_default.error("Cloudflare send error", {
2628
+ provider: this.providerName,
2629
+ error: errorText,
2630
+ rawError: error,
2631
+ to: options.to,
2632
+ subject: options.subject
2633
+ });
2634
+ }
2635
+ return { success: false, error: errorText };
2636
+ }
2637
+ }
2638
+ };
2639
+
2575
2640
  // src/lib/email/adapters/factory.ts
2576
2641
  function createEmailAdapter(env) {
2577
- logger_default.info("Creating email adapter", { provider: "resend" });
2578
- if (!env.RESEND_API_KEY) {
2579
- throw new Error("RESEND_API_KEY is required");
2642
+ const provider = env.EMAIL_PROVIDER ?? "resend";
2643
+ logger_default.info("Creating email adapter", { provider });
2644
+ switch (provider) {
2645
+ case "resend":
2646
+ if (!env.RESEND_API_KEY) {
2647
+ throw new Error("RESEND_API_KEY is required");
2648
+ }
2649
+ return new ResendAdapter(env.RESEND_API_KEY);
2650
+ case "cloudflare": {
2651
+ if (!env.EMAIL) {
2652
+ throw new Error(
2653
+ 'EMAIL send_email binding is required when EMAIL_PROVIDER=cloudflare - add [[send_email]] name = "EMAIL" to wrangler.toml'
2654
+ );
2655
+ }
2656
+ const environment = env.ENVIRONMENT?.toLowerCase();
2657
+ const dryRun = environment !== "production" && environment !== "staging";
2658
+ return new CloudflareEmailAdapter(env.EMAIL, { dryRun });
2659
+ }
2660
+ default:
2661
+ throw new Error(`Unknown EMAIL_PROVIDER: ${String(provider)}`);
2580
2662
  }
2581
- return new ResendAdapter(env.RESEND_API_KEY);
2582
2663
  }
2583
2664
 
2584
2665
  // src/lib/email/email-service.ts
@@ -2586,6 +2667,9 @@ var EmailService = class {
2586
2667
  adapter;
2587
2668
  env;
2588
2669
  get fromAddress() {
2670
+ if (this.env.EMAIL_FROM) {
2671
+ return this.env.EMAIL_FROM;
2672
+ }
2589
2673
  const appName = this.env.APP_NAME ?? "Your App";
2590
2674
  const appUrl = this.env.APP_URL ?? "";
2591
2675
  let domain = "example.com";
@@ -3442,7 +3526,8 @@ var signupHandler = async (c) => {
3442
3526
  verificationUrl,
3443
3527
  firstName: newUser.name || void 0
3444
3528
  },
3445
- database
3529
+ database,
3530
+ schema.users
3446
3531
  );
3447
3532
  const fingerprint = await generateFingerprint(c.req.raw);
3448
3533
  const ipAddress = getClientIp(c.req.raw);
@@ -3916,7 +4001,7 @@ var forgotPasswordRoute = (0, import_zod_openapi7.createRoute)({
3916
4001
  });
3917
4002
  var forgotPasswordHandler = async (c) => {
3918
4003
  const { email, turnstileToken } = c.req.valid("json");
3919
- const database = c.get("db");
4004
+ const { db: database, schema } = getAuthContext(c);
3920
4005
  const env = c.env;
3921
4006
  const turnstileValid = await verifyTurnstileToken(
3922
4007
  turnstileToken,
@@ -3927,7 +4012,11 @@ var forgotPasswordHandler = async (c) => {
3927
4012
  if (!turnstileValid) {
3928
4013
  return problems.badRequest(c, "Invalid captcha");
3929
4014
  }
3930
- const resetResult = await createPasswordResetToken(database, email);
4015
+ const resetResult = await createPasswordResetToken(
4016
+ database,
4017
+ { users: schema.users, passwordResetTokens: schema.passwordResetTokens },
4018
+ email
4019
+ );
3931
4020
  if (!resetResult) {
3932
4021
  return c.json({ success: true });
3933
4022
  }
@@ -3939,7 +4028,8 @@ var forgotPasswordHandler = async (c) => {
3939
4028
  token: resetResult.token,
3940
4029
  resetUrl
3941
4030
  },
3942
- database
4031
+ database,
4032
+ schema.users
3943
4033
  );
3944
4034
  logSecurityEvent("password_reset_requested", "medium", { userId: resetResult.userId });
3945
4035
  return c.json({ success: true });
@@ -4081,7 +4171,7 @@ var changePasswordHandler = async (c) => {
4081
4171
  const userId = c.get("userId");
4082
4172
  if (!userId) return problems.unauthorized(c, "Not authenticated");
4083
4173
  const { currentPassword, newPassword } = c.req.valid("json");
4084
- const database = c.get("db");
4174
+ const { db: database, schema } = getAuthContext(c);
4085
4175
  const env = c.env;
4086
4176
  const passwordValidation = validatePassword(newPassword);
4087
4177
  if (!passwordValidation.valid) {
@@ -4098,7 +4188,8 @@ var changePasswordHandler = async (c) => {
4098
4188
  newPassword,
4099
4189
  currentSessionId,
4100
4190
  pepper: env.PASSWORD_PEPPER_V1,
4101
- db: database
4191
+ db: database,
4192
+ tables: { users: schema.users, sessions: schema.sessions }
4102
4193
  });
4103
4194
  if (!result.success) {
4104
4195
  return problems.badRequest(c, result.error || "Failed to change password");
@@ -4177,7 +4268,7 @@ var changeEmailHandler = async (c) => {
4177
4268
  const userId = c.get("userId");
4178
4269
  if (!userId) return problems.unauthorized(c, "Not authenticated");
4179
4270
  const { password, newEmail } = c.req.valid("json");
4180
- const database = c.get("db");
4271
+ const { db: database, schema } = getAuthContext(c);
4181
4272
  const env = c.env;
4182
4273
  const appUrl = env.APP_URL || "http://localhost:5173";
4183
4274
  const result = await requestEmailChange({
@@ -4185,7 +4276,8 @@ var changeEmailHandler = async (c) => {
4185
4276
  password,
4186
4277
  newEmail,
4187
4278
  pepper: env.PASSWORD_PEPPER_V1,
4188
- db: database
4279
+ db: database,
4280
+ tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
4189
4281
  });
4190
4282
  if (!result.success) {
4191
4283
  if (result.error === "Incorrect password") {
@@ -4197,7 +4289,8 @@ var changeEmailHandler = async (c) => {
4197
4289
  const confirmUrl = `${appUrl}/confirm-email-change?token=${result.confirmToken}`;
4198
4290
  const confirmResult = await emailService.sendEmailChangeConfirmation(
4199
4291
  { newEmail, confirmUrl },
4200
- database
4292
+ database,
4293
+ schema.users
4201
4294
  );
4202
4295
  if (!confirmResult.success) {
4203
4296
  return problems.badRequest(
@@ -4208,7 +4301,8 @@ var changeEmailHandler = async (c) => {
4208
4301
  const cancelUrl = `${appUrl}/cancel-email-change?token=${result.cancelToken}`;
4209
4302
  await emailService.sendEmailChangeNotification(
4210
4303
  { oldEmail: result.oldEmail, newEmail, cancelUrl },
4211
- database
4304
+ database,
4305
+ schema.users
4212
4306
  );
4213
4307
  logSecurityEvent("email_change_requested", "medium", { userId });
4214
4308
  return c.json({ success: true });
@@ -4249,8 +4343,12 @@ var confirmEmailChangeRoute = (0, import_zod_openapi13.createRoute)({
4249
4343
  });
4250
4344
  var confirmEmailChangeHandler = async (c) => {
4251
4345
  const { token } = c.req.valid("json");
4252
- const database = c.get("db");
4253
- const result = await confirmEmailChange({ token, db: database });
4346
+ const { db: database, schema } = getAuthContext(c);
4347
+ const result = await confirmEmailChange({
4348
+ token,
4349
+ db: database,
4350
+ tables: { users: schema.users, emailChangeTokens: schema.emailChangeTokens }
4351
+ });
4254
4352
  if (!result.success) {
4255
4353
  return problems.badRequest(c, result.error || "Invalid or expired token");
4256
4354
  }
@@ -4291,8 +4389,12 @@ var cancelEmailChangeRoute = (0, import_zod_openapi15.createRoute)({
4291
4389
  });
4292
4390
  var cancelEmailChangeHandler = async (c) => {
4293
4391
  const { token } = c.req.valid("json");
4294
- const database = c.get("db");
4295
- const result = await cancelEmailChange({ token, db: database });
4392
+ const { db: database, schema } = getAuthContext(c);
4393
+ const result = await cancelEmailChange({
4394
+ token,
4395
+ db: database,
4396
+ tables: { emailChangeTokens: schema.emailChangeTokens }
4397
+ });
4296
4398
  if (!result.success) {
4297
4399
  return problems.badRequest(c, result.error || "Invalid or expired token");
4298
4400
  }
@@ -4431,7 +4533,7 @@ var refreshHandler = async (c) => {
4431
4533
  if (!session) {
4432
4534
  return problems.unauthorized(c, "Invalid or expired refresh token");
4433
4535
  }
4434
- await deleteSession(db, session.id);
4536
+ await deleteSession(db, { sessions: schema.sessions }, session.id);
4435
4537
  const newSessionId = await createSession(
4436
4538
  db,
4437
4539
  { sessions: schema.sessions },
@@ -4522,7 +4624,8 @@ var resendVerificationHandler = async (c) => {
4522
4624
  verificationUrl,
4523
4625
  firstName: user.name || void 0
4524
4626
  },
4525
- db
4627
+ db,
4628
+ schema.users
4526
4629
  );
4527
4630
  logger_default.info("Verification email resent", { userId: user.id, email: user.email });
4528
4631
  return c.json({
@@ -4848,7 +4951,8 @@ var totpVerifyHandler = async (c) => {
4848
4951
  const firstName = user.name?.split(" ")[0] || null;
4849
4952
  await emailService.send2faEnabledEmail(
4850
4953
  { email: user.email, firstName, method: "totp" },
4851
- db
4954
+ db,
4955
+ schema.users
4852
4956
  );
4853
4957
  }
4854
4958
  logSecurityEvent("2fa_totp_enrolled", "high", { userId });
@@ -4905,12 +5009,12 @@ var totpDisableHandler = async (c) => {
4905
5009
  if (remaining.length === 0) {
4906
5010
  await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm23.eq)(schema.userBackupCodes.userId, userId));
4907
5011
  await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm23.eq)(schema.userTrustedDevices.userId, userId));
4908
- await invalidateAllUserSessions(db, userId);
5012
+ await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
4909
5013
  const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm23.eq)(schema.users.id, userId)).limit(1);
4910
5014
  if (user) {
4911
5015
  const emailService = new EmailService(env);
4912
5016
  const firstName = user.name?.split(" ")[0] || null;
4913
- await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
5017
+ await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
4914
5018
  }
4915
5019
  logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "totp" });
4916
5020
  return c.json({ success: true, sessionInvalidated: true });
@@ -4966,7 +5070,7 @@ var emailSetupHandler = async (c) => {
4966
5070
  await env.OAUTH_STATES.put(`email_2fa_setup:${userId}`, codeHash, { expirationTtl: 300 });
4967
5071
  const emailService = new EmailService(env);
4968
5072
  const firstName = user.name?.split(" ")[0] || null;
4969
- await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
5073
+ await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
4970
5074
  logSecurityEvent("2fa_email_setup_initiated", "low", { userId });
4971
5075
  return c.json({ success: true });
4972
5076
  };
@@ -5047,7 +5151,8 @@ var emailVerifyHandler = async (c) => {
5047
5151
  const firstName = user.name?.split(" ")[0] || null;
5048
5152
  await emailService.send2faEnabledEmail(
5049
5153
  { email: user.email, firstName, method: "email" },
5050
- db
5154
+ db,
5155
+ schema.users
5051
5156
  );
5052
5157
  }
5053
5158
  logSecurityEvent("2fa_email_enrolled", "high", { userId });
@@ -5101,7 +5206,8 @@ var emailSendCodeHandler = async (c) => {
5101
5206
  const firstName = user.name?.split(" ")[0] || null;
5102
5207
  const emailResult = await emailService.send2faCodeEmail(
5103
5208
  { email: user.email, firstName, code },
5104
- db
5209
+ db,
5210
+ schema.users
5105
5211
  );
5106
5212
  if (!emailResult.success) {
5107
5213
  return problems.badRequest(
@@ -5163,12 +5269,12 @@ var emailDisableHandler = async (c) => {
5163
5269
  if (remaining.length === 0) {
5164
5270
  await db.delete(schema.userBackupCodes).where((0, import_drizzle_orm27.eq)(schema.userBackupCodes.userId, userId));
5165
5271
  await db.delete(schema.userTrustedDevices).where((0, import_drizzle_orm27.eq)(schema.userTrustedDevices.userId, userId));
5166
- await invalidateAllUserSessions(db, userId);
5272
+ await invalidateAllUserSessions(db, { sessions: schema.sessions, users: schema.users }, userId);
5167
5273
  const [user] = await db.select({ email: schema.users.email, name: schema.users.name }).from(schema.users).where((0, import_drizzle_orm27.eq)(schema.users.id, userId)).limit(1);
5168
5274
  if (user) {
5169
5275
  const emailService = new EmailService(env);
5170
5276
  const firstName = user.name?.split(" ")[0] || null;
5171
- await emailService.send2faDisabledEmail({ email: user.email, firstName }, db);
5277
+ await emailService.send2faDisabledEmail({ email: user.email, firstName }, db, schema.users);
5172
5278
  }
5173
5279
  logSecurityEvent("2fa_disabled", "critical", { userId, lastMethod: "email" });
5174
5280
  return c.json({ success: true, sessionInvalidated: true });
@@ -5489,7 +5595,7 @@ var challengeResendHandler = async (c) => {
5489
5595
  });
5490
5596
  const emailService = new EmailService(c.env);
5491
5597
  const firstName = user.name?.split(" ")[0] || null;
5492
- await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db);
5598
+ await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
5493
5599
  logger_default.info("Email 2FA code resent", { userId: payload.userId });
5494
5600
  return c.json({ success: true });
5495
5601
  };
@@ -6013,6 +6119,7 @@ function verifyWebhookSignature(payload, signature, secret) {
6013
6119
  0 && (module.exports = {
6014
6120
  AUTH_DEFAULTS,
6015
6121
  CHALLENGE_TTL_MS,
6122
+ CloudflareEmailAdapter,
6016
6123
  EmailService,
6017
6124
  MAX_CHALLENGE_ATTEMPTS,
6018
6125
  ProblemTypes,