better-auth 1.6.11 → 1.6.12
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 +8 -2
- package/dist/api/routes/callback.d.mts +1 -1
- package/dist/api/routes/callback.mjs +36 -40
- package/dist/api/routes/email-verification.d.mts +1 -0
- package/dist/api/routes/email-verification.mjs +4 -3
- package/dist/api/routes/session.mjs +14 -9
- package/dist/api/routes/sign-in.d.mts +1 -0
- package/dist/api/routes/sign-in.mjs +2 -1
- package/dist/api/routes/sign-up.d.mts +1 -0
- package/dist/api/routes/sign-up.mjs +9 -7
- package/dist/api/routes/update-user.mjs +4 -4
- package/dist/client/parser.mjs +0 -1
- package/dist/client/plugins/index.d.mts +3 -3
- package/dist/client/proxy.mjs +2 -1
- package/dist/context/helpers.mjs +3 -2
- package/dist/cookies/cookie-utils.d.mts +24 -1
- package/dist/cookies/cookie-utils.mjs +85 -22
- package/dist/cookies/index.d.mts +2 -3
- package/dist/cookies/index.mjs +39 -11
- package/dist/cookies/session-store.mjs +4 -23
- package/dist/db/get-migration.mjs +4 -4
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +3 -2
- package/dist/db/internal-adapter.mjs +37 -30
- package/dist/db/schema.d.mts +15 -2
- package/dist/db/schema.mjs +26 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/errors.mjs +16 -1
- package/dist/oauth2/link-account.mjs +3 -3
- package/dist/oauth2/state.mjs +8 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/access/access.mjs +11 -6
- package/dist/plugins/admin/admin.mjs +0 -4
- package/dist/plugins/admin/client.d.mts +1 -1
- package/dist/plugins/bearer/index.mjs +4 -9
- package/dist/plugins/captcha/index.mjs +2 -2
- package/dist/plugins/generic-oauth/index.d.mts +1 -1
- package/dist/plugins/generic-oauth/index.mjs +6 -6
- package/dist/plugins/generic-oauth/routes.mjs +34 -32
- package/dist/plugins/generic-oauth/types.d.mts +7 -0
- package/dist/plugins/last-login-method/client.mjs +2 -2
- package/dist/plugins/magic-link/index.mjs +0 -1
- package/dist/plugins/mcp/index.mjs +0 -4
- package/dist/plugins/multi-session/index.mjs +2 -2
- package/dist/plugins/oauth-proxy/index.mjs +44 -31
- package/dist/plugins/oauth-proxy/utils.mjs +3 -10
- package/dist/plugins/oidc-provider/index.mjs +0 -4
- package/dist/plugins/open-api/generator.mjs +16 -5
- package/dist/plugins/organization/adapter.mjs +61 -56
- package/dist/plugins/organization/client.d.mts +2 -1
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.mjs +3 -0
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/plugins/username/index.d.mts +24 -2
- package/dist/plugins/username/index.mjs +49 -3
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +18 -4
- package/dist/test-utils/headers.mjs +2 -7
- package/dist/test-utils/test-instance.d.mts +24 -6
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/url.d.mts +2 -1
- package/dist/utils/url.mjs +9 -3
- package/package.json +15 -14
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/cookies/cookie-utils.ts
|
|
2
2
|
function tryDecode(str) {
|
|
3
|
+
if (str.indexOf("%") === -1) return str;
|
|
3
4
|
try {
|
|
4
5
|
return decodeURIComponent(str);
|
|
5
6
|
} catch {
|
|
@@ -49,9 +50,9 @@ function parseSetCookieHeader(setCookie) {
|
|
|
49
50
|
splitSetCookieHeader(setCookie).forEach((cookieString) => {
|
|
50
51
|
const [nameValue, ...attributes] = cookieString.split(";").map((part) => part.trim());
|
|
51
52
|
const [name, ...valueParts] = (nameValue || "").split("=");
|
|
52
|
-
const value = valueParts.join("=");
|
|
53
|
-
if (!name
|
|
54
|
-
const attrObj = { value:
|
|
53
|
+
const value = unquoteCookieValue(valueParts.join("="));
|
|
54
|
+
if (!name) return;
|
|
55
|
+
const attrObj = { value: tryDecode(value) };
|
|
55
56
|
attributes.forEach((attribute) => {
|
|
56
57
|
const [attrName, ...attrValueParts] = attribute.split("=");
|
|
57
58
|
const attrValue = attrValueParts.join("=");
|
|
@@ -103,6 +104,69 @@ function toCookieOptions(attributes) {
|
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
107
|
+
* Cookie-name token char set per RFC 7230 §3.2.6.
|
|
108
|
+
*
|
|
109
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
|
|
110
|
+
*/
|
|
111
|
+
const cookieNameRegex = /^[\w!#$%&'*.^`|~+-]+$/;
|
|
112
|
+
/**
|
|
113
|
+
* Cookie-value char set per RFC 6265 §4.1.1, plus space and comma.
|
|
114
|
+
*
|
|
115
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
|
|
116
|
+
* @see https://github.com/golang/go/issues/7243
|
|
117
|
+
*/
|
|
118
|
+
const cookieValueRegex = /^[ !#-:<-[\]-~]*$/;
|
|
119
|
+
/**
|
|
120
|
+
* Strip surrounding double-quotes per RFC 6265 §4.1.1 quoted-string form.
|
|
121
|
+
*
|
|
122
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
|
|
123
|
+
*/
|
|
124
|
+
function unquoteCookieValue(value) {
|
|
125
|
+
if (value.length < 2 || !value.startsWith("\"") || !value.endsWith("\"")) return value;
|
|
126
|
+
return value.slice(1, -1);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Trim leading/trailing OWS (space / horizontal tab) per RFC 7230 §3.2.3.
|
|
130
|
+
* Narrower than `String.prototype.trim()`, which strips CR/LF and other
|
|
131
|
+
* whitespace and would let CTLs escape `cookieValueRegex`.
|
|
132
|
+
*
|
|
133
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3
|
|
134
|
+
*/
|
|
135
|
+
function trimOWS(s) {
|
|
136
|
+
let start = 0;
|
|
137
|
+
let end = s.length;
|
|
138
|
+
while (start < end) {
|
|
139
|
+
const c = s.charCodeAt(start);
|
|
140
|
+
if (c !== 32 && c !== 9) break;
|
|
141
|
+
start++;
|
|
142
|
+
}
|
|
143
|
+
while (end > start) {
|
|
144
|
+
const c = s.charCodeAt(end - 1);
|
|
145
|
+
if (c !== 32 && c !== 9) break;
|
|
146
|
+
end--;
|
|
147
|
+
}
|
|
148
|
+
return start === 0 && end === s.length ? s : s.slice(start, end);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Tolerates `;` separators without the SP that RFC 6265 §4.2.1 mandates,
|
|
152
|
+
* since proxies and runtimes commonly strip it. Silently drops entries
|
|
153
|
+
* whose name violates RFC 7230 token or whose value violates RFC 6265
|
|
154
|
+
* cookie-octet (plus space and comma). Strips optional surrounding
|
|
155
|
+
* double-quotes per RFC 6265 §4.1.1.
|
|
156
|
+
*/
|
|
157
|
+
function parseCookies(cookie) {
|
|
158
|
+
const cookieMap = /* @__PURE__ */ new Map();
|
|
159
|
+
if (cookie.length < 2) return cookieMap;
|
|
160
|
+
for (const chunk of cookie.split(";")) {
|
|
161
|
+
const eq = chunk.indexOf("=");
|
|
162
|
+
if (eq === -1) continue;
|
|
163
|
+
const key = trimOWS(chunk.slice(0, eq));
|
|
164
|
+
const val = unquoteCookieValue(trimOWS(chunk.slice(eq + 1)));
|
|
165
|
+
if (cookieNameRegex.test(key) && cookieValueRegex.test(val)) cookieMap.set(key, tryDecode(val));
|
|
166
|
+
}
|
|
167
|
+
return cookieMap;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
106
170
|
* Add or replace a cookie in the request `Cookie` header.
|
|
107
171
|
*
|
|
108
172
|
* Cookie pairs are joined with `; `, but `headers.append("cookie", ...)`
|
|
@@ -111,30 +175,29 @@ function toCookieOptions(attributes) {
|
|
|
111
175
|
* parse-mutate-serialize.
|
|
112
176
|
*/
|
|
113
177
|
function setRequestCookie(headers, name, value) {
|
|
114
|
-
const cookieMap =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
const cookieMap = parseCookies(headers.get("cookie") || "");
|
|
179
|
+
if (cookieNameRegex.test(name)) cookieMap.set(name, value);
|
|
180
|
+
headers.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${encodeURIComponent(v)}`).join("; "));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Merge `Set-Cookie` header values into the target's `Cookie` header.
|
|
184
|
+
* Mutates `target`.
|
|
185
|
+
*
|
|
186
|
+
* Name/value-level merge only. RFC 6265 §5 user-agent semantics
|
|
187
|
+
* (expiration, domain/path scoping, ordering) are out of scope. Suitable
|
|
188
|
+
* for single-request proxy, middleware, and test contexts.
|
|
189
|
+
*/
|
|
190
|
+
function applySetCookies(target, setCookieValues) {
|
|
191
|
+
const cookieMap = parseCookies(target.get("cookie") || "");
|
|
192
|
+
for (const setCookie of setCookieValues) for (const [name, attr] of parseSetCookieHeader(setCookie)) if (cookieNameRegex.test(name)) cookieMap.set(name, attr.value);
|
|
193
|
+
target.set("cookie", Array.from(cookieMap, ([k, v]) => `${k}=${encodeURIComponent(v)}`).join("; "));
|
|
122
194
|
}
|
|
123
195
|
function setCookieToHeader(headers) {
|
|
124
196
|
return (context) => {
|
|
125
197
|
const setCookieHeader = context.response.headers.get("set-cookie");
|
|
126
198
|
if (!setCookieHeader) return;
|
|
127
|
-
|
|
128
|
-
(headers.get("cookie") || "").split(";").forEach((cookie) => {
|
|
129
|
-
const [name, ...rest] = cookie.trim().split("=");
|
|
130
|
-
if (name && rest.length > 0) cookieMap.set(name, rest.join("="));
|
|
131
|
-
});
|
|
132
|
-
parseSetCookieHeader(setCookieHeader).forEach((value, name) => {
|
|
133
|
-
cookieMap.set(name, value.value);
|
|
134
|
-
});
|
|
135
|
-
const updatedCookies = Array.from(cookieMap.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
136
|
-
headers.set("cookie", updatedCookies);
|
|
199
|
+
applySetCookies(headers, [setCookieHeader]);
|
|
137
200
|
};
|
|
138
201
|
}
|
|
139
202
|
//#endregion
|
|
140
|
-
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
203
|
+
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
package/dist/cookies/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Session, User } from "../types/models.mjs";
|
|
2
|
-
import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
2
|
+
import { CookieAttributes, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
3
3
|
import { createSessionStore, getAccountCookie, getChunkedCookie } from "./session-store.mjs";
|
|
4
4
|
import { BetterAuthCookie, BetterAuthCookies, BetterAuthOptions, GenericEndpointContext } from "@better-auth/core";
|
|
5
5
|
import * as better_call0 from "better-call";
|
|
@@ -95,7 +95,6 @@ declare function setSessionCookie(ctx: GenericEndpointContext, session: {
|
|
|
95
95
|
*/
|
|
96
96
|
declare function expireCookie(ctx: GenericEndpointContext, cookie: BetterAuthCookie): void;
|
|
97
97
|
declare function deleteSessionCookie(ctx: GenericEndpointContext, skipDontRememberMe?: boolean | undefined): void;
|
|
98
|
-
declare function parseCookies(cookieHeader: string): Map<string, string>;
|
|
99
98
|
type EligibleCookies = (string & {}) | (keyof BetterAuthCookies & {});
|
|
100
99
|
declare const getSessionCookie: (request: Request | Headers, config?: {
|
|
101
100
|
cookiePrefix?: string;
|
|
@@ -116,4 +115,4 @@ declare const getCookieCache: <S extends {
|
|
|
116
115
|
version?: string | ((session: Session & Record<string, any>, user: User & Record<string, any>) => string) | ((session: Session & Record<string, any>, user: User & Record<string, any>) => Promise<string>);
|
|
117
116
|
} | undefined) => Promise<S | null>;
|
|
118
117
|
//#endregion
|
|
119
|
-
export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
118
|
+
export { CookieAttributes, EligibleCookies, HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
package/dist/cookies/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { parseUserOutput } from "../db/schema.mjs";
|
|
|
4
4
|
import { getDate } from "../utils/date.mjs";
|
|
5
5
|
import { isPromise } from "../utils/is-promise.mjs";
|
|
6
6
|
import { sec } from "../utils/time.mjs";
|
|
7
|
-
import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
7
|
+
import { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, parseCookies, parseSetCookieHeader, setCookieToHeader, setRequestCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions } from "./cookie-utils.mjs";
|
|
8
8
|
import { createAccountStore, createSessionStore, getAccountCookie, getChunkedCookie, setAccountCookie } from "./session-store.mjs";
|
|
9
9
|
import { env, isProduction } from "@better-auth/core/env";
|
|
10
10
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
@@ -134,9 +134,46 @@ async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
|
|
|
134
134
|
ctx.context.setNewSession(session);
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
|
+
* Remove any prior `Set-Cookie` entries on the current response whose cookie
|
|
138
|
+
* name matches `cookieName` or any chunked variant (`${cookieName}.0`, etc.).
|
|
139
|
+
*
|
|
140
|
+
* Prevents a valid cookie value from leaking on the wire when the same cookie
|
|
141
|
+
* is set and then expired within a single request (e.g. `/sign-in/email`
|
|
142
|
+
* writes credential session cookies and the 2FA after-hook expires them).
|
|
143
|
+
* Browsers honor the expiring entry, but anything reading the raw response
|
|
144
|
+
* headers — proxy/LB logs, server-side SDK consumers, observability tools —
|
|
145
|
+
* sees the earlier valid value and could replay it (bypassing the 2FA gate
|
|
146
|
+
* when the cookie cache is enabled).
|
|
147
|
+
*
|
|
148
|
+
* Scrubs both the local middleware scope's `responseHeaders` and the outer
|
|
149
|
+
* endpoint scope's `ctx.context.responseHeaders`, because plugin after-hooks
|
|
150
|
+
* run in a fresh local scope while accumulated response headers live on the
|
|
151
|
+
* outer one. `scoped.context` is required by {@link GenericEndpointContext}
|
|
152
|
+
* but unit-test mocks pass a minimal object via `as any`, so we use optional
|
|
153
|
+
* chaining defensively. The `Set` collapses the case where both scopes
|
|
154
|
+
* reference the same `Headers`.
|
|
155
|
+
*/
|
|
156
|
+
function removeSetCookieEntries(ctx, cookieName) {
|
|
157
|
+
const scoped = ctx;
|
|
158
|
+
const targets = /* @__PURE__ */ new Set();
|
|
159
|
+
if (scoped.responseHeaders) targets.add(scoped.responseHeaders);
|
|
160
|
+
if (scoped.context?.responseHeaders) targets.add(scoped.context.responseHeaders);
|
|
161
|
+
const exact = `${cookieName}=`;
|
|
162
|
+
const chunk = `${cookieName}.`;
|
|
163
|
+
for (const headers of targets) {
|
|
164
|
+
const existing = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitSetCookieHeader(headers.get("set-cookie") || "");
|
|
165
|
+
if (!existing.length) continue;
|
|
166
|
+
const survivors = existing.filter((entry) => !entry.startsWith(exact) && !entry.startsWith(chunk));
|
|
167
|
+
if (survivors.length === existing.length) continue;
|
|
168
|
+
headers.delete("set-cookie");
|
|
169
|
+
for (const entry of survivors) headers.append("set-cookie", entry);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
137
173
|
* Expires a cookie by setting `maxAge: 0` while preserving its attributes
|
|
138
174
|
*/
|
|
139
175
|
function expireCookie(ctx, cookie) {
|
|
176
|
+
removeSetCookieEntries(ctx, cookie.name);
|
|
140
177
|
ctx.setCookie(cookie.name, "", {
|
|
141
178
|
...cookie.attributes,
|
|
142
179
|
maxAge: 0
|
|
@@ -157,15 +194,6 @@ function deleteSessionCookie(ctx, skipDontRememberMe) {
|
|
|
157
194
|
sessionStore.setCookies(cleanCookies);
|
|
158
195
|
if (!skipDontRememberMe) expireCookie(ctx, ctx.context.authCookies.dontRememberToken);
|
|
159
196
|
}
|
|
160
|
-
function parseCookies(cookieHeader) {
|
|
161
|
-
const cookies = cookieHeader.split("; ");
|
|
162
|
-
const cookieMap = /* @__PURE__ */ new Map();
|
|
163
|
-
cookies.forEach((cookie) => {
|
|
164
|
-
const [name, value] = cookie.split(/=(.*)/s);
|
|
165
|
-
cookieMap.set(name, value);
|
|
166
|
-
});
|
|
167
|
-
return cookieMap;
|
|
168
|
-
}
|
|
169
197
|
const getSessionCookie = (request, config) => {
|
|
170
198
|
const cookies = (request instanceof Headers || !("headers" in request) ? request : request.headers).get("cookie");
|
|
171
199
|
if (!cookies) return null;
|
|
@@ -258,4 +286,4 @@ const getCookieCache = async (request, config) => {
|
|
|
258
286
|
return null;
|
|
259
287
|
};
|
|
260
288
|
//#endregion
|
|
261
|
-
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
289
|
+
export { HOST_COOKIE_PREFIX, SECURE_COOKIE_PREFIX, applySetCookies, cookieNameRegex, createCookieGetter, createSessionStore, deleteSessionCookie, expireCookie, getAccountCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setRequestCookie, setSessionCookie, splitSetCookieHeader, stripSecureCookiePrefix, toCookieOptions };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { symmetricDecodeJWT, symmetricEncodeJWT } from "../crypto/jwt.mjs";
|
|
2
|
+
import { parseCookies } from "./cookie-utils.mjs";
|
|
2
3
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
3
4
|
import * as z from "zod";
|
|
4
5
|
//#region src/cookies/session-store.ts
|
|
@@ -6,20 +7,6 @@ const ALLOWED_COOKIE_SIZE = 4096;
|
|
|
6
7
|
const ESTIMATED_EMPTY_COOKIE_SIZE = 200;
|
|
7
8
|
const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE;
|
|
8
9
|
/**
|
|
9
|
-
* Parse cookies from the request headers
|
|
10
|
-
*/
|
|
11
|
-
function parseCookiesFromContext(ctx) {
|
|
12
|
-
const cookieHeader = ctx.headers?.get("cookie");
|
|
13
|
-
if (!cookieHeader) return {};
|
|
14
|
-
const cookies = {};
|
|
15
|
-
const pairs = cookieHeader.split("; ");
|
|
16
|
-
for (const pair of pairs) {
|
|
17
|
-
const [name, ...valueParts] = pair.split("=");
|
|
18
|
-
if (name && valueParts.length > 0) cookies[name] = valueParts.join("=");
|
|
19
|
-
}
|
|
20
|
-
return cookies;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
10
|
* Extract the chunk index from a cookie name
|
|
24
11
|
*/
|
|
25
12
|
function getChunkIndex(cookieName) {
|
|
@@ -33,8 +20,8 @@ function getChunkIndex(cookieName) {
|
|
|
33
20
|
*/
|
|
34
21
|
function readExistingChunks(cookieName, ctx) {
|
|
35
22
|
const chunks = {};
|
|
36
|
-
const cookies =
|
|
37
|
-
for (const [name, value] of
|
|
23
|
+
const cookies = parseCookies(ctx.headers?.get("cookie") || "");
|
|
24
|
+
for (const [name, value] of cookies) if (name.startsWith(cookieName)) chunks[name] = value;
|
|
38
25
|
return chunks;
|
|
39
26
|
}
|
|
40
27
|
/**
|
|
@@ -140,13 +127,7 @@ function getChunkedCookie(ctx, cookieName) {
|
|
|
140
127
|
const chunks = [];
|
|
141
128
|
const cookieHeader = ctx.headers?.get("cookie");
|
|
142
129
|
if (!cookieHeader) return null;
|
|
143
|
-
const
|
|
144
|
-
const pairs = cookieHeader.split("; ");
|
|
145
|
-
for (const pair of pairs) {
|
|
146
|
-
const [name, ...valueParts] = pair.split("=");
|
|
147
|
-
if (name && valueParts.length > 0) cookies[name] = valueParts.join("=");
|
|
148
|
-
}
|
|
149
|
-
for (const [name, val] of Object.entries(cookies)) if (name.startsWith(cookieName + ".")) {
|
|
130
|
+
for (const [name, val] of parseCookies(cookieHeader)) if (name.startsWith(cookieName + ".")) {
|
|
150
131
|
const indexStr = name.split(".").at(-1);
|
|
151
132
|
const index = parseInt(indexStr || "0", 10);
|
|
152
133
|
if (!isNaN(index)) chunks.push({
|
|
@@ -269,13 +269,14 @@ async function getMigrations(config) {
|
|
|
269
269
|
return `${model}.${field}`;
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
+
const deferredIndexes = [];
|
|
272
273
|
if (toBeAdded.length) for (const table of toBeAdded) for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
273
274
|
const type = getType(field, fieldName);
|
|
274
275
|
const builder = db.schema.alterTable(table.table);
|
|
275
276
|
if (field.index) {
|
|
276
277
|
const indexName = `${table.table}_${fieldName}_${field.unique ? "uidx" : "idx"}`;
|
|
277
278
|
const indexBuilder = db.schema.createIndex(indexName).on(table.table).columns([fieldName]);
|
|
278
|
-
|
|
279
|
+
deferredIndexes.push(field.unique ? indexBuilder.unique() : indexBuilder);
|
|
279
280
|
}
|
|
280
281
|
const built = builder.addColumn(fieldName, type, (col) => {
|
|
281
282
|
col = field.required !== false ? col.notNull() : col;
|
|
@@ -287,7 +288,6 @@ async function getMigrations(config) {
|
|
|
287
288
|
});
|
|
288
289
|
migrations.push(built);
|
|
289
290
|
}
|
|
290
|
-
const toBeIndexed = [];
|
|
291
291
|
if (toBeCreated.length) for (const table of toBeCreated) {
|
|
292
292
|
const idType = getType({ type: useNumberId ? "number" : "string" }, "id");
|
|
293
293
|
let dbT = db.schema.createTable(table.table).addColumn("id", idType, (col) => {
|
|
@@ -315,12 +315,12 @@ async function getMigrations(config) {
|
|
|
315
315
|
});
|
|
316
316
|
if (field.index) {
|
|
317
317
|
const builder = db.schema.createIndex(`${table.table}_${fieldName}_${field.unique ? "uidx" : "idx"}`).on(table.table).columns([fieldName]);
|
|
318
|
-
|
|
318
|
+
deferredIndexes.push(field.unique ? builder.unique() : builder);
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
migrations.push(dbT);
|
|
322
322
|
}
|
|
323
|
-
|
|
323
|
+
for (const index of deferredIndexes) migrations.push(index);
|
|
324
324
|
async function runMigrations() {
|
|
325
325
|
for (const migration of migrations) await migration.execute();
|
|
326
326
|
}
|
package/dist/db/index.d.mts
CHANGED
|
@@ -3,7 +3,7 @@ import { convertFromDB, convertToDB } from "./field-converter.mjs";
|
|
|
3
3
|
import { getSchema } from "./get-schema.mjs";
|
|
4
4
|
import { DatabaseHooksEntry, getWithHooks } from "./with-hooks.mjs";
|
|
5
5
|
import { createInternalAdapter } from "./internal-adapter.mjs";
|
|
6
|
-
import { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
|
|
6
|
+
import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
|
|
7
7
|
import { FieldAttributeToSchema, toZodSchema } from "./to-zod.mjs";
|
|
8
8
|
export * from "@better-auth/core/db";
|
|
9
|
-
export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
|
|
9
|
+
export { DatabaseHooksEntry, FieldAttributeToObject, FieldAttributeToSchema, InferAdditionalFieldsFromPluginOptions, InferFieldsInputClient, InferFieldsOutput, RemoveFieldsWithReturnedFalse, buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
|
package/dist/db/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __exportAll, __reExport } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
2
|
import { getSchema } from "./get-schema.mjs";
|
|
3
|
-
import { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
|
|
3
|
+
import { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput } from "./schema.mjs";
|
|
4
4
|
import { convertFromDB, convertToDB } from "./field-converter.mjs";
|
|
5
5
|
import { getWithHooks } from "./with-hooks.mjs";
|
|
6
6
|
import { createInternalAdapter } from "./internal-adapter.mjs";
|
|
@@ -8,6 +8,7 @@ import { toZodSchema } from "./to-zod.mjs";
|
|
|
8
8
|
export * from "@better-auth/core/db";
|
|
9
9
|
//#region src/db/index.ts
|
|
10
10
|
var db_exports = /* @__PURE__ */ __exportAll({
|
|
11
|
+
buildSyntheticUserOutput: () => buildSyntheticUserOutput,
|
|
11
12
|
convertFromDB: () => convertFromDB,
|
|
12
13
|
convertToDB: () => convertToDB,
|
|
13
14
|
createInternalAdapter: () => createInternalAdapter,
|
|
@@ -28,4 +29,4 @@ var db_exports = /* @__PURE__ */ __exportAll({
|
|
|
28
29
|
import * as import__better_auth_core_db from "@better-auth/core/db";
|
|
29
30
|
__reExport(db_exports, import__better_auth_core_db);
|
|
30
31
|
//#endregion
|
|
31
|
-
export { convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
|
|
32
|
+
export { buildSyntheticUserOutput, convertFromDB, convertToDB, createInternalAdapter, db_exports, getSchema, getSessionDefaultFields, getWithHooks, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput, toZodSchema };
|
|
@@ -639,17 +639,23 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
639
639
|
const storageOption = getStorageOption(identifier, options.verification?.storeIdentifier);
|
|
640
640
|
const storedIdentifier = await processIdentifier(identifier, storageOption);
|
|
641
641
|
const identifiersToTry = storageOption && storageOption !== "plain" ? [storedIdentifier, identifier] : [storedIdentifier];
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
642
|
+
const hydrateCachedVerification = (raw) => {
|
|
643
|
+
if (!raw) return null;
|
|
644
|
+
const candidate = typeof raw === "string" ? safeJSONParse(raw) : typeof raw === "object" ? raw : null;
|
|
645
|
+
if (!candidate) return null;
|
|
646
|
+
const expiresAt = new Date(candidate.expiresAt);
|
|
647
|
+
if (!Number.isFinite(expiresAt.getTime())) return null;
|
|
648
|
+
return {
|
|
649
|
+
...candidate,
|
|
650
|
+
expiresAt
|
|
648
651
|
};
|
|
652
|
+
};
|
|
653
|
+
let consumed = null;
|
|
654
|
+
if (secondaryStorage && !options.verification?.storeInDatabase) {
|
|
649
655
|
const consumeCacheKey = async (key) => {
|
|
650
|
-
if (secondaryStorage.getAndDelete) return
|
|
656
|
+
if (secondaryStorage.getAndDelete) return hydrateCachedVerification(await secondaryStorage.getAndDelete(key));
|
|
651
657
|
return withVerificationConsumeLock(key, async () => {
|
|
652
|
-
const parsed =
|
|
658
|
+
const parsed = hydrateCachedVerification(await secondaryStorage.get(key));
|
|
653
659
|
if (!parsed) return null;
|
|
654
660
|
await secondaryStorage.delete(key);
|
|
655
661
|
return parsed;
|
|
@@ -659,17 +665,16 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
659
665
|
const cached = await consumeCacheKey(`verification:${stored}`);
|
|
660
666
|
if (!cached) continue;
|
|
661
667
|
await Promise.all(identifiersToTry.filter((candidate) => candidate !== stored).map((candidate) => secondaryStorage.delete(`verification:${candidate}`)));
|
|
662
|
-
|
|
668
|
+
consumed = cached;
|
|
669
|
+
break;
|
|
663
670
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
async function consumeByIdentifier(id) {
|
|
667
|
-
const where = [{
|
|
668
|
-
field: "identifier",
|
|
669
|
-
value: id
|
|
670
|
-
}];
|
|
671
|
-
return withVerificationConsumeLock(`verification:${id}`, () => runWithTransaction(adapter, async () => {
|
|
671
|
+
} else {
|
|
672
|
+
const consumeByIdentifier = async (id) => withVerificationConsumeLock(`verification:${id}`, () => runWithTransaction(adapter, async () => {
|
|
672
673
|
const txAdapter = await getCurrentAdapter(adapter);
|
|
674
|
+
const where = [{
|
|
675
|
+
field: "identifier",
|
|
676
|
+
value: id
|
|
677
|
+
}];
|
|
673
678
|
const latest = (await txAdapter.findMany({
|
|
674
679
|
model: "verification",
|
|
675
680
|
where,
|
|
@@ -680,30 +685,32 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
680
685
|
limit: 1
|
|
681
686
|
}))[0] ?? null;
|
|
682
687
|
if (!latest) return null;
|
|
683
|
-
|
|
688
|
+
return consumeOneWithHooks("verification", [{
|
|
684
689
|
field: "id",
|
|
685
690
|
value: latest.id
|
|
686
|
-
}]
|
|
687
|
-
|
|
688
|
-
const consumed = await txAdapter.consumeOne({
|
|
691
|
+
}], async () => {
|
|
692
|
+
const row = await txAdapter.consumeOne({
|
|
689
693
|
model: "verification",
|
|
690
|
-
where:
|
|
694
|
+
where: [{
|
|
695
|
+
field: "id",
|
|
696
|
+
value: latest.id
|
|
697
|
+
}]
|
|
691
698
|
});
|
|
692
|
-
if (!
|
|
699
|
+
if (!row) return null;
|
|
693
700
|
await txAdapter.deleteMany({
|
|
694
701
|
model: "verification",
|
|
695
702
|
where
|
|
696
703
|
});
|
|
697
|
-
return
|
|
704
|
+
return row;
|
|
698
705
|
}, latest);
|
|
699
706
|
}));
|
|
707
|
+
for (const stored of identifiersToTry) {
|
|
708
|
+
consumed = await consumeByIdentifier(stored);
|
|
709
|
+
if (consumed) break;
|
|
710
|
+
}
|
|
711
|
+
if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
|
|
700
712
|
}
|
|
701
|
-
|
|
702
|
-
for (const stored of identifiersToTry) {
|
|
703
|
-
consumed = await consumeByIdentifier(stored);
|
|
704
|
-
if (consumed) break;
|
|
705
|
-
}
|
|
706
|
-
if (consumed && secondaryStorage) await Promise.all(identifiersToTry.map((stored) => secondaryStorage.delete(`verification:${stored}`)));
|
|
713
|
+
if (!consumed || consumed.expiresAt < /* @__PURE__ */ new Date()) return null;
|
|
707
714
|
return consumed;
|
|
708
715
|
},
|
|
709
716
|
updateVerificationByIdentifier: async (identifier, data) => {
|
package/dist/db/schema.d.mts
CHANGED
|
@@ -4,8 +4,21 @@ import { BetterAuthPluginDBSchema, DBFieldAttribute } from "@better-auth/core/db
|
|
|
4
4
|
|
|
5
5
|
//#region src/db/schema.d.ts
|
|
6
6
|
declare function parseUserOutput<T extends User$1>(options: BetterAuthOptions, user: T): T;
|
|
7
|
+
/**
|
|
8
|
+
* Builds a synthetic user object that matches the shape of a real user
|
|
9
|
+
* returned from the database. This ensures enumeration protection works
|
|
10
|
+
* correctly by making synthetic and real user responses indistinguishable.
|
|
11
|
+
*
|
|
12
|
+
* The function iterates over the user output schema and:
|
|
13
|
+
* - Includes all fields that should be returned (returned !== false)
|
|
14
|
+
* - Uses provided values when available
|
|
15
|
+
* - Sets optional fields to null when no value is provided
|
|
16
|
+
* - Applies default values where defined
|
|
17
|
+
* - Always includes the 'id' field (not part of schema but always present)
|
|
18
|
+
*/
|
|
19
|
+
declare function buildSyntheticUserOutput(options: BetterAuthOptions, data: Record<string, unknown>): Record<string, unknown>;
|
|
7
20
|
declare function parseSessionOutput<T extends Session$1>(options: BetterAuthOptions, session: T): T;
|
|
8
|
-
declare function parseAccountOutput<T extends Account>(options: BetterAuthOptions, account: T): Omit<T, "idToken" | "accessToken" | "refreshToken" | "
|
|
21
|
+
declare function parseAccountOutput<T extends Account>(options: BetterAuthOptions, account: T): Omit<T, "idToken" | "accessToken" | "refreshToken" | "password" | "accessTokenExpiresAt" | "refreshTokenExpiresAt">;
|
|
9
22
|
declare function parseInputData<T extends Record<string, any>>(data: T, schema: {
|
|
10
23
|
fields: Record<string, DBFieldAttribute>;
|
|
11
24
|
action?: ("create" | "update") | undefined;
|
|
@@ -45,4 +58,4 @@ declare function mergeSchema<S extends BetterAuthPluginDBSchema>(schema: S, newS
|
|
|
45
58
|
} | undefined;
|
|
46
59
|
} | undefined } | undefined): S;
|
|
47
60
|
//#endregion
|
|
48
|
-
export { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
|
|
61
|
+
export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
|
package/dist/db/schema.mjs
CHANGED
|
@@ -24,6 +24,31 @@ function getFields(options, modelName, mode) {
|
|
|
24
24
|
function parseUserOutput(options, user) {
|
|
25
25
|
return filterOutputFields(user, getFields(options, "user", "output"));
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Builds a synthetic user object that matches the shape of a real user
|
|
29
|
+
* returned from the database. This ensures enumeration protection works
|
|
30
|
+
* correctly by making synthetic and real user responses indistinguishable.
|
|
31
|
+
*
|
|
32
|
+
* The function iterates over the user output schema and:
|
|
33
|
+
* - Includes all fields that should be returned (returned !== false)
|
|
34
|
+
* - Uses provided values when available
|
|
35
|
+
* - Sets optional fields to null when no value is provided
|
|
36
|
+
* - Applies default values where defined
|
|
37
|
+
* - Always includes the 'id' field (not part of schema but always present)
|
|
38
|
+
*/
|
|
39
|
+
function buildSyntheticUserOutput(options, data) {
|
|
40
|
+
const schema = getFields(options, "user", "output");
|
|
41
|
+
const result = {};
|
|
42
|
+
for (const key in schema) {
|
|
43
|
+
const fieldAttr = schema[key];
|
|
44
|
+
if (fieldAttr.returned === false) continue;
|
|
45
|
+
if (key in data && data[key] !== void 0) result[key] = data[key];
|
|
46
|
+
else if (fieldAttr.defaultValue !== void 0) result[key] = typeof fieldAttr.defaultValue === "function" ? fieldAttr.defaultValue() : fieldAttr.defaultValue;
|
|
47
|
+
else if (!fieldAttr.required) result[key] = null;
|
|
48
|
+
}
|
|
49
|
+
if ("id" in data) result.id = data.id;
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
27
52
|
function parseSessionOutput(options, session) {
|
|
28
53
|
return filterOutputFields(session, getFields(options, "session", "output"));
|
|
29
54
|
}
|
|
@@ -121,4 +146,4 @@ function mergeSchema(schema, newSchema) {
|
|
|
121
146
|
return schema;
|
|
122
147
|
}
|
|
123
148
|
//#endregion
|
|
124
|
-
export { getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
|
|
149
|
+
export { buildSyntheticUserOutput, getSessionDefaultFields, mergeSchema, parseAccountInput, parseAccountOutput, parseAdditionalUserInput, parseInputData, parseSessionInput, parseSessionOutput, parseUserInput, parseUserOutput };
|
package/dist/index.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ import { betterAuth } from "./auth/full.mjs";
|
|
|
10
10
|
import { generateState, parseState } from "./oauth2/state.mjs";
|
|
11
11
|
import { StateData, generateGenericState, parseGenericState } from "./state.mjs";
|
|
12
12
|
import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
|
|
13
|
-
import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL } from "./utils/url.mjs";
|
|
13
|
+
import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
|
|
14
14
|
import { APIError } from "./api/index.mjs";
|
|
15
15
|
import { StandardSchemaV1 } from "@better-auth/core";
|
|
16
16
|
import { getCurrentAdapter } from "@better-auth/core/context";
|
|
@@ -27,4 +27,4 @@ export * from "@better-auth/core/utils/json";
|
|
|
27
27
|
export * from "@better-auth/core/social-providers";
|
|
28
28
|
export * from "better-call";
|
|
29
29
|
export * from "zod";
|
|
30
|
-
export { APIError, Account, AdditionalSessionFieldsInput, AdditionalUserFieldsInput, Auth, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, ClientAtomListener, ClientStore, DBAdapter, DBAdapterInstance, DBAdapterSchemaCreation, DBTransactionAdapter, ExtractPluginField, FilteredAPI, HIDE_METADATA, HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPluginFieldFromTuple, InferPluginIDs, InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, JoinConfig, JoinOption, OverrideMerge, Prettify, PrettifyDeep, RateLimit, RequiredKeysOf, Session, SessionQueryParams, type StandardSchemaV1, StateData, StoreIdentifierOption, StripEmptyObjects, type TelemetryEvent, UnionToIntersection, User, Verification, Where, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL };
|
|
30
|
+
export { APIError, Account, AdditionalSessionFieldsInput, AdditionalUserFieldsInput, Auth, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, ClientAtomListener, ClientStore, DBAdapter, DBAdapterInstance, DBAdapterSchemaCreation, DBTransactionAdapter, ExtractPluginField, FilteredAPI, HIDE_METADATA, HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPluginFieldFromTuple, InferPluginIDs, InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, JoinConfig, JoinOption, OverrideMerge, Prettify, PrettifyDeep, RateLimit, RequiredKeysOf, Session, SessionQueryParams, type StandardSchemaV1, StateData, StoreIdentifierOption, StripEmptyObjects, type TelemetryEvent, UnionToIntersection, User, Verification, Where, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL } from "./utils/url.mjs";
|
|
1
|
+
import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
|
|
2
2
|
import { generateGenericState, parseGenericState } from "./state.mjs";
|
|
3
3
|
import { generateState, parseState } from "./oauth2/state.mjs";
|
|
4
4
|
import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
|
|
@@ -14,4 +14,4 @@ export * from "@better-auth/core/oauth2";
|
|
|
14
14
|
export * from "@better-auth/core/utils/error-codes";
|
|
15
15
|
export * from "@better-auth/core/utils/id";
|
|
16
16
|
export * from "@better-auth/core/utils/json";
|
|
17
|
-
export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL };
|
|
17
|
+
export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
|
package/dist/oauth2/errors.mjs
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
//#region src/oauth2/errors.ts
|
|
2
2
|
const HANDLING_DOCS_URL = "https://www.better-auth.com/docs/concepts/oauth#handling-providers-without-email";
|
|
3
3
|
/**
|
|
4
|
+
* Redirect the user to the OAuth error page with a machine-readable `error`
|
|
5
|
+
* code (and optional `error_description`).
|
|
6
|
+
*
|
|
7
|
+
* Every OAuth callback path routes its failures through this helper so the
|
|
8
|
+
* query parameter name, the `?`/`&` separator, and URL encoding are decided in
|
|
9
|
+
* one place. The error page reads the `error` query parameter, so callers must
|
|
10
|
+
* never hand-build the redirect with a different parameter name.
|
|
11
|
+
*/
|
|
12
|
+
function redirectOnError(ctx, errorURL, error, description) {
|
|
13
|
+
const params = new URLSearchParams({ error });
|
|
14
|
+
if (description) params.set("error_description", description);
|
|
15
|
+
const sep = errorURL.includes("?") ? "&" : "?";
|
|
16
|
+
throw ctx.redirect(`${errorURL}${sep}${params.toString()}`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
4
19
|
* Build the logger message shown when an OAuth provider does not return an
|
|
5
20
|
* email address. Kept in one place so every rejection site points users at
|
|
6
21
|
* the same workaround docs.
|
|
@@ -9,4 +24,4 @@ function missingEmailLogMessage(providerId, options) {
|
|
|
9
24
|
return `${options?.source === "generic" ? `Generic OAuth provider "${providerId}"` : `Provider "${providerId}"`} did not return an email${options?.source === "id_token" ? " in the id token" : ""}. Either request the provider's email scope, or synthesize one via \`mapProfileToUser\`. See ${HANDLING_DOCS_URL}`;
|
|
10
25
|
}
|
|
11
26
|
//#endregion
|
|
12
|
-
export { missingEmailLogMessage };
|
|
27
|
+
export { missingEmailLogMessage, redirectOnError };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAPIError } from "../utils/is-api-error.mjs";
|
|
2
2
|
import { setAccountCookie } from "../cookies/session-store.mjs";
|
|
3
|
+
import { redirectOnError } from "./errors.mjs";
|
|
3
4
|
import { setTokenUtil } from "./utils.mjs";
|
|
4
5
|
import { createEmailVerificationToken } from "../api/routes/email-verification.mjs";
|
|
5
6
|
import { isDevelopment, logger } from "@better-auth/core/env";
|
|
@@ -8,8 +9,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
8
9
|
const { userInfo, account, callbackURL, disableSignUp, overrideUserInfo } = opts;
|
|
9
10
|
const dbUser = await c.context.internalAdapter.findOAuthUser(userInfo.email.toLowerCase(), account.accountId, account.providerId).catch((e) => {
|
|
10
11
|
logger.error("Better auth was unable to query your database.\nError: ", e);
|
|
11
|
-
|
|
12
|
-
throw c.redirect(`${errorURL}?error=internal_server_error`);
|
|
12
|
+
redirectOnError(c, c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`, "internal_server_error");
|
|
13
13
|
});
|
|
14
14
|
let user = dbUser?.user;
|
|
15
15
|
const isRegister = !user;
|
|
@@ -96,7 +96,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
96
96
|
if (c.context.options.account?.storeAccountCookie) await setAccountCookie(c, createdAccount);
|
|
97
97
|
if (!userInfo.emailVerified && user && c.context.options.emailVerification?.sendOnSignUp && c.context.options.emailVerification?.sendVerificationEmail) {
|
|
98
98
|
const token = await createEmailVerificationToken(c.context.secret, user.email, void 0, c.context.options.emailVerification?.expiresIn);
|
|
99
|
-
const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
99
|
+
const url = `${c.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(callbackURL || "/")}`;
|
|
100
100
|
await c.context.runInBackgroundOrAwait(c.context.options.emailVerification.sendVerificationEmail({
|
|
101
101
|
user,
|
|
102
102
|
url,
|