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.
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.mjs +3 -4
- package/dist/api/middlewares/origin-check.mjs +5 -1
- package/dist/api/rate-limiter/index.mjs +259 -73
- package/dist/api/routes/account.mjs +22 -7
- package/dist/api/routes/callback.mjs +2 -2
- package/dist/api/routes/index.d.mts +1 -1
- package/dist/api/routes/password.mjs +3 -4
- package/dist/api/routes/session.d.mts +12 -1
- package/dist/api/routes/session.mjs +13 -1
- package/dist/api/routes/sign-in.mjs +5 -5
- package/dist/api/routes/sign-up.mjs +2 -2
- package/dist/api/routes/update-session.mjs +2 -3
- package/dist/api/routes/update-user.mjs +10 -12
- package/dist/auth/base.mjs +11 -7
- package/dist/client/equality.d.mts +19 -0
- package/dist/client/equality.mjs +42 -0
- package/dist/client/index.d.mts +5 -4
- package/dist/client/index.mjs +2 -1
- package/dist/client/lynx/index.d.mts +4 -2
- package/dist/client/path-to-object.d.mts +5 -2
- package/dist/client/plugins/index.d.mts +4 -1
- package/dist/client/plugins/index.mjs +4 -1
- package/dist/client/query.d.mts +4 -3
- package/dist/client/query.mjs +27 -17
- package/dist/client/react/index.d.mts +4 -2
- package/dist/client/session-atom.mjs +129 -4
- package/dist/client/session-refresh.d.mts +3 -18
- package/dist/client/session-refresh.mjs +38 -49
- package/dist/client/solid/index.d.mts +4 -2
- package/dist/client/svelte/index.d.mts +4 -2
- package/dist/client/types.d.mts +27 -16
- package/dist/client/vanilla.d.mts +4 -2
- package/dist/client/vue/index.d.mts +4 -2
- package/dist/context/create-context.mjs +2 -1
- package/dist/context/store-capabilities.mjs +12 -0
- package/dist/cookies/index.mjs +25 -2
- package/dist/db/internal-adapter.mjs +51 -0
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +49 -19
- package/dist/plugins/admin/routes.mjs +10 -3
- package/dist/plugins/captcha/constants.mjs +8 -1
- package/dist/plugins/captcha/index.mjs +8 -2
- package/dist/plugins/captcha/types.d.mts +21 -0
- package/dist/plugins/captcha/verify-handlers/captchafox.mjs +2 -0
- package/dist/plugins/captcha/verify-handlers/cloudflare-turnstile.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/google-recaptcha.mjs +7 -2
- package/dist/plugins/captcha/verify-handlers/h-captcha.mjs +2 -0
- package/dist/plugins/device-authorization/routes.mjs +16 -9
- package/dist/plugins/email-otp/routes.mjs +22 -52
- package/dist/plugins/generic-oauth/index.mjs +7 -2
- package/dist/plugins/generic-oauth/routes.mjs +16 -12
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +5 -1
- package/dist/plugins/index.d.mts +6 -2
- package/dist/plugins/index.mjs +4 -1
- package/dist/plugins/jwt/index.mjs +2 -2
- package/dist/plugins/mcp/client/index.mjs +1 -0
- package/dist/plugins/mcp/index.mjs +8 -0
- package/dist/plugins/multi-session/index.mjs +7 -5
- package/dist/plugins/oauth-popup/client.d.mts +82 -0
- package/dist/plugins/oauth-popup/client.mjs +203 -0
- package/dist/plugins/oauth-popup/constants.d.mts +11 -0
- package/dist/plugins/oauth-popup/constants.mjs +11 -0
- package/dist/plugins/oauth-popup/error-codes.d.mts +11 -0
- package/dist/plugins/oauth-popup/error-codes.mjs +10 -0
- package/dist/plugins/oauth-popup/index.d.mts +67 -0
- package/dist/plugins/oauth-popup/index.mjs +227 -0
- package/dist/plugins/oauth-popup/types.d.mts +30 -0
- package/dist/plugins/oauth-proxy/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/utils.mjs +16 -2
- package/dist/plugins/oidc-provider/index.mjs +10 -0
- package/dist/plugins/one-tap/client.mjs +12 -6
- package/dist/plugins/one-tap/index.d.mts +1 -0
- package/dist/plugins/one-tap/index.mjs +9 -5
- package/dist/plugins/one-time-token/index.mjs +1 -3
- package/dist/plugins/open-api/generator.d.mts +66 -57
- package/dist/plugins/open-api/generator.mjs +185 -67
- package/dist/plugins/open-api/index.d.mts +2 -2
- package/dist/plugins/organization/adapter.d.mts +29 -1
- package/dist/plugins/organization/adapter.mjs +66 -6
- package/dist/plugins/organization/routes/crud-invites.mjs +49 -34
- package/dist/plugins/organization/routes/crud-members.mjs +42 -6
- package/dist/plugins/organization/routes/crud-team.mjs +36 -3
- package/dist/plugins/phone-number/routes.mjs +41 -36
- package/dist/plugins/siwe/index.mjs +2 -3
- package/dist/plugins/two-factor/backup-codes/index.mjs +1 -1
- package/dist/plugins/two-factor/otp/index.mjs +11 -13
- package/dist/plugins/two-factor/totp/index.mjs +1 -1
- package/dist/plugins/two-factor/verify-two-factor.mjs +6 -2
- package/dist/plugins/username/index.mjs +6 -6
- package/dist/test-utils/test-instance.d.mts +26 -23
- 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.
|
|
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.
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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;
|
|
@@ -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({
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
14
|
-
if (zodType instanceof z.ZodDefault) return
|
|
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
|
|
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)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
70
|
-
if (zodType instanceof z.ZodOptional)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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] =
|
|
98
|
-
if (!(value
|
|
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
|
-
|
|
106
|
-
};
|
|
177
|
+
...required.length > 0 ? { required } : {}
|
|
178
|
+
}, zodType);
|
|
107
179
|
}
|
|
108
180
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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: {
|
|
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.
|
|
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 };
|