@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/README.md CHANGED
@@ -18,12 +18,7 @@ Authentication library for Hono + Drizzle + Cloudflare Workers.
18
18
  ## Installation
19
19
 
20
20
  ```bash
21
- npm install @syncello/auth --registry=https://npm.pkg.github.com
22
- ```
23
-
24
- Configure `.npmrc`:
25
- ```
26
- @syncello:registry=https://npm.pkg.github.com
21
+ npm install @syncello/auth
27
22
  ```
28
23
 
29
24
  ## Quick Start
package/dist/index.cjs CHANGED
@@ -898,16 +898,16 @@ function createDeviceToken() {
898
898
  };
899
899
  }
900
900
  async function validateTrustedDevice(db, userTrustedDevicesTable, userId, tokenHash) {
901
- const { eq: eq31, and: and14, gt: gt6 } = await import("drizzle-orm");
901
+ const { eq: eq33, and: and14, gt: gt6 } = await import("drizzle-orm");
902
902
  const [device] = await db.select().from(userTrustedDevicesTable).where(
903
903
  and14(
904
- eq31(userTrustedDevicesTable.userId, userId),
905
- eq31(userTrustedDevicesTable.tokenHash, tokenHash),
904
+ eq33(userTrustedDevicesTable.userId, userId),
905
+ eq33(userTrustedDevicesTable.tokenHash, tokenHash),
906
906
  gt6(userTrustedDevicesTable.expiresAt, Date.now())
907
907
  )
908
908
  ).limit(1);
909
909
  if (device) {
910
- await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq31(userTrustedDevicesTable.id, device.id));
910
+ await db.update(userTrustedDevicesTable).set({ lastUsedAt: Date.now() }).where(eq33(userTrustedDevicesTable.id, device.id));
911
911
  return true;
912
912
  }
913
913
  return false;
@@ -1829,7 +1829,7 @@ var requireVerifiedEmail = (0, import_factory5.createMiddleware)(
1829
1829
  );
1830
1830
 
1831
1831
  // src/routes/index.ts
1832
- var import_zod_openapi37 = require("@hono/zod-openapi");
1832
+ var import_zod_openapi42 = require("@hono/zod-openapi");
1833
1833
 
1834
1834
  // src/routes/signup.ts
1835
1835
  var import_zod_openapi2 = require("@hono/zod-openapi");
@@ -3207,19 +3207,19 @@ var signupRequestSchema = import_zod_openapi.z.object({
3207
3207
  var loginRequestSchema = import_zod_openapi.z.object({
3208
3208
  email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
3209
3209
  password: import_zod_openapi.z.string().min(1).openapi({ example: "SecurePass123!" }),
3210
- turnstileToken: import_zod_openapi.z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
3210
+ turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
3211
3211
  }).openapi("LoginRequest");
3212
3212
  var verifyEmailRequestSchema = import_zod_openapi.z.object({
3213
3213
  token: import_zod_openapi.z.string().min(1).openapi({ example: "verification-token-abc123" })
3214
3214
  }).openapi("VerifyEmailRequest");
3215
3215
  var forgotPasswordRequestSchema = import_zod_openapi.z.object({
3216
3216
  email: import_zod_openapi.z.string().email().openapi({ example: "user@example.com" }),
3217
- turnstileToken: import_zod_openapi.z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
3217
+ turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
3218
3218
  }).openapi("ForgotPasswordRequest");
3219
3219
  var resetPasswordRequestSchema = import_zod_openapi.z.object({
3220
3220
  token: import_zod_openapi.z.string().min(1).openapi({ example: "reset-token-xyz789" }),
3221
3221
  password: import_zod_openapi.z.string().min(8).openapi({ example: "NewSecurePass456!" }),
3222
- turnstileToken: import_zod_openapi.z.string().min(1).openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
3222
+ turnstileToken: import_zod_openapi.z.string().optional().openapi({ example: "XXXX.DUMMY.TOKEN.XXXX" })
3223
3223
  }).openapi("ResetPasswordRequest");
3224
3224
  var changePasswordRequestSchema = import_zod_openapi.z.object({
3225
3225
  currentPassword: import_zod_openapi.z.string().min(1).openapi({ example: "OldPassword123!" }),
@@ -5498,11 +5498,8 @@ var challengeResendHandler = async (c) => {
5498
5498
  var twoFa = new import_zod_openapi36.OpenAPIHono();
5499
5499
  twoFa.use("*", csrf);
5500
5500
  twoFa.use("/status", requireAuth);
5501
- twoFa.openapi(statusRoute, statusHandler);
5502
5501
  twoFa.use("/totp/setup", requireAuth);
5503
- twoFa.openapi(totpSetupRoute, totpSetupHandler);
5504
5502
  twoFa.use("/totp/verify", requireAuth);
5505
- twoFa.openapi(totpVerifyRoute, totpVerifyHandler);
5506
5503
  twoFa.use(
5507
5504
  "/totp/disable",
5508
5505
  requireAuth,
@@ -5514,11 +5511,8 @@ twoFa.use(
5514
5511
  // 5 attempts per 15 minutes
5515
5512
  })
5516
5513
  );
5517
- twoFa.openapi(totpDisableRoute, totpDisableHandler);
5518
5514
  twoFa.use("/email/setup", requireAuth);
5519
- twoFa.openapi(emailSetupRoute, emailSetupHandler);
5520
5515
  twoFa.use("/email/verify", requireAuth);
5521
- twoFa.openapi(emailVerifyRoute, emailVerifyHandler);
5522
5516
  twoFa.use(
5523
5517
  "/email/send-code",
5524
5518
  requireAuth,
@@ -5530,7 +5524,6 @@ twoFa.use(
5530
5524
  // 3 per 5 minutes
5531
5525
  })
5532
5526
  );
5533
- twoFa.openapi(emailSendCodeRoute, emailSendCodeHandler);
5534
5527
  twoFa.use(
5535
5528
  "/email/disable",
5536
5529
  requireAuth,
@@ -5542,11 +5535,8 @@ twoFa.use(
5542
5535
  // 5 attempts per 15 minutes
5543
5536
  })
5544
5537
  );
5545
- twoFa.openapi(emailDisableRoute, emailDisableHandler);
5546
5538
  twoFa.use("/trusted-devices", requireAuth);
5547
- twoFa.openapi(trustedDevicesGetRoute, trustedDevicesGetHandler);
5548
5539
  twoFa.use("/trusted-devices/:id", requireAuth);
5549
- twoFa.openapi(trustedDevicesDeleteRoute, trustedDevicesDeleteHandler);
5550
5540
  twoFa.use(
5551
5541
  "/backup-codes/regenerate",
5552
5542
  requireAuth,
@@ -5558,7 +5548,6 @@ twoFa.use(
5558
5548
  // 3 per hour per user
5559
5549
  })
5560
5550
  );
5561
- twoFa.openapi(backupCodesRegenerateRoute, backupCodesRegenerateHandler);
5562
5551
  twoFa.use(
5563
5552
  "/challenge",
5564
5553
  rateLimit({
@@ -5569,7 +5558,6 @@ twoFa.use(
5569
5558
  // 10 per 5 min per IP
5570
5559
  })
5571
5560
  );
5572
- twoFa.openapi(challengeRoute, challengeHandler);
5573
5561
  twoFa.use(
5574
5562
  "/challenge/resend",
5575
5563
  rateLimit({
@@ -5580,40 +5568,423 @@ twoFa.use(
5580
5568
  // 3 per 5 min per IP
5581
5569
  })
5582
5570
  );
5583
- twoFa.openapi(challengeResendRoute, challengeResendHandler);
5584
- var fa_default = twoFa;
5571
+ 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);
5572
+ var fa_default = twoFaRoutes;
5573
+
5574
+ // src/routes/oauth/index.ts
5575
+ var import_zod_openapi41 = require("@hono/zod-openapi");
5576
+
5577
+ // src/routes/oauth/authorize.ts
5578
+ var import_zod_openapi38 = require("@hono/zod-openapi");
5579
+
5580
+ // src/routes/oauth/providers.ts
5581
+ var PROVIDERS = {
5582
+ google: {
5583
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
5584
+ tokenUrl: "https://oauth2.googleapis.com/token",
5585
+ userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
5586
+ scopes: ["email"],
5587
+ emailExtractor: (data) => data.email || null,
5588
+ userIdExtractor: (data) => data.id || null
5589
+ },
5590
+ microsoft: {
5591
+ authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
5592
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
5593
+ userinfoUrl: "https://graph.microsoft.com/v1.0/me",
5594
+ scopes: ["openid", "email", "User.Read"],
5595
+ emailExtractor: (data) => data.mail || data.userPrincipalName || null,
5596
+ userIdExtractor: (data) => data.id || null
5597
+ }
5598
+ };
5599
+ function getProviderCredentials(provider, env) {
5600
+ if (provider === "google" && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
5601
+ return { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET };
5602
+ }
5603
+ if (provider === "microsoft" && env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) {
5604
+ return { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET };
5605
+ }
5606
+ return null;
5607
+ }
5608
+
5609
+ // src/routes/oauth/schemas.ts
5610
+ var import_zod_openapi37 = require("@hono/zod-openapi");
5611
+ var oauthProviderParamSchema = import_zod_openapi37.z.object({
5612
+ provider: import_zod_openapi37.z.enum(["google", "microsoft"]).openapi({ example: "google" })
5613
+ });
5614
+ var oauthAuthorizeQuerySchema = import_zod_openapi37.z.object({
5615
+ redirect: import_zod_openapi37.z.string().optional().openapi({ example: "/dashboard" }),
5616
+ invitationToken: import_zod_openapi37.z.string().optional().openapi({ example: "token123" }),
5617
+ code_challenge: import_zod_openapi37.z.string().min(43).max(128).optional().openapi({ example: "E9Mrozoa2owUednMY..." }),
5618
+ code_challenge_method: import_zod_openapi37.z.enum(["S256"]).optional().openapi({ example: "S256" }),
5619
+ redirect_uri: import_zod_openapi37.z.string().url().optional().openapi({ example: "myapp://oauth-callback" })
5620
+ }).openapi("OAuthAuthorizeQuery");
5621
+ var oauthCallbackQuerySchema = import_zod_openapi37.z.object({
5622
+ code: import_zod_openapi37.z.string().optional(),
5623
+ state: import_zod_openapi37.z.string().optional(),
5624
+ error: import_zod_openapi37.z.string().optional()
5625
+ });
5626
+ var oauthCompleteRequestSchema = import_zod_openapi37.z.object({
5627
+ code: import_zod_openapi37.z.string().uuid("Invalid code format").openapi({ example: "a1b2c3d4-..." })
5628
+ }).openapi("OAuthCompleteRequest");
5629
+ var oauthCompleteResponseSchema = import_zod_openapi37.z.object({
5630
+ message: import_zod_openapi37.z.string().openapi({ example: "Login successful" }),
5631
+ redirect: import_zod_openapi37.z.string().openapi({ example: "/" }),
5632
+ user: import_zod_openapi37.z.object({
5633
+ id: import_zod_openapi37.z.string().uuid(),
5634
+ email: import_zod_openapi37.z.string().email(),
5635
+ emailVerified: import_zod_openapi37.z.boolean()
5636
+ })
5637
+ }).openapi("OAuthCompleteResponse");
5638
+ var errorResponseSchema4 = import_zod_openapi37.z.object({
5639
+ type: import_zod_openapi37.z.string().url(),
5640
+ title: import_zod_openapi37.z.string(),
5641
+ status: import_zod_openapi37.z.number().int(),
5642
+ detail: import_zod_openapi37.z.string().optional()
5643
+ }).openapi("ErrorResponse");
5644
+
5645
+ // src/routes/oauth/authorize.ts
5646
+ var authorizeRoute = (0, import_zod_openapi38.createRoute)({
5647
+ method: "get",
5648
+ path: "/{provider}/authorize",
5649
+ tags: ["OAuth"],
5650
+ summary: "Start OAuth login flow",
5651
+ description: "Generates a CSRF state token and redirects the user to the OAuth provider.",
5652
+ request: {
5653
+ params: oauthProviderParamSchema,
5654
+ query: oauthAuthorizeQuerySchema
5655
+ },
5656
+ responses: {
5657
+ 302: { description: "Redirect to OAuth provider or error page" }
5658
+ }
5659
+ });
5660
+ var authorizeHandler = async (c) => {
5661
+ const { provider } = c.req.valid("param");
5662
+ const {
5663
+ redirect: loginRedirect,
5664
+ invitationToken,
5665
+ code_challenge,
5666
+ code_challenge_method,
5667
+ redirect_uri: mobileRedirectUri
5668
+ } = c.req.valid("query");
5669
+ const providerConfig = PROVIDERS[provider];
5670
+ if (!providerConfig) {
5671
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_provider`);
5672
+ }
5673
+ const credentials = getProviderCredentials(provider, c.env);
5674
+ if (!credentials) {
5675
+ logError(new Error(`OAuth credentials not configured for ${provider}`), {
5676
+ context: "oauth_authorize",
5677
+ provider
5678
+ });
5679
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_not_configured`);
5680
+ }
5681
+ if (code_challenge && !code_challenge_method || !code_challenge && code_challenge_method) {
5682
+ return c.redirect(`${c.env.APP_URL}/login?error=oauth_invalid_pkce`);
5683
+ }
5684
+ const state = crypto.randomUUID();
5685
+ await c.env.OAUTH_STATES.put(
5686
+ `oauth:state:${state}`,
5687
+ JSON.stringify({
5688
+ provider,
5689
+ invitationToken: invitationToken || null,
5690
+ redirect: loginRedirect || null,
5691
+ codeChallenge: code_challenge || null,
5692
+ codeChallengeMethod: code_challenge_method || null,
5693
+ mobileRedirectUri: mobileRedirectUri || null
5694
+ }),
5695
+ { expirationTtl: 600 }
5696
+ // 10 minutes
5697
+ );
5698
+ const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
5699
+ const authUrl = new URL(providerConfig.authorizeUrl);
5700
+ authUrl.searchParams.set("client_id", credentials.clientId);
5701
+ authUrl.searchParams.set("redirect_uri", redirectUri);
5702
+ authUrl.searchParams.set("response_type", "code");
5703
+ if (providerConfig.scopes.length > 0) {
5704
+ authUrl.searchParams.set("scope", providerConfig.scopes.join(" "));
5705
+ }
5706
+ authUrl.searchParams.set("state", state);
5707
+ if (provider === "google") {
5708
+ authUrl.searchParams.set("prompt", "select_account");
5709
+ }
5710
+ logger_default.info("OAuth authorize initiated", { provider, hasPkce: !!code_challenge });
5711
+ return c.redirect(authUrl.toString());
5712
+ };
5713
+
5714
+ // src/routes/oauth/callback.ts
5715
+ var import_zod_openapi39 = require("@hono/zod-openapi");
5716
+ var import_drizzle_orm32 = require("drizzle-orm");
5717
+ var callbackRoute = (0, import_zod_openapi39.createRoute)({
5718
+ method: "get",
5719
+ path: "/{provider}/callback",
5720
+ tags: ["OAuth"],
5721
+ summary: "OAuth provider callback",
5722
+ 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.",
5723
+ request: {
5724
+ params: oauthProviderParamSchema,
5725
+ query: oauthCallbackQuerySchema
5726
+ },
5727
+ responses: {
5728
+ 302: { description: "Redirect to app with login code or signup token" }
5729
+ }
5730
+ });
5731
+ var callbackHandler = async (c) => {
5732
+ const { provider } = c.req.valid("param");
5733
+ const { code, state, error } = c.req.valid("query");
5734
+ const appUrl = c.env.APP_URL;
5735
+ if (error) {
5736
+ logger_default.info("OAuth provider returned error", { provider, error });
5737
+ const errorType = error === "access_denied" || error === "consent_required" ? "oauth_cancelled" : "oauth_failed";
5738
+ return c.redirect(`${appUrl}/login?error=${errorType}`);
5739
+ }
5740
+ if (!code || !state) {
5741
+ logger_default.warn("Missing code or state in OAuth callback", { provider });
5742
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5743
+ }
5744
+ const storedState = await c.env.OAUTH_STATES.get(`oauth:state:${state}`);
5745
+ if (!storedState) {
5746
+ logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
5747
+ return c.redirect(`${appUrl}/login?error=oauth_expired`);
5748
+ }
5749
+ let stateData;
5750
+ try {
5751
+ stateData = JSON.parse(storedState);
5752
+ } catch (err) {
5753
+ logError(err, { context: "oauth_state_parse", provider });
5754
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5755
+ }
5756
+ if (stateData.provider !== provider) {
5757
+ logSecurityEvent("oauth_csrf_attempt", "high", { state, provider });
5758
+ return c.redirect(`${appUrl}/login?error=oauth_expired`);
5759
+ }
5760
+ await c.env.OAUTH_STATES.delete(`oauth:state:${state}`);
5761
+ const {
5762
+ provider: _stateProvider,
5763
+ invitationToken,
5764
+ redirect: loginRedirect,
5765
+ codeChallenge,
5766
+ codeChallengeMethod,
5767
+ mobileRedirectUri
5768
+ } = stateData;
5769
+ const codeVerifier = c.req.query("code_verifier");
5770
+ if (codeChallenge && codeChallengeMethod === "S256") {
5771
+ if (!codeVerifier) {
5772
+ logger_default.warn("OAuth PKCE: missing code_verifier", { provider });
5773
+ return c.redirect(`${appUrl}/login?error=oauth_pkce_missing`);
5774
+ }
5775
+ const pkceValid = await verifyCodeChallenge(codeVerifier, codeChallenge);
5776
+ if (!pkceValid) {
5777
+ logger_default.warn("OAuth PKCE: invalid code_verifier", { provider });
5778
+ return c.redirect(`${appUrl}/login?error=oauth_pkce_invalid`);
5779
+ }
5780
+ }
5781
+ const providerConfig = PROVIDERS[provider];
5782
+ if (!providerConfig) {
5783
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5784
+ }
5785
+ const credentials = getProviderCredentials(provider, c.env);
5786
+ if (!credentials) {
5787
+ return c.redirect(`${appUrl}/login?error=oauth_not_configured`);
5788
+ }
5789
+ const redirectUri = new URL(`/v1/auth/oauth/${provider}/callback`, c.req.url).toString();
5790
+ try {
5791
+ const tokenResponse = await fetch(providerConfig.tokenUrl, {
5792
+ method: "POST",
5793
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5794
+ body: new URLSearchParams({
5795
+ grant_type: "authorization_code",
5796
+ code,
5797
+ redirect_uri: redirectUri,
5798
+ client_id: credentials.clientId,
5799
+ client_secret: credentials.clientSecret
5800
+ }),
5801
+ signal: AbortSignal.timeout(1e4)
5802
+ });
5803
+ if (!tokenResponse.ok) {
5804
+ const body = await tokenResponse.text();
5805
+ logError(new Error(`OAuth token exchange failed: ${tokenResponse.status} ${body}`), {
5806
+ context: "oauth_token_exchange",
5807
+ provider
5808
+ });
5809
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5810
+ }
5811
+ const tokenData = await tokenResponse.json();
5812
+ if (!tokenData.access_token) {
5813
+ logError(new Error("No access_token in OAuth response"), {
5814
+ context: "oauth_token_exchange",
5815
+ provider
5816
+ });
5817
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5818
+ }
5819
+ const userinfoResponse = await fetch(providerConfig.userinfoUrl, {
5820
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
5821
+ signal: AbortSignal.timeout(1e4)
5822
+ });
5823
+ if (!userinfoResponse.ok) {
5824
+ logError(new Error(`OAuth userinfo failed: ${userinfoResponse.status}`), {
5825
+ context: "oauth_userinfo",
5826
+ provider
5827
+ });
5828
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5829
+ }
5830
+ const userInfo = await userinfoResponse.json();
5831
+ const email = providerConfig.emailExtractor(userInfo)?.toLowerCase();
5832
+ const providerUserId = providerConfig.userIdExtractor(userInfo);
5833
+ if (!email) {
5834
+ logger_default.warn("No email returned from OAuth provider", { provider });
5835
+ return c.redirect(`${appUrl}/login?error=oauth_no_email`);
5836
+ }
5837
+ if (!providerUserId) {
5838
+ logger_default.warn("No user ID returned from OAuth provider", { provider });
5839
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5840
+ }
5841
+ const { db, schema } = getAuthContext(c);
5842
+ const [existingUser] = await db.select({ id: schema.users.id, email: schema.users.email }).from(schema.users).where((0, import_drizzle_orm32.eq)(schema.users.email, email)).limit(1);
5843
+ if (existingUser) {
5844
+ const loginCode = crypto.randomUUID();
5845
+ await c.env.OAUTH_STATES.put(
5846
+ `oauth:login:${loginCode}`,
5847
+ JSON.stringify({
5848
+ userId: existingUser.id,
5849
+ email: existingUser.email,
5850
+ provider,
5851
+ providerUserId,
5852
+ redirect: loginRedirect,
5853
+ invitationToken
5854
+ }),
5855
+ { expirationTtl: 60 }
5856
+ );
5857
+ logSecurityEvent("oauth_login_initiated", "low", {
5858
+ userId: existingUser.id,
5859
+ email: existingUser.email,
5860
+ provider
5861
+ });
5862
+ if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
5863
+ const mobileUrl = new URL(mobileRedirectUri);
5864
+ mobileUrl.searchParams.set("code", loginCode);
5865
+ mobileUrl.searchParams.set("action", "login");
5866
+ return c.redirect(mobileUrl.toString());
5867
+ }
5868
+ return c.redirect(`${appUrl}/auth/callback?code=${loginCode}&action=login`);
5869
+ } else {
5870
+ const signupToken = crypto.randomUUID();
5871
+ await c.env.OAUTH_STATES.put(
5872
+ `oauth:signup:${signupToken}`,
5873
+ JSON.stringify({ email, provider, providerUserId, invitationToken }),
5874
+ { expirationTtl: 600 }
5875
+ );
5876
+ logger_default.info("OAuth signup initiated", { provider, email });
5877
+ if (mobileRedirectUri && isDeepLinkUri(mobileRedirectUri)) {
5878
+ const mobileUrl = new URL(mobileRedirectUri);
5879
+ mobileUrl.searchParams.set("oauth_token", signupToken);
5880
+ mobileUrl.searchParams.set("email", email);
5881
+ if (invitationToken) {
5882
+ mobileUrl.searchParams.set("invitation", invitationToken);
5883
+ }
5884
+ return c.redirect(mobileUrl.toString());
5885
+ }
5886
+ let signupUrl = `${appUrl}/signup?oauth_token=${signupToken}&email=${encodeURIComponent(email)}`;
5887
+ if (invitationToken) signupUrl += `&invitation=${invitationToken}`;
5888
+ return c.redirect(signupUrl);
5889
+ }
5890
+ } catch (err) {
5891
+ logError(err, { context: "oauth_callback", provider });
5892
+ return c.redirect(`${appUrl}/login?error=oauth_failed`);
5893
+ }
5894
+ };
5895
+
5896
+ // src/routes/oauth/complete.ts
5897
+ var import_zod_openapi40 = require("@hono/zod-openapi");
5898
+ var import_drizzle_orm33 = require("drizzle-orm");
5899
+ var completeRoute = (0, import_zod_openapi40.createRoute)({
5900
+ method: "post",
5901
+ path: "/complete",
5902
+ tags: ["OAuth"],
5903
+ summary: "Complete OAuth login",
5904
+ description: "Exchanges a one-time login code (from the OAuth callback) for a session cookie. Called by the /auth/callback frontend page.",
5905
+ request: {
5906
+ body: {
5907
+ content: { "application/json": { schema: oauthCompleteRequestSchema } },
5908
+ required: true
5909
+ }
5910
+ },
5911
+ responses: {
5912
+ 200: {
5913
+ description: "Session created successfully",
5914
+ content: { "application/json": { schema: oauthCompleteResponseSchema } }
5915
+ },
5916
+ 401: {
5917
+ description: "Invalid or expired login code",
5918
+ content: { "application/json": { schema: errorResponseSchema4 } }
5919
+ }
5920
+ }
5921
+ });
5922
+ var completeMiddleware = [
5923
+ rateLimit({
5924
+ identifier: (c) => c.req.header("cf-connecting-ip") || "unknown",
5925
+ action: "oauth_complete",
5926
+ maxAttempts: 10,
5927
+ windowMs: 9e5
5928
+ // 15 minutes
5929
+ })
5930
+ ];
5931
+ var completeHandler = async (c) => {
5932
+ const { code } = c.req.valid("json");
5933
+ const stored = await c.env.OAUTH_STATES.get(`oauth:login:${code}`);
5934
+ if (!stored) {
5935
+ return problems.unauthorized(c, "Invalid or expired login code");
5936
+ }
5937
+ await c.env.OAUTH_STATES.delete(`oauth:login:${code}`);
5938
+ const {
5939
+ userId,
5940
+ email,
5941
+ provider,
5942
+ providerUserId,
5943
+ redirect: storedRedirect
5944
+ } = JSON.parse(stored);
5945
+ const { db, schema } = getAuthContext(c);
5946
+ await db.update(schema.users).set({ emailVerified: true }).where((0, import_drizzle_orm33.eq)(schema.users.id, userId));
5947
+ await db.insert(schema.oauthAccounts).values({ userId, provider, providerUserId, email }).onConflictDoNothing();
5948
+ const fingerprint = await generateFingerprint(c.req.raw);
5949
+ const ipAddress = getClientIp(c.req.raw);
5950
+ const sessionId = await createSession(db, { sessions: schema.sessions }, userId, fingerprint, ipAddress);
5951
+ const redirect = storedRedirect?.startsWith("/") && !storedRedirect.startsWith("//") ? storedRedirect : "/";
5952
+ logger_default.info("OAuth login completed", { userId, provider, sessionId: sessionId.slice(0, 8) });
5953
+ logSecurityEvent("oauth_login_success", "low", {
5954
+ userId,
5955
+ email,
5956
+ provider,
5957
+ ip: c.req.header("cf-connecting-ip")
5958
+ });
5959
+ return c.json(
5960
+ { message: "Login successful", redirect, user: { id: userId, email, emailVerified: true } },
5961
+ 200,
5962
+ { "Set-Cookie": setSessionCookie(sessionId, c.env) }
5963
+ );
5964
+ };
5965
+
5966
+ // src/routes/oauth/index.ts
5967
+ var oauth = new import_zod_openapi41.OpenAPIHono();
5968
+ oauth.use("/complete", ...completeMiddleware);
5969
+ var oauthRoutes = oauth.openapi(authorizeRoute, authorizeHandler).openapi(callbackRoute, callbackHandler).openapi(completeRoute, completeHandler);
5970
+ var oauth_default = oauthRoutes;
5585
5971
 
5586
5972
  // src/routes/index.ts
5587
- var auth = new import_zod_openapi37.OpenAPIHono();
5973
+ var auth = new import_zod_openapi42.OpenAPIHono();
5588
5974
  auth.use("*", csrf);
5589
5975
  auth.use("/signup", ...signupMiddleware);
5590
- auth.openapi(signupRoute, signupHandler);
5591
5976
  auth.use("/login", ...loginMiddleware);
5592
- auth.openapi(loginRoute, loginHandler);
5593
- auth.openapi(logoutRoute, logoutHandler);
5594
5977
  auth.use("/me", ...meMiddleware);
5595
- auth.openapi(meRoute, meHandler);
5596
- auth.openapi(verifyEmailRoute, verifyEmailHandler);
5597
5978
  auth.use("/forgot-password", ...forgotPasswordMiddleware);
5598
- auth.openapi(forgotPasswordRoute, forgotPasswordHandler);
5599
5979
  auth.use("/reset-password", ...resetPasswordMiddleware);
5600
- auth.openapi(resetPasswordRoute, resetPasswordHandler);
5601
5980
  auth.use("/change-password", ...changePasswordMiddleware);
5602
- auth.openapi(changePasswordRoute, changePasswordHandler);
5603
5981
  auth.use("/heartbeat", ...heartbeatMiddleware);
5604
- auth.openapi(heartbeatRoute, heartbeatHandler);
5605
5982
  auth.use("/change-email", ...changeEmailMiddleware);
5606
- auth.openapi(changeEmailRoute, changeEmailHandler);
5607
- auth.openapi(confirmEmailChangeRoute, confirmEmailChangeHandler);
5608
- auth.openapi(cancelEmailChangeRoute, cancelEmailChangeHandler);
5609
5983
  auth.use("/account", ...deleteAccountMiddleware);
5610
- auth.openapi(deleteAccountRoute, deleteAccountHandler);
5611
5984
  auth.use("/refresh", ...refreshMiddleware);
5612
- auth.openapi(refreshRoute, refreshHandler);
5613
5985
  auth.use("/resend-verification", ...resendVerificationMiddleware);
5614
- auth.openapi(resendVerificationRoute, resendVerificationHandler);
5615
- auth.route("/2fa", fa_default);
5616
- var routes_default = auth;
5986
+ 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);
5987
+ var routes_default = authRoutes;
5617
5988
 
5618
5989
  // src/lib/email/webhook-verifier.ts
5619
5990
  var import_svix = require("svix");