feat: new client and more

This commit is contained in:
Bereket Engida
2024-08-24 23:34:12 +03:00
parent 7fd1a59eee
commit f53ce02ceb
44 changed files with 730 additions and 665 deletions
+1
View File
@@ -32,6 +32,7 @@
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.52.2",
"react-qr-code": "^2.0.15",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.0",
"tailwindcss-animate": "^1.0.7",
Binary file not shown.
+14 -5
View File
@@ -11,8 +11,9 @@ import {
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/client";
import { authClient } from "@/lib/auth-client";
import { useState } from "react";
import { Key } from "lucide-react";
export default function Page() {
const [email, setEmail] = useState("");
@@ -59,7 +60,7 @@ export default function Page() {
/>
</div>
<Button type="submit" className="w-full" onClick={async () => {
const res = await authClient.signInCredential({
await authClient.signIn.credential({
body: {
email,
password,
@@ -73,14 +74,22 @@ export default function Page() {
variant="outline"
className="w-full"
onClick={async () => {
await authClient.signInOAuth({
provider: "github",
callbackURL: "http://localhost:3000/",
await authClient.signIn.oauth({
body: {
provider: "github",
callbackURL: "http://localhost:3000",
}
});
}}
>
Login with Github
</Button>
<Button variant="secondary" className="gap-2" onClick={async () => {
}}>
<Key size={16} />
Login with Passkey
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
+1 -1
View File
@@ -13,7 +13,7 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import { authClient } from "@/lib/client";
import { authClient } from "@/lib/auth-client";
export default function SignUpForm() {
const [firstName, setFirstName] = useState("");
@@ -0,0 +1,77 @@
'use client'
import { useState } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
import { AlertCircle, CheckCircle2, Mail } from "lucide-react"
import { authClient } from '@/lib/auth-client'
export default function Component() {
const [otp, setOtp] = useState('')
const [isOtpSent, setIsOtpSent] = useState(false)
const [message, setMessage] = useState('')
const [isError, setIsError] = useState(false)
const [isValidated, setIsValidated] = useState(false)
// In a real app, this email would come from your authentication context
const userEmail = "user@example.com"
const requestOTP = async () => {
await authClient.twoFactor.sendOtp();
// In a real app, this would call your backend API to send the OTP
setMessage('OTP sent to your email')
setIsError(false)
setIsOtpSent(true)
}
const validateOTP = async () => {
await authClient.twoFactor.verifyOtp({
body: {
code: otp,
}
})
}
return (
<main className='flex flex-col items-center justify-center min-h-screen'>
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Two-Factor Authentication</CardTitle>
<CardDescription>Verify your identity with a one-time password</CardDescription>
</CardHeader>
<CardContent>
<div className="grid w-full items-center gap-4">
{!isOtpSent ? (
<Button onClick={requestOTP} className="w-full">
<Mail className="mr-2 h-4 w-4" /> Send OTP to Email
</Button>
) : (
<>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="otp">One-Time Password</Label>
<Input
id="otp"
placeholder="Enter 6-digit OTP"
value={otp}
onChange={(e) => setOtp(e.target.value)}
maxLength={6}
/>
</div>
<Button onClick={validateOTP} disabled={otp.length !== 6 || isValidated}>
Validate OTP
</Button>
</>
)}
</div>
{message && (
<div className={`flex items-center gap-2 mt-4 ${isError ? 'text-destructive' : 'text-primary'}`}>
{isError ? <AlertCircle className="h-4 w-4" /> : <CheckCircle2 className="h-4 w-4" />}
<p className="text-sm">{message}</p>
</div>
)}
</CardContent>
</Card>
</main>
)
}
@@ -6,7 +6,8 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { AlertCircle, CheckCircle2 } from "lucide-react"
import { authClient } from "@/lib/client"
import { authClient } from "@/lib/auth-client"
import Link from "next/link"
export default function Component() {
const [totpCode, setTotpCode] = useState("")
@@ -19,10 +20,10 @@ export default function Component() {
setError("TOTP code must be 6 digits")
return
}
authClient.verifyTotp({
authClient.twoFactor.verify({
body: {
code: totpCode,
callbackURL: "/"
with: "totp",
}
}).then((res) => {
console.log(res)
@@ -78,8 +79,12 @@ export default function Component() {
</div>
)}
</CardContent>
<CardFooter className="text-sm text-muted-foreground">
Protect your account with TOTP-based authentication
<CardFooter className="text-sm text-muted-foreground gap-2">
<Link href="/two-factor/otp">
<Button variant="link" size="sm">
Switch to Email Verification
</Button>
</Link>
</CardFooter>
</Card>
</main>
+1 -1
View File
@@ -28,7 +28,7 @@ export default async function Home() {
<Button>signin</Button>
</Link>
)}
<Organization />
{/* <Organization /> */}
<Client />
</main>
);
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import { setCounter } from "@/server/counter";
import { Button } from "./ui/button";
import { client } from "@/lib/client";
import { client } from "@/lib/auth-client";
export async function AddCount() {
return (
+60 -19
View File
@@ -1,10 +1,15 @@
"use client";
import { authClient } from "@/lib/client";
import { authClient } from "@/lib/auth-client";
import { useAuthStore } from "better-auth/react"
import { Button } from "./ui/button";
import QRCode from "react-qr-code";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader } from "./ui/card";
import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog";
export function Client() {
const session = useAuthStore(authClient.$session)
const [uri, setUri] = useState<string>()
const session = authClient.useSession()
type S = NonNullable<typeof session>
const a: S['user'] = {
id: "1",
@@ -14,23 +19,59 @@ export function Client() {
createdAt: new Date(),
updatedAt: new Date(),
}
useEffect(() => {
if (session?.user?.twoFactorEnabled) {
authClient.twoFactor.getTotpUri().then((res) => {
if (res.data) {
setUri(res.data.totpURI)
}
})
}
}, [session])
return (
<div>
{
session ? <div>
<Button onClick={async () => {
if (session.user.twoFactorEnabled) {
await authClient.disableTotp()
} else {
await authClient.enableTotp()
}
}}>
{
session.user.twoFactorEnabled ? "Disable" : "Enable"
}
</Button>
</div> : null
}
</div>
<Card>
<CardHeader>
</CardHeader>
<CardContent className="flex items-center justify-center gap-2">
{
session ? <div>
<Button onClick={async () => {
if (session.user?.twoFactorEnabled) {
await authClient.twoFactor.disable()
} else {
await authClient.twoFactor.enable()
}
}}>
{
session.user?.twoFactorEnabled ? "Disable 2FA" : "Enable 2FA"
}
</Button>
</div> : null
}
{
uri ? <Dialog>
<DialogTrigger asChild>
<Button variant="outline">
View TOTP URI
</Button>
</DialogTrigger>
<DialogContent className="flex flex-col items-center justify-center">
{
uri ? <div>
<p>
URI to scan
</p>
<QRCode value={uri} />
</div> : null
}
</DialogContent>
</Dialog> : null
}
</CardContent>
</Card>
)
}
+1 -1
View File
@@ -26,7 +26,7 @@ import {
TableHeader,
TableRow,
} from "./ui/table";
import { authClient } from "@/lib/client";
import { authClient } from "@/lib/auth-client";
import { useAuthStore } from "better-auth/react";
export const Organization = () => {
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { authClient } from "@/lib/client";
import { authClient } from "@/lib/auth-client";
import { Button } from "./ui/button";
export const SignOut = () => {
@@ -1,4 +1,4 @@
import { createAuthClient } from "better-auth/client";
import { createAuthClient } from "better-auth/react";
import { auth } from "./auth";
export const authClient = createAuthClient<typeof auth>({
+7 -5
View File
@@ -1,6 +1,6 @@
import { betterAuth } from "better-auth";
import { github, passkey } from "better-auth/provider";
import { twoFactor } from "better-auth/plugins";
import { organization, twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
basePath: "/api/auth",
@@ -9,10 +9,6 @@ export const auth = betterAuth({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
}),
passkey({
rpID: "localhost",
rpName: "Better Auth",
}),
],
database: {
provider: "sqlite",
@@ -23,9 +19,15 @@ export const auth = betterAuth({
enabled: true,
},
plugins: [
organization(),
twoFactor({
issuer: "BetterAuth",
twoFactorURL: "/two-factor",
otpOptions: {
async sendOTP(user, otp) {
console.log({ user, otp });
},
},
}),
],
});
+2 -1
View File
@@ -52,12 +52,13 @@
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.1",
"arctic": "^1.9.2",
"better-call": "^0.1.28",
"better-call": "^0.1.33",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"consola": "^3.2.3",
"dotenv": "^16.4.5",
"jiti": "^1.21.6",
"jose": "^5.7.0",
"kysely": "^0.27.4",
"mysql2": "^3.11.0",
"nanostores": "^0.11.2",
@@ -1,4 +1,6 @@
import { z } from "zod";
import { BetterAuthOptions } from "../types";
import { FieldAttribute } from "../db";
export const accountSchema = z.object({
id: z.string(),
@@ -39,3 +41,53 @@ export interface MigrationTable {
name: string;
timestamp: string;
}
export function parseData<T extends Record<string, any>>(
data: T,
schema: {
fields: Record<string, FieldAttribute>;
},
) {
const fields = schema.fields;
const parsedData: Record<string, any> = {};
for (const key in data) {
const field = fields[key];
if (!field) {
parsedData[key] = data[key];
continue;
}
if (field.returned === false) {
continue;
}
parsedData[key] = data[key];
}
return parsedData as T;
}
export function getAllFields(options: BetterAuthOptions, table: string) {
let schema: Record<string, FieldAttribute> = {};
for (const plugin of options.plugins || []) {
if (plugin.schema && plugin.schema[table]) {
schema = {
...schema,
...plugin.schema[table].fields,
};
}
}
return schema;
}
export function parseUser(options: BetterAuthOptions, user: User) {
const schema = getAllFields(options, "user");
return parseData(user, { fields: schema });
}
export function parseAccount(options: BetterAuthOptions, account: Account) {
const schema = getAllFields(options, "account");
return parseData(account, { fields: schema });
}
export function parseSession(options: BetterAuthOptions, session: Session) {
const schema = getAllFields(options, "session");
return parseData(session, { fields: schema });
}
+20 -2
View File
@@ -10,6 +10,7 @@ import { AuthContext } from "../init";
import { csrfMiddleware } from "./middlewares/csrf";
import { getCSRFToken } from "./routes/csrf";
import { signUpCredential } from "./routes/signup";
import { parseAccount, parseSession, parseUser } from "../adapters/schema";
export const router = <C extends AuthContext>(ctx: C) => {
const pluginEndpoints = ctx.options.plugins?.reduce(
@@ -101,8 +102,25 @@ export const router = <C extends AuthContext>(ctx: C) => {
},
...middlewares,
],
onError(e) {
ctx.logger.error(e);
/**
* this is to remove any sensitive data from the response
*/
async transformResponse(res) {
const body = await res.json();
if (body?.user) {
body.user = parseUser(ctx.options, body.user);
}
if (body?.session) {
body.session = parseSession(ctx.options, body.session);
}
if (body?.account) {
body.account = parseAccount(ctx.options, body.account);
}
return new Response(body ? JSON.stringify(body) : null, {
headers: res.headers,
status: res.status,
statusText: res.statusText,
});
},
});
};
@@ -0,0 +1,14 @@
import { z } from "zod";
import { createAuthEndpoint } from "../call";
export const confirmEmail = createAuthEndpoint(
"/confirm-email",
{
method: "POST",
body: z.object({
token: z.string(),
email: z.string().email(),
}),
},
async (ctx) => {},
);
@@ -0,0 +1,46 @@
import { z } from "zod";
import { createAuthEndpoint } from "../call";
import { createJWT } from "oslo/jwt";
import { TimeSpan } from "oslo";
export const forgetPassword = createAuthEndpoint(
"/send-forget-password",
{
method: "POST",
body: z.object({
email: z.string().email(),
}),
},
async (ctx) => {
const { email } = ctx.body;
const user = await ctx.context.internalAdapter.findUserByEmail(email);
if (!user) {
return ctx.json(
{
error: "User not found",
},
{
status: 400,
statusText: "USER_NOT_FOUND",
body: {
message: "User not found",
},
},
);
}
const token = await createJWT(
"HS256",
Buffer.from(ctx.context.secret),
{
email: user.user.email,
},
{
expiresIn: new TimeSpan(1, "h"),
issuer: "better-auth",
subject: "forget-password",
audiences: [user.user.email],
includeIssuedTimestamp: true,
},
);
},
);
@@ -7,7 +7,7 @@ import { oAuthProviderList } from "../../providers";
import { Argon2id } from "oslo/password";
export const signInOAuth = createAuthEndpoint(
"/signin/oauth",
"/sign-in/oauth",
{
method: "POST",
query: z
+36 -117
View File
@@ -1,125 +1,44 @@
import { Endpoint, Prettify } from "better-call";
import { BetterAuth } from "../auth";
import { HasRequiredKeys, UnionToIntersection } from "type-fest";
import {
BetterFetchOption,
BetterFetchPlugin,
BetterFetchResponse,
createFetch,
} from "@better-fetch/fetch";
import { BetterAuthError } from "../error/better-auth-error";
import { createDynamicPathProxy } from "./proxy";
import { createClient } from "better-call/client";
import { BetterFetch, createFetch } from "@better-fetch/fetch";
import { getSessionAtom } from "./session-atom";
import { getOrganizationAtoms } from "./org-atoms";
import { getPasskeyActions } from "./passkey-actions";
import { addCurrentURL, csrfPlugin, redirectPlugin } from "./client-plugins";
import { InferRoutes } from "./path-to-object";
import { ClientOptions } from "./type";
import { getBaseURL } from "./client-utils";
type InferContext<T> = T extends (ctx: infer Ctx) => any
? Ctx extends
| {
body: infer Body;
}
| {
params: infer Param;
}
? (Body extends undefined
? {}
: {
body: Body;
}) &
(Param extends undefined
? {}
: {
params: Param;
})
: never
: never;
export interface ClientOptions extends BetterFetchOption {}
const redirectPlugin = {
id: "redirect",
name: "Redirect",
hooks: {
onSuccess(context) {
if (context.data.url && context.data.redirect) {
console.log("redirecting to", context.data.url);
}
},
},
} satisfies BetterFetchPlugin;
function inferBaeURL() {
const url =
process.env.AUTH_URL ||
process.env.NEXT_PUBLIC_AUTH_URL ||
process.env.BETTER_AUTH_URL ||
process.env.NEXT_PUBLIC_BETTER_AUTH_URL ||
process.env.VERCEL_URL ||
process.env.NEXT_PUBLIC_VERCEL_URL;
if (url) {
return url;
}
if (
!url &&
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
) {
return "http://localhost:3000";
}
throw new BetterAuthError(
"Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",
);
}
export const createBaseClient = <Auth extends BetterAuth = BetterAuth>(
export const createVanillaClient = <Auth extends BetterAuth = never>(
options?: ClientOptions,
) => {
const fetch = createFetch({
baseURL: options?.baseURL || inferBaeURL(),
type API = Auth extends never ? BetterAuth["api"] : Auth["api"];
const $fetch = createFetch({
...options,
plugins: [...(options?.plugins || []), redirectPlugin],
baseURL: getBaseURL(options?.baseURL),
plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
});
type API = Auth["api"];
type Options = API extends {
[key: string]: infer T;
}
? T extends Endpoint
? {
[key in T["path"]]: T;
}
: {}
: {};
type O = Prettify<UnionToIntersection<Options>>;
return async <OPT extends O, K extends keyof OPT>(
path: K,
...options: HasRequiredKeys<InferContext<OPT[K]>> extends true
? [
BetterFetchOption<
InferContext<OPT[K]>["body"],
any,
InferContext<OPT[K]>["params"]
>,
]
: [
BetterFetchOption<
InferContext<OPT[K]>["body"],
InferContext<OPT[K]>["params"]
>?,
]
): Promise<
BetterFetchResponse<
Awaited<ReturnType<OPT[K] extends Endpoint ? OPT[K] : never>>
>
> => {
const opts = options[0] as {
params?: Record<string, any>;
body?: Record<string, any>;
};
return (await fetch(path as string, {
...options[0],
body: opts.body,
params: opts.params,
method: opts.body ? "POST" : "GET",
onRequest(context) {
console.log("request", context.url);
},
})) as any;
const { $session, $sessionSignal } = getSessionAtom<Auth>($fetch);
const { signInPasskey, signUpPasskey } = getPasskeyActions($fetch);
const { $activeOrganization, $listOrganizations, activeOrgId, $listOrg } =
getOrganizationAtoms($fetch, $session);
const actions = {
setActiveOrg: (orgId: string | null) => {
activeOrgId.set(orgId);
},
signInPasskey,
signUpPasskey,
$atoms: {
$session,
$activeOrganization,
$listOrganizations,
},
};
const proxy = createDynamicPathProxy(actions, $fetch, {
"/create/organization": $listOrg,
"/two-factor/enable": $sessionSignal,
"/two-factor/disable": $sessionSignal,
}) as unknown as InferRoutes<API> & typeof actions;
return proxy;
};
+32 -11
View File
@@ -1,23 +1,44 @@
import { BetterAuthError } from "../error/better-auth-error";
export const HIDE_ON_CLIENT_METADATA = {
onClient: "hide" as const,
};
export function inferBaeURL() {
const url =
function checkHasPath(url: string): boolean {
try {
const parsedUrl = new URL(url);
return parsedUrl.pathname !== "/";
} catch (error) {
console.error("Invalid URL:", error);
return false;
}
}
function withPath(url: string) {
const hasPath = checkHasPath(url);
if (hasPath) {
return url;
}
return `${url}/api/auth`;
}
export function getBaseURL(url?: string) {
if (url) {
return withPath(url);
}
const fromEnv =
process.env.AUTH_URL ||
process.env.NEXT_PUBLIC_AUTH_URL ||
process.env.BETTER_AUTH_URL ||
process.env.NEXT_PUBLIC_BETTER_AUTH_URL ||
process.env.VERCEL_URL ||
process.env.NEXT_PUBLIC_VERCEL_URL;
if (url) {
return url;
process.env.NEXT_PUBLIC_BETTER_AUTH_URL;
if (fromEnv) {
return withPath(fromEnv);
}
if (
!url &&
!fromEnv &&
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
) {
return "http://localhost:3000";
return "http://localhost:3000/api/auth";
}
throw new BetterAuthError(
throw new Error(
"Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",
);
}
@@ -0,0 +1,27 @@
import { describe, it } from "vitest";
import { getTestInstance } from "../test-utils/test-instance";
import { createAuthClient } from "./react";
import { passkey } from "../providers";
import { createClient } from "better-call/client";
describe("client path to object", async () => {
const auth = await getTestInstance({
providers: [
passkey({
rpID: "test",
rpName: "test",
}),
],
});
it("should return a path to object", async () => {
const client = createAuthClient({
baseURL: "http://localhost:3000/api/auth",
customFetchImpl: async (url, options) => {
console.log({ url, options });
return new Response();
},
});
console.log(client.$atoms.$session.get());
});
});
+1 -81
View File
@@ -1,81 +1 @@
import { ClientOptions } from "./base";
import { BetterAuth } from "../auth";
import {
InferredActions,
PickDefaultPaths,
PickOrganizationPaths,
PickProvidePaths,
} from "./type";
import { getProxy } from "./proxy";
import { createClient } from "better-call/client";
import { BetterFetch, createFetch } from "@better-fetch/fetch";
import { OAuthProvider, OAuthProviderList } from "../types/provider";
import { getSessionAtom } from "./session-atom";
import { getOrganizationAtoms } from "./org-atoms";
import { getPasskeyActions } from "./passkey-actions";
import { inferBaeURL } from "./client-utils";
import { addCurrentURL, csrfPlugin, redirectPlugin } from "./client-plugins";
export const createAuthClient = <Auth extends BetterAuth = BetterAuth>(
options?: ClientOptions,
) => {
type API = BetterAuth["api"];
const client = createClient<API>({
...options,
baseURL: options?.baseURL || inferBaeURL(),
plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
});
const $fetch = createFetch({
...options,
baseURL: options?.baseURL || inferBaeURL(),
plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
});
const signInOAuth = async (data: {
provider: Auth["options"]["providers"] extends Array<infer T>
? T extends OAuthProvider
? T["id"]
: never
: OAuthProviderList[number];
callbackURL: string;
}) => {
const res = await client("@post/signin/oauth", {
body: data,
});
if (res.data?.redirect) {
window.location.href = res.data.url;
}
return res;
};
const { $session } = getSessionAtom<Auth>($fetch);
const { signInPasskey, signUpPasskey } = getPasskeyActions($fetch);
const { $activeOrganization, $listOrganizations, activeOrgId, $listOrg } =
getOrganizationAtoms($fetch, $session);
const actions = {
signInOAuth,
$session,
$activeOrganization,
$listOrganizations,
setActiveOrg: (orgId: string | null) => {
activeOrgId.set(orgId);
},
signInPasskey,
signUpPasskey,
};
type PickedActions = Pick<
typeof actions,
| PickOrganizationPaths<Auth>
| PickDefaultPaths
| PickProvidePaths<"passkey", "signInPasskey" | "signUpPasskey", Auth>
>;
const proxy = getProxy(actions, client as BetterFetch, {
"create/organization": $listOrg,
}) as InferredActions<Auth> & PickedActions;
return proxy;
};
export * from "./base";
@@ -0,0 +1,46 @@
import { BetterFetchResponse } from "@better-fetch/fetch";
import { Context, Endpoint } from "better-call";
import {
HasRequiredKeys,
Prettify,
UnionToIntersection,
} from "../types/helper";
type CamelCase<S extends string> =
S extends `${infer P1}-${infer P2}${infer P3}`
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
: Lowercase<S>;
export type PathToObject<
T extends string,
Fn extends (...args: any[]) => any,
> = T extends `/${infer Segment}/${infer Rest}`
? { [K in CamelCase<Segment>]: PathToObject<`/${Rest}`, Fn> }
: T extends `/${infer Segment}`
? { [K in CamelCase<Segment>]: Fn }
: never;
type MergeRoutes<T> = UnionToIntersection<T>;
type InferRoute<API> = API extends {
[key: string]: infer T;
}
? T extends Endpoint
? T["options"]["metadata"] extends {
onClient: "hide";
}
? {}
: PathToObject<
T["path"],
T extends (ctx: infer C) => infer R
? C extends Context<any, any>
? (
...data: HasRequiredKeys<C> extends true
? [Prettify<C>]
: [Prettify<C>?]
) => Promise<BetterFetchResponse<Awaited<R>>>
: never
: never
>
: never
: never;
export type InferRoutes<API> = MergeRoutes<InferRoute<API>>;
+21
View File
@@ -1,2 +1,23 @@
import { useStore } from "@nanostores/react";
export const useAuthStore = useStore;
import { createVanillaClient } from "./base";
import { BetterFetchOption } from "@better-fetch/fetch";
export const createAuthClient = (options?: BetterFetchOption) => {
const client = createVanillaClient(options);
function useSession() {
return useStore(client.$atoms.$session);
}
function useActiveOrganization() {
return useStore(client.$atoms.$activeOrganization);
}
function useListOrganization() {
return useStore(client.$atoms.$listOrganizations);
}
return Object.assign(client, {
useSession,
useActiveOrganization,
useListOrganization,
});
};
+41 -74
View File
@@ -1,53 +1,12 @@
import { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
import { createBaseClient } from "./base";
import { Atom, PreinitializedWritableAtom } from "nanostores";
function fromCamelCase(str: string) {
const path = str
.split(/(?=[A-Z])/)
.join("/")
.toLowerCase();
return `/${path}`;
}
const knownCases = [
["sign", "in"],
["sign", "up"],
["sign", "out"],
["invite", "member"],
["update", "member"],
["delete", "member"],
["accept", "invitation"],
["reject", "invitation"],
["cancel", "invitation"],
["has", "permission"],
];
/**
* Handles edge cases like signInCredential and
* signUpCredential
*/
function handleEdgeCases(str: string) {
const splits = str.split("/").filter((s) => s);
let index = 0;
for (const path of splits) {
const secondPath = splits[index + 1]?.trim();
if (secondPath) {
const isKnownCase = knownCases.some(
([a, b]) => a === path && b === secondPath,
);
if (isKnownCase) {
splits[index] = `${path}-${secondPath}`;
splits.splice(1, index + 1);
}
}
}
return splits.join("/");
}
import { PreinitializedWritableAtom } from "nanostores";
const knownPathMethods: Record<string, "POST" | "GET"> = {
"/sign-out": "POST",
"enable/totp": "POST",
"/two-factor/disable": "POST",
"/two-factor/enable": "POST",
"/two-factor/send-otp": "POST",
};
function getMethod(path: string, args?: BetterFetchOption) {
@@ -61,40 +20,48 @@ function getMethod(path: string, args?: BetterFetchOption) {
return "GET";
}
export function getProxy(
actions: Record<string, any>,
export function createDynamicPathProxy<T extends Record<string, any>>(
routes: T,
client: BetterFetch,
$signal?: {
[key: string]: PreinitializedWritableAtom<boolean>;
},
) {
return new Proxy(actions, {
get(target, key) {
if (key in target) {
return target[key as keyof typeof actions];
): T {
const handler: ProxyHandler<any> = {
get(target, prop: string) {
// If the property exists in the initial object, return it directly
if (prop in routes) {
return routes[prop as string];
}
return (args?: BetterFetchOption) => {
key = fromCamelCase(key as string);
key = handleEdgeCases(key);
if (args?.params) {
const paramPlaceholder = Object.keys(args?.params)
.map((key) => `:${key}`)
return new Proxy(() => {}, {
get: (_, nestedProp: string) => {
//@ts-expect-error
return handler.get(target, `${prop}.${nestedProp}`);
},
apply: async (_, __, args) => {
if (prop in target) {
return target[prop](...args);
}
const path = prop
.split(".")
.map((segment) =>
segment.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
)
.join("/");
key = paramPlaceholder.length
? `${key as string}/${paramPlaceholder}`
: key;
}
return client(key as "/signin/oauth", {
...(args || {}),
method: getMethod(key, args),
onSuccess() {
const signal = $signal?.[key as string];
if (signal) {
signal.set(!signal.get());
}
},
});
};
const routePath = `/${path}`;
return await client(routePath, {
...args[0],
method: getMethod(routePath, args[0]),
onSuccess() {
const signal = $signal?.[routePath as string];
if (signal) {
signal.set(!signal.get());
}
},
});
},
});
},
});
};
return new Proxy(routes, handler);
}
+24
View File
@@ -1,2 +1,26 @@
import { useStore } from "@nanostores/react";
import { createVanillaClient } from "./base";
import { BetterFetchOption } from "@better-fetch/fetch";
import { BetterAuth } from "../auth";
export const createAuthClient = <Auth extends BetterAuth>(
options?: BetterFetchOption,
) => {
const client = createVanillaClient<Auth>(options);
function useSession() {
return useStore(client.$atoms.$session);
}
function useActiveOrganization() {
return useStore(client.$atoms.$activeOrganization);
}
function useListOrganization() {
return useStore(client.$atoms.$listOrganizations);
}
return Object.assign(client, {
useSession,
useActiveOrganization,
useListOrganization,
});
};
export const useAuthStore = useStore;
@@ -1,6 +1,6 @@
import { atom, computed, task } from "nanostores";
import { Session, User } from "../adapters/schema";
import { Prettify } from "../types/helper";
import { Prettify, UnionToIntersection } from "../types/helper";
import { BetterAuth } from "../auth";
import { FieldAttribute, InferFieldOutput } from "../db";
import { BetterFetch } from "@better-fetch/fetch";
@@ -54,8 +54,10 @@ export function getSessionAtom<Auth extends BetterAuth>(client: BetterFetch) {
: {}
: {};
type UserWithAdditionalFields = User & AdditionalUserFields;
type SessionWithAdditionalFields = Session & AdditionalSessionFields;
type UserWithAdditionalFields = User &
UnionToIntersection<AdditionalUserFields>;
type SessionWithAdditionalFields = Session &
UnionToIntersection<AdditionalSessionFields>;
const $signal = atom<boolean>(false);
const $session = computed($signal, () =>
@@ -70,5 +72,5 @@ export function getSessionAtom<Auth extends BetterAuth>(client: BetterFetch) {
} | null;
}),
);
return { $session };
return { $session, $sessionSignal: $signal };
}
-2
View File
@@ -1,2 +0,0 @@
import { useStore } from "@nanostores/solid";
export const useAuthStore = useStore;
+7 -90
View File
@@ -1,67 +1,11 @@
import { Context, Endpoint } from "better-call";
import { CamelCase } from "type-fest";
import {
Prettify,
HasRequiredKeys,
UnionToIntersection,
} from "../types/helper";
import { BetterFetchResponse } from "@better-fetch/fetch";
import { UnionToIntersection } from "../types/helper";
import { BetterAuth } from "../auth";
import { CustomProvider } from "../providers";
import { BetterFetchOption } from "@better-fetch/fetch";
import type { useAuthStore as reactStore } from "./react";
import type { useAuthStore as vueStore } from "./vue";
export type InferKeys<T> = T extends `/${infer A}/${infer B}`
? CamelCase<`${A}-${InferKeys<B>}`>
: T extends `${infer I}/:${infer _}`
? I
: T extends `${infer I}:${infer _}`
? I
: T extends `/${infer I}`
? CamelCase<I>
: CamelCase<T>;
export type InferActions<Actions> = Actions extends {
[key: string]: infer T;
}
? UnionToIntersection<
T extends Endpoint
? {
[key in InferKeys<T["path"]>]: T extends (ctx: infer C) => infer R
? C extends Context<any, any>
? (
...data: HasRequiredKeys<C> extends true
? [Prettify<C>]
: [Prettify<C>?]
) => Promise<BetterFetchResponse<Awaited<R>>>
: never
: never;
}
: never
>
: never;
export type ExcludeCredentialPaths<Auth extends BetterAuth> =
Auth["options"]["emailAndPassword"] extends {
enabled: true;
}
? ""
: "signUpCredential" | "signInCredential";
export type ExcludedPasskeyPaths =
| "passkeyGenerateAuthenticateOptions"
| "passkeyGenerateRegisterOptions"
| "verifyPasskey";
export type ExcludedPaths<Auth extends BetterAuth> =
| "signinOauth"
| "signUpOauth"
| "callback"
| "session"
| ExcludeCredentialPaths<Auth>
| ExcludedPasskeyPaths;
export type OrganizationPaths = "$activeOrganization" | "setActiveOrg";
type ProviderEndpoint<Auth extends BetterAuth> = UnionToIntersection<
export type ProviderEndpoint<Auth extends BetterAuth> = UnionToIntersection<
Auth["options"]["providers"] extends Array<infer T>
? T extends CustomProvider
? T["endpoints"]
@@ -69,32 +13,5 @@ type ProviderEndpoint<Auth extends BetterAuth> = UnionToIntersection<
: {}
>;
export type Actions<Auth extends BetterAuth> = ProviderEndpoint<Auth> &
Auth["api"];
export type InferredActions<Auth extends BetterAuth> = Prettify<
Omit<InferActions<Actions<Auth>>, ExcludedPaths<Auth>>
>;
export type PickOrganizationPaths<Auth extends BetterAuth> =
Auth["options"]["plugins"] extends Array<infer T>
? T extends {
id: "organization";
}
? OrganizationPaths
: never
: never;
export type PickProvidePaths<
ID extends string,
PickedPath extends string,
Auth extends BetterAuth,
> = Auth["options"]["providers"] extends Array<infer P>
? P extends {
id: ID;
}
? PickedPath
: never
: never;
export type PickDefaultPaths = "$session";
export type AuthStore = typeof reactStore | typeof vueStore;
export interface ClientOptions extends BetterFetchOption {}
+21
View File
@@ -1,2 +1,23 @@
import { useStore } from "@nanostores/vue";
import { BetterFetchOption } from "@better-fetch/fetch";
import { createVanillaClient } from "./base";
export const createAuthClient = (options?: BetterFetchOption) => {
const client = createVanillaClient(options);
function useSession() {
return useStore(client.$atoms.$session);
}
function useActiveOrganization() {
return useStore(client.$atoms.$activeOrganization);
}
function useListOrganization() {
return useStore(client.$atoms.$listOrganizations);
}
return Object.assign(client, {
useSession,
useActiveOrganization,
useListOrganization,
});
};
export const useAuthStore = useStore;
@@ -3,7 +3,7 @@ import { TwoFactorProvider, UserWithTwoFactor } from "../types";
import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
import { z } from "zod";
import { createAuthEndpoint } from "../../../api/call";
import { verifyTwoFactorMiddleware } from "../verify-middleware";
import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
import { sessionMiddleware } from "../../../api/middlewares/session";
export interface BackupCodeOptions {
@@ -78,37 +78,37 @@ export function getBackupCodes(user: UserWithTwoFactor, key: string) {
export const backupCode2fa = (options?: BackupCodeOptions) => {
return {
id: "backup_code",
verify: createAuthEndpoint(
"/verify/backup-code",
{
method: "POST",
body: z.object({
code: z.string(),
}),
use: [verifyTwoFactorMiddleware],
},
async (ctx) => {
const validate = verifyBackupCode(
{
user: ctx.context.session.user,
code: ctx.body.code,
},
ctx.context.secret,
);
if (!validate) {
return ctx.json(
{ status: false },
endpoints: {
verifyBackupCode: createAuthEndpoint(
"/two-factor/verify-backup-code",
{
method: "POST",
body: z.object({
code: z.string(),
}),
use: [verifyTwoFactorMiddleware],
},
async (ctx) => {
const validate = verifyBackupCode(
{
status: 401,
user: ctx.context.session.user,
code: ctx.body.code,
},
ctx.context.secret,
);
}
return ctx.json({ status: true });
},
),
customActions: {
if (!validate) {
return ctx.json(
{ status: false },
{
status: 401,
},
);
}
return ctx.json({ status: true });
},
),
generateBackupCodes: createAuthEndpoint(
"/generate/backup-codes",
"/two-factor/generate-backup-codes",
{
method: "POST",
use: [sessionMiddleware],
@@ -6,7 +6,7 @@ import { TwoFactorOptions, UserWithTwoFactor } from "./types";
import {
twoFactorMiddleware,
verifyTwoFactorMiddleware,
} from "./verify-middleware";
} from "./two-fa-middleware";
import { sessionMiddleware } from "../../api/middlewares/session";
import { alphabet, generateRandomString } from "oslo/crypto";
import { backupCode2fa, generateBackupCodes } from "./backup-codes";
@@ -24,11 +24,11 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
return {
id: "two-factor",
endpoints: {
...totp.customActions,
...otp.customActions,
...backupCode.customActions,
...totp.endpoints,
...otp.endpoints,
...backupCode.endpoints,
enableTwoFactor: createAuthEndpoint(
"/enable/two-factor",
"/two-factor/enable",
{
method: "POST",
use: [sessionMiddleware],
@@ -40,6 +40,7 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
key: ctx.context.secret,
data: secret,
});
console.log({ encryptedSecret });
const backupCodes = await generateBackupCodes(
ctx.context.secret,
options.backupCodeOptions,
@@ -62,7 +63,7 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
},
),
disableTwoFactor: createAuthEndpoint(
"/disable/two-factor",
"/two-factor/disable",
{
method: "POST",
use: [sessionMiddleware],
@@ -84,51 +85,6 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
return ctx.json({ status: true });
},
),
verifyTwoFactor: createAuthEndpoint(
"/verify/two-factor",
{
method: "POST",
body: z.object({
/**
* The code to validate
*/
code: z.string(),
with: z.enum(["totp", "otp", "backup_code"]),
callbackURL: z.string().optional(),
}),
use: [verifyTwoFactorMiddleware],
},
async (ctx) => {
const providerId = ctx.body.with;
const provider = providers.find((p) => p.id === providerId);
if (!provider) {
return ctx.json(
{ status: false },
{
status: 401,
},
);
}
const res = await provider.verify(ctx);
if (!res.status) {
return ctx.json(
{ status: false },
{
status: 401,
},
);
}
await ctx.context.createSession();
if (ctx.body.callbackURL) {
return ctx.json({
status: true,
callbackURL: ctx.body.callbackURL,
redirect: true,
});
}
return ctx.json({ status: true });
},
),
},
options: options,
middlewares: [
@@ -149,18 +105,11 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
type: "string",
required: false,
},
backupCodes: {
twoFactorBackupCodes: {
type: "string",
required: false,
returned: false,
},
/**
* list of two factor providers id separated by comma
*/
twoFactorProviders: {
type: "string",
required: false,
},
},
},
},
@@ -6,7 +6,7 @@ import { generateHOTP } from "oslo/otp";
import { generateRandomInteger } from "oslo/crypto";
import { OTP_RANDOM_NUMBER_COOKIE_NAME } from "../constant";
import { z } from "zod";
import { verifyTwoFactorMiddleware } from "../verify-middleware";
import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
export interface OTPOptions {
/**
@@ -25,11 +25,11 @@ export const otp2fa = (options?: OTPOptions) => {
/**
* Generate OTP and send it to the user.
*/
const generateOTP = createAuthEndpoint(
"/generate/otp",
const send2FaOTP = createAuthEndpoint(
"/two-factor/send-otp",
{
method: "POST",
use: [sessionMiddleware],
use: [verifyTwoFactorMiddleware],
},
async (ctx) => {
if (!options || !options.sendOTP) {
@@ -63,7 +63,7 @@ export const otp2fa = (options?: OTPOptions) => {
);
const verifyOTP = createAuthEndpoint(
"/verify/otp",
"/two-factor/verify-otp",
{
method: "POST",
body: z.object({
@@ -86,34 +86,35 @@ export const otp2fa = (options?: OTPOptions) => {
ctx.context.secret,
);
if (!randomNumber) {
throw new APIError("BAD_REQUEST", {
message: "counter cookie not found",
throw new APIError("UNAUTHORIZED", {
message: "OTP is expired",
});
}
const toCheckOtp = await generateHOTP(
Buffer.from(ctx.context.secret),
parseInt(randomNumber),
);
console.log(toCheckOtp, ctx.body.code);
if (toCheckOtp !== ctx.body.code) {
await ctx.context.createSession();
return ctx.json({ status: true });
if (toCheckOtp === ctx.body.code) {
ctx.setCookie(cookie.name, "", {
path: "/",
sameSite: "lax",
httpOnly: true,
secure: false,
maxAge: 0,
});
return ctx.context.valid();
} else {
return ctx.json(
{ status: false },
{
status: 401,
},
);
return ctx.context.invalid();
}
},
);
return {
id: "otp",
verify: verifyOTP,
customActions: {
generateOTP: generateOTP,
endpoints: {
send2FaOTP,
verifyOTP,
},
} satisfies TwoFactorProvider;
};
@@ -1,13 +1,12 @@
import { TimeSpan } from "oslo";
import { alphabet, generateRandomString } from "oslo/crypto";
import { TOTPController, createTOTPKeyURI } from "oslo/otp";
import { z } from "zod";
import { createAuthEndpoint } from "../../../api/call";
import { sessionMiddleware } from "../../../api/middlewares/session";
import { APIError } from "better-call";
import { TwoFactorProvider, UserWithTwoFactor } from "../types";
import { verifyTwoFactorMiddleware } from "../verify-middleware";
import { BackupCodeOptions, generateBackupCodes } from "../backup-codes";
import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
import { BackupCodeOptions } from "../backup-codes";
import { symmetricDecrypt } from "../../../crypto";
export type TOTPOptions = {
@@ -36,105 +35,10 @@ export const totp2fa = (options: TOTPOptions) => {
const opts = {
digits: 6,
period: new TimeSpan(options?.period || 30, "s"),
secret: {
field: "twoFactorSecret",
},
};
const enableTOTP = createAuthEndpoint(
"/enable/totp",
{
method: "POST",
use: [sessionMiddleware],
},
async (ctx) => {
if (!options) {
ctx.context.logger.error(
"totp isn't configured. please pass totp option on two factor plugin to enable totp",
);
throw new APIError("BAD_REQUEST", {
message: "totp isn't configured",
});
}
const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
const user = ctx.context.session.user as UserWithTwoFactor;
const uri = createTOTPKeyURI(
options.issuer || "BetterAuth",
user.name,
Buffer.from(secret),
opts,
);
const backupCodes = await generateBackupCodes(
secret,
options.backupCodes,
);
await ctx.context.adapter.update({
model: "user",
update: {
twoFactorSecret: secret,
twoFactorEnabled: true,
backupCodes: backupCodes.encryptedBackupCodes,
},
where: [
{
field: "id",
value: user.id,
},
],
});
return ctx.json({ uri, backupCodes: backupCodes.backupCodes });
},
);
async function enable(user: UserWithTwoFactor) {
const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
const uri = createTOTPKeyURI(
options.issuer,
user.name,
Buffer.from(secret),
opts,
);
const backupCodes = await generateBackupCodes(secret, options.backupCodes);
return {
uri,
backupCodes: backupCodes.backupCodes,
};
}
const disableTOTP = createAuthEndpoint(
"/disable/totp",
{
method: "POST",
use: [sessionMiddleware],
},
async (ctx) => {
if (!options) {
ctx.context.logger.error(
"totp isn't configured. please pass totp option on two factor plugin to enable totp",
);
throw new APIError("BAD_REQUEST", {
message: "totp isn't configured",
});
}
const user = ctx.context.session.user as UserWithTwoFactor;
await ctx.context.adapter.update({
model: "user",
update: {
twoFactorEnabled: false,
},
where: [
{
field: "id",
value: user.id,
},
],
});
return ctx.json({ status: true });
},
);
const generateTOTP = createAuthEndpoint(
"/generate/totp",
"/totp/generate",
{
method: "POST",
use: [sessionMiddleware],
@@ -148,16 +52,15 @@ export const totp2fa = (options: TOTPOptions) => {
message: "totp isn't configured",
});
}
const session = ctx.context.session;
const session = ctx.context.session.user as UserWithTwoFactor;
const totp = new TOTPController(opts);
const secret = (session.user as any).secret;
const code = await totp.generate(secret);
const code = await totp.generate(Buffer.from(session.twoFactorSecret));
return { code };
},
);
const getTOTPURI = createAuthEndpoint(
"/get/totp/uri",
"/two-factor/get-totp-uri",
{
method: "GET",
use: [sessionMiddleware],
@@ -175,7 +78,7 @@ export const totp2fa = (options: TOTPOptions) => {
return {
totpURI: createTOTPKeyURI(
options?.issuer || "BetterAuth",
user.name,
user.email,
Buffer.from(user.twoFactorSecret),
opts,
),
@@ -184,7 +87,7 @@ export const totp2fa = (options: TOTPOptions) => {
);
const verifyTOTP = createAuthEndpoint(
"/verify/totp",
"/two-factor/verify-totp",
{
method: "POST",
body: z.object({
@@ -210,17 +113,18 @@ export const totp2fa = (options: TOTPOptions) => {
}),
);
const status = await totp.verify(ctx.body.code, secret);
return {
status,
};
if (!status) {
return ctx.context.invalid();
}
return ctx.context.valid();
},
);
return {
id: "totp",
verify: verifyTOTP,
customActions: {
endpoints: {
generateTOTP: generateTOTP,
viewTOTPURI: getTOTPURI,
verifyTOTP,
},
} satisfies TwoFactorProvider;
};
@@ -5,6 +5,7 @@ import { hs256 } from "../../crypto";
import { TWO_FACTOR_COOKIE_NAME } from "./constant";
import { TwoFactorOptions, UserWithTwoFactor } from "./types";
import { z } from "zod";
import { signInCredential } from "../../api/routes";
export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
const cookie = await ctx.getSignedCookie(
@@ -62,7 +63,7 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
}
if (hashToMatch === hash) {
return {
createSession: async () => {
valid: async () => {
/**
* Set the session cookie
*/
@@ -72,6 +73,26 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
ctx.context.secret,
ctx.context.authCookies.sessionToken.options,
);
if (ctx.body.callbackURL) {
return ctx.json({
status: true,
callbackURL: ctx.body.callbackURL,
redirect: true,
});
}
return ctx.json({ status: true });
},
invalid: async () => {
return ctx.json(
{ status: false },
{
status: 401,
body: {
message: "Invalid code",
},
},
);
},
session: {
id: session.id,
@@ -45,12 +45,5 @@ export interface UserWithTwoFactor extends User {
export interface TwoFactorProvider {
id: LiteralString;
enable?: (user: UserWithTwoFactor) => Promise<void>;
disable?: () => Promise<void>;
verify: Endpoint<
(ctx: any) => Promise<{
status: boolean;
}>
>;
customActions?: Record<string, AuthEndpoint>;
endpoints?: Record<string, AuthEndpoint>;
}
@@ -82,6 +82,9 @@ export const passkey = (options: PasskeyOptions) => {
{
method: "GET",
use: [sessionMiddleware],
metadata: {
client: false,
},
},
async (ctx) => {
const session = ctx.context.session;
@@ -4,9 +4,12 @@ import { beforeAll, afterAll } from "vitest";
import { type Listener, listen } from "listhen";
import { toNodeHandler } from "better-call";
import fs from "fs/promises";
import { BetterAuthOptions } from "../types";
export async function getTestInstance() {
const auth = betterAuth({
export async function getTestInstance<O extends Partial<BetterAuthOptions>>(
options?: O,
) {
const opts = {
providers: [
github({
clientId: "test",
@@ -26,7 +29,12 @@ export async function getTestInstance() {
emailAndPassword: {
enabled: true,
},
});
} satisfies BetterAuthOptions;
const auth = betterAuth({
...opts,
...options,
} as O extends undefined ? typeof opts : O & typeof opts);
let server: Listener;
@@ -146,11 +146,5 @@ export interface BetterAuthOptions {
* @default 32
*/
minPasswordLength?: number;
/**
* Two factor configuration
*/
twoFactor?: {
enabled: boolean;
};
};
}
+1 -1
View File
@@ -74,7 +74,7 @@ export function createCookieGetter(options: BetterAuthOptions) {
name:
process.env.NODE_ENV === "production"
? `${secureCookiePrefix}${cookiePrefix}.${cookieName}`
: `${cookiePrefix}${cookieName}`,
: `${cookiePrefix}.${cookieName}`,
options: {
secure,
sameSite: "lax",
+1 -1
View File
@@ -8,7 +8,6 @@ export default defineConfig({
cli: "./src/cli/index.ts",
react: "./src/client/react.ts",
preact: "./src/client/preact.ts",
solid: "./src/client/solid.ts",
vue: "./src/client/vue.ts",
plugins: "./src/plugins/index.ts",
},
@@ -19,6 +18,7 @@ export default defineConfig({
external: [
"react",
"svelte",
"solid-js",
"$app/environment",
"next",
"pg",
+46 -5
View File
@@ -127,6 +127,9 @@ importers:
react-hook-form:
specifier: ^7.52.2
version: 7.52.2(react@18.3.1)
react-qr-code:
specifier: ^2.0.15
version: 2.0.15(react@18.3.1)
sonner:
specifier: ^1.5.0
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -198,8 +201,8 @@ importers:
specifier: ^1.9.2
version: 1.9.2
better-call:
specifier: ^0.1.28
version: 0.1.28(typescript@5.5.4)
specifier: ^0.1.33
version: 0.1.33(typescript@5.5.4)
chalk:
specifier: ^5.3.0
version: 5.3.0
@@ -215,6 +218,9 @@ importers:
jiti:
specifier: ^1.21.6
version: 1.21.6
jose:
specifier: ^5.7.0
version: 5.7.0
kysely:
specifier: ^0.27.4
version: 0.27.4
@@ -1733,8 +1739,8 @@ packages:
peerDependencies:
typescript: ^5.0.0
better-call@0.1.28:
resolution: {integrity: sha512-9jl3q7Mb+3lRGeJUqkHAzAHph8BKcGHsj84h1gkfcMy2skyrCUbpFuhxMNAKBDtg2ppa66wTWDv0mGLOshxtEA==}
better-call@0.1.33:
resolution: {integrity: sha512-gzthE/AnimwMCNBNyy9LRqqAtjXkqO+dR4n1OjCiUhiBK4X+NZMKekQUKIzpfwDRC4k3hCshb1LsPhqwiSM7Bw==}
peerDependencies:
typescript: ^5.0.0
@@ -2233,6 +2239,9 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
jose@5.7.0:
resolution: {integrity: sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@@ -2717,6 +2726,9 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
@@ -2731,6 +2743,9 @@ packages:
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
engines: {node: '>=6.0.0'}
qr.js@0.0.0:
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -2755,9 +2770,17 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-qr-code@2.0.15:
resolution: {integrity: sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==}
peerDependencies:
react: '*'
react-remove-scroll-bar@2.3.6:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
@@ -4487,7 +4510,7 @@ snapshots:
rou3: 0.5.1
typescript: 5.5.4
better-call@0.1.28(typescript@5.5.4):
better-call@0.1.33(typescript@5.5.4):
dependencies:
'@better-fetch/fetch': 1.1.4
'@types/set-cookie-parser': 2.4.10
@@ -5009,6 +5032,8 @@ snapshots:
jiti@1.21.6: {}
jose@5.7.0: {}
joycon@3.1.1: {}
js-tokens@4.0.0: {}
@@ -5469,6 +5494,12 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
pump@3.0.0:
dependencies:
end-of-stream: 1.4.4
@@ -5482,6 +5513,8 @@ snapshots:
pvutils@1.1.3: {}
qr.js@0.0.0: {}
queue-microtask@1.2.3: {}
radix3@1.1.2: {}
@@ -5508,8 +5541,16 @@ snapshots:
dependencies:
react: 18.3.1
react-is@16.13.1: {}
react-is@18.3.1: {}
react-qr-code@2.0.15(react@18.3.1):
dependencies:
prop-types: 15.8.1
qr.js: 0.0.0
react: 18.3.1
react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):
dependencies:
react: 18.3.1
+2
View File
@@ -0,0 +1,2 @@
## TODO
[ ] handle migration when the config removes existing schema