better-auth 1.6.16 → 1.6.18

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.
Files changed (93) hide show
  1. package/dist/api/index.d.mts +2 -2
  2. package/dist/api/index.mjs +3 -4
  3. package/dist/api/middlewares/origin-check.mjs +5 -1
  4. package/dist/api/rate-limiter/index.mjs +259 -73
  5. package/dist/api/routes/account.mjs +22 -7
  6. package/dist/api/routes/callback.mjs +2 -2
  7. package/dist/api/routes/index.d.mts +1 -1
  8. package/dist/api/routes/password.mjs +3 -4
  9. package/dist/api/routes/session.d.mts +12 -1
  10. package/dist/api/routes/session.mjs +13 -1
  11. package/dist/api/routes/sign-in.mjs +5 -5
  12. package/dist/api/routes/sign-up.mjs +2 -2
  13. package/dist/api/routes/update-session.mjs +2 -3
  14. package/dist/api/routes/update-user.mjs +10 -12
  15. package/dist/auth/base.mjs +11 -7
  16. package/dist/client/equality.d.mts +19 -0
  17. package/dist/client/equality.mjs +42 -0
  18. package/dist/client/index.d.mts +5 -4
  19. package/dist/client/index.mjs +2 -1
  20. package/dist/client/lynx/index.d.mts +4 -2
  21. package/dist/client/path-to-object.d.mts +5 -2
  22. package/dist/client/plugins/index.d.mts +4 -1
  23. package/dist/client/plugins/index.mjs +4 -1
  24. package/dist/client/query.d.mts +4 -3
  25. package/dist/client/query.mjs +27 -17
  26. package/dist/client/react/index.d.mts +4 -2
  27. package/dist/client/session-atom.mjs +129 -4
  28. package/dist/client/session-refresh.d.mts +3 -18
  29. package/dist/client/session-refresh.mjs +38 -49
  30. package/dist/client/solid/index.d.mts +4 -2
  31. package/dist/client/svelte/index.d.mts +4 -2
  32. package/dist/client/types.d.mts +27 -16
  33. package/dist/client/vanilla.d.mts +4 -2
  34. package/dist/client/vue/index.d.mts +4 -2
  35. package/dist/context/create-context.mjs +2 -1
  36. package/dist/context/store-capabilities.mjs +12 -0
  37. package/dist/cookies/index.mjs +25 -2
  38. package/dist/db/internal-adapter.mjs +51 -0
  39. package/dist/package.mjs +1 -1
  40. package/dist/plugins/access/access.mjs +49 -19
  41. package/dist/plugins/admin/routes.mjs +10 -3
  42. package/dist/plugins/captcha/constants.mjs +8 -1
  43. package/dist/plugins/captcha/index.mjs +8 -2
  44. package/dist/plugins/captcha/types.d.mts +21 -0
  45. package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
  46. package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
  47. package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
  48. package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
  49. package/dist/plugins/device-authorization/routes.mjs +16 -9
  50. package/dist/plugins/email-otp/routes.mjs +22 -52
  51. package/dist/plugins/generic-oauth/index.mjs +7 -2
  52. package/dist/plugins/generic-oauth/routes.mjs +16 -12
  53. package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
  54. package/dist/plugins/haveibeenpwned/index.mjs +5 -1
  55. package/dist/plugins/index.d.mts +6 -2
  56. package/dist/plugins/index.mjs +4 -1
  57. package/dist/plugins/jwt/index.mjs +2 -2
  58. package/dist/plugins/mcp/client/index.mjs +1 -0
  59. package/dist/plugins/mcp/index.mjs +8 -0
  60. package/dist/plugins/multi-session/index.mjs +7 -5
  61. package/dist/plugins/oauth-popup/client.d.mts +82 -0
  62. package/dist/plugins/oauth-popup/client.mjs +203 -0
  63. package/dist/plugins/oauth-popup/constants.d.mts +11 -0
  64. package/dist/plugins/oauth-popup/constants.mjs +11 -0
  65. package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
  66. package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
  67. package/dist/plugins/oauth-popup/index.d.mts +67 -0
  68. package/dist/plugins/oauth-popup/index.mjs +227 -0
  69. package/dist/plugins/oauth-popup/types.d.mts +30 -0
  70. package/dist/plugins/oauth-proxy/index.mjs +2 -2
  71. package/dist/plugins/oauth-proxy/utils.mjs +16 -2
  72. package/dist/plugins/oidc-provider/index.mjs +10 -0
  73. package/dist/plugins/one-tap/client.mjs +12 -6
  74. package/dist/plugins/one-tap/index.d.mts +1 -0
  75. package/dist/plugins/one-tap/index.mjs +9 -5
  76. package/dist/plugins/one-time-token/index.mjs +1 -3
  77. package/dist/plugins/open-api/generator.d.mts +66 -57
  78. package/dist/plugins/open-api/generator.mjs +185 -67
  79. package/dist/plugins/open-api/index.d.mts +2 -2
  80. package/dist/plugins/organization/adapter.d.mts +29 -1
  81. package/dist/plugins/organization/adapter.mjs +66 -6
  82. package/dist/plugins/organization/routes/crud-invites.mjs +49 -34
  83. package/dist/plugins/organization/routes/crud-members.mjs +42 -6
  84. package/dist/plugins/organization/routes/crud-team.mjs +36 -3
  85. package/dist/plugins/phone-number/routes.mjs +41 -36
  86. package/dist/plugins/siwe/index.mjs +2 -3
  87. package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
  88. package/dist/plugins/two-factor/otp/index.mjs +11 -13
  89. package/dist/plugins/two-factor/totp/index.mjs +1 -1
  90. package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
  91. package/dist/plugins/username/index.mjs +6 -6
  92. package/dist/test-utils/test-instance.d.mts +26 -23
  93. package/package.json +9 -9
@@ -0,0 +1,30 @@
1
+ import { OAUTH_POPUP_MESSAGE_TYPE } from "./constants.mjs";
2
+
3
+ //#region src/plugins/oauth-popup/types.d.ts
4
+ /** OAuth error relayed to the opener when the flow fails. */
5
+ interface OAuthPopupError {
6
+ code: string;
7
+ description?: string;
8
+ }
9
+ /**
10
+ * Message the completion page posts to its opener — success carries the token,
11
+ * failure carries the error.
12
+ */
13
+ interface OAuthPopupMessage {
14
+ type: typeof OAUTH_POPUP_MESSAGE_TYPE;
15
+ /** Echoes the request nonce so the opener can correlate the handoff. */
16
+ nonce: string;
17
+ /** The session token, sent as `Authorization: Bearer <token>` (on success). */
18
+ token?: string;
19
+ /** Where the flow would have redirected (callbackURL / newUserURL). */
20
+ redirectTo?: string;
21
+ /** The OAuth error (on failure). */
22
+ error?: OAuthPopupError;
23
+ }
24
+ /** Payload embedded in the completion page's data block. */
25
+ interface OAuthPopupData extends OAuthPopupMessage {
26
+ /** Exact origin the message is posted to (the trusted popup opener). */
27
+ targetOrigin: string;
28
+ }
29
+ //#endregion
30
+ export { OAuthPopupData, OAuthPopupMessage };
@@ -183,13 +183,13 @@ const oAuthProxy = (opts) => {
183
183
  }
184
184
  if (error) throw redirectOnError(ctx, errorURL, error);
185
185
  if (!code) {
186
- ctx.context.logger.error("OAuth callback missing authorization code");
186
+ ctx.context.logger.warn("OAuth callback missing authorization code");
187
187
  throw redirectOnError(ctx, errorURL, "no_code");
188
188
  }
189
189
  const providerId = ctx.params?.id;
190
190
  const provider = ctx.context.socialProviders.find((p) => p.id === providerId);
191
191
  if (!provider) {
192
- ctx.context.logger.error("OAuth provider not found", providerId);
192
+ ctx.context.logger.warn("OAuth provider not found", { providerId });
193
193
  throw redirectOnError(ctx, errorURL, "oauth_provider_not_found");
194
194
  }
195
195
  let tokens;
@@ -21,10 +21,24 @@ function getVendorBaseURL() {
21
21
  return vercel || netlify || render || aws || google || azure;
22
22
  }
23
23
  /**
24
- * Resolve the current URL from various sources
24
+ * Resolve the current URL from various sources.
25
+ *
26
+ * The request URL host can come from an untrusted source (`Host` / forwarded host),
27
+ * and this origin becomes the receiver for the encrypted OAuth profile replay.
28
+ * So a request-derived origin is only honored when it is an explicitly trusted
29
+ * origin; otherwise resolution falls back to the configured platform/base URL,
30
+ * never the raw request host. An explicit `opts.currentURL` and the vendor/base
31
+ * URLs are configured by the developer and trusted as-is.
25
32
  */
26
33
  function resolveCurrentURL(ctx, opts) {
27
- return new URL(opts?.currentURL || ctx.request?.url || getVendorBaseURL() || ctx.context.baseURL);
34
+ if (opts?.currentURL) return new URL(opts.currentURL);
35
+ const requestURL = ctx.request?.url;
36
+ if (requestURL) {
37
+ const origin = getOrigin(requestURL);
38
+ if (origin && ctx.context.isTrustedOrigin(origin)) return new URL(requestURL);
39
+ }
40
+ const vendorBaseURL = getVendorBaseURL();
41
+ return new URL(vendorBaseURL && getOrigin(vendorBaseURL) ? vendorBaseURL : ctx.context.baseURL);
28
42
  }
29
43
  /**
30
44
  * Check if the proxy should be skipped for this request
@@ -1082,6 +1082,16 @@ const oidcProvider = (options) => {
1082
1082
  });
1083
1083
  }
1084
1084
  const session = await getSessionFromCtx(ctx);
1085
+ if (ctx.request && (validatedUserId || session)) {
1086
+ const fetchSite = ctx.request.headers.get("Sec-Fetch-Site");
1087
+ const originHeader = ctx.request.headers.get("origin") || ctx.request.headers.get("referer");
1088
+ const isSameSiteRequest = fetchSite === "same-origin" || fetchSite === "same-site" || fetchSite === "none" || !!originHeader && ctx.context.isTrustedOrigin(originHeader, { allowRelativePaths: false });
1089
+ const hintMatchesSession = !!validatedUserId && validatedUserId === session?.user.id;
1090
+ if (!isSameSiteRequest && !hintMatchesSession) throw new APIError("FORBIDDEN", {
1091
+ error: "invalid_request",
1092
+ error_description: "Logout must be same-site or carry an id_token_hint for the current session"
1093
+ });
1094
+ }
1085
1095
  if (validatedUserId || session) {
1086
1096
  const userId = validatedUserId || session?.user.id;
1087
1097
  if (userId) await ctx.context.adapter.deleteMany({
@@ -44,12 +44,15 @@ const oneTapClient = (options) => {
44
44
  return;
45
45
  }
46
46
  async function callback(idToken) {
47
- await $fetch("/one-tap/callback", {
47
+ if ((await $fetch("/one-tap/callback", {
48
48
  method: "POST",
49
- body: { idToken },
49
+ body: {
50
+ idToken,
51
+ callbackURL: opts?.callbackURL
52
+ },
50
53
  ...opts?.fetchOptions,
51
54
  ...fetchOptions
52
- });
55
+ }))?.error) return;
53
56
  if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
54
57
  const target = opts?.callbackURL ?? "/";
55
58
  if (isSafeUrlScheme(target)) window.location.href = target;
@@ -80,12 +83,15 @@ const oneTapClient = (options) => {
80
83
  return;
81
84
  }
82
85
  async function callback(idToken) {
83
- await $fetch("/one-tap/callback", {
86
+ if ((await $fetch("/one-tap/callback", {
84
87
  method: "POST",
85
- body: { idToken },
88
+ body: {
89
+ idToken,
90
+ callbackURL: opts?.callbackURL
91
+ },
86
92
  ...opts?.fetchOptions,
87
93
  ...fetchOptions
88
- });
94
+ }))?.error) return;
89
95
  if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
90
96
  const target = opts?.callbackURL ?? "/";
91
97
  if (isSafeUrlScheme(target)) window.location.href = target;
@@ -32,6 +32,7 @@ declare const oneTap: (options?: OneTapOptions | undefined) => {
32
32
  method: "POST";
33
33
  body: z.ZodObject<{
34
34
  idToken: z.ZodString;
35
+ callbackURL: z.ZodOptional<z.ZodString>;
35
36
  }, z.core.$strip>;
36
37
  metadata: {
37
38
  openapi: {
@@ -8,7 +8,10 @@ import { createAuthEndpoint } from "@better-auth/core/api";
8
8
  import * as z from "zod";
9
9
  import { createRemoteJWKSet, jwtVerify } from "jose";
10
10
  //#region src/plugins/one-tap/index.ts
11
- const oneTapCallbackBodySchema = z.object({ idToken: z.string().meta({ description: "Google ID token, which the client obtains from the One Tap API" }) });
11
+ const oneTapCallbackBodySchema = z.object({
12
+ idToken: z.string().meta({ description: "Google ID token, which the client obtains from the One Tap API" }),
13
+ callbackURL: z.string().meta({ description: "URL to redirect to after a successful sign-in" }).optional()
14
+ });
12
15
  const oneTap = (options) => ({
13
16
  id: "one-tap",
14
17
  version: PACKAGE_VERSION,
@@ -34,13 +37,14 @@ const oneTap = (options) => ({
34
37
  } }
35
38
  }, async (ctx) => {
36
39
  const { idToken } = ctx.body;
40
+ const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
41
+ const audience = options?.clientId || googleProvider?.clientId;
42
+ if (!audience || Array.isArray(audience) && audience.length === 0) throw new APIError("BAD_REQUEST", { message: "Google client ID is required for One Tap. Set it on the oneTap plugin (clientId) or on socialProviders.google." });
37
43
  let payload;
38
44
  try {
39
- const JWKS = createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs"));
40
- const googleProvider = typeof ctx.context.options.socialProviders?.google === "function" ? await ctx.context.options.socialProviders?.google() : ctx.context.options.socialProviders?.google;
41
- const { payload: verifiedPayload } = await jwtVerify(idToken, JWKS, {
45
+ const { payload: verifiedPayload } = await jwtVerify(idToken, createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs")), {
42
46
  issuer: ["https://accounts.google.com", "accounts.google.com"],
43
- audience: options?.clientId || googleProvider?.clientId
47
+ audience
44
48
  });
45
49
  payload = verifiedPayload;
46
50
  } catch {
@@ -47,10 +47,8 @@ const oneTimeToken = (options) => {
47
47
  }, async (c) => {
48
48
  const { token } = c.body;
49
49
  const storedToken = await storeToken(c, token);
50
- const verificationValue = await c.context.internalAdapter.findVerificationValue(`one-time-token:${storedToken}`);
50
+ const verificationValue = await c.context.internalAdapter.consumeVerificationValue(`one-time-token:${storedToken}`);
51
51
  if (!verificationValue) throw c.error("BAD_REQUEST", { message: "Invalid token" });
52
- await c.context.internalAdapter.deleteVerificationByIdentifier(`one-time-token:${storedToken}`);
53
- if (verificationValue.expiresAt < /* @__PURE__ */ new Date()) throw c.error("BAD_REQUEST", { message: "Token expired" });
54
52
  const session = await c.context.internalAdapter.findSession(verificationValue.value);
55
53
  if (!session) throw c.error("BAD_REQUEST", { message: "Session not found" });
56
54
  if (!opts?.disableSetSessionCookie) await setSessionCookie(c, session);
@@ -4,63 +4,72 @@ import { OpenAPIParameter, OpenAPISchemaType } from "better-call";
4
4
 
5
5
  //#region src/plugins/open-api/generator.d.ts
6
6
  interface Path {
7
- get?: {
8
- tags?: string[];
9
- operationId?: string;
10
- description?: string;
11
- security?: [{
12
- bearerAuth: string[];
13
- }];
14
- parameters?: OpenAPIParameter[];
15
- responses?: { [key in string]: {
16
- description?: string;
17
- content: {
18
- "application/json": {
19
- schema: {
20
- type?: OpenAPISchemaType;
21
- properties?: Record<string, any>;
22
- required?: string[];
23
- $ref?: string;
24
- };
25
- };
26
- };
27
- } };
28
- } | undefined;
29
- post?: {
30
- tags?: string[];
31
- operationId?: string;
32
- description?: string;
33
- security?: [{
34
- bearerAuth: string[];
35
- }];
36
- parameters?: OpenAPIParameter[];
37
- requestBody?: {
38
- content: {
39
- "application/json": {
40
- schema: {
41
- type?: OpenAPISchemaType;
42
- properties?: Record<string, any>;
43
- required?: string[];
44
- $ref?: string;
45
- };
46
- };
47
- };
48
- };
49
- responses?: { [key in string]: {
50
- description?: string;
51
- content: {
52
- "application/json": {
53
- schema: {
54
- type?: OpenAPISchemaType;
55
- properties?: Record<string, any>;
56
- required?: string[];
57
- $ref?: string;
58
- };
59
- };
60
- };
61
- } };
62
- } | undefined;
7
+ get?: OpenAPIOperation | undefined;
8
+ post?: OpenAPIOperation | undefined;
9
+ put?: OpenAPIOperation | undefined;
10
+ patch?: OpenAPIOperation | undefined;
11
+ delete?: OpenAPIOperation | undefined;
63
12
  }
13
+ type OpenAPISchemaPrimitiveType = OpenAPISchemaType | "null";
14
+ type OpenAPISchema = {
15
+ type?: OpenAPISchemaPrimitiveType | OpenAPISchemaPrimitiveType[];
16
+ properties?: Record<string, OpenAPISchema>;
17
+ required?: string[];
18
+ $ref?: string;
19
+ description?: string;
20
+ default?: unknown;
21
+ readOnly?: boolean;
22
+ format?: string;
23
+ deprecated?: boolean;
24
+ enum?: unknown[];
25
+ items?: OpenAPISchema;
26
+ minLength?: number;
27
+ maxLength?: number;
28
+ minimum?: number;
29
+ maximum?: number;
30
+ additionalProperties?: boolean | OpenAPISchema;
31
+ propertyNames?: OpenAPISchema;
32
+ allOf?: OpenAPISchema[];
33
+ anyOf?: OpenAPISchema[];
34
+ oneOf?: OpenAPISchema[];
35
+ const?: unknown;
36
+ example?: unknown;
37
+ };
38
+ type OpenAPIParameter$1 = Omit<OpenAPIParameter, "schema"> & {
39
+ schema?: OpenAPISchema;
40
+ };
41
+ type OpenAPIMediaTypeObject = {
42
+ schema?: OpenAPISchema;
43
+ };
44
+ type OpenAPIResponseContent = {
45
+ "application/json"?: OpenAPIMediaTypeObject;
46
+ "text/plain"?: OpenAPIMediaTypeObject;
47
+ "text/html"?: OpenAPIMediaTypeObject;
48
+ [contentType: string]: OpenAPIMediaTypeObject | undefined;
49
+ };
50
+ type OpenAPIResponse = {
51
+ description?: string;
52
+ content?: OpenAPIResponseContent;
53
+ };
54
+ type OpenAPIRequestBody = {
55
+ required?: boolean;
56
+ content: {
57
+ "application/json": {
58
+ schema: OpenAPISchema;
59
+ };
60
+ };
61
+ };
62
+ type OpenAPIOperation = {
63
+ tags?: string[];
64
+ operationId?: string;
65
+ description?: string;
66
+ security?: [{
67
+ bearerAuth: string[];
68
+ }];
69
+ parameters?: OpenAPIParameter$1[];
70
+ requestBody?: OpenAPIRequestBody;
71
+ responses?: Record<string, OpenAPIResponse>;
72
+ };
64
73
  type FieldSchema = {
65
74
  type: DBFieldType;
66
75
  default?: (DBFieldAttributeConfig["defaultValue"] | "Generated at runtime") | undefined;
@@ -111,4 +120,4 @@ declare function generator(ctx: AuthContext, options: BetterAuthOptions): Promis
111
120
  paths: Record<string, Path>;
112
121
  }>;
113
122
  //#endregion
114
- export { FieldSchema, OpenAPIModelSchema, Path, generator };
123
+ export { FieldSchema, OpenAPIModelSchema, OpenAPIParameter$1 as OpenAPIParameter, OpenAPISchema, Path, generator };
@@ -3,17 +3,17 @@ import { getEndpoints } from "../../api/index.mjs";
3
3
  import * as z from "zod";
4
4
  import { toPascalCase } from "@better-auth/core/utils/string";
5
5
  //#region src/plugins/open-api/generator.ts
6
- const allowedType = new Set([
6
+ const OPEN_API_SCHEMA_TYPES = new Set([
7
7
  "string",
8
8
  "number",
9
9
  "boolean",
10
10
  "array",
11
11
  "object"
12
12
  ]);
13
- function getTypeFromZodType(zodType) {
14
- if (zodType instanceof z.ZodDefault) return getTypeFromZodType(zodType.unwrap());
13
+ function getOpenApiTypeFromZodType(zodType) {
14
+ if (zodType instanceof z.ZodDefault || zodType instanceof z.ZodPrefault) return getOpenApiTypeFromZodType(unwrapZodSchema(zodType));
15
15
  const type = zodType.type;
16
- return allowedType.has(type) ? type : "string";
16
+ return OPEN_API_SCHEMA_TYPES.has(type) ? type : "string";
17
17
  }
18
18
  function getFieldSchema(field) {
19
19
  const schema = {
@@ -24,6 +24,48 @@ function getFieldSchema(field) {
24
24
  if (field.input === false) schema.readOnly = true;
25
25
  return schema;
26
26
  }
27
+ function asZodSchema(schema) {
28
+ return schema;
29
+ }
30
+ function unwrapZodSchema(zodType) {
31
+ return asZodSchema(zodType.unwrap());
32
+ }
33
+ function getZodDef(zodType) {
34
+ return zodType._def;
35
+ }
36
+ function getZodDescription(zodType) {
37
+ return zodType.description;
38
+ }
39
+ function withDescription(schema, zodType) {
40
+ const description = getZodDescription(zodType);
41
+ return description ? {
42
+ ...schema,
43
+ description
44
+ } : schema;
45
+ }
46
+ function addNullType(schema) {
47
+ if (schema.type) {
48
+ const type = Array.isArray(schema.type) ? schema.type : [schema.type];
49
+ const nullableType = Array.from(new Set([...type, "null"]));
50
+ return {
51
+ ...schema,
52
+ type: nullableType
53
+ };
54
+ }
55
+ return { anyOf: [schema, { type: "null" }] };
56
+ }
57
+ function getZodStringSchemaConstraints(zodType) {
58
+ const minLength = zodType.minLength;
59
+ const maxLength = zodType.maxLength;
60
+ return {
61
+ ...typeof minLength === "number" ? { minLength } : {},
62
+ ...typeof maxLength === "number" ? { maxLength } : {}
63
+ };
64
+ }
65
+ function getZodPipeSchema(zodType) {
66
+ const def = getZodDef(zodType);
67
+ return def.in instanceof z.ZodTransform && def.out instanceof z.ZodType ? def.out : def.in;
68
+ }
27
69
  function getParameters(options) {
28
70
  const parameters = [];
29
71
  if (options.metadata?.openapi?.parameters) {
@@ -31,62 +73,93 @@ function getParameters(options) {
31
73
  return parameters;
32
74
  }
33
75
  if (options.query instanceof z.ZodObject) Object.entries(options.query.shape).forEach(([key, value]) => {
34
- if (value instanceof z.ZodType) parameters.push({
35
- name: key,
36
- in: "query",
37
- schema: {
38
- ...processZodType(value),
39
- ..."minLength" in value && value.minLength ? { minLength: value.minLength } : {}
40
- }
41
- });
76
+ if (value instanceof z.ZodType) {
77
+ const parameterSchema = toOpenApiSchema(value);
78
+ parameters.push({
79
+ name: key,
80
+ in: "query",
81
+ schema: parameterSchema
82
+ });
83
+ }
42
84
  });
43
85
  return parameters;
44
86
  }
87
+ function getRequestBodySchemaInfo(zodType) {
88
+ return {
89
+ required: !schemaAcceptsUndefined(zodType),
90
+ schema: zodType
91
+ };
92
+ }
93
+ function schemaAcceptsUndefined(zodType) {
94
+ if (zodType instanceof z.ZodOptional || zodType instanceof z.ZodDefault || zodType instanceof z.ZodPrefault || zodType instanceof z.ZodCatch || zodType instanceof z.ZodUndefined || zodType instanceof z.ZodVoid) return true;
95
+ if (zodType instanceof z.ZodNonOptional) return false;
96
+ if (zodType instanceof z.ZodNullable || zodType instanceof z.ZodReadonly) return schemaAcceptsUndefined(unwrapZodSchema(zodType));
97
+ if (zodType instanceof z.ZodPipe) return schemaAcceptsUndefined(getZodPipeSchema(zodType));
98
+ if (zodType instanceof z.ZodUnion) return getZodDef(zodType).options.some((option) => schemaAcceptsUndefined(option));
99
+ if (zodType instanceof z.ZodIntersection) {
100
+ const def = getZodDef(zodType);
101
+ return schemaAcceptsUndefined(def.left) && schemaAcceptsUndefined(def.right);
102
+ }
103
+ return false;
104
+ }
105
+ function isUndefinedOnlySchema(zodType) {
106
+ return zodType instanceof z.ZodUndefined || zodType instanceof z.ZodVoid;
107
+ }
108
+ function isMergeableObjectSchema(schema) {
109
+ const type = schema?.type;
110
+ return !!schema && (type === "object" || Array.isArray(type) && type.includes("object")) && schema.$ref === void 0 && schema.allOf === void 0 && schema.anyOf === void 0;
111
+ }
112
+ function schemaAllowsNull(schema) {
113
+ const type = schema?.type;
114
+ return Array.isArray(type) && type.includes("null");
115
+ }
116
+ function areSchemasEqual(left, right) {
117
+ return JSON.stringify(left) === JSON.stringify(right);
118
+ }
119
+ function areSchemaMembersCompatible(left, right) {
120
+ if (left === void 0 || right === void 0) return true;
121
+ if (typeof left === "boolean" || typeof right === "boolean") return left === right;
122
+ return areSchemasEqual(left, right);
123
+ }
124
+ function mergeObjectSchemas(left, right, description) {
125
+ const properties = { ...left.properties || {} };
126
+ for (const [key, value] of Object.entries(right.properties || {})) {
127
+ if (properties[key] !== void 0 && !areSchemasEqual(properties[key], value)) return;
128
+ properties[key] = value;
129
+ }
130
+ const required = Array.from(new Set([...left.required || [], ...right.required || []]));
131
+ const leftAdditionalProperties = left.additionalProperties;
132
+ const rightAdditionalProperties = right.additionalProperties;
133
+ if (!areSchemaMembersCompatible(leftAdditionalProperties, rightAdditionalProperties)) return;
134
+ const leftPropertyNames = left.propertyNames;
135
+ const rightPropertyNames = right.propertyNames;
136
+ if (!areSchemaMembersCompatible(leftPropertyNames, rightPropertyNames)) return;
137
+ const additionalProperties = leftAdditionalProperties ?? rightAdditionalProperties;
138
+ const propertyNames = leftPropertyNames ?? rightPropertyNames;
139
+ return {
140
+ type: schemaAllowsNull(left) && schemaAllowsNull(right) ? ["object", "null"] : "object",
141
+ ...Object.keys(properties).length > 0 ? { properties } : {},
142
+ ...required.length > 0 ? { required } : {},
143
+ ...additionalProperties !== void 0 ? { additionalProperties } : {},
144
+ ...propertyNames !== void 0 ? { propertyNames } : {},
145
+ ...description ?? left.description ?? right.description ? { description: description ?? left.description ?? right.description } : {}
146
+ };
147
+ }
45
148
  function getRequestBody(options) {
46
149
  if (options.metadata?.openapi?.requestBody) return options.metadata.openapi.requestBody;
47
150
  if (!options.body) return void 0;
48
- if (options.body instanceof z.ZodObject || options.body instanceof z.ZodOptional) {
49
- const shape = options.body.shape;
50
- if (!shape) return void 0;
51
- const properties = {};
52
- const required = [];
53
- Object.entries(shape).forEach(([key, value]) => {
54
- if (value instanceof z.ZodType) {
55
- properties[key] = processZodType(value);
56
- if (!(value instanceof z.ZodOptional)) required.push(key);
57
- }
58
- });
59
- return {
60
- required: options.body instanceof z.ZodOptional ? false : options.body ? true : false,
61
- content: { "application/json": { schema: {
62
- type: "object",
63
- properties,
64
- required
65
- } } }
66
- };
67
- }
151
+ const requestBodySchemaInfo = getRequestBodySchemaInfo(options.body);
152
+ const schema = toOpenApiSchema(requestBodySchemaInfo.schema);
153
+ return {
154
+ required: requestBodySchemaInfo.required,
155
+ content: { "application/json": { schema } }
156
+ };
68
157
  }
69
- function processZodType(zodType) {
70
- if (zodType instanceof z.ZodOptional) {
71
- const innerSchema = processZodType(zodType.unwrap());
72
- if (innerSchema.type) {
73
- const type = Array.isArray(innerSchema.type) ? innerSchema.type : [innerSchema.type];
74
- return {
75
- ...innerSchema,
76
- type: Array.from(new Set([...type, "null"]))
77
- };
78
- }
79
- return { anyOf: [innerSchema, { type: "null" }] };
80
- }
81
- if (zodType instanceof z.ZodDefault) {
82
- const innerSchema = processZodType(zodType.unwrap());
83
- const defaultValueDef = zodType._def.defaultValue;
84
- const defaultValue = typeof defaultValueDef === "function" ? defaultValueDef() : defaultValueDef;
85
- return {
86
- ...innerSchema,
87
- default: defaultValue
88
- };
89
- }
158
+ function toOpenApiSchema(zodType) {
159
+ if (zodType instanceof z.ZodOptional) return toOpenApiSchema(unwrapZodSchema(zodType));
160
+ if (zodType instanceof z.ZodNullable) return addNullType(toOpenApiSchema(unwrapZodSchema(zodType)));
161
+ if (zodType instanceof z.ZodDefault || zodType instanceof z.ZodPrefault || zodType instanceof z.ZodNonOptional) return toOpenApiSchema(unwrapZodSchema(zodType));
162
+ if (zodType instanceof z.ZodAny) return withDescription({}, zodType);
90
163
  if (zodType instanceof z.ZodObject) {
91
164
  const shape = zodType.shape;
92
165
  if (shape) {
@@ -94,22 +167,64 @@ function processZodType(zodType) {
94
167
  const required = [];
95
168
  Object.entries(shape).forEach(([key, value]) => {
96
169
  if (value instanceof z.ZodType) {
97
- properties[key] = processZodType(value);
98
- if (!(value instanceof z.ZodOptional)) required.push(key);
170
+ properties[key] = toOpenApiSchema(value);
171
+ if (!schemaAcceptsUndefined(value)) required.push(key);
99
172
  }
100
173
  });
101
- return {
174
+ return withDescription({
102
175
  type: "object",
103
176
  properties,
104
- ...required.length > 0 ? { required } : {},
105
- description: zodType.description
106
- };
177
+ ...required.length > 0 ? { required } : {}
178
+ }, zodType);
107
179
  }
108
180
  }
109
- return {
110
- type: getTypeFromZodType(zodType),
111
- description: zodType.description
112
- };
181
+ if (zodType instanceof z.ZodRecord) {
182
+ const def = getZodDef(zodType);
183
+ return withDescription({
184
+ type: "object",
185
+ propertyNames: toOpenApiSchema(def.keyType),
186
+ additionalProperties: toOpenApiSchema(def.valueType)
187
+ }, zodType);
188
+ }
189
+ if (zodType instanceof z.ZodIntersection) {
190
+ const def = getZodDef(zodType);
191
+ const leftSchema = toOpenApiSchema(def.left);
192
+ const rightSchema = toOpenApiSchema(def.right);
193
+ if (isMergeableObjectSchema(leftSchema) && isMergeableObjectSchema(rightSchema)) {
194
+ const mergedSchema = mergeObjectSchemas(leftSchema, rightSchema, getZodDescription(zodType));
195
+ if (mergedSchema) return mergedSchema;
196
+ }
197
+ return withDescription({ allOf: [leftSchema, rightSchema] }, zodType);
198
+ }
199
+ if (zodType instanceof z.ZodUnion) {
200
+ const def = getZodDef(zodType);
201
+ const schemas = def.options.filter((option) => !isUndefinedOnlySchema(option)).map((option) => toOpenApiSchema(option));
202
+ if (schemas.length === 0) return withDescription({}, zodType);
203
+ if (schemas.length === 1) {
204
+ const schema = schemas[0];
205
+ if (!schema) return withDescription({}, zodType);
206
+ return withDescription(schema, zodType);
207
+ }
208
+ return withDescription(def.inclusive === false ? { oneOf: schemas } : { anyOf: schemas }, zodType);
209
+ }
210
+ if (zodType instanceof z.ZodArray) return withDescription({
211
+ type: "array",
212
+ items: toOpenApiSchema(getZodDef(zodType).element)
213
+ }, zodType);
214
+ if (zodType instanceof z.ZodLiteral) return withDescription({ enum: Array.from(zodType.values) }, zodType);
215
+ if (zodType instanceof z.ZodEnum) return withDescription({
216
+ type: "string",
217
+ enum: zodType.options
218
+ }, zodType);
219
+ if (zodType instanceof z.ZodPipe) return withDescription(toOpenApiSchema(getZodPipeSchema(zodType)), zodType);
220
+ if (zodType instanceof z.ZodCatch || zodType instanceof z.ZodReadonly) return withDescription(toOpenApiSchema(getZodDef(zodType).innerType), zodType);
221
+ if (zodType instanceof z.ZodNull) return withDescription({ type: "null" }, zodType);
222
+ if (zodType instanceof z.ZodUndefined) return withDescription({}, zodType);
223
+ if (zodType instanceof z.ZodVoid) return withDescription({}, zodType);
224
+ return withDescription({
225
+ type: getOpenApiTypeFromZodType(zodType),
226
+ ...zodType instanceof z.ZodString ? getZodStringSchemaConstraints(zodType) : {}
227
+ }, zodType);
113
228
  }
114
229
  function getResponse(responses) {
115
230
  return {
@@ -178,12 +293,15 @@ async function generator(ctx, options) {
178
293
  const components = { schemas: { ...Object.entries(tables).reduce((acc, [key, value]) => {
179
294
  const modelName = key.charAt(0).toUpperCase() + key.slice(1);
180
295
  const fields = value.fields;
181
- const required = [];
182
- const properties = { id: { type: "string" } };
296
+ const required = new Set(["id"]);
297
+ const properties = { id: {
298
+ type: "string",
299
+ readOnly: true
300
+ } };
183
301
  Object.entries(fields).forEach(([fieldKey, fieldValue]) => {
184
302
  if (!fieldValue) return;
185
303
  properties[fieldKey] = getFieldSchema(fieldValue);
186
- if (fieldValue.required && fieldValue.input !== false) required.push(fieldKey);
304
+ if (fieldValue.required && fieldValue.returned !== false) required.add(fieldKey);
187
305
  });
188
306
  Object.entries(properties).forEach(([key, prop]) => {
189
307
  const field = value.fields[key];
@@ -192,7 +310,7 @@ async function generator(ctx, options) {
192
310
  acc[modelName] = {
193
311
  type: "object",
194
312
  properties,
195
- required
313
+ required: Array.from(required)
196
314
  };
197
315
  return acc;
198
316
  }, {}) } };
@@ -1,4 +1,4 @@
1
- import { FieldSchema, OpenAPIModelSchema, Path, generator } from "./generator.mjs";
1
+ import { FieldSchema, OpenAPIModelSchema, OpenAPIParameter, OpenAPISchema, Path, generator } from "./generator.mjs";
2
2
  import { LiteralString } from "@better-auth/core";
3
3
  import * as better_call0 from "better-call";
4
4
 
@@ -94,4 +94,4 @@ declare const openAPI: <O extends OpenAPIOptions>(options?: O | undefined) => {
94
94
  options: NoInfer<O>;
95
95
  };
96
96
  //#endregion
97
- export { type FieldSchema, type OpenAPIModelSchema, OpenAPIOptions, type Path, generator, openAPI };
97
+ export { type FieldSchema, type OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, type Path, generator, openAPI };