@syncello/auth 2.5.1 → 3.1.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";
@@ -2816,19 +2816,19 @@ var signupRequestSchema = z.object({
2816
2816
  var loginRequestSchema = z.object({
2817
2817
  email: z.string().email().openapi({ example: "user@example.com" }),
2818
2818
  password: z.string().min(1).openapi({ example: "SecurePass123!" }),
2819
- turnstileToken: z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2819
+ turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2820
2820
  }).openapi("LoginRequest");
2821
2821
  var verifyEmailRequestSchema = z.object({
2822
2822
  token: z.string().min(1).openapi({ example: "verification-token-abc123" })
2823
2823
  }).openapi("VerifyEmailRequest");
2824
2824
  var forgotPasswordRequestSchema = z.object({
2825
2825
  email: z.string().email().openapi({ example: "user@example.com" }),
2826
- turnstileToken: z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2826
+ turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2827
2827
  }).openapi("ForgotPasswordRequest");
2828
2828
  var resetPasswordRequestSchema = z.object({
2829
2829
  token: z.string().min(1).openapi({ example: "reset-token-xyz789" }),
2830
2830
  password: z.string().min(8).openapi({ example: "NewSecurePass456!" }),
2831
- turnstileToken: z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2831
+ turnstileToken: z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
2832
2832
  }).openapi("ResetPasswordRequest");
2833
2833
  var changePasswordRequestSchema = z.object({
2834
2834
  currentPassword: z.string().min(1).openapi({ example: "OldPassword123!" }),
@@ -5107,11 +5107,8 @@ var challengeResendHandler = async (c) => {
5107
5107
  var twoFa = new OpenAPIHono();
5108
5108
  twoFa.use("*", csrf);
5109
5109
  twoFa.use("/status", requireAuth);
5110
- twoFa.openapi(statusRoute, statusHandler);
5111
5110
  twoFa.use("/totp/setup", requireAuth);
5112
- twoFa.openapi(totpSetupRoute, totpSetupHandler);
5113
5111
  twoFa.use("/totp/verify", requireAuth);
5114
- twoFa.openapi(totpVerifyRoute, totpVerifyHandler);
5115
5112
  twoFa.use(
5116
5113
  "/totp/disable",
5117
5114
  requireAuth,
@@ -5123,11 +5120,8 @@ twoFa.use(
5123
5120
  // 5 attempts per 15 minutes
5124
5121
  })
5125
5122
  );
5126
- twoFa.openapi(totpDisableRoute, totpDisableHandler);
5127
5123
  twoFa.use("/email/setup", requireAuth);
5128
- twoFa.openapi(emailSetupRoute, emailSetupHandler);
5129
5124
  twoFa.use("/email/verify", requireAuth);
5130
- twoFa.openapi(emailVerifyRoute, emailVerifyHandler);
5131
5125
  twoFa.use(
5132
5126
  "/email/send-code",
5133
5127
  requireAuth,
@@ -5139,7 +5133,6 @@ twoFa.use(
5139
5133
  // 3 per 5 minutes
5140
5134
  })
5141
5135
  );
5142
- twoFa.openapi(emailSendCodeRoute, emailSendCodeHandler);
5143
5136
  twoFa.use(
5144
5137
  "/email/disable",
5145
5138
  requireAuth,
@@ -5151,11 +5144,8 @@ twoFa.use(
5151
5144
  // 5 attempts per 15 minutes
5152
5145
  })
5153
5146
  );
5154
- twoFa.openapi(emailDisableRoute, emailDisableHandler);
5155
5147
  twoFa.use("/trusted-devices", requireAuth);
5156
- twoFa.openapi(trustedDevicesGetRoute, trustedDevicesGetHandler);
5157
5148
  twoFa.use("/trusted-devices/:id", requireAuth);
5158
- twoFa.openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler);
5159
5149
  twoFa.use(
5160
5150
  "/backup-codes/regenerate",
5161
5151
  requireAuth,
@@ -5167,7 +5157,6 @@ twoFa.use(
5167
5157
  // 3 per hour per user
5168
5158
  })
5169
5159
  );
5170
- twoFa.openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler);
5171
5160
  twoFa.use(
5172
5161
  "/challenge",
5173
5162
  rateLimit({
@@ -5178,7 +5167,6 @@ twoFa.use(
5178
5167
  // 10 per 5 min per IP
5179
5168
  })
5180
5169
  );
5181
- twoFa.openapi(challengeRoute, challengeHandler);
5182
5170
  twoFa.use(
5183
5171
  "/challenge/resend",
5184
5172
  rateLimit({
@@ -5189,40 +5177,423 @@ twoFa.use(
5189
5177
  // 3 per 5 min per IP
5190
5178
  })
5191
5179
  );
5192
- twoFa.openapi(challengeResendRoute, challengeResendHandler);
5193
- var fa_default = twoFa;
5180
+ 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);
5181
+ var fa_default = twoFaRoutes;
5182
+
5183
+ // src/routes/oauth/index.ts
5184
+ import { OpenAPIHono as OpenAPIHono2 } from "@hono/zod-openapi";
5185
+
5186
+ // src/routes/oauth/authorize.ts
5187
+ import { createRoute as createRoute28 } from "@hono/zod-openapi";
5188
+
5189
+ // src/routes/oauth/providers.ts
5190
+ var PROVIDERS = {
5191
+ google: {
5192
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
5193
+ tokenUrl: "https://oauth2.googleapis.com/token",
5194
+ userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
5195
+ scopes: ["email"],
5196
+ emailExtractor: (data) => data.email || null,
5197
+ userIdExtractor: (data) => data.id || null
5198
+ },
5199
+ microsoft: {
5200
+ authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
5201
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
5202
+ userinfoUrl: "https://graph.microsoft.com/v1.0/me",
5203
+ scopes: ["openid", "email", "User.Read"],
5204
+ emailExtractor: (data) => data.mail || data.userPrincipalName || null,
5205
+ userIdExtractor: (data) => data.id || null
5206
+ }
5207
+ };
5208
+ function getProviderCredentials(provider, env) {
5209
+ if (provider === "google" && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
5210
+ return { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET };
5211
+ }
5212
+ if (provider === "microsoft" && env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) {
5213
+ return { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET };
5214
+ }
5215
+ return null;
5216
+ }
5217
+
5218
+ // src/routes/oauth/schemas.ts
5219
+ import { z as z10 } from "@hono/zod-openapi";
5220
+ var oauthProviderParamSchema = z10.object({
5221
+ provider: z10.enum(["google", "microsoft"]).openapi({ example: "google" })
5222
+ });
5223
+ var oauthAuthorizeQuerySchema = z10.object({
5224
+ redirect: z10.string().optional().openapi({ example: "/dashboard" }),
5225
+ invitationToken: z10.string().optional().openapi({ example: "token123" }),
5226
+ code_challenge: z10.string().min(43).max(128).optional().openapi({ example: "E9Mrozoa2owUednMY..." }),
5227
+ code_challenge_method: z10.enum(["S256"]).optional().openapi({ example: "S256" }),
5228
+ redirect_uri: z10.string().url().optional().openapi({ example: "myapp://oauth-callback" })
5229
+ }).openapi("OAuthAuthorizeQuery");
5230
+ var oauthCallbackQuerySchema = z10.object({
5231
+ code: z10.string().optional(),
5232
+ state: z10.string().optional(),
5233
+ error: z10.string().optional()
5234
+ });
5235
+ var oauthCompleteRequestSchema = z10.object({
5236
+ code: z10.string().uuid("Invalid code format").openapi({ example: "a1b2c3d4-..." })
5237
+ }).openapi("OAuthCompleteRequest");
5238
+ var oauthCompleteResponseSchema = z10.object({
5239
+ message: z10.string().openapi({ example: "Login successful" }),
5240
+ redirect: z10.string().openapi({ example: "/" }),
5241
+ user: z10.object({
5242
+ id: z10.string().uuid(),
5243
+ email: z10.string().email(),
5244
+ emailVerified: z10.boolean()
5245
+ })
5246
+ }).openapi("OAuthCompleteResponse");
5247
+ var errorResponseSchema4 = z10.object({
5248
+ type: z10.string().url(),
5249
+ title: z10.string(),
5250
+ status: z10.number().int(),
5251
+ detail: z10.string().optional()
5252
+ }).openapi("ErrorResponse");
5253
+
5254
+ // src/routes/oauth/authorize.ts
5255
+ var authorizeRoute = createRoute28({
5256
+ method: "get",
5257
+ path: "/{provider}/authorize",
5258
+ tags: ["OAuth"],
5259
+ summary: "Start OAuth login flow",
5260
+ description: "Generates a CSRF state token and redirects the user to the OAuth provider.",
5261
+ request: {
5262
+ params: oauthProviderParamSchema,
5263
+ query: oauthAuthorizeQuerySchema
5264
+ },
5265
+ responses: {
5266
+ 302: { description: "Redirect to OAuth provider or error page" }
5267
+ }
5268
+ });
5269
+ var authorizeHandler = async (c) => {
5270
+ const { provider } = c.req.valid("param");
5271
+ const {
5272
+ redirect: loginRedirect,
5273
+ invitationToken,
5274
+ code_challenge,
5275
+ code_challenge_method,
5276
+ redirect_uri: mobileRedirectUri
5277
+ } = c.req.valid("query");
5278
+ const providerConfig = PROVIDERS[provider];
5279
+ if (!providerConfig) {
5280
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_provider`);
5281
+ }
5282
+ const credentials = getProviderCredentials(provider, c.env);
5283
+ if (!credentials) {
5284
+ logError(new Error(`OAuth credentials not configured for ${provider}`), {
5285
+ context: "oauth_authorize",
5286
+ provider
5287
+ });
5288
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_not_configured`);
5289
+ }
5290
+ if (code_challenge && !code_challenge_method || !code_challenge && code_challenge_method) {
5291
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_pkce`);
5292
+ }
5293
+ const state = crypto.randomUUID();
5294
+ await c.env.OAUTH_STATES.put(
5295
+ `oauth:state:${state}`,
5296
+ JSON.stringify({
5297
+ provider,
5298
+ invitationToken: invitationToken || null,
5299
+ redirect: loginRedirect || null,
5300
+ codeChallenge: code_challenge || null,
5301
+ codeChallengeMethod: code_challenge_method || null,
5302
+ mobileRedirectUri: mobileRedirectUri || null
5303
+ }),
5304
+ { expirationTtl: 600 }
5305
+ // 10 minutes
5306
+ );
5307
+ const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
5308
+ const authUrl = new URL(providerConfig.authorizeUrl);
5309
+ authUrl.searchParams.set("client_id", credentials.clientId);
5310
+ authUrl.searchParams.set("redirect_uri", redirectUri);
5311
+ authUrl.searchParams.set("response_type", "code");
5312
+ if (providerConfig.scopes.length > 0) {
5313
+ authUrl.searchParams.set("scope", providerConfig.scopes.join(" "));
5314
+ }
5315
+ authUrl.searchParams.set("state", state);
5316
+ if (provider === "google") {
5317
+ authUrl.searchParams.set("prompt", "select_account");
5318
+ }
5319
+ logger_default.info("OAuth authorize initiated", { provider, hasPkce: !!code_challenge });
5320
+ return c.redirect(authUrl.toString());
5321
+ };
5322
+
5323
+ // src/routes/oauth/callback.ts
5324
+ import { createRoute as createRoute29 } from "@hono/zod-openapi";
5325
+ import { eq as eq31 } from "drizzle-orm";
5326
+ var callbackRoute = createRoute29({
5327
+ method: "get",
5328
+ path: "/{provider}/callback",
5329
+ tags: ["OAuth"],
5330
+ summary: "OAuth provider callback",
5331
+ 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.",
5332
+ request: {
5333
+ params: oauthProviderParamSchema,
5334
+ query: oauthCallbackQuerySchema
5335
+ },
5336
+ responses: {
5337
+ 302: { description: "Redirect to app with login code or signup token" }
5338
+ }
5339
+ });
5340
+ var callbackHandler = async (c) => {
5341
+ const { provider } = c.req.valid("param");
5342
+ const { code, state, error } = c.req.valid("query");
5343
+ const appUrl = c.env.APP_URL;
5344
+ if (error) {
5345
+ logger_default.info("OAuth provider returned error", { provider, error });
5346
+ const errorType = error === "access_denied" || error === "consent_required" ? "oauth_cancelled" : "oauth_failed";
5347
+ return c.redirect(`${appUrl}/login?error=${errorType}`);
5348
+ }
5349
+ if (!code || !state) {
5350
+ logger_default.warn("Missing code or state in OAuth callback", { provider });
5351
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5352
+ }
5353
+ const storedState = await c.env.OAUTH_STATES.get(`oauth:state:${state}`);
5354
+ if (!storedState) {
5355
+ logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
5356
+ return c.redirect(`${appUrl}/login?error=oauth_expired`);
5357
+ }
5358
+ let stateData;
5359
+ try {
5360
+ stateData = JSON.parse(storedState);
5361
+ } catch (err) {
5362
+ logError(err, { context: "oauth_state_parse", provider });
5363
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5364
+ }
5365
+ if (stateData.provider !== provider) {
5366
+ logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
5367
+ return c.redirect(`${appUrl}/login?error=oauth_expired`);
5368
+ }
5369
+ await c.env.OAUTH_STATES.delete(`oauth:state:${state}`);
5370
+ const {
5371
+ provider: _stateProvider,
5372
+ invitationToken,
5373
+ redirect: loginRedirect,
5374
+ codeChallenge,
5375
+ codeChallengeMethod,
5376
+ mobileRedirectUri
5377
+ } = stateData;
5378
+ const codeVerifier = c.req.query("code_verifier");
5379
+ if (codeChallenge && codeChallengeMethod === "S256") {
5380
+ if (!codeVerifier) {
5381
+ logger_default.warn("OAuth PKCE: missing code_verifier", { provider });
5382
+ return c.redirect(`${appUrl}/login?error=oauth_pkce_missing`);
5383
+ }
5384
+ const pkceValid = await verifyCodeChallenge(codeVerifier, codeChallenge);
5385
+ if (!pkceValid) {
5386
+ logger_default.warn("OAuth PKCE: invalid code_verifier", { provider });
5387
+ return c.redirect(`${appUrl}/login?error=oauth_pkce_invalid`);
5388
+ }
5389
+ }
5390
+ const providerConfig = PROVIDERS[provider];
5391
+ if (!providerConfig) {
5392
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5393
+ }
5394
+ const credentials = getProviderCredentials(provider, c.env);
5395
+ if (!credentials) {
5396
+ return c.redirect(`${appUrl}/login?error=oauth_not_configured`);
5397
+ }
5398
+ const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
5399
+ try {
5400
+ const tokenResponse = await fetch(providerConfig.tokenUrl, {
5401
+ method: "POST",
5402
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5403
+ body: new URLSearchParams({
5404
+ grant_type: "authorization_code",
5405
+ code,
5406
+ redirect_uri: redirectUri,
5407
+ client_id: credentials.clientId,
5408
+ client_secret: credentials.clientSecret
5409
+ }),
5410
+ signal: AbortSignal.timeout(1e4)
5411
+ });
5412
+ if (!tokenResponse.ok) {
5413
+ const body = await tokenResponse.text();
5414
+ logError(new Error(`OAuth token exchange failed: ${tokenResponse.status} ${body}`), {
5415
+ context: "oauth_token_exchange",
5416
+ provider
5417
+ });
5418
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5419
+ }
5420
+ const tokenData = await tokenResponse.json();
5421
+ if (!tokenData.access_token) {
5422
+ logError(new Error("No access_token in OAuth response"), {
5423
+ context: "oauth_token_exchange",
5424
+ provider
5425
+ });
5426
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5427
+ }
5428
+ const userinfoResponse = await fetch(providerConfig.userinfoUrl, {
5429
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
5430
+ signal: AbortSignal.timeout(1e4)
5431
+ });
5432
+ if (!userinfoResponse.ok) {
5433
+ logError(new Error(`OAuth userinfo failed: ${userinfoResponse.status}`), {
5434
+ context: "oauth_userinfo",
5435
+ provider
5436
+ });
5437
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5438
+ }
5439
+ const userInfo = await userinfoResponse.json();
5440
+ const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
5441
+ const providerUserId = providerConfig.userIdExtractor(userInfo);
5442
+ if (!email) {
5443
+ logger_default.warn("No email returned from OAuth provider", { provider });
5444
+ return c.redirect(`${appUrl}/login?error=oauth_no_email`);
5445
+ }
5446
+ if (!providerUserId) {
5447
+ logger_default.warn("No user ID returned from OAuth provider", { provider });
5448
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5449
+ }
5450
+ const { db, schema } = getAuthContext(c);
5451
+ const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where(eq31(schema.users.email, email)).limit(1);
5452
+ if (existingUser) {
5453
+ const loginCode = crypto.randomUUID();
5454
+ await c.env.OAUTH_STATES.put(
5455
+ `oauth:login:${loginCode}`,
5456
+ JSON.stringify({
5457
+ userId: existingUser.id,
5458
+ email: existingUser.email,
5459
+ provider,
5460
+ providerUserId,
5461
+ redirect: loginRedirect,
5462
+ invitationToken
5463
+ }),
5464
+ { expirationTtl: 60 }
5465
+ );
5466
+ logSecurityEvent("oauth_login_initiated", "low", {
5467
+ userId: existingUser.id,
5468
+ email: existingUser.email,
5469
+ provider
5470
+ });
5471
+ if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
5472
+ const mobileUrl = new URL(mobileRedirectUri);
5473
+ mobileUrl.searchParams.set("code", loginCode);
5474
+ mobileUrl.searchParams.set("action", "login");
5475
+ return c.redirect(mobileUrl.toString());
5476
+ }
5477
+ return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
5478
+ } else {
5479
+ const signupToken = crypto.randomUUID();
5480
+ await c.env.OAUTH_STATES.put(
5481
+ `oauth:signup:${signupToken}`,
5482
+ JSON.stringify({ email, provider, providerUserId, invitationToken }),
5483
+ { expirationTtl: 600 }
5484
+ );
5485
+ logger_default.info("OAuth signup initiated", { provider, email });
5486
+ if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
5487
+ const mobileUrl = new URL(mobileRedirectUri);
5488
+ mobileUrl.searchParams.set("oauth_token", signupToken);
5489
+ mobileUrl.searchParams.set("email", email);
5490
+ if (invitationToken) {
5491
+ mobileUrl.searchParams.set("invitation", invitationToken);
5492
+ }
5493
+ return c.redirect(mobileUrl.toString());
5494
+ }
5495
+ let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
5496
+ if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
5497
+ return c.redirect(signupUrl);
5498
+ }
5499
+ } catch (err) {
5500
+ logError(err, { context: "oauth_callback", provider });
5501
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5502
+ }
5503
+ };
5504
+
5505
+ // src/routes/oauth/complete.ts
5506
+ import { createRoute as createRoute30 } from "@hono/zod-openapi";
5507
+ import { eq as eq32 } from "drizzle-orm";
5508
+ var completeRoute = createRoute30({
5509
+ method: "post",
5510
+ path: "/complete",
5511
+ tags: ["OAuth"],
5512
+ summary: "Complete OAuth login",
5513
+ description: "Exchanges a one-time login code (from the OAuth callback) for a session cookie. Called by the /auth/callback frontend page.",
5514
+ request: {
5515
+ body: {
5516
+ content: { "application/json": { schema: oauthCompleteRequestSchema } },
5517
+ required: true
5518
+ }
5519
+ },
5520
+ responses: {
5521
+ 200: {
5522
+ description: "Session created successfully",
5523
+ content: { "application/json": { schema: oauthCompleteResponseSchema } }
5524
+ },
5525
+ 401: {
5526
+ description: "Invalid or expired login code",
5527
+ content: { "application/json": { schema: errorResponseSchema4 } }
5528
+ }
5529
+ }
5530
+ });
5531
+ var completeMiddleware = [
5532
+ rateLimit({
5533
+ identifier: (c) => c.req.header("cf-connecting-ip") || "unknown",
5534
+ action: "oauth_complete",
5535
+ maxAttempts: 10,
5536
+ windowMs: 9e5
5537
+ // 15 minutes
5538
+ })
5539
+ ];
5540
+ var completeHandler = async (c) => {
5541
+ const { code } = c.req.valid("json");
5542
+ const stored = await c.env.OAUTH_STATES.get(`oauth:login:${code}`);
5543
+ if (!stored) {
5544
+ return problems.unauthorized(c, "Invalid or expired login code");
5545
+ }
5546
+ await c.env.OAUTH_STATES.delete(`oauth:login:${code}`);
5547
+ const {
5548
+ userId,
5549
+ email,
5550
+ provider,
5551
+ providerUserId,
5552
+ redirect: storedRedirect
5553
+ } = JSON.parse(stored);
5554
+ const { db, schema } = getAuthContext(c);
5555
+ await db.update(schema.users).set({ emailVerified: true }).where(eq32(schema.users.id, userId));
5556
+ await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
5557
+ const fingerprint = await generateFingerprint(c.req.raw);
5558
+ const ipAddress = getClientIp(c.req.raw);
5559
+ const sessionId = await createSession(db, { sessions: schema.sessions }, userId, fingerprint, ipAddress);
5560
+ const redirect = storedRedirect?.startsWith("/") && !storedRedirect.startsWith("//") ? storedRedirect : "/";
5561
+ logger_default.info("OAuth login completed", { userId, provider, sessionId: sessionId.slice(0, 8) });
5562
+ logSecurityEvent("oauth_login_success", "low", {
5563
+ userId,
5564
+ email,
5565
+ provider,
5566
+ ip: c.req.header("cf-connecting-ip")
5567
+ });
5568
+ return c.json(
5569
+ { message: "Login successful", redirect, user: { id: userId, email, emailVerified: true } },
5570
+ 200,
5571
+ { "Set-Cookie": setSessionCookie(sessionId, c.env) }
5572
+ );
5573
+ };
5574
+
5575
+ // src/routes/oauth/index.ts
5576
+ var oauth = new OpenAPIHono2();
5577
+ oauth.use("/complete", ...completeMiddleware);
5578
+ var oauthRoutes = oauth.openapi(authorizeRoute, authorizeHandler).openapi(callbackRoute, callbackHandler).openapi(completeRoute, completeHandler);
5579
+ var oauth_default = oauthRoutes;
5194
5580
 
5195
5581
  // src/routes/index.ts
5196
- var auth = new OpenAPIHono2();
5582
+ var auth = new OpenAPIHono3();
5197
5583
  auth.use("*", csrf);
5198
5584
  auth.use("/signup", ...signupMiddleware);
5199
- auth.openapi(signupRoute, signupHandler);
5200
5585
  auth.use("/login", ...loginMiddleware);
5201
- auth.openapi(loginRoute, loginHandler);
5202
- auth.openapi(logoutRoute, logoutHandler);
5203
5586
  auth.use("/me", ...meMiddleware);
5204
- auth.openapi(meRoute, meHandler);
5205
- auth.openapi(verifyEmailRoute, verifyEmailHandler);
5206
5587
  auth.use("/forgot-password", ...forgotPasswordMiddleware);
5207
- auth.openapi(forgotPasswordRoute, forgotPasswordHandler);
5208
5588
  auth.use("/reset-password", ...resetPasswordMiddleware);
5209
- auth.openapi(resetPasswordRoute, resetPasswordHandler);
5210
5589
  auth.use("/change-password", ...changePasswordMiddleware);
5211
- auth.openapi(changePasswordRoute, changePasswordHandler);
5212
5590
  auth.use("/heartbeat", ...heartbeatMiddleware);
5213
- auth.openapi(heartbeatRoute, heartbeatHandler);
5214
5591
  auth.use("/change-email", ...changeEmailMiddleware);
5215
- auth.openapi(changeEmailRoute, changeEmailHandler);
5216
- auth.openapi(confirmEmailChangeRoute, confirmEmailChangeHandler);
5217
- auth.openapi(cancelEmailChangeRoute, cancelEmailChangeHandler);
5218
5592
  auth.use("/account", ...deleteAccountMiddleware);
5219
- auth.openapi(deleteAccountRoute, deleteAccountHandler);
5220
5593
  auth.use("/refresh", ...refreshMiddleware);
5221
- auth.openapi(refreshRoute, refreshHandler);
5222
5594
  auth.use("/resend-verification", ...resendVerificationMiddleware);
5223
- auth.openapi(resendVerificationRoute, resendVerificationHandler);
5224
- auth.route("/2fa", fa_default);
5225
- var routes_default = auth;
5595
+ 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);
5596
+ var routes_default = authRoutes;
5226
5597
 
5227
5598
  // src/lib/email/webhook-verifier.ts
5228
5599
  import { Webhook } from "svix";