@spfn/auth 0.2.0-beta.71 → 0.2.0-beta.74

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/server.js CHANGED
@@ -7604,6 +7604,10 @@ var COOKIE_NAMES = {
7604
7604
  /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
7605
7605
  get OAUTH_PENDING() {
7606
7606
  return `spfn_oauth_pending${getCookieSuffix()}`;
7607
+ },
7608
+ /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
7609
+ get OAUTH_CSRF() {
7610
+ return `spfn_oauth_csrf${getCookieSuffix()}`;
7607
7611
  }
7608
7612
  };
7609
7613
  function parseDuration(duration) {
@@ -8317,11 +8321,14 @@ function generateNonce() {
8317
8321
  crypto.getRandomValues(array);
8318
8322
  return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
8319
8323
  }
8324
+ function generateOAuthNonce() {
8325
+ return generateNonce();
8326
+ }
8320
8327
  async function createOAuthState(params) {
8321
8328
  const key = await getStateKey();
8322
8329
  const state = {
8323
8330
  returnUrl: params.returnUrl,
8324
- nonce: generateNonce(),
8331
+ nonce: params.nonce ?? generateNonce(),
8325
8332
  provider: params.provider,
8326
8333
  publicKey: params.publicKey,
8327
8334
  keyId: params.keyId,
@@ -8539,7 +8546,7 @@ function tokenExpiryDate(expiresIn) {
8539
8546
  return new Date(Date.now() + expiresIn * 1e3);
8540
8547
  }
8541
8548
  async function oauthStartService(params) {
8542
- const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
8549
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata, nonce } = params;
8543
8550
  const oauthProvider = requireEnabledProvider(provider);
8544
8551
  const state = await createOAuthState({
8545
8552
  provider,
@@ -8548,13 +8555,19 @@ async function oauthStartService(params) {
8548
8555
  keyId,
8549
8556
  fingerprint,
8550
8557
  algorithm,
8551
- metadata
8558
+ metadata,
8559
+ nonce
8552
8560
  });
8553
8561
  return { authUrl: oauthProvider.getAuthUrl(state) };
8554
8562
  }
8555
8563
  async function oauthCallbackService(params) {
8556
- const { provider, code, state } = params;
8564
+ const { provider, code, state, expectedNonce } = params;
8557
8565
  const stateData = await verifyOAuthState(state);
8566
+ if (!expectedNonce || stateData.nonce !== expectedNonce) {
8567
+ throw new ValidationError5({
8568
+ message: "OAuth state validation failed"
8569
+ });
8570
+ }
8558
8571
  if (stateData.provider !== provider) {
8559
8572
  throw new ValidationError5({
8560
8573
  message: "OAuth state provider mismatch"
@@ -8764,7 +8777,56 @@ async function persistNativeLogin(identity, params) {
8764
8777
  // src/server/routes/auth/index.ts
8765
8778
  init_esm();
8766
8779
  import { Transactional } from "@spfn/core/db";
8767
- import { rateLimit } from "@spfn/core/middleware";
8780
+ import { rateLimitPolicy } from "@spfn/core/middleware";
8781
+
8782
+ // src/server/lib/rate-limit-keys.ts
8783
+ import { getClientIp } from "@spfn/core/middleware";
8784
+ async function readJsonBody(c) {
8785
+ try {
8786
+ return await c.req.json();
8787
+ } catch {
8788
+ return {};
8789
+ }
8790
+ }
8791
+ function accountKey(body) {
8792
+ if (typeof body.email === "string" && body.email.trim()) {
8793
+ return `email:${body.email.trim().toLowerCase()}`;
8794
+ }
8795
+ if (typeof body.phone === "string" && body.phone.trim()) {
8796
+ return `phone:${body.phone.trim()}`;
8797
+ }
8798
+ return void 0;
8799
+ }
8800
+ function targetKey(body) {
8801
+ if (typeof body.target !== "string" || !body.target.trim()) {
8802
+ return void 0;
8803
+ }
8804
+ const type = typeof body.targetType === "string" ? body.targetType : "target";
8805
+ const value = type === "email" ? body.target.trim().toLowerCase() : body.target.trim();
8806
+ return `${type}:${value}`;
8807
+ }
8808
+ function byIpAndAccount(options = {}) {
8809
+ return async (c) => {
8810
+ const body = await readJsonBody(c);
8811
+ const account = accountKey(body);
8812
+ return [
8813
+ { key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
8814
+ account ? `acct:${account}` : void 0
8815
+ ];
8816
+ };
8817
+ }
8818
+ function byIpAndTarget(options = {}) {
8819
+ return async (c) => {
8820
+ const body = await readJsonBody(c);
8821
+ const target = targetKey(body);
8822
+ return [
8823
+ { key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
8824
+ target ? `tgt:${target}` : void 0
8825
+ ];
8826
+ };
8827
+ }
8828
+
8829
+ // src/server/routes/auth/index.ts
8768
8830
  import { defineRouter, route } from "@spfn/core/route";
8769
8831
  var sendVerificationCode = route.post("/_auth/codes").input({
8770
8832
  body: Type.Object({
@@ -8774,7 +8836,7 @@ var sendVerificationCode = route.post("/_auth/codes").input({
8774
8836
  targetType: TargetTypeSchema,
8775
8837
  purpose: VerificationPurposeSchema
8776
8838
  })
8777
- }).use([rateLimit({ limit: 5, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
8839
+ }).use([rateLimitPolicy("auth-code-send", { limit: 5, windowMs: 6e4, by: byIpAndTarget({ ipLimit: 20 }) })]).skip(["auth"]).handler(async (c) => {
8778
8840
  const { body } = await c.data();
8779
8841
  return await sendVerificationCodeService(body);
8780
8842
  });
@@ -8792,7 +8854,7 @@ var verifyCode = route.post("/_auth/codes/verify").input({
8792
8854
  }),
8793
8855
  purpose: VerificationPurposeSchema
8794
8856
  })
8795
- }).use([rateLimit({ limit: 10, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
8857
+ }).use([rateLimitPolicy("auth-code-verify", { limit: 10, windowMs: 6e4, by: byIpAndTarget({ ipLimit: 50 }) })]).skip(["auth"]).handler(async (c) => {
8796
8858
  const { body } = await c.data();
8797
8859
  return await verifyCodeService(body);
8798
8860
  });
@@ -8819,7 +8881,7 @@ var register = route.post("/_auth/register").input({
8819
8881
  fingerprint: Type.String({ description: "Key fingerprint" }),
8820
8882
  algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" })
8821
8883
  })
8822
- }).use([Transactional()]).skip(["auth"]).handler(async (c) => {
8884
+ }).use([rateLimitPolicy("auth-register", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 100 }) }), Transactional()]).skip(["auth"]).handler(async (c) => {
8823
8885
  const { body } = await c.data();
8824
8886
  return await registerService(body);
8825
8887
  });
@@ -8844,7 +8906,7 @@ var login = route.post("/_auth/login").input({
8844
8906
  algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
8845
8907
  oldKeyId: Type.Optional(Type.String({ description: "Previous key ID for rotation" }))
8846
8908
  })
8847
- }).use([rateLimit({ limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
8909
+ }).use([rateLimitPolicy("auth-login", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 100 }) }), Transactional()]).skip(["auth"]).handler(async (c) => {
8848
8910
  const { body } = await c.data();
8849
8911
  return await loginService(body);
8850
8912
  });
@@ -8884,7 +8946,7 @@ var changePassword = route.put("/_auth/password").input({
8884
8946
  })),
8885
8947
  newPassword: PasswordSchema
8886
8948
  })
8887
- }).handler(async (c) => {
8949
+ }).use([rateLimitPolicy("auth-password-change", { limit: 10, windowMs: 6e4 })]).handler(async (c) => {
8888
8950
  const { body } = await c.data();
8889
8951
  const user = getUser(c);
8890
8952
  await changePasswordService({
@@ -9244,6 +9306,7 @@ function extractOTTHeader(header) {
9244
9306
  init_types();
9245
9307
  init_esm();
9246
9308
  import { Transactional as Transactional2 } from "@spfn/core/db";
9309
+ import { rateLimitPolicy as rateLimitPolicy2 } from "@spfn/core/middleware";
9247
9310
  import { defineRouter as defineRouter2, route as route2 } from "@spfn/core/route";
9248
9311
  var INVITATION_STATUSES2 = ["pending", "accepted", "expired", "cancelled"];
9249
9312
  var getInvitation = route2.get("/_auth/invitations/:token").input({
@@ -9253,7 +9316,7 @@ var getInvitation = route2.get("/_auth/invitations/:token").input({
9253
9316
  description: "Invitation token (UUID v4)"
9254
9317
  })
9255
9318
  })
9256
- }).skip(["auth"]).handler(async (c) => {
9319
+ }).use([rateLimitPolicy2("auth-invitation-lookup", { limit: 30, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9257
9320
  const { params } = await c.data();
9258
9321
  const token = params.token;
9259
9322
  const validation = await validateInvitation(token);
@@ -9292,7 +9355,7 @@ var acceptInvitation2 = route2.post("/_auth/invitations/accept").input({
9292
9355
  fingerprint: Type.String({ description: "Key fingerprint" }),
9293
9356
  algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" })
9294
9357
  })
9295
- }).use([Transactional2()]).skip(["auth"]).handler(async (c) => {
9358
+ }).use([rateLimitPolicy2("auth-invitation-accept", { limit: 10, windowMs: 6e4 }), Transactional2()]).skip(["auth"]).handler(async (c) => {
9296
9359
  const { body } = await c.data();
9297
9360
  return await acceptInvitation({
9298
9361
  token: body.token,
@@ -9442,6 +9505,7 @@ var invitationRouter = defineRouter2({
9442
9505
 
9443
9506
  // src/server/routes/users/index.ts
9444
9507
  init_esm();
9508
+ import { rateLimitPolicy as rateLimitPolicy3 } from "@spfn/core/middleware";
9445
9509
  import { defineRouter as defineRouter3, route as route3 } from "@spfn/core/route";
9446
9510
  var getUserProfile = route3.get("/_auth/users/profile").handler(async (c) => {
9447
9511
  const { userId } = getAuth(c);
@@ -9473,7 +9537,7 @@ var checkUsername = route3.get("/_auth/users/username/check").input({
9473
9537
  query: Type.Object({
9474
9538
  username: Type.String({ minLength: 1 })
9475
9539
  })
9476
- }).handler(async (c) => {
9540
+ }).use([rateLimitPolicy3("auth-username-check", { limit: 30, windowMs: 6e4 })]).handler(async (c) => {
9477
9541
  const { query } = await c.data();
9478
9542
  return { available: await checkUsernameAvailableService(query.username) };
9479
9543
  });
@@ -9508,10 +9572,197 @@ var userRouter = defineRouter3({
9508
9572
 
9509
9573
  // src/server/routes/oauth/index.ts
9510
9574
  init_esm();
9575
+
9576
+ // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/url.js
9577
+ var tryDecode = (str, decoder) => {
9578
+ try {
9579
+ return decoder(str);
9580
+ } catch {
9581
+ return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
9582
+ try {
9583
+ return decoder(match);
9584
+ } catch {
9585
+ return match;
9586
+ }
9587
+ });
9588
+ }
9589
+ };
9590
+ var decodeURIComponent_ = decodeURIComponent;
9591
+
9592
+ // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/cookie.js
9593
+ var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
9594
+ var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
9595
+ var trimCookieWhitespace = (value) => {
9596
+ let start = 0;
9597
+ let end = value.length;
9598
+ while (start < end) {
9599
+ const charCode = value.charCodeAt(start);
9600
+ if (charCode !== 32 && charCode !== 9) {
9601
+ break;
9602
+ }
9603
+ start++;
9604
+ }
9605
+ while (end > start) {
9606
+ const charCode = value.charCodeAt(end - 1);
9607
+ if (charCode !== 32 && charCode !== 9) {
9608
+ break;
9609
+ }
9610
+ end--;
9611
+ }
9612
+ return start === 0 && end === value.length ? value : value.slice(start, end);
9613
+ };
9614
+ var parse = (cookie, name) => {
9615
+ if (name && cookie.indexOf(name) === -1) {
9616
+ return {};
9617
+ }
9618
+ const pairs = cookie.split(";");
9619
+ const parsedCookie = /* @__PURE__ */ Object.create(null);
9620
+ for (const pairStr of pairs) {
9621
+ const valueStartPos = pairStr.indexOf("=");
9622
+ if (valueStartPos === -1) {
9623
+ continue;
9624
+ }
9625
+ const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
9626
+ if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) {
9627
+ continue;
9628
+ }
9629
+ let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
9630
+ if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
9631
+ cookieValue = cookieValue.slice(1, -1);
9632
+ }
9633
+ if (validCookieValueRegEx.test(cookieValue)) {
9634
+ parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? tryDecode(cookieValue, decodeURIComponent_) : cookieValue;
9635
+ if (name) {
9636
+ break;
9637
+ }
9638
+ }
9639
+ }
9640
+ return parsedCookie;
9641
+ };
9642
+ var _serialize = (name, value, opt = {}) => {
9643
+ if (!validCookieNameRegEx.test(name)) {
9644
+ throw new Error("Invalid cookie name");
9645
+ }
9646
+ let cookie = `${name}=${value}`;
9647
+ if (name.startsWith("__Secure-") && !opt.secure) {
9648
+ throw new Error("__Secure- Cookie must have Secure attributes");
9649
+ }
9650
+ if (name.startsWith("__Host-")) {
9651
+ if (!opt.secure) {
9652
+ throw new Error("__Host- Cookie must have Secure attributes");
9653
+ }
9654
+ if (opt.path !== "/") {
9655
+ throw new Error('__Host- Cookie must have Path attributes with "/"');
9656
+ }
9657
+ if (opt.domain) {
9658
+ throw new Error("__Host- Cookie must not have Domain attributes");
9659
+ }
9660
+ }
9661
+ for (const key of ["domain", "path", "sameSite", "priority"]) {
9662
+ if (opt[key] && /[;\r\n]/.test(opt[key])) {
9663
+ throw new Error(`${key} must not contain ";", "\\r", or "\\n"`);
9664
+ }
9665
+ }
9666
+ if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) {
9667
+ if (opt.maxAge > 3456e4) {
9668
+ throw new Error(
9669
+ "Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."
9670
+ );
9671
+ }
9672
+ cookie += `; Max-Age=${opt.maxAge | 0}`;
9673
+ }
9674
+ if (opt.domain && opt.prefix !== "host") {
9675
+ cookie += `; Domain=${opt.domain}`;
9676
+ }
9677
+ if (opt.path) {
9678
+ cookie += `; Path=${opt.path}`;
9679
+ }
9680
+ if (opt.expires) {
9681
+ if (opt.expires.getTime() - Date.now() > 3456e7) {
9682
+ throw new Error(
9683
+ "Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."
9684
+ );
9685
+ }
9686
+ cookie += `; Expires=${opt.expires.toUTCString()}`;
9687
+ }
9688
+ if (opt.httpOnly) {
9689
+ cookie += "; HttpOnly";
9690
+ }
9691
+ if (opt.secure) {
9692
+ cookie += "; Secure";
9693
+ }
9694
+ if (opt.sameSite) {
9695
+ cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`;
9696
+ }
9697
+ if (opt.priority) {
9698
+ cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`;
9699
+ }
9700
+ if (opt.partitioned) {
9701
+ if (!opt.secure) {
9702
+ throw new Error("Partitioned Cookie must have Secure attributes");
9703
+ }
9704
+ cookie += "; Partitioned";
9705
+ }
9706
+ return cookie;
9707
+ };
9708
+ var serialize = (name, value, opt) => {
9709
+ value = encodeURIComponent(value);
9710
+ return _serialize(name, value, opt);
9711
+ };
9712
+
9713
+ // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/helper/cookie/index.js
9714
+ var getCookie = (c, key, prefix) => {
9715
+ const cookie = c.req.raw.headers.get("Cookie");
9716
+ if (typeof key === "string") {
9717
+ if (!cookie) {
9718
+ return void 0;
9719
+ }
9720
+ let finalKey = key;
9721
+ if (prefix === "secure") {
9722
+ finalKey = "__Secure-" + key;
9723
+ } else if (prefix === "host") {
9724
+ finalKey = "__Host-" + key;
9725
+ }
9726
+ const obj2 = parse(cookie, finalKey);
9727
+ return obj2[finalKey];
9728
+ }
9729
+ if (!cookie) {
9730
+ return {};
9731
+ }
9732
+ const obj = parse(cookie);
9733
+ return obj;
9734
+ };
9735
+ var generateCookie = (name, value, opt) => {
9736
+ let cookie;
9737
+ if (opt?.prefix === "secure") {
9738
+ cookie = serialize("__Secure-" + name, value, { path: "/", ...opt, secure: true });
9739
+ } else if (opt?.prefix === "host") {
9740
+ cookie = serialize("__Host-" + name, value, {
9741
+ ...opt,
9742
+ path: "/",
9743
+ secure: true,
9744
+ domain: void 0
9745
+ });
9746
+ } else {
9747
+ cookie = serialize(name, value, { path: "/", ...opt });
9748
+ }
9749
+ return cookie;
9750
+ };
9751
+ var setCookie = (c, name, value, opt) => {
9752
+ const cookie = generateCookie(name, value, opt);
9753
+ c.header("Set-Cookie", cookie, { append: true });
9754
+ };
9755
+ var deleteCookie = (c, name, opt) => {
9756
+ const deletedCookie = getCookie(c, name, opt?.prefix);
9757
+ setCookie(c, name, "", { ...opt, maxAge: 0 });
9758
+ return deletedCookie;
9759
+ };
9760
+
9761
+ // src/server/routes/oauth/index.ts
9511
9762
  init_types();
9512
9763
  import { Transactional as Transactional3 } from "@spfn/core/db";
9513
9764
  import { ValidationError as ValidationError7 } from "@spfn/core/errors";
9514
- import { rateLimit as rateLimit2 } from "@spfn/core/middleware";
9765
+ import { rateLimitPolicy as rateLimitPolicy4 } from "@spfn/core/middleware";
9515
9766
  import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
9516
9767
  var providerParams = Type.Object({
9517
9768
  provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
@@ -9524,7 +9775,7 @@ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
9524
9775
  description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
9525
9776
  })
9526
9777
  })
9527
- }).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9778
+ }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9528
9779
  const { query } = await c.data();
9529
9780
  if (!isGoogleOAuthEnabled()) {
9530
9781
  return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
@@ -9547,7 +9798,7 @@ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
9547
9798
  description: "Error description from Google"
9548
9799
  }))
9549
9800
  })
9550
- }).use([Transactional3()]).skip(["auth"]).handler(async (c) => {
9801
+ }).use([rateLimitPolicy4("oauth-callback", { limit: 30, windowMs: 6e4 }), Transactional3()]).skip(["auth"]).handler(async (c) => {
9551
9802
  const { query } = await c.data();
9552
9803
  if (query.error) {
9553
9804
  const errorMessage = query.error_description || query.error;
@@ -9556,11 +9807,14 @@ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
9556
9807
  if (!query.code || !query.state) {
9557
9808
  return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
9558
9809
  }
9810
+ const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
9811
+ deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
9559
9812
  try {
9560
9813
  const result = await oauthCallbackService({
9561
9814
  provider: "google",
9562
9815
  code: query.code,
9563
- state: query.state
9816
+ state: query.state,
9817
+ expectedNonce
9564
9818
  });
9565
9819
  return c.redirect(result.redirectUrl);
9566
9820
  } catch (err) {
@@ -9592,9 +9846,17 @@ var oauthStart = route4.post("/_auth/oauth/start").input({
9592
9846
  description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
9593
9847
  }))
9594
9848
  })
9595
- }).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9849
+ }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9596
9850
  const { body } = await c.data();
9597
- const result = await oauthStartService(body);
9851
+ const nonce = generateOAuthNonce();
9852
+ setCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, nonce, {
9853
+ httpOnly: true,
9854
+ secure: process.env.NODE_ENV === "production",
9855
+ sameSite: "Lax",
9856
+ maxAge: 600,
9857
+ path: "/"
9858
+ });
9859
+ const result = await oauthStartService({ ...body, nonce });
9598
9860
  return result;
9599
9861
  });
9600
9862
  var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
@@ -9611,7 +9873,7 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
9611
9873
  description: "Encrypted OAuth state (injected by interceptor)"
9612
9874
  }))
9613
9875
  })
9614
- }).skip(["auth"]).handler(async (c) => {
9876
+ }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9615
9877
  const { body } = await c.data();
9616
9878
  if (!isGoogleOAuthEnabled()) {
9617
9879
  throw new ValidationError7({ message: "Google OAuth is not configured" });
@@ -9629,7 +9891,7 @@ var oauthFinalize = route4.post("/_auth/oauth/finalize").input({
9629
9891
  keyId: Type.String({ description: "Key ID from OAuth state" }),
9630
9892
  returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
9631
9893
  })
9632
- }).skip(["auth"]).handler(async (c) => {
9894
+ }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9633
9895
  const { body } = await c.data();
9634
9896
  return {
9635
9897
  success: true,
@@ -9645,7 +9907,7 @@ var oauthProviderStart = route4.get("/_auth/oauth/:provider").input({
9645
9907
  description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
9646
9908
  })
9647
9909
  })
9648
- }).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9910
+ }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9649
9911
  const { params, query } = await c.data();
9650
9912
  const provider = getOAuthProvider(params.provider);
9651
9913
  if (!provider?.isEnabled()) {
@@ -9669,7 +9931,7 @@ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input(
9669
9931
  description: "Error description from provider"
9670
9932
  }))
9671
9933
  })
9672
- }).use([Transactional3()]).skip(["auth"]).handler(async (c) => {
9934
+ }).use([rateLimitPolicy4("oauth-callback", { limit: 30, windowMs: 6e4 }), Transactional3()]).skip(["auth"]).handler(async (c) => {
9673
9935
  const { params, query } = await c.data();
9674
9936
  if (query.error) {
9675
9937
  const errorMessage = query.error_description || query.error;
@@ -9678,11 +9940,14 @@ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input(
9678
9940
  if (!query.code || !query.state) {
9679
9941
  return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
9680
9942
  }
9943
+ const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
9944
+ deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
9681
9945
  try {
9682
9946
  const result = await oauthCallbackService({
9683
9947
  provider: params.provider,
9684
9948
  code: query.code,
9685
- state: query.state
9949
+ state: query.state,
9950
+ expectedNonce
9686
9951
  });
9687
9952
  return c.redirect(result.redirectUrl);
9688
9953
  } catch (err) {
@@ -9700,7 +9965,7 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
9700
9965
  description: "Encrypted OAuth state (injected by interceptor)"
9701
9966
  }))
9702
9967
  })
9703
- }).skip(["auth"]).handler(async (c) => {
9968
+ }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9704
9969
  const { params, body } = await c.data();
9705
9970
  const provider = requireEnabledProvider(params.provider);
9706
9971
  if (!body.state) {
@@ -9730,7 +9995,7 @@ var oauthNative = route4.post("/_auth/oauth/:provider/native").input({
9730
9995
  description: "Custom metadata passed to auth events (e.g. referral code, UTM params)"
9731
9996
  }))
9732
9997
  })
9733
- }).skip(["auth"]).handler(async (c) => {
9998
+ }).use([rateLimitPolicy4("oauth-native", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9734
9999
  const { params, body } = await c.data();
9735
10000
  return await oauthNativeService({ provider: params.provider, ...body });
9736
10001
  });
@@ -10264,6 +10529,7 @@ export {
10264
10529
  generateKeyPair,
10265
10530
  generateKeyPairES256,
10266
10531
  generateKeyPairRS256,
10532
+ generateOAuthNonce,
10267
10533
  generateToken,
10268
10534
  getAllRoles,
10269
10535
  getAuth,