@better-auth/core 1.7.0-beta.3 → 1.7.0-beta.5
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 +3 -3
- package/dist/context/global.mjs +1 -1
- package/dist/db/adapter/factory.mjs +62 -0
- package/dist/db/adapter/index.d.mts +35 -1
- package/dist/db/adapter/types.d.mts +1 -1
- package/dist/db/get-tables.mjs +3 -3
- package/dist/db/schema/account.d.mts +1 -1
- package/dist/db/schema/account.mjs +1 -1
- package/dist/db/type.d.mts +12 -0
- package/dist/env/env-impl.mjs +1 -1
- package/dist/error/codes.d.mts +6 -0
- package/dist/error/codes.mjs +6 -0
- package/dist/index.d.mts +2 -2
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/authorization-params.d.mts +12 -0
- package/dist/oauth2/authorization-params.mjs +12 -0
- package/dist/oauth2/basic-credentials.d.mts +30 -0
- package/dist/oauth2/basic-credentials.mjs +64 -0
- package/dist/oauth2/client-assertion.d.mts +38 -22
- package/dist/oauth2/client-assertion.mjs +63 -28
- package/dist/oauth2/client-credentials-token.d.mts +19 -40
- package/dist/oauth2/client-credentials-token.mjs +18 -29
- package/dist/oauth2/create-authorization-url.d.mts +13 -2
- package/dist/oauth2/create-authorization-url.mjs +28 -7
- package/dist/oauth2/index.d.mts +13 -8
- package/dist/oauth2/index.mjs +11 -7
- package/dist/oauth2/oauth-provider.d.mts +149 -11
- package/dist/oauth2/refresh-access-token.d.mts +20 -40
- package/dist/oauth2/refresh-access-token.mjs +20 -33
- package/dist/oauth2/scopes.d.mts +76 -0
- package/dist/oauth2/scopes.mjs +96 -0
- package/dist/oauth2/token-endpoint-auth.d.mts +17 -0
- package/dist/oauth2/token-endpoint-auth.mjs +89 -0
- package/dist/oauth2/utils.d.mts +9 -1
- package/dist/oauth2/utils.mjs +14 -2
- package/dist/oauth2/validate-authorization-code.d.mts +17 -52
- package/dist/oauth2/validate-authorization-code.mjs +17 -30
- package/dist/oauth2/verify-id-token.d.mts +26 -0
- package/dist/oauth2/verify-id-token.mjs +62 -0
- package/dist/oauth2/verify.d.mts +14 -0
- package/dist/oauth2/verify.mjs +38 -12
- package/dist/social-providers/apple.d.mts +18 -20
- package/dist/social-providers/apple.mjs +15 -28
- package/dist/social-providers/atlassian.d.mts +8 -2
- package/dist/social-providers/atlassian.mjs +9 -6
- package/dist/social-providers/cognito.d.mts +29 -3
- package/dist/social-providers/cognito.mjs +30 -34
- package/dist/social-providers/discord.d.mts +8 -2
- package/dist/social-providers/discord.mjs +20 -6
- package/dist/social-providers/dropbox.d.mts +8 -2
- package/dist/social-providers/dropbox.mjs +10 -9
- package/dist/social-providers/facebook.d.mts +24 -3
- package/dist/social-providers/facebook.mjs +51 -24
- package/dist/social-providers/figma.d.mts +8 -2
- package/dist/social-providers/figma.mjs +8 -7
- package/dist/social-providers/github.d.mts +8 -2
- package/dist/social-providers/github.mjs +9 -8
- package/dist/social-providers/gitlab.d.mts +8 -2
- package/dist/social-providers/gitlab.mjs +8 -7
- package/dist/social-providers/google.d.mts +32 -4
- package/dist/social-providers/google.mjs +26 -29
- package/dist/social-providers/huggingface.d.mts +8 -2
- package/dist/social-providers/huggingface.mjs +11 -10
- package/dist/social-providers/index.d.mts +322 -75
- package/dist/social-providers/kakao.d.mts +8 -2
- package/dist/social-providers/kakao.mjs +11 -10
- package/dist/social-providers/kick.d.mts +8 -2
- package/dist/social-providers/kick.mjs +7 -6
- package/dist/social-providers/line.d.mts +11 -3
- package/dist/social-providers/line.mjs +14 -15
- package/dist/social-providers/linear.d.mts +8 -2
- package/dist/social-providers/linear.mjs +7 -6
- package/dist/social-providers/linkedin.d.mts +8 -2
- package/dist/social-providers/linkedin.mjs +12 -11
- package/dist/social-providers/microsoft-entra-id.d.mts +33 -7
- package/dist/social-providers/microsoft-entra-id.mjs +28 -38
- package/dist/social-providers/naver.d.mts +8 -2
- package/dist/social-providers/naver.mjs +7 -6
- package/dist/social-providers/notion.d.mts +8 -2
- package/dist/social-providers/notion.mjs +9 -6
- package/dist/social-providers/paybin.d.mts +8 -2
- package/dist/social-providers/paybin.mjs +12 -11
- package/dist/social-providers/paypal.d.mts +8 -3
- package/dist/social-providers/paypal.mjs +10 -14
- package/dist/social-providers/polar.d.mts +8 -2
- package/dist/social-providers/polar.mjs +11 -10
- package/dist/social-providers/railway.d.mts +8 -2
- package/dist/social-providers/railway.mjs +11 -10
- package/dist/social-providers/reddit.d.mts +8 -2
- package/dist/social-providers/reddit.mjs +11 -9
- package/dist/social-providers/roblox.d.mts +8 -2
- package/dist/social-providers/roblox.mjs +15 -5
- package/dist/social-providers/salesforce.d.mts +8 -2
- package/dist/social-providers/salesforce.mjs +11 -10
- package/dist/social-providers/slack.d.mts +8 -2
- package/dist/social-providers/slack.mjs +18 -15
- package/dist/social-providers/spotify.d.mts +8 -2
- package/dist/social-providers/spotify.mjs +7 -6
- package/dist/social-providers/tiktok.d.mts +8 -2
- package/dist/social-providers/tiktok.mjs +21 -5
- package/dist/social-providers/twitch.d.mts +8 -2
- package/dist/social-providers/twitch.mjs +7 -6
- package/dist/social-providers/twitter.d.mts +7 -2
- package/dist/social-providers/twitter.mjs +11 -10
- package/dist/social-providers/vercel.d.mts +8 -2
- package/dist/social-providers/vercel.mjs +7 -9
- package/dist/social-providers/vk.d.mts +8 -2
- package/dist/social-providers/vk.mjs +7 -6
- package/dist/social-providers/wechat.d.mts +8 -2
- package/dist/social-providers/wechat.mjs +16 -6
- package/dist/social-providers/zoom.d.mts +10 -3
- package/dist/social-providers/zoom.mjs +14 -15
- package/dist/types/context.d.mts +33 -11
- package/dist/types/index.d.mts +1 -1
- package/dist/types/init-options.d.mts +121 -6
- package/dist/utils/ip.d.mts +5 -4
- package/dist/utils/ip.mjs +3 -3
- package/dist/utils/redirect-uri.d.mts +20 -0
- package/dist/utils/redirect-uri.mjs +48 -0
- package/dist/utils/string.d.mts +5 -1
- package/dist/utils/string.mjs +20 -1
- package/dist/utils/url.d.mts +18 -1
- package/dist/utils/url.mjs +30 -1
- package/package.json +13 -12
- package/src/db/adapter/factory.ts +126 -0
- package/src/db/adapter/index.ts +32 -0
- package/src/db/adapter/types.ts +1 -0
- package/src/db/get-tables.ts +8 -3
- package/src/db/schema/account.ts +14 -2
- package/src/db/type.ts +12 -0
- package/src/env/env-impl.ts +1 -2
- package/src/error/codes.ts +6 -0
- package/src/oauth2/authorization-params.ts +28 -0
- package/src/oauth2/basic-credentials.ts +87 -0
- package/src/oauth2/client-assertion.ts +131 -58
- package/src/oauth2/client-credentials-token.ts +48 -72
- package/src/oauth2/create-authorization-url.ts +30 -8
- package/src/oauth2/index.ts +42 -10
- package/src/oauth2/oauth-provider.ts +161 -12
- package/src/oauth2/refresh-access-token.ts +52 -78
- package/src/oauth2/scopes.ts +118 -0
- package/src/oauth2/token-endpoint-auth.ts +221 -0
- package/src/oauth2/utils.ts +21 -5
- package/src/oauth2/validate-authorization-code.ts +55 -85
- package/src/oauth2/verify-id-token.ts +111 -0
- package/src/oauth2/verify.ts +82 -15
- package/src/social-providers/apple.ts +32 -45
- package/src/social-providers/atlassian.ts +20 -9
- package/src/social-providers/cognito.ts +51 -48
- package/src/social-providers/discord.ts +37 -22
- package/src/social-providers/dropbox.ts +20 -12
- package/src/social-providers/facebook.ts +108 -57
- package/src/social-providers/figma.ts +21 -10
- package/src/social-providers/github.ts +16 -10
- package/src/social-providers/gitlab.ts +16 -8
- package/src/social-providers/google.ts +67 -46
- package/src/social-providers/huggingface.ts +20 -9
- package/src/social-providers/kakao.ts +18 -9
- package/src/social-providers/kick.ts +20 -8
- package/src/social-providers/line.ts +39 -37
- package/src/social-providers/linear.ts +20 -7
- package/src/social-providers/linkedin.ts +16 -10
- package/src/social-providers/microsoft-entra-id.ts +66 -64
- package/src/social-providers/naver.ts +14 -7
- package/src/social-providers/notion.ts +20 -7
- package/src/social-providers/paybin.ts +16 -11
- package/src/social-providers/paypal.ts +12 -25
- package/src/social-providers/polar.ts +20 -9
- package/src/social-providers/railway.ts +20 -9
- package/src/social-providers/reddit.ts +22 -10
- package/src/social-providers/roblox.ts +31 -15
- package/src/social-providers/salesforce.ts +21 -10
- package/src/social-providers/slack.ts +31 -16
- package/src/social-providers/spotify.ts +20 -7
- package/src/social-providers/tiktok.ts +32 -13
- package/src/social-providers/twitch.ts +14 -9
- package/src/social-providers/twitter.ts +18 -8
- package/src/social-providers/vercel.ts +24 -11
- package/src/social-providers/vk.ts +20 -7
- package/src/social-providers/wechat.ts +28 -8
- package/src/social-providers/zoom.ts +28 -19
- package/src/types/context.ts +33 -12
- package/src/types/index.ts +7 -0
- package/src/types/init-options.ts +148 -5
- package/src/utils/ip.ts +12 -13
- package/src/utils/redirect-uri.ts +54 -0
- package/src/utils/string.ts +37 -0
- package/src/utils/url.ts +28 -0
|
@@ -27,6 +27,73 @@ type GenerateIdFn = (options: {
|
|
|
27
27
|
model: ModelNames;
|
|
28
28
|
size?: number | undefined;
|
|
29
29
|
}) => string | false;
|
|
30
|
+
/**
|
|
31
|
+
* What Better Auth is about to do with an incoming identity when
|
|
32
|
+
* {@link BetterAuthOptions.user}'s `validateUserInfo` runs.
|
|
33
|
+
*
|
|
34
|
+
* - `create-user`: a brand-new user record is about to be created.
|
|
35
|
+
* - `link-account`: a new provider account is about to be linked to an
|
|
36
|
+
* already-existing user.
|
|
37
|
+
* - `sign-in`: an existing OAuth or SSO user is signing in again. This is the
|
|
38
|
+
* one case where the provider can assert *changed* data, so the hook receives
|
|
39
|
+
* the fresh provider email and profile (not the stored row), letting a domain
|
|
40
|
+
* or org policy reject a user whose provider identity moved out of bounds.
|
|
41
|
+
*
|
|
42
|
+
* Non-provider returning sign-ins are not re-validated: they carry only the
|
|
43
|
+
* stored row, which has not changed since `create-user` gated it. Use the admin
|
|
44
|
+
* plugin's ban controls or a `databaseHooks.session.create.before` hook to
|
|
45
|
+
* block those.
|
|
46
|
+
*/
|
|
47
|
+
type ValidateUserInfoAction = "create-user" | "link-account" | "sign-in";
|
|
48
|
+
/**
|
|
49
|
+
* The authentication method that produced the incoming user info. The named
|
|
50
|
+
* methods cover Better Auth's built-ins; the open `string` keeps it extensible
|
|
51
|
+
* for plugins (for example `"scim"`).
|
|
52
|
+
*/
|
|
53
|
+
type ValidateUserInfoMethod = "oauth" | "sso-oidc" | "sso-saml" | "email-password" | "magic-link" | "email-otp" | "anonymous" | "siwe" | "phone-number" | "admin" | (string & {});
|
|
54
|
+
/** OAuth-specific provisioning context; present only when `method` is `"oauth"`. */
|
|
55
|
+
type ValidateUserInfoOAuthInfo = {
|
|
56
|
+
/** The social or generic OAuth provider id (e.g. `"google"`). */providerId: string; /** The raw provider profile (userinfo or id-token claims), unmapped. */
|
|
57
|
+
profile?: Record<string, unknown> | undefined;
|
|
58
|
+
};
|
|
59
|
+
/** SSO-specific provisioning context; present for OIDC and SAML SSO methods. */
|
|
60
|
+
type ValidateUserInfoSSOInfo = {
|
|
61
|
+
/** The configured SSO provider id. */providerId: string; /** The raw OIDC claims or SAML assertion attributes, unmapped. */
|
|
62
|
+
profile?: Record<string, unknown> | undefined;
|
|
63
|
+
};
|
|
64
|
+
/** Provisioning origin passed to `createUser`; the creation seam adds `action: "create-user"` to build {@link ValidateUserInfoSource}. */
|
|
65
|
+
type UserProvisioningSource = {
|
|
66
|
+
method: ValidateUserInfoMethod; /** Provider id and raw profile; present iff `method` is `"oauth"`. */
|
|
67
|
+
oauth?: ValidateUserInfoOAuthInfo | undefined; /** Provider id and raw profile; present iff `method` is `"sso-oidc"` or `"sso-saml"`. */
|
|
68
|
+
sso?: ValidateUserInfoSSOInfo | undefined;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The context passed to `validateUserInfo`: the lifecycle
|
|
72
|
+
* {@link ValidateUserInfoAction}, the {@link ValidateUserInfoMethod}, and (for
|
|
73
|
+
* OAuth/SSO provider methods) protocol-specific provider metadata.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* // Scope to one OAuth provider:
|
|
77
|
+
* if (source.oauth?.providerId !== "google") return;
|
|
78
|
+
* // Branch on the method:
|
|
79
|
+
* if (source.method === "anonymous") return { error: "no_anonymous" };
|
|
80
|
+
* // Inspect SSO claims:
|
|
81
|
+
* if (source.method === "sso-saml" && source.sso?.profile?.department !== "eng") {
|
|
82
|
+
* return { error: "invalid_department" };
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
type ValidateUserInfoSource = UserProvisioningSource & {
|
|
87
|
+
action: ValidateUserInfoAction;
|
|
88
|
+
};
|
|
89
|
+
type ValidateUserInfoResult = {
|
|
90
|
+
/** A short, machine-readable rejection code, surfaced to the client. */error: string;
|
|
91
|
+
/**
|
|
92
|
+
* A human-readable reason, surfaced to the client. Do not put sensitive
|
|
93
|
+
* details here.
|
|
94
|
+
*/
|
|
95
|
+
errorDescription?: string | undefined;
|
|
96
|
+
};
|
|
30
97
|
/**
|
|
31
98
|
* Configuration for dynamic base URL resolution.
|
|
32
99
|
* Allows Better Auth to work with multiple domains (e.g., Vercel preview deployments).
|
|
@@ -157,12 +224,13 @@ type BetterAuthAdvancedOptions = {
|
|
|
157
224
|
*/
|
|
158
225
|
disableIpTracking?: boolean;
|
|
159
226
|
/**
|
|
160
|
-
* IPv6
|
|
161
|
-
*
|
|
227
|
+
* IPv6 prefix length used to collapse addresses before rate-limit keying.
|
|
228
|
+
* Any integer from 0 to 128 is accepted; common values are 32, 48, 56, 64, 128.
|
|
229
|
+
* Out-of-range values fall back to safe behavior (negative -> mask all, > 128 -> no mask).
|
|
162
230
|
*
|
|
163
231
|
* @default 64
|
|
164
232
|
*/
|
|
165
|
-
ipv6Subnet?:
|
|
233
|
+
ipv6Subnet?: number;
|
|
166
234
|
} | undefined;
|
|
167
235
|
/**
|
|
168
236
|
* Force cookies to always use the `Secure` attribute. By default,
|
|
@@ -683,6 +751,30 @@ type BetterAuthOptions = {
|
|
|
683
751
|
* User configuration
|
|
684
752
|
*/
|
|
685
753
|
user?: (BetterAuthDBOptions<"user", keyof BaseUser> & {
|
|
754
|
+
/**
|
|
755
|
+
* Gate which identities Better Auth admits. Called just before
|
|
756
|
+
* `create-user`, `link-account`, and (for OAuth) `sign-in`, across
|
|
757
|
+
* every authentication method, including stateless setups with no
|
|
758
|
+
* persistent database. On `sign-in` the hook receives the *fresh*
|
|
759
|
+
* provider email and profile, so a domain policy can reject a user
|
|
760
|
+
* whose provider identity moved out of bounds.
|
|
761
|
+
*
|
|
762
|
+
* Non-provider returning sign-ins are not re-validated; use the admin
|
|
763
|
+
* plugin's ban controls or a `databaseHooks.session.create.before`
|
|
764
|
+
* hook for those.
|
|
765
|
+
*
|
|
766
|
+
* Return nothing to allow; return `{ error }` to reject. Browser flows
|
|
767
|
+
* redirect to the configured error URL; programmatic flows surface a
|
|
768
|
+
* `403`.
|
|
769
|
+
*
|
|
770
|
+
* TODO: rename to `validateUser` (and the `ValidateUserInfo*` types).
|
|
771
|
+
* "UserInfo" is the OIDC term and misleads for the email/password,
|
|
772
|
+
* SIWE, phone, and admin methods.
|
|
773
|
+
*/
|
|
774
|
+
validateUserInfo?: (data: {
|
|
775
|
+
user: Partial<User> & Record<string, unknown>;
|
|
776
|
+
source: ValidateUserInfoSource;
|
|
777
|
+
}, context: GenericEndpointContext) => Awaitable<void | ValidateUserInfoResult>;
|
|
686
778
|
/**
|
|
687
779
|
* Changing email configuration
|
|
688
780
|
*/
|
|
@@ -906,6 +998,25 @@ type BetterAuthOptions = {
|
|
|
906
998
|
* @default false
|
|
907
999
|
*/
|
|
908
1000
|
disableImplicitLinking?: boolean;
|
|
1001
|
+
/**
|
|
1002
|
+
* Require the existing local user row to have
|
|
1003
|
+
* `emailVerified: true` before implicit account linking
|
|
1004
|
+
* uses the IdP's `email_verified` claim as ownership
|
|
1005
|
+
* proof. Defaults to `true` so an attacker who
|
|
1006
|
+
* pre-registers an unverified account at a victim's
|
|
1007
|
+
* email cannot have the victim's OAuth identity linked
|
|
1008
|
+
* into the attacker-owned row on first sign-in. Set to
|
|
1009
|
+
* `false` for backward compatibility on apps whose
|
|
1010
|
+
* users sign up via OAuth without verifying their email
|
|
1011
|
+
* locally; understand the takeover risk before doing
|
|
1012
|
+
* so.
|
|
1013
|
+
*
|
|
1014
|
+
* @default true
|
|
1015
|
+
*
|
|
1016
|
+
* @deprecated The option will be removed on the next
|
|
1017
|
+
* minor; the gate will become unconditional.
|
|
1018
|
+
*/
|
|
1019
|
+
requireLocalEmailVerified?: boolean;
|
|
909
1020
|
/**
|
|
910
1021
|
* List of trusted providers. Can be a static array or a function
|
|
911
1022
|
* that returns providers dynamically. The function is called
|
|
@@ -943,7 +1054,11 @@ type BetterAuthOptions = {
|
|
|
943
1054
|
*/
|
|
944
1055
|
allowUnlinkingAll?: boolean;
|
|
945
1056
|
/**
|
|
946
|
-
*
|
|
1057
|
+
* When enabled, linking an account copies the provider's profile onto
|
|
1058
|
+
* the local user, matching the fields persisted on sign-up (`name`,
|
|
1059
|
+
* `image`, and any `mapProfileToUser` fields). The local `email` and
|
|
1060
|
+
* `emailVerified` are never changed, so a link cannot rebind the
|
|
1061
|
+
* account's identity.
|
|
947
1062
|
*
|
|
948
1063
|
* @default false
|
|
949
1064
|
*/
|
|
@@ -976,7 +1091,7 @@ type BetterAuthOptions = {
|
|
|
976
1091
|
* - "cookie": Store state in an encrypted cookie (stateless)
|
|
977
1092
|
* - "database": Store state in the database
|
|
978
1093
|
*
|
|
979
|
-
* @default "cookie"
|
|
1094
|
+
* @default "database" when `database` or `secondaryStorage` is configured, "cookie" otherwise
|
|
980
1095
|
*/
|
|
981
1096
|
storeStateStrategy?: "database" | "cookie";
|
|
982
1097
|
/**
|
|
@@ -1355,4 +1470,4 @@ type BetterAuthOptions = {
|
|
|
1355
1470
|
};
|
|
1356
1471
|
};
|
|
1357
1472
|
//#endregion
|
|
1358
|
-
export { BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, DynamicBaseURLConfig, GenerateIdFn, StoreIdentifierOption };
|
|
1473
|
+
export { BaseURLConfig, BetterAuthAdvancedOptions, BetterAuthDBOptions, BetterAuthOptions, BetterAuthRateLimitOptions, BetterAuthRateLimitRule, BetterAuthRateLimitStorage, DynamicBaseURLConfig, GenerateIdFn, StoreIdentifierOption, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource };
|
package/dist/utils/ip.d.mts
CHANGED
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
interface NormalizeIPOptions {
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
* Common values: 32, 48, 64, 128
|
|
13
|
+
* Prefix length used to collapse IPv6 addresses before keying.
|
|
14
|
+
* Any integer from 0 to 128 is accepted. Common values: 32, 48, 56, 64, 128.
|
|
15
|
+
* Values outside 0-128 are clamped.
|
|
15
16
|
*
|
|
16
|
-
* @default
|
|
17
|
+
* @default 64
|
|
17
18
|
*/
|
|
18
|
-
ipv6Subnet?:
|
|
19
|
+
ipv6Subnet?: number;
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Checks if an IP is valid IPv4 or IPv6
|
package/dist/utils/ip.mjs
CHANGED
|
@@ -60,8 +60,8 @@ function expandIPv6(ipv6) {
|
|
|
60
60
|
*/
|
|
61
61
|
function normalizeIPv6(ipv6, subnetPrefix) {
|
|
62
62
|
const groups = expandIPv6(ipv6);
|
|
63
|
-
if (subnetPrefix && subnetPrefix < 128) {
|
|
64
|
-
let bitsRemaining = subnetPrefix;
|
|
63
|
+
if (subnetPrefix !== void 0 && subnetPrefix < 128) {
|
|
64
|
+
let bitsRemaining = Math.max(0, Math.floor(subnetPrefix));
|
|
65
65
|
return groups.map((group) => {
|
|
66
66
|
if (bitsRemaining <= 0) return "0000";
|
|
67
67
|
if (bitsRemaining >= 16) {
|
|
@@ -99,7 +99,7 @@ function normalizeIP(ip, options = {}) {
|
|
|
99
99
|
if (!isIPv6(ip)) return ip.toLowerCase();
|
|
100
100
|
const ipv4 = extractIPv4FromMapped(ip);
|
|
101
101
|
if (ipv4) return ipv4.toLowerCase();
|
|
102
|
-
return normalizeIPv6(ip, options.ipv6Subnet
|
|
102
|
+
return normalizeIPv6(ip, options.ipv6Subnet ?? 64);
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* Creates a rate limit key from IP and path
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/redirect-uri.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
|
|
6
|
+
* server stores and later hands back to a browser.
|
|
7
|
+
*
|
|
8
|
+
* - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
|
|
9
|
+
* - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
|
|
10
|
+
* - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
|
|
11
|
+
* `*.localhost` per RFC 6761), where HTTP is allowed for local development.
|
|
12
|
+
* - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
|
|
13
|
+
*
|
|
14
|
+
* This is the single source of truth for redirect-URI validation across the
|
|
15
|
+
* OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
|
|
16
|
+
* rather than re-implementing the scheme policy per plugin.
|
|
17
|
+
*/
|
|
18
|
+
declare const SafeUrlSchema: z.ZodURL;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { SafeUrlSchema };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isLoopbackHost } from "./host.mjs";
|
|
2
|
+
import { DANGEROUS_URL_SCHEMES } from "./url.mjs";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
//#region src/utils/redirect-uri.ts
|
|
5
|
+
/**
|
|
6
|
+
* Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
|
|
7
|
+
* server stores and later hands back to a browser.
|
|
8
|
+
*
|
|
9
|
+
* - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
|
|
10
|
+
* - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
|
|
11
|
+
* - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
|
|
12
|
+
* `*.localhost` per RFC 6761), where HTTP is allowed for local development.
|
|
13
|
+
* - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
|
|
14
|
+
*
|
|
15
|
+
* This is the single source of truth for redirect-URI validation across the
|
|
16
|
+
* OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
|
|
17
|
+
* rather than re-implementing the scheme policy per plugin.
|
|
18
|
+
*/
|
|
19
|
+
const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
20
|
+
let u;
|
|
21
|
+
try {
|
|
22
|
+
u = new URL(val);
|
|
23
|
+
} catch {
|
|
24
|
+
ctx.addIssue({
|
|
25
|
+
code: "custom",
|
|
26
|
+
message: "URL must be parseable",
|
|
27
|
+
fatal: true
|
|
28
|
+
});
|
|
29
|
+
return z.NEVER;
|
|
30
|
+
}
|
|
31
|
+
if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: "custom",
|
|
34
|
+
message: "URL cannot use javascript:, data:, or vbscript: scheme"
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (val.includes("#")) ctx.addIssue({
|
|
39
|
+
code: "custom",
|
|
40
|
+
message: "Redirect URI must not contain a fragment component"
|
|
41
|
+
});
|
|
42
|
+
if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
|
|
43
|
+
code: "custom",
|
|
44
|
+
message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
export { SafeUrlSchema };
|
package/dist/utils/string.d.mts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/utils/string.d.ts
|
|
2
2
|
declare function capitalizeFirstLetter(str: string): string;
|
|
3
|
+
declare function toSnakeCase(input: string): string;
|
|
4
|
+
declare function toKebabCase(input: string): string;
|
|
5
|
+
declare function toCamelCase(input: string): string;
|
|
6
|
+
declare function toPascalCase(input: string): string;
|
|
3
7
|
//#endregion
|
|
4
|
-
export { capitalizeFirstLetter };
|
|
8
|
+
export { capitalizeFirstLetter, toCamelCase, toKebabCase, toPascalCase, toSnakeCase };
|
package/dist/utils/string.mjs
CHANGED
|
@@ -2,5 +2,24 @@
|
|
|
2
2
|
function capitalizeFirstLetter(str) {
|
|
3
3
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
4
4
|
}
|
|
5
|
+
const WORD_PATTERN = /[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
|
|
6
|
+
const APOSTROPHE_PATTERN = /['\u2019]/g;
|
|
7
|
+
function splitWords(input) {
|
|
8
|
+
return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
|
|
9
|
+
}
|
|
10
|
+
function toSnakeCase(input) {
|
|
11
|
+
return splitWords(input).map((word) => word.toLowerCase()).join("_");
|
|
12
|
+
}
|
|
13
|
+
function toKebabCase(input) {
|
|
14
|
+
return splitWords(input).map((word) => word.toLowerCase()).join("-");
|
|
15
|
+
}
|
|
16
|
+
function toCamelCase(input) {
|
|
17
|
+
return splitWords(input).reduce((acc, word, i) => {
|
|
18
|
+
return acc + (i === 0 ? word.toLowerCase() : `${word[0].toUpperCase()}${word.slice(1)}`);
|
|
19
|
+
}, "");
|
|
20
|
+
}
|
|
21
|
+
function toPascalCase(input) {
|
|
22
|
+
return splitWords(input).map((word) => `${word[0].toUpperCase()}${word.slice(1).toLowerCase()}`).join("");
|
|
23
|
+
}
|
|
5
24
|
//#endregion
|
|
6
|
-
export { capitalizeFirstLetter };
|
|
25
|
+
export { capitalizeFirstLetter, toCamelCase, toKebabCase, toPascalCase, toSnakeCase };
|
package/dist/utils/url.d.mts
CHANGED
|
@@ -16,5 +16,22 @@
|
|
|
16
16
|
* // Returns: "/sso/saml2/callback/provider1"
|
|
17
17
|
*/
|
|
18
18
|
declare function normalizePathname(requestUrl: string, basePath: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Schemes that execute or embed code when navigated to or accepted as a
|
|
21
|
+
* redirect target. These are never safe as an OAuth `redirect_uri` or as a
|
|
22
|
+
* client-side navigation target (`window.location.href`, `location.assign`, ...).
|
|
23
|
+
*/
|
|
24
|
+
declare const DANGEROUS_URL_SCHEMES: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Returns `false` only when `value` is an absolute URL using a dangerous scheme
|
|
27
|
+
* (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
|
|
28
|
+
* safe absolute schemes (`http`, `https`, custom app schemes such as
|
|
29
|
+
* `myapp://`) return `true`.
|
|
30
|
+
*
|
|
31
|
+
* Use this to guard browser navigation sinks and any redirect target that may
|
|
32
|
+
* originate from untrusted input. It is intentionally narrow: it blocks code
|
|
33
|
+
* execution schemes without rejecting relative paths or mobile deep links.
|
|
34
|
+
*/
|
|
35
|
+
declare function isSafeUrlScheme(value: string): boolean;
|
|
19
36
|
//#endregion
|
|
20
|
-
export { normalizePathname };
|
|
37
|
+
export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
|
package/dist/utils/url.mjs
CHANGED
|
@@ -27,5 +27,34 @@ function normalizePathname(requestUrl, basePath) {
|
|
|
27
27
|
if (pathname.startsWith(basePath + "/")) return pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
|
|
28
28
|
return pathname;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Schemes that execute or embed code when navigated to or accepted as a
|
|
32
|
+
* redirect target. These are never safe as an OAuth `redirect_uri` or as a
|
|
33
|
+
* client-side navigation target (`window.location.href`, `location.assign`, ...).
|
|
34
|
+
*/
|
|
35
|
+
const DANGEROUS_URL_SCHEMES = [
|
|
36
|
+
"javascript:",
|
|
37
|
+
"data:",
|
|
38
|
+
"vbscript:"
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Returns `false` only when `value` is an absolute URL using a dangerous scheme
|
|
42
|
+
* (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
|
|
43
|
+
* safe absolute schemes (`http`, `https`, custom app schemes such as
|
|
44
|
+
* `myapp://`) return `true`.
|
|
45
|
+
*
|
|
46
|
+
* Use this to guard browser navigation sinks and any redirect target that may
|
|
47
|
+
* originate from untrusted input. It is intentionally narrow: it blocks code
|
|
48
|
+
* execution schemes without rejecting relative paths or mobile deep links.
|
|
49
|
+
*/
|
|
50
|
+
function isSafeUrlScheme(value) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
|
|
58
|
+
}
|
|
30
59
|
//#endregion
|
|
31
|
-
export { normalizePathname };
|
|
60
|
+
export { DANGEROUS_URL_SCHEMES, isSafeUrlScheme, normalizePathname };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/core",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.5",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -93,12 +93,13 @@
|
|
|
93
93
|
"./instrumentation": {
|
|
94
94
|
"dev-source": "./src/instrumentation/index.ts",
|
|
95
95
|
"types": "./dist/instrumentation/index.d.mts",
|
|
96
|
+
"workerd": "./dist/instrumentation/pure.index.mjs",
|
|
97
|
+
"edge": "./dist/instrumentation/pure.index.mjs",
|
|
98
|
+
"browser": "./dist/instrumentation/pure.index.mjs",
|
|
96
99
|
"node": "./dist/instrumentation/index.mjs",
|
|
97
100
|
"deno": "./dist/instrumentation/index.mjs",
|
|
98
101
|
"bun": "./dist/instrumentation/index.mjs",
|
|
99
|
-
"
|
|
100
|
-
"workerd": "./dist/instrumentation/pure.index.mjs",
|
|
101
|
-
"browser": "./dist/instrumentation/pure.index.mjs",
|
|
102
|
+
"import": "./dist/instrumentation/index.mjs",
|
|
102
103
|
"default": "./dist/instrumentation/index.mjs"
|
|
103
104
|
}
|
|
104
105
|
},
|
|
@@ -151,26 +152,26 @@
|
|
|
151
152
|
"zod": "^4.3.6"
|
|
152
153
|
},
|
|
153
154
|
"devDependencies": {
|
|
154
|
-
"@better-auth/utils": "0.4.
|
|
155
|
-
"@better-fetch/fetch": "1.
|
|
155
|
+
"@better-auth/utils": "0.4.1",
|
|
156
|
+
"@better-fetch/fetch": "1.2.2",
|
|
156
157
|
"@opentelemetry/api": "^1.9.0",
|
|
157
158
|
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
158
159
|
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
159
|
-
"better-call": "1.3.
|
|
160
|
+
"better-call": "1.3.6",
|
|
160
161
|
"@cloudflare/workers-types": "^4.20250121.0",
|
|
161
162
|
"jose": "^6.1.3",
|
|
162
|
-
"kysely": "^0.28.
|
|
163
|
+
"kysely": "^0.28.17 || ^0.29.0",
|
|
163
164
|
"nanostores": "^1.1.1",
|
|
164
165
|
"tsdown": "0.21.1"
|
|
165
166
|
},
|
|
166
167
|
"peerDependencies": {
|
|
167
|
-
"@better-auth/utils": "0.4.
|
|
168
|
-
"@better-fetch/fetch": "1.
|
|
168
|
+
"@better-auth/utils": "0.4.1",
|
|
169
|
+
"@better-fetch/fetch": "1.2.2",
|
|
169
170
|
"@opentelemetry/api": "^1.9.0",
|
|
170
|
-
"better-call": "1.3.
|
|
171
|
+
"better-call": "1.3.6",
|
|
171
172
|
"@cloudflare/workers-types": ">=4",
|
|
172
173
|
"jose": "^6.1.0",
|
|
173
|
-
"kysely": "^0.28.5",
|
|
174
|
+
"kysely": "^0.28.5 || ^0.29.0",
|
|
174
175
|
"nanostores": "^1.0.1"
|
|
175
176
|
},
|
|
176
177
|
"peerDependenciesMeta": {
|
|
@@ -133,6 +133,11 @@ export const createAdapterFactory =
|
|
|
133
133
|
!config.debugLogs.deleteMany
|
|
134
134
|
) {
|
|
135
135
|
return;
|
|
136
|
+
} else if (
|
|
137
|
+
method === "consumeOne" &&
|
|
138
|
+
!config.debugLogs.consumeOne
|
|
139
|
+
) {
|
|
140
|
+
return;
|
|
136
141
|
} else if (method === "count" && !config.debugLogs.count) {
|
|
137
142
|
return;
|
|
138
143
|
}
|
|
@@ -485,6 +490,7 @@ export const createAdapterFactory =
|
|
|
485
490
|
| "updateMany"
|
|
486
491
|
| "delete"
|
|
487
492
|
| "deleteMany"
|
|
493
|
+
| "consumeOne"
|
|
488
494
|
| "count";
|
|
489
495
|
}): W extends undefined ? undefined : CleanedWhere[] => {
|
|
490
496
|
if (!where) return undefined as any;
|
|
@@ -1312,6 +1318,126 @@ export const createAdapterFactory =
|
|
|
1312
1318
|
);
|
|
1313
1319
|
return res;
|
|
1314
1320
|
},
|
|
1321
|
+
consumeOne: async <T>({
|
|
1322
|
+
model: unsafeModel,
|
|
1323
|
+
where: unsafeWhere,
|
|
1324
|
+
}: {
|
|
1325
|
+
model: string;
|
|
1326
|
+
where: Where[];
|
|
1327
|
+
}): Promise<T | null> => {
|
|
1328
|
+
transactionId++;
|
|
1329
|
+
const thisTransactionId = transactionId;
|
|
1330
|
+
const model = getModelName(unsafeModel);
|
|
1331
|
+
const where = transformWhereClause({
|
|
1332
|
+
model: unsafeModel,
|
|
1333
|
+
where: unsafeWhere,
|
|
1334
|
+
action: "consumeOne",
|
|
1335
|
+
});
|
|
1336
|
+
unsafeModel = getDefaultModelName(unsafeModel);
|
|
1337
|
+
debugLog(
|
|
1338
|
+
{ method: "consumeOne" },
|
|
1339
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(1, 3)}`,
|
|
1340
|
+
`${formatMethod("consumeOne")} ${formatAction("ConsumeOne")}:`,
|
|
1341
|
+
{ model, where },
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
let res: T | null;
|
|
1345
|
+
let resultNeedsOutputTransform = true;
|
|
1346
|
+
if (adapterInstance.consumeOne) {
|
|
1347
|
+
res = await withSpan(
|
|
1348
|
+
`db consumeOne ${model}`,
|
|
1349
|
+
{
|
|
1350
|
+
[ATTR_DB_OPERATION_NAME]: "consumeOne",
|
|
1351
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
1352
|
+
},
|
|
1353
|
+
() => adapterInstance.consumeOne!<T>({ model, where }),
|
|
1354
|
+
);
|
|
1355
|
+
} else {
|
|
1356
|
+
// TODO(consume-one-required): adapters without native `consumeOne`
|
|
1357
|
+
// fall back to `transaction(findMany + deleteMany)`. Race-safe on
|
|
1358
|
+
// engines with real transaction isolation; race window narrows
|
|
1359
|
+
// (does not close) on adapters that fall through to sequential
|
|
1360
|
+
// execution. Remove this branch when consumeOne becomes required.
|
|
1361
|
+
// FIXME(consume-one-nested-transaction): custom adapters without a
|
|
1362
|
+
// native consumeOne have no portable signal for "already inside a
|
|
1363
|
+
// transaction". First-party adapters mark transaction-scoped
|
|
1364
|
+
// adapters as as-is; make that capability explicit in the next
|
|
1365
|
+
// breaking adapter contract.
|
|
1366
|
+
res = await withSpan(
|
|
1367
|
+
`db consumeOne ${model}`,
|
|
1368
|
+
{
|
|
1369
|
+
[ATTR_DB_OPERATION_NAME]: "consumeOne",
|
|
1370
|
+
[ATTR_DB_COLLECTION_NAME]: model,
|
|
1371
|
+
},
|
|
1372
|
+
() =>
|
|
1373
|
+
adapter.transaction(async (trx) => {
|
|
1374
|
+
const rows = await trx.findMany<Record<string, any>>({
|
|
1375
|
+
model: unsafeModel,
|
|
1376
|
+
where: unsafeWhere,
|
|
1377
|
+
limit: 1,
|
|
1378
|
+
});
|
|
1379
|
+
const target = rows[0];
|
|
1380
|
+
if (!target) return null;
|
|
1381
|
+
const deleted = await trx.deleteMany({
|
|
1382
|
+
model: unsafeModel,
|
|
1383
|
+
where: [
|
|
1384
|
+
...unsafeWhere,
|
|
1385
|
+
{
|
|
1386
|
+
field: "id",
|
|
1387
|
+
value: target.id,
|
|
1388
|
+
operator: "eq",
|
|
1389
|
+
connector: "AND",
|
|
1390
|
+
mode: "sensitive",
|
|
1391
|
+
},
|
|
1392
|
+
],
|
|
1393
|
+
});
|
|
1394
|
+
// `deleteMany` is typed `Promise<number>`. A non-number breaks
|
|
1395
|
+
// the contract, so fail loud. A finite-number check then closes
|
|
1396
|
+
// the NaN/Infinity hole: `NaN > 0` is false and `Infinity > 0`
|
|
1397
|
+
// is true, so a bare `deleted > 0` would misclassify both. Only
|
|
1398
|
+
// a finite positive count proves we won the delete race; any
|
|
1399
|
+
// other value fails closed (returns null) so a single-use row
|
|
1400
|
+
// is never reported consumed without proof.
|
|
1401
|
+
if (typeof deleted !== "number") {
|
|
1402
|
+
throw new BetterAuthError(
|
|
1403
|
+
`Adapter "${config.adapterId}" returned a non-numeric value from deleteMany during the consumeOne fallback. Return the number of deleted rows, or implement a native consumeOne for atomic single-use consumption.`,
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return Number.isFinite(deleted) && deleted > 0
|
|
1407
|
+
? (target as T)
|
|
1408
|
+
: null;
|
|
1409
|
+
}),
|
|
1410
|
+
);
|
|
1411
|
+
resultNeedsOutputTransform = false;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
debugLog(
|
|
1415
|
+
{ method: "consumeOne" },
|
|
1416
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(2, 3)}`,
|
|
1417
|
+
`${formatMethod("consumeOne")} ${formatAction("DB Result")}:`,
|
|
1418
|
+
{ model, data: res },
|
|
1419
|
+
);
|
|
1420
|
+
let transformed: any = res;
|
|
1421
|
+
if (
|
|
1422
|
+
!config.disableTransformOutput &&
|
|
1423
|
+
resultNeedsOutputTransform &&
|
|
1424
|
+
res
|
|
1425
|
+
) {
|
|
1426
|
+
transformed = await transformOutput(
|
|
1427
|
+
res as Record<string, any>,
|
|
1428
|
+
unsafeModel,
|
|
1429
|
+
undefined,
|
|
1430
|
+
undefined,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
debugLog(
|
|
1434
|
+
{ method: "consumeOne" },
|
|
1435
|
+
`${formatTransactionId(thisTransactionId)} ${formatStep(3, 3)}`,
|
|
1436
|
+
`${formatMethod("consumeOne")} ${formatAction("Parsed Result")}:`,
|
|
1437
|
+
{ model, data: transformed },
|
|
1438
|
+
);
|
|
1439
|
+
return transformed as T | null;
|
|
1440
|
+
},
|
|
1315
1441
|
count: async ({
|
|
1316
1442
|
model: unsafeModel,
|
|
1317
1443
|
where: unsafeWhere,
|
package/src/db/adapter/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type DBAdapterDebugLogOption =
|
|
|
15
15
|
findMany?: boolean | undefined;
|
|
16
16
|
delete?: boolean | undefined;
|
|
17
17
|
deleteMany?: boolean | undefined;
|
|
18
|
+
consumeOne?: boolean | undefined;
|
|
18
19
|
count?: boolean | undefined;
|
|
19
20
|
}
|
|
20
21
|
| {
|
|
@@ -211,6 +212,7 @@ export interface DBAdapterFactoryConfig<
|
|
|
211
212
|
| "updateMany"
|
|
212
213
|
| "delete"
|
|
213
214
|
| "deleteMany"
|
|
215
|
+
| "consumeOne"
|
|
214
216
|
| "count";
|
|
215
217
|
/**
|
|
216
218
|
* The model name.
|
|
@@ -445,6 +447,23 @@ export type DBAdapter<Options extends BetterAuthOptions = BetterAuthOptions> = {
|
|
|
445
447
|
}) => Promise<number>;
|
|
446
448
|
delete: <_T>(data: { model: string; where: Where[] }) => Promise<void>;
|
|
447
449
|
deleteMany: (data: { model: string; where: Where[] }) => Promise<number>;
|
|
450
|
+
/**
|
|
451
|
+
* Atomically consume a single row matching the where clause: delete it and
|
|
452
|
+
* return the deleted row, or return `null` if no row matched.
|
|
453
|
+
* Implementations MUST NOT delete any additional rows that also match a
|
|
454
|
+
* non-unique predicate.
|
|
455
|
+
*
|
|
456
|
+
* Under concurrent invocation against the same row, exactly one caller
|
|
457
|
+
* receives the row; subsequent racers receive `null`. This is the
|
|
458
|
+
* race-safe primitive for consuming single-use credentials
|
|
459
|
+
* (verification tokens, authorization codes, one-time tokens).
|
|
460
|
+
*
|
|
461
|
+
* Always defined on the factory-wrapped adapter. When the underlying
|
|
462
|
+
* `CustomAdapter` does not implement `consumeOne`, the factory provides
|
|
463
|
+
* a fallback that wraps `findMany + deleteMany` in `transaction(...)`
|
|
464
|
+
* and returns the row only when the delete reports an affected row.
|
|
465
|
+
*/
|
|
466
|
+
consumeOne: <T>(data: { model: string; where: Where[] }) => Promise<T | null>;
|
|
448
467
|
/**
|
|
449
468
|
* Execute multiple operations in a transaction.
|
|
450
469
|
* If the adapter doesn't support transactions, operations will be executed sequentially.
|
|
@@ -531,6 +550,19 @@ export interface CustomAdapter {
|
|
|
531
550
|
model: string;
|
|
532
551
|
where: CleanedWhere[];
|
|
533
552
|
}) => Promise<number>;
|
|
553
|
+
/**
|
|
554
|
+
* Optional native atomic single-row consume. When omitted, the adapter
|
|
555
|
+
* factory falls back to `transaction(findMany + deleteMany)`.
|
|
556
|
+
* Implementing this method natively (e.g. `DELETE ... RETURNING *`,
|
|
557
|
+
* `findOneAndDelete`, `OUTPUT deleted.*`) gives one round trip and the
|
|
558
|
+
* strongest race-safety guarantee. Implementations must delete at most
|
|
559
|
+
* one matching row. TODO(consume-one-required): tighten to required in the
|
|
560
|
+
* next minor on `next`.
|
|
561
|
+
*/
|
|
562
|
+
consumeOne?: <T>(data: {
|
|
563
|
+
model: string;
|
|
564
|
+
where: CleanedWhere[];
|
|
565
|
+
}) => Promise<T | null>;
|
|
534
566
|
count: ({
|
|
535
567
|
model,
|
|
536
568
|
where,
|
package/src/db/adapter/types.ts
CHANGED