@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.d.cts CHANGED
@@ -318,6 +318,46 @@ interface AuthDatabase {
318
318
  delete: (...args: any[]) => any;
319
319
  query: Record<string, any>;
320
320
  }
321
+ /** A newly-created user, passed to invitation hooks. */
322
+ interface InvitationHookUser {
323
+ id: string;
324
+ email: string;
325
+ name: string | null;
326
+ }
327
+ /**
328
+ * App-owned invitation hooks. The library stays agnostic of any specific
329
+ * invitation model — the consuming app validates the token and links the user.
330
+ *
331
+ * When `/signup` (or OAuth signup) is called with an `invitationToken`, the
332
+ * library calls `verify`. If it returns a matching email, the account is created
333
+ * with `emailVerified: true` and NO verification email is sent — the token, which
334
+ * was delivered to that address, is itself the proof of ownership (the same trust
335
+ * model as OAuth-provider signup). `onAccepted` then runs so the app can link the
336
+ * user (e.g. create a team membership) atomically with signup.
337
+ */
338
+ interface InvitationHooks<TEnv = Record<string, unknown>> {
339
+ /**
340
+ * Validate that `token` is a live invitation for `email`. Return the invited
341
+ * email (normalized) to auto-verify the new account, or `null` to reject the
342
+ * token. MUST confirm the token matches this email — returning a non-matching
343
+ * email is treated as a rejection by the caller.
344
+ */
345
+ verify(args: {
346
+ token: string;
347
+ email: string;
348
+ db: AuthDatabase;
349
+ env: TEnv;
350
+ }): Promise<{
351
+ email: string;
352
+ } | null>;
353
+ /** Link the just-created, verified user (create membership, mark the invite accepted). */
354
+ onAccepted?(args: {
355
+ token: string;
356
+ user: InvitationHookUser;
357
+ db: AuthDatabase;
358
+ env: TEnv;
359
+ }): Promise<void>;
360
+ }
321
361
  /**
322
362
  * Configuration for createAuth
323
363
  */
@@ -330,6 +370,8 @@ interface AuthConfig<TEnv = Record<string, unknown>> {
330
370
  getEnv: (c: Context) => TEnv;
331
371
  /** Optional email branding tokens */
332
372
  emailBranding?: EmailBranding;
373
+ /** Optional app-owned invitation hooks for token-verified signup. */
374
+ invitations?: InvitationHooks<TEnv>;
333
375
  }
334
376
  /**
335
377
  * Auth context available in all auth operations.
@@ -340,6 +382,7 @@ interface AuthContext<TEnv = Record<string, unknown>> {
340
382
  schema: AuthSchema;
341
383
  env: TEnv;
342
384
  emailBranding?: EmailBranding;
385
+ invitations?: InvitationHooks<TEnv>;
343
386
  }
344
387
  /**
345
388
  * Creates an auth instance with injected database and schema.
@@ -1662,6 +1705,7 @@ declare const authRoutes: OpenAPIHono<{
1662
1705
  name?: string | undefined;
1663
1706
  turnstileToken?: string | undefined;
1664
1707
  oauthToken?: string | undefined;
1708
+ invitationToken?: string | undefined;
1665
1709
  };
1666
1710
  };
1667
1711
  output: {
@@ -1683,6 +1727,7 @@ declare const authRoutes: OpenAPIHono<{
1683
1727
  name?: string | undefined;
1684
1728
  turnstileToken?: string | undefined;
1685
1729
  oauthToken?: string | undefined;
1730
+ invitationToken?: string | undefined;
1686
1731
  };
1687
1732
  };
1688
1733
  output: {
@@ -1704,6 +1749,7 @@ declare const authRoutes: OpenAPIHono<{
1704
1749
  name?: string | undefined;
1705
1750
  turnstileToken?: string | undefined;
1706
1751
  oauthToken?: string | undefined;
1752
+ invitationToken?: string | undefined;
1707
1753
  };
1708
1754
  };
1709
1755
  output: {
@@ -2700,4 +2746,4 @@ declare function calculateBounceRate(db: PostgresJsDatabase<Record<string, unkno
2700
2746
  */
2701
2747
  declare function calculateComplaintRate(db: PostgresJsDatabase<Record<string, unknown>>, emailEventsTable: EmailEventsTable, hoursAgo?: number): Promise<number>;
2702
2748
 
2703
- export { AUTH_DEFAULTS, type AccountLockoutTables, type Auth, type AuthConfig, type AuthContext, type AuthDatabase, type AuthSchema, CHALLENGE_TTL_MS, type ChallengePayload, CloudflareEmailAdapter, type DeviceType, type EmailAdapter, type EmailBranding, type EmailChangeConfirmationData, type EmailChangeNotificationData, type EmailChangeTables, type EmailSendOptions, type EmailSendResult, EmailService, type EmailTemplate, type EmailType, type Env, type InferNewSession, type InferNewUser, type InferSession, type InferUser, MAX_CHALLENGE_ATTEMPTS, type PasswordResetEmailData, type ProblemDetails, ProblemTypes, ResendAdapter, type ResendEventType, type ResendWebhookPayload, type SendEmailBinding, type SendEmailResult, type SessionData, type SessionTables, TRUSTED_DEVICE_COOKIE_BASE, TRUSTED_DEVICE_TTL_MS, type TotpVerifyResult, type TwoFactorCodeEmailData, type TwoFactorDisabledEmailData, type TwoFactorEnabledEmailData, type ValidationResult, type Variables, type VerificationEmailData, authRoutes, calculateBounceRate, calculateComplaintRate, cancelEmailChange, checkAccountLocked, checkBreachedPassword, clearAccountLockout, clearChallengeCookie, clearSessionCookie, clearTrustedDeviceCookie, confirmEmailChange, createAuth, createAuthMiddleware, createChallengeToken, createDeviceToken, createEmailAdapter, createProblemDetails, createSession, csrf, decryptTotpSecret, deleteAllUserSessions, deleteChallengeToken, deleteSession, encryptTotpSecret, extractAppUrl, formatBackupCode, generate2faCodeEmail, generate2faDisabledEmail, generate2faEnabledEmail, generateApiKey, generateBackupCodes, generateCodeChallenge, generateCodeVerifier, generateEmailChangeConfirmation, generateEmailChangeNotification, generateFingerprint, generatePasswordResetEmail, generateQrCodeUri, generateSecureToken, generateTotpSecret, generateTraceId, generateVerificationEmail, getAllowedOrigins, getAuthContext, getChallengeCookieName, getClientIp, getKeyPrefix, getMinutesUntilUnlock, getSessionCookieName, getTotpCounter, getTrustedDeviceCookieName, handleWebhookEvent, hashApiKey, hashBackupCode, hashPassword, hashToken, incrementFailedAttempts, invalidateAllUserSessions, isCapacitorApp, isCloudflarePreviewUrl, isDeepLinkUri, isValidApiKeyFormat, logError, logSecurityEvent, logger, normalizeBackupCode, optionalAuth, parseDeviceName, parseDeviceType, problemJson, problems, rateLimit, refreshSession, requestEmailChange, requireAuth, requireVerifiedEmail, retrieveChallengeToken, setChallengeCookie, setSessionCookie, setTrustedDeviceCookie, storeChallengeToken, updateChallengeAttempts, validateChallengePayload, validateOrigin, validatePassword, validatePasswordWithBreachCheck, validateSession, validateTrustedDevice, verifyBackupCode, verifyCodeChallenge, verifyPassword, verifyPasswordWithRotation, verifyTotpCode, verifyTurnstileToken, verifyWebhookSignature };
2749
+ export { AUTH_DEFAULTS, type AccountLockoutTables, type Auth, type AuthConfig, type AuthContext, type AuthDatabase, type AuthSchema, CHALLENGE_TTL_MS, type ChallengePayload, CloudflareEmailAdapter, type DeviceType, type EmailAdapter, type EmailBranding, type EmailChangeConfirmationData, type EmailChangeNotificationData, type EmailChangeTables, type EmailSendOptions, type EmailSendResult, EmailService, type EmailTemplate, type EmailType, type Env, type InferNewSession, type InferNewUser, type InferSession, type InferUser, type InvitationHookUser, type InvitationHooks, MAX_CHALLENGE_ATTEMPTS, type PasswordResetEmailData, type ProblemDetails, ProblemTypes, ResendAdapter, type ResendEventType, type ResendWebhookPayload, type SendEmailBinding, type SendEmailResult, type SessionData, type SessionTables, TRUSTED_DEVICE_COOKIE_BASE, TRUSTED_DEVICE_TTL_MS, type TotpVerifyResult, type TwoFactorCodeEmailData, type TwoFactorDisabledEmailData, type TwoFactorEnabledEmailData, type ValidationResult, type Variables, type VerificationEmailData, authRoutes, calculateBounceRate, calculateComplaintRate, cancelEmailChange, checkAccountLocked, checkBreachedPassword, clearAccountLockout, clearChallengeCookie, clearSessionCookie, clearTrustedDeviceCookie, confirmEmailChange, createAuth, createAuthMiddleware, createChallengeToken, createDeviceToken, createEmailAdapter, createProblemDetails, createSession, csrf, decryptTotpSecret, deleteAllUserSessions, deleteChallengeToken, deleteSession, encryptTotpSecret, extractAppUrl, formatBackupCode, generate2faCodeEmail, generate2faDisabledEmail, generate2faEnabledEmail, generateApiKey, generateBackupCodes, generateCodeChallenge, generateCodeVerifier, generateEmailChangeConfirmation, generateEmailChangeNotification, generateFingerprint, generatePasswordResetEmail, generateQrCodeUri, generateSecureToken, generateTotpSecret, generateTraceId, generateVerificationEmail, getAllowedOrigins, getAuthContext, getChallengeCookieName, getClientIp, getKeyPrefix, getMinutesUntilUnlock, getSessionCookieName, getTotpCounter, getTrustedDeviceCookieName, handleWebhookEvent, hashApiKey, hashBackupCode, hashPassword, hashToken, incrementFailedAttempts, invalidateAllUserSessions, isCapacitorApp, isCloudflarePreviewUrl, isDeepLinkUri, isValidApiKeyFormat, logError, logSecurityEvent, logger, normalizeBackupCode, optionalAuth, parseDeviceName, parseDeviceType, problemJson, problems, rateLimit, refreshSession, requestEmailChange, requireAuth, requireVerifiedEmail, retrieveChallengeToken, setChallengeCookie, setSessionCookie, setTrustedDeviceCookie, storeChallengeToken, updateChallengeAttempts, validateChallengePayload, validateOrigin, validatePassword, validatePasswordWithBreachCheck, validateSession, validateTrustedDevice, verifyBackupCode, verifyCodeChallenge, verifyPassword, verifyPasswordWithRotation, verifyTotpCode, verifyTurnstileToken, verifyWebhookSignature };
package/dist/index.d.ts CHANGED
@@ -318,6 +318,46 @@ interface AuthDatabase {
318
318
  delete: (...args: any[]) => any;
319
319
  query: Record<string, any>;
320
320
  }
321
+ /** A newly-created user, passed to invitation hooks. */
322
+ interface InvitationHookUser {
323
+ id: string;
324
+ email: string;
325
+ name: string | null;
326
+ }
327
+ /**
328
+ * App-owned invitation hooks. The library stays agnostic of any specific
329
+ * invitation model — the consuming app validates the token and links the user.
330
+ *
331
+ * When `/signup` (or OAuth signup) is called with an `invitationToken`, the
332
+ * library calls `verify`. If it returns a matching email, the account is created
333
+ * with `emailVerified: true` and NO verification email is sent — the token, which
334
+ * was delivered to that address, is itself the proof of ownership (the same trust
335
+ * model as OAuth-provider signup). `onAccepted` then runs so the app can link the
336
+ * user (e.g. create a team membership) atomically with signup.
337
+ */
338
+ interface InvitationHooks<TEnv = Record<string, unknown>> {
339
+ /**
340
+ * Validate that `token` is a live invitation for `email`. Return the invited
341
+ * email (normalized) to auto-verify the new account, or `null` to reject the
342
+ * token. MUST confirm the token matches this email — returning a non-matching
343
+ * email is treated as a rejection by the caller.
344
+ */
345
+ verify(args: {
346
+ token: string;
347
+ email: string;
348
+ db: AuthDatabase;
349
+ env: TEnv;
350
+ }): Promise<{
351
+ email: string;
352
+ } | null>;
353
+ /** Link the just-created, verified user (create membership, mark the invite accepted). */
354
+ onAccepted?(args: {
355
+ token: string;
356
+ user: InvitationHookUser;
357
+ db: AuthDatabase;
358
+ env: TEnv;
359
+ }): Promise<void>;
360
+ }
321
361
  /**
322
362
  * Configuration for createAuth
323
363
  */
@@ -330,6 +370,8 @@ interface AuthConfig<TEnv = Record<string, unknown>> {
330
370
  getEnv: (c: Context) => TEnv;
331
371
  /** Optional email branding tokens */
332
372
  emailBranding?: EmailBranding;
373
+ /** Optional app-owned invitation hooks for token-verified signup. */
374
+ invitations?: InvitationHooks<TEnv>;
333
375
  }
334
376
  /**
335
377
  * Auth context available in all auth operations.
@@ -340,6 +382,7 @@ interface AuthContext<TEnv = Record<string, unknown>> {
340
382
  schema: AuthSchema;
341
383
  env: TEnv;
342
384
  emailBranding?: EmailBranding;
385
+ invitations?: InvitationHooks<TEnv>;
343
386
  }
344
387
  /**
345
388
  * Creates an auth instance with injected database and schema.
@@ -1662,6 +1705,7 @@ declare const authRoutes: OpenAPIHono<{
1662
1705
  name?: string | undefined;
1663
1706
  turnstileToken?: string | undefined;
1664
1707
  oauthToken?: string | undefined;
1708
+ invitationToken?: string | undefined;
1665
1709
  };
1666
1710
  };
1667
1711
  output: {
@@ -1683,6 +1727,7 @@ declare const authRoutes: OpenAPIHono<{
1683
1727
  name?: string | undefined;
1684
1728
  turnstileToken?: string | undefined;
1685
1729
  oauthToken?: string | undefined;
1730
+ invitationToken?: string | undefined;
1686
1731
  };
1687
1732
  };
1688
1733
  output: {
@@ -1704,6 +1749,7 @@ declare const authRoutes: OpenAPIHono<{
1704
1749
  name?: string | undefined;
1705
1750
  turnstileToken?: string | undefined;
1706
1751
  oauthToken?: string | undefined;
1752
+ invitationToken?: string | undefined;
1707
1753
  };
1708
1754
  };
1709
1755
  output: {
@@ -2700,4 +2746,4 @@ declare function calculateBounceRate(db: PostgresJsDatabase<Record<string, unkno
2700
2746
  */
2701
2747
  declare function calculateComplaintRate(db: PostgresJsDatabase<Record<string, unknown>>, emailEventsTable: EmailEventsTable, hoursAgo?: number): Promise<number>;
2702
2748
 
2703
- export { AUTH_DEFAULTS, type AccountLockoutTables, type Auth, type AuthConfig, type AuthContext, type AuthDatabase, type AuthSchema, CHALLENGE_TTL_MS, type ChallengePayload, CloudflareEmailAdapter, type DeviceType, type EmailAdapter, type EmailBranding, type EmailChangeConfirmationData, type EmailChangeNotificationData, type EmailChangeTables, type EmailSendOptions, type EmailSendResult, EmailService, type EmailTemplate, type EmailType, type Env, type InferNewSession, type InferNewUser, type InferSession, type InferUser, MAX_CHALLENGE_ATTEMPTS, type PasswordResetEmailData, type ProblemDetails, ProblemTypes, ResendAdapter, type ResendEventType, type ResendWebhookPayload, type SendEmailBinding, type SendEmailResult, type SessionData, type SessionTables, TRUSTED_DEVICE_COOKIE_BASE, TRUSTED_DEVICE_TTL_MS, type TotpVerifyResult, type TwoFactorCodeEmailData, type TwoFactorDisabledEmailData, type TwoFactorEnabledEmailData, type ValidationResult, type Variables, type VerificationEmailData, authRoutes, calculateBounceRate, calculateComplaintRate, cancelEmailChange, checkAccountLocked, checkBreachedPassword, clearAccountLockout, clearChallengeCookie, clearSessionCookie, clearTrustedDeviceCookie, confirmEmailChange, createAuth, createAuthMiddleware, createChallengeToken, createDeviceToken, createEmailAdapter, createProblemDetails, createSession, csrf, decryptTotpSecret, deleteAllUserSessions, deleteChallengeToken, deleteSession, encryptTotpSecret, extractAppUrl, formatBackupCode, generate2faCodeEmail, generate2faDisabledEmail, generate2faEnabledEmail, generateApiKey, generateBackupCodes, generateCodeChallenge, generateCodeVerifier, generateEmailChangeConfirmation, generateEmailChangeNotification, generateFingerprint, generatePasswordResetEmail, generateQrCodeUri, generateSecureToken, generateTotpSecret, generateTraceId, generateVerificationEmail, getAllowedOrigins, getAuthContext, getChallengeCookieName, getClientIp, getKeyPrefix, getMinutesUntilUnlock, getSessionCookieName, getTotpCounter, getTrustedDeviceCookieName, handleWebhookEvent, hashApiKey, hashBackupCode, hashPassword, hashToken, incrementFailedAttempts, invalidateAllUserSessions, isCapacitorApp, isCloudflarePreviewUrl, isDeepLinkUri, isValidApiKeyFormat, logError, logSecurityEvent, logger, normalizeBackupCode, optionalAuth, parseDeviceName, parseDeviceType, problemJson, problems, rateLimit, refreshSession, requestEmailChange, requireAuth, requireVerifiedEmail, retrieveChallengeToken, setChallengeCookie, setSessionCookie, setTrustedDeviceCookie, storeChallengeToken, updateChallengeAttempts, validateChallengePayload, validateOrigin, validatePassword, validatePasswordWithBreachCheck, validateSession, validateTrustedDevice, verifyBackupCode, verifyCodeChallenge, verifyPassword, verifyPasswordWithRotation, verifyTotpCode, verifyTurnstileToken, verifyWebhookSignature };
2749
+ export { AUTH_DEFAULTS, type AccountLockoutTables, type Auth, type AuthConfig, type AuthContext, type AuthDatabase, type AuthSchema, CHALLENGE_TTL_MS, type ChallengePayload, CloudflareEmailAdapter, type DeviceType, type EmailAdapter, type EmailBranding, type EmailChangeConfirmationData, type EmailChangeNotificationData, type EmailChangeTables, type EmailSendOptions, type EmailSendResult, EmailService, type EmailTemplate, type EmailType, type Env, type InferNewSession, type InferNewUser, type InferSession, type InferUser, type InvitationHookUser, type InvitationHooks, MAX_CHALLENGE_ATTEMPTS, type PasswordResetEmailData, type ProblemDetails, ProblemTypes, ResendAdapter, type ResendEventType, type ResendWebhookPayload, type SendEmailBinding, type SendEmailResult, type SessionData, type SessionTables, TRUSTED_DEVICE_COOKIE_BASE, TRUSTED_DEVICE_TTL_MS, type TotpVerifyResult, type TwoFactorCodeEmailData, type TwoFactorDisabledEmailData, type TwoFactorEnabledEmailData, type ValidationResult, type Variables, type VerificationEmailData, authRoutes, calculateBounceRate, calculateComplaintRate, cancelEmailChange, checkAccountLocked, checkBreachedPassword, clearAccountLockout, clearChallengeCookie, clearSessionCookie, clearTrustedDeviceCookie, confirmEmailChange, createAuth, createAuthMiddleware, createChallengeToken, createDeviceToken, createEmailAdapter, createProblemDetails, createSession, csrf, decryptTotpSecret, deleteAllUserSessions, deleteChallengeToken, deleteSession, encryptTotpSecret, extractAppUrl, formatBackupCode, generate2faCodeEmail, generate2faDisabledEmail, generate2faEnabledEmail, generateApiKey, generateBackupCodes, generateCodeChallenge, generateCodeVerifier, generateEmailChangeConfirmation, generateEmailChangeNotification, generateFingerprint, generatePasswordResetEmail, generateQrCodeUri, generateSecureToken, generateTotpSecret, generateTraceId, generateVerificationEmail, getAllowedOrigins, getAuthContext, getChallengeCookieName, getClientIp, getKeyPrefix, getMinutesUntilUnlock, getSessionCookieName, getTotpCounter, getTrustedDeviceCookieName, handleWebhookEvent, hashApiKey, hashBackupCode, hashPassword, hashToken, incrementFailedAttempts, invalidateAllUserSessions, isCapacitorApp, isCloudflarePreviewUrl, isDeepLinkUri, isValidApiKeyFormat, logError, logSecurityEvent, logger, normalizeBackupCode, optionalAuth, parseDeviceName, parseDeviceType, problemJson, problems, rateLimit, refreshSession, requestEmailChange, requireAuth, requireVerifiedEmail, retrieveChallengeToken, setChallengeCookie, setSessionCookie, setTrustedDeviceCookie, storeChallengeToken, updateChallengeAttempts, validateChallengePayload, validateOrigin, validatePassword, validatePasswordWithBreachCheck, validateSession, validateTrustedDevice, verifyBackupCode, verifyCodeChallenge, verifyPassword, verifyPasswordWithRotation, verifyTotpCode, verifyTurnstileToken, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -949,12 +949,13 @@ import { eq as eq4, and as and2, gt as gt2 } from "drizzle-orm";
949
949
 
950
950
  // src/factory.ts
951
951
  function createAuth(config) {
952
- const { db, schema, getEnv, emailBranding } = config;
952
+ const { db, schema, getEnv, emailBranding, invitations } = config;
953
953
  const getContext = (c) => ({
954
954
  db,
955
955
  schema,
956
956
  env: getEnv(c),
957
- emailBranding
957
+ emailBranding,
958
+ invitations
958
959
  });
959
960
  return {
960
961
  db,
@@ -2932,7 +2933,10 @@ var signupRequestSchema = z.object({
2932
2933
  name: z.string().min(1).optional().openapi({ example: "John Doe" }),
2933
2934
  turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" }),
2934
2935
  // OAuth signup completion token (from /signup?oauth_token=...)
2935
- oauthToken: z.string().optional().openapi({ example: "a1b2c3d4-..." })
2936
+ oauthToken: z.string().optional().openapi({ example: "a1b2c3d4-..." }),
2937
+ // App invitation token (from /signup?token=...). When it validates for this
2938
+ // email, the account is created already-verified with no verification email.
2939
+ invitationToken: z.string().optional().openapi({ example: "iIB8tDJJ..." })
2936
2940
  }).openapi("SignupRequest");
2937
2941
  var loginRequestSchema = z.object({
2938
2942
  email: z.string().email().openapi({ example: "user@example.com" }),
@@ -3062,8 +3066,8 @@ var signupRoute = createRoute({
3062
3066
  }
3063
3067
  });
3064
3068
  var signupHandler = async (c) => {
3065
- const { email, password, name, turnstileToken, oauthToken } = c.req.valid("json");
3066
- const { db: database, schema, emailBranding } = getAuthContext(c);
3069
+ const { email, password, name, turnstileToken, oauthToken, invitationToken } = c.req.valid("json");
3070
+ const { db: database, schema, emailBranding, invitations } = getAuthContext(c);
3067
3071
  const env = c.env;
3068
3072
  if (oauthToken) {
3069
3073
  const stored = await env.OAUTH_STATES.get(`oauth:signup:${oauthToken}`);
@@ -3145,40 +3149,74 @@ var signupHandler = async (c) => {
3145
3149
  return problems.badRequest(c, passwordValidation.error || "Invalid password");
3146
3150
  }
3147
3151
  const hashedPassword = await hashPassword(password, env.PASSWORD_PEPPER_V1);
3152
+ let invitedVerified = false;
3153
+ if (invitationToken && invitations) {
3154
+ const match = await invitations.verify({
3155
+ token: invitationToken,
3156
+ email: email.toLowerCase(),
3157
+ db: database,
3158
+ env
3159
+ });
3160
+ if (!match || match.email.toLowerCase() !== email.toLowerCase()) {
3161
+ return problems.badRequest(
3162
+ c,
3163
+ "This invitation is invalid, expired, or for a different email address"
3164
+ );
3165
+ }
3166
+ invitedVerified = true;
3167
+ }
3148
3168
  const [newUser] = await database.insert(schema.users).values({
3149
3169
  email: email.toLowerCase(),
3150
3170
  name: name || null,
3151
3171
  hashedPassword,
3152
3172
  pepperKid: "v1",
3153
- emailVerified: false,
3173
+ emailVerified: invitedVerified,
3154
3174
  activatedAt: /* @__PURE__ */ new Date()
3155
3175
  }).returning();
3156
- const emailToken = generateSecureToken(32);
3157
- const tokenHash = await hashToken(emailToken);
3158
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3159
- await database.insert(schema.emailVerificationTokens).values({
3160
- tokenHash,
3161
- userId: newUser.id,
3162
- email: newUser.email,
3163
- expiresAt,
3164
- createdAt: Date.now()
3165
- });
3166
- const emailService = new EmailService(env, void 0, emailBranding);
3167
- const verificationUrl = `${env.APP_URL}/verify-email?token=${emailToken}`;
3168
- await emailService.sendVerificationEmail(
3169
- {
3176
+ if (!invitedVerified) {
3177
+ const emailToken = generateSecureToken(32);
3178
+ const tokenHash = await hashToken(emailToken);
3179
+ const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3180
+ await database.insert(schema.emailVerificationTokens).values({
3181
+ tokenHash,
3182
+ userId: newUser.id,
3170
3183
  email: newUser.email,
3171
- token: emailToken,
3172
- verificationUrl,
3173
- firstName: newUser.name || void 0
3174
- },
3175
- database,
3176
- schema.users
3177
- );
3184
+ expiresAt,
3185
+ createdAt: Date.now()
3186
+ });
3187
+ const emailService = new EmailService(env, void 0, emailBranding);
3188
+ const verificationUrl = `${env.APP_URL}/verify-email?token=${emailToken}`;
3189
+ await emailService.sendVerificationEmail(
3190
+ {
3191
+ email: newUser.email,
3192
+ token: emailToken,
3193
+ verificationUrl,
3194
+ firstName: newUser.name || void 0
3195
+ },
3196
+ database,
3197
+ schema.users
3198
+ );
3199
+ }
3200
+ if (invitedVerified && invitationToken && invitations?.onAccepted) {
3201
+ try {
3202
+ await invitations.onAccepted({
3203
+ token: invitationToken,
3204
+ user: { id: newUser.id, email: newUser.email, name: newUser.name },
3205
+ db: database,
3206
+ env
3207
+ });
3208
+ } catch (err) {
3209
+ logError(err, { context: "signup_invitation_onAccepted_failed", userId: newUser.id });
3210
+ }
3211
+ }
3178
3212
  const fingerprint = await generateFingerprint(c.req.raw);
3179
3213
  const ipAddress = getClientIp(c.req.raw);
3180
3214
  const sessionId = await createSession(database, { sessions: schema.sessions }, newUser.id, fingerprint, ipAddress);
3181
- logSecurityEvent("user_signup", "low", { userId: newUser.id, email: newUser.email });
3215
+ logSecurityEvent("user_signup", "low", {
3216
+ userId: newUser.id,
3217
+ email: newUser.email,
3218
+ invited: invitedVerified
3219
+ });
3182
3220
  c.header("Set-Cookie", setSessionCookie(sessionId, c.env), { append: true });
3183
3221
  return c.json({
3184
3222
  user: {
@@ -3220,6 +3258,16 @@ function generateEmailOtp() {
3220
3258
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
3221
3259
  return String(num % 1e6).padStart(6, "0");
3222
3260
  }
3261
+ var EMAIL_CODE_TTL_SECONDS = 300;
3262
+ async function sendEmailChallengeCode(env, database, usersTable, user, emailBranding) {
3263
+ const code = generateEmailOtp();
3264
+ await env.OAUTH_STATES.put(`email_2fa_challenge:${user.id}`, await hashToken(code), {
3265
+ expirationTtl: EMAIL_CODE_TTL_SECONDS
3266
+ });
3267
+ const emailService = new EmailService(env, void 0, emailBranding);
3268
+ const firstName = user.name?.split(" ")[0] || null;
3269
+ await emailService.send2faCodeEmail({ email: user.email, firstName, code }, database, usersTable);
3270
+ }
3223
3271
  async function verifyAny2faCode(database, env, userId, code, method, tables2) {
3224
3272
  const { user2faMethods: user2faMethodsTable, userBackupCodes: userBackupCodesTable } = tables2;
3225
3273
  if (method === "totp") {
@@ -3304,7 +3352,7 @@ var loginRoute = createRoute2({
3304
3352
  });
3305
3353
  var loginHandler = async (c) => {
3306
3354
  const { email, password, turnstileToken } = c.req.valid("json");
3307
- const { db: database, schema } = getAuthContext(c);
3355
+ const { db: database, schema, emailBranding } = getAuthContext(c);
3308
3356
  const env = c.env;
3309
3357
  const turnstileValid = await verifyTurnstileToken(
3310
3358
  turnstileToken,
@@ -3389,6 +3437,10 @@ var loginHandler = async (c) => {
3389
3437
  };
3390
3438
  await storeChallengeToken(env.OAUTH_STATES, challengeToken, challengePayload);
3391
3439
  setChallengeCookie(c, env, challengeToken);
3440
+ const emailMethod = enabled2faMethods.find((m) => m.method === "email");
3441
+ if (emailMethod && (emailMethod.isPrimary || enabled2faMethods.length === 1)) {
3442
+ await sendEmailChallengeCode(env, database, schema.users, user, emailBranding);
3443
+ }
3392
3444
  return c.json({
3393
3445
  requires2fa: true,
3394
3446
  methods: enabled2faMethods.map((m) => m.method)
@@ -5245,14 +5297,7 @@ var challengeResendHandler = async (c) => {
5245
5297
  logger_default.error("User not found during 2FA resend", { userId: payload.userId });
5246
5298
  return problems.notFound(c, "User not found");
5247
5299
  }
5248
- const code = generateEmailOtp();
5249
- const codeHash = await hashToken(code);
5250
- await c.env.OAUTH_STATES.put(`email_2fa_challenge:${payload.userId}`, codeHash, {
5251
- expirationTtl: 300
5252
- });
5253
- const emailService = new EmailService(c.env, void 0, emailBranding);
5254
- const firstName = user.name?.split(" ")[0] || null;
5255
- await emailService.send2faCodeEmail({ email: user.email, firstName, code }, db, schema.users);
5300
+ await sendEmailChallengeCode(c.env, db, schema.users, { id: payload.userId, ...user }, emailBranding);
5256
5301
  logger_default.info("Email 2FA code resent", { userId: payload.userId });
5257
5302
  return c.json({ success: true });
5258
5303
  };