feat: new client primitives

This commit is contained in:
Bereket Engida
2024-08-31 17:13:51 +03:00
parent 58f1da9772
commit 9de80b35d8
72 changed files with 1189 additions and 423 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
link-workspace-packages=true

View File

@@ -31,8 +31,7 @@
".next",
".svelte-kit",
"package.json",
".contentlayer",
"dev"
".contentlayer"
]
}
}

23
dev/bc-fe/hono/auth.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
export type Auth = {
"baseURL": "http://localhost:3000",
"basePath": "/auth",
"database": {
"provider": "sqlite",
"url": "./db.sqlite"
},
"socialProvider": [
{
"id": "github"
}
],
"plugins": [
{
"id": "two-factor",
"endpoints": {}
},
{
"id": "organization",
"endpoints": {}
}
]
}

BIN
dev/bc-fe/hono/db.sqlite Normal file

Binary file not shown.

View File

@@ -0,0 +1,17 @@
{
"name": "hono",
"scripts": {
"dev": "tsx -r dotenv/config src/index.ts",
"auth": "pnpm better-auth"
},
"dependencies": {
"@hono/node-server": "^1.12.2",
"better-auth": "^0.0.2-beta.8",
"dotenv": "^16.4.5",
"hono": "^4.5.9",
"tsx": "^4.19.0"
},
"devDependencies": {
"@types/bun": "latest"
}
}

View File

@@ -0,0 +1,27 @@
import { betterAuth } from "better-auth";
import { organization, twoFactor } from "better-auth/plugins";
import { github } from "better-auth/social-providers";
export const auth = betterAuth({
baseURL: "http://localhost:3000",
basePath: "/auth",
database: {
provider: "sqlite",
url: "./db.sqlite",
},
socialProvider: [
github({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
}),
],
plugins: [
twoFactor({
issuer: "BetterAuth",
}),
organization(),
],
emailAndPassword: {
enabled: true,
},
});

View File

@@ -0,0 +1,24 @@
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
const app = new Hono();
app.use(
"/api/auth/**",
cors({
origin: "http://localhost:5173",
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);
app.on(["POST", "GET"], "/api/auth/**", (c) => {
return auth.handler(c.req.raw);
});
serve(app);

View File

@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"better-auth": "workspace:^0.0.2-beta.8",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

117
dev/bc-fe/react/src/App.tsx Normal file
View File

@@ -0,0 +1,117 @@
import { useState } from "react";
import "./App.css";
import { auth } from "./lib/auth";
function App() {
const session = auth.useSession()
return (
<>
<p>
Better Auth
</p>
<div>
{
session ? (
<div style={{
borderRadius: "10px",
border: "1px solid #4B453F",
padding: "10px",
gap: "10px",
}}>
<p>
{session.user.name}
</p>
<p>
{session.user.email}
</p>
<div className="flex gap-2">
{
session.user.twoFactorEnabled ? (
<button onClick={async () => {
await auth.twoFactor.disable()
}}>
Disable 2FA
</button>
) : (
<button onClick={async () => {
await auth.twoFactor.enable()
}}>
Enable 2FA
</button>
)
}
<button onClick={async () => {
await auth.signOut()
}}>
Signout
</button>
</div>
</div>
) : (
<div>
<button onClick={async () => {
await auth.signIn.social({
provider: "github",
})
}}>
Continue with github
</button>
<SignUp />
</div>
)
}
</div>
</>
);
}
export default App;
function SignUp() {
const [email, setEmail] = useState("")
const [name, setName] = useState("")
const [password, setPassword] = useState("")
return (
<div style={{
display: "flex",
flexDirection: "column",
gap: "10px",
borderRadius: "10px",
border: "1px solid #4B453F",
padding: "20px",
marginTop: "10px"
}}>
<input type="email" id="email" placeholder="Email" style={{
width: "100%",
}}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input type="name" id="name" placeholder="Name" style={{
width: "100%"
}}
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input type="password" id="password" placeholder="Password" style={{
width: "100%"
}}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={async () => {
const res = await auth.signUp.credential({
email,
password,
name
})
console.log(res)
}}>
Sign Up
</button>
</div>
)
}

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,7 @@
import { createAuthClient } from "better-auth/react";
import { twoFactorClient } from "better-auth/client";
export const auth = createAuthClient({
baseURL: "http://localhost:3000/api/auth",
authPlugins: [twoFactorClient],
});

View File

@@ -1,13 +0,0 @@
{
"name": "hono",
"scripts": {
"dev": "bun run --hot src/index.ts"
},
"dependencies": {
"hono": "^4.5.9",
"better-auth": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest"
}
}

View File

@@ -1,13 +0,0 @@
import { betterAuth } from "better-auth";
import { github } from "better-auth/social-providers";
export const auth = betterAuth({
database: {
provider: "sqlite",
url: "./db.sqlite",
},
socialProvider: [github({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
})],
});

View File

@@ -1,14 +0,0 @@
import { Hono } from "hono";
import { auth } from "./auth";
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello Hono!");
});
app.all("/auth/**", async(c)=>{
return auth.handler(c.req.raw)
})
export default app;

View File

@@ -1,35 +0,0 @@
import { useState } from "react";
import viteLogo from "/vite.svg";
import reactLogo from "./assets/react.svg";
import "./App.css";
function App() {
const [count, setCount] = useState(0);
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
);
}
export default App;

View File

@@ -5,6 +5,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"auth": "pnpm better-auth",
"start": "next start",
"lint": "next lint"
},
@@ -23,7 +24,7 @@
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@tabler/icons-react": "^3.12.0",
"better-auth": "workspace:*",
"better-auth": "^0.0.2-beta.8",
"better-call": "^0.1.0",
"better-sqlite3": "^11.1.2",
"class-variance-authority": "^0.7.0",

View File

@@ -5,7 +5,7 @@ import {
twoFactor,
validEmail,
} from "better-auth/plugins";
import { github } from "better-auth/provider";
import { github } from "better-auth/social-providers";
export const auth = betterAuth({
basePath: "/api/auth",

View File

@@ -9,19 +9,27 @@
"typecheck": "tsc --noEmit"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": "./dist/cli.js",
"publishConfig": {
"executableFiles": [
"./dist/cli.js"
]
},
"exports": {
".": "./dist/index.js",
"./social-providers": "./dist/social.js",
"./client": "./dist/client.js",
"./client/plugins": "./dist/client/plugins.js",
"./types": "./dist/types.js",
"./cli": "./dist/cli.js",
"./react": "./dist/react.js",
"./preact": "./dist/preact.js",
"./solid": "./dist/solid.js",
"./vue": "./dist/vue.js",
"./plugins": "./dist/plugins.js"
"./plugins": "./dist/plugins.js",
"./package.json": "./package.json"
},
"devDependencies": {
"@simplewebauthn/types": "^10.0.0",
@@ -68,6 +76,7 @@
"oslo": "^1.2.1",
"pg": "^8.12.0",
"prompts": "^2.4.2",
"ts-morph": "^23.0.0",
"zod": "^3.22.5"
},
"files": [

View File

@@ -16,8 +16,9 @@ import {
verifyEmail,
} from "./routes";
import { getCSRFToken } from "./routes/csrf";
import { ok } from "./routes/ok";
import { ok, welcome } from "./routes/ok";
import { signUpCredential } from "./routes/sign-up";
import { error } from "./routes/error";
export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
ctx: C,
@@ -93,6 +94,8 @@ export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
...baseEndpoints,
...pluginEndpoints,
ok,
welcome,
error,
};
let api: Record<string, any> = {};
for (const [key, value] of Object.entries(endpoints)) {
@@ -166,7 +169,9 @@ export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
let body: Record<string, any> = {};
try {
body = await res.json();
} catch (e) {}
} catch (e) {
return res;
}
if (body?.user) {
body.user = parseUser(ctx.options, body.user);
}

View File

@@ -0,0 +1,99 @@
import { createAuthEndpoint } from "../call";
const html = (errorCode: string = "Unknown") => `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authentication Error</title>
<style>
:root {
--bg-color: #f8f9fa;
--text-color: #212529;
--accent-color: #000000;
--error-color: #dc3545;
--border-color: #e9ecef;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
line-height: 1.5;
}
.error-container {
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
padding: 2.5rem;
text-align: center;
max-width: 90%;
width: 400px;
}
h1 {
color: var(--error-color);
font-size: 1.75rem;
margin-bottom: 1rem;
font-weight: 600;
}
p {
margin-bottom: 1.5rem;
color: #495057;
}
.btn {
background-color: var(--accent-color);
color: #ffffff;
text-decoration: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
transition: all 0.3s ease;
display: inline-block;
font-weight: 500;
border: 2px solid var(--accent-color);
}
.btn:hover {
background-color: #131721;
}
.error-code {
font-size: 0.875rem;
color: #6c757d;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.icon {
font-size: 3rem;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="error-container">
<div class="icon">⚠️</div>
<h1>Better Auth Error</h1>
<p>We encountered an issue while processing your request. Please try again or contact the application owner if the problem persists.</p>
<a href="#" id="returnLink" class="btn">Return to Application</a>
<div class="error-code">Error Code: <span id="errorCode">${errorCode}</span></div>
</div>
</body>
</html>`;
export const error = createAuthEndpoint(
"/error",
{
method: "GET",
},
async (c) => {
const query =
new URL(c.request?.url || "").searchParams.get("error") || "Unknown";
return new Response(html(query), {
headers: {
"Content-Type": "text/html",
},
});
},
);

View File

@@ -11,3 +11,13 @@ export const ok = createAuthEndpoint(
});
},
);
export const welcome = createAuthEndpoint(
"/welcome/ok",
{
method: "GET",
},
async () => {
return new Response("Welcome to Better Auth");
},
);

View File

@@ -22,8 +22,8 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
};
};
export type BetterAuth<Endpoints extends Record<string, any> = {}> = {
export type Auth = {
handler: (request: Request) => Promise<Response>;
api: Endpoints;
api: ReturnType<typeof router>["endpoints"];
options: BetterAuthOptions;
};

View File

@@ -0,0 +1,62 @@
import { Command } from "commander";
import { getConfig } from "../get-config";
import { Project } from "ts-morph";
import type { Provider } from "../../social-providers";
import * as fs from "fs/promises";
import type { BetterAuthPlugin } from "../../types/plugins";
export const exportTypes = new Command()
.name("export")
.description("Export types")
.option("--config <config>", "the path to the configuration file")
.option(
"--cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd(),
)
.action(async (opts) => {
const config = await getConfig({ cwd: opts.cwd, configPath: opts.config });
if (config?.socialProvider) {
config.socialProvider = config.socialProvider.map((provider) => {
if ("scopes" in provider) {
provider.scopes = [];
}
if ("handler" in provider) {
provider.handler = undefined;
}
return provider;
});
}
if (config?.database) {
config.database = {
provider: "sqlite",
url: "./db.sqlite",
};
}
if (config?.socialProvider) {
config.socialProvider = config.socialProvider.map((provider) => {
return {
id: provider.id,
} as Provider;
});
}
if (config?.plugins) {
config.plugins = config.plugins.map((plugin) => {
return {
id: plugin.id,
endpoints: plugin.endpoints,
};
});
}
const project = new Project();
const sourceFile = project.createSourceFile(
"auth.d.ts",
(writer) =>
writer.write(`export type Auth = ${JSON.stringify(config, null, 2)}`),
{
overwrite: true,
},
);
await sourceFile.save();
});

View File

@@ -1,10 +1,11 @@
import { Command } from "commander";
import "dotenv/config";
import { migrate } from "./commands/migrate";
import { exportTypes } from "./commands/export-type";
async function main() {
const program = new Command().name("better-auth");
program.addCommand(migrate);
program.addCommand(migrate).addCommand(exportTypes);
program.parse();
}

View File

@@ -1,22 +1,33 @@
import { createFetch } from "@better-fetch/fetch";
import type { router } from "../api";
import type { BetterAuth } from "../auth";
import type { Auth } from "../auth";
import { getBaseURL } from "../utils/base-url";
import { addCurrentURL, csrfPlugin, redirectPlugin } from "./fetch-plugins";
import type { InferRoutes } from "./path-to-object";
import { getOrganizationAtoms } from "./plugins/org-client";
import { getPasskeyActions } from "./plugins/passkey-client";
import { createDynamicPathProxy } from "./proxy";
import { createDynamicPathProxy, type AuthProxySignal } from "./proxy";
import { getSessionAtom } from "./session-atom";
import type { ClientOptions, HasPlugin } from "./type";
import type { AuthPlugin, ClientOptions } from "./type";
import type { UnionToIntersection } from "../types/helper";
import type { PreinitializedWritableAtom } from "nanostores";
import type { BetterAuthPlugin } from "../types/plugins";
export const createAuthClient = <Auth extends BetterAuth = never>(
options?: ClientOptions,
export const createAuthClient = <O extends ClientOptions = ClientOptions>(
options?: O,
) => {
type BAuth = Auth extends never ? BetterAuth : Auth;
type API = Auth extends never
? ReturnType<typeof router>["endpoints"]
: BAuth["api"];
type API = O["authPlugins"] extends Array<any>
? (O["authPlugins"] extends Array<infer Pl>
? UnionToIntersection<
//@ts-expect-error
ReturnType<Pl> extends {
plugin: infer Plug;
}
? Plug extends BetterAuthPlugin
? Plug["endpoints"]
: {}
: {}
>
: {}) &
Auth["api"]
: Auth["api"];
const $fetch = createFetch({
method: "GET",
...options,
@@ -28,72 +39,101 @@ export const createAuthClient = <Auth extends BetterAuth = never>(
...(options?.plugins || []),
],
});
const { $session, $sessionSignal } = getSessionAtom<Auth>($fetch);
const { signInPasskey, register } = getPasskeyActions($fetch);
const {
$activeOrganization,
$listOrganizations,
activeOrgId,
$listOrg,
$activeOrgSignal,
$activeInvitationId,
$invitation,
} = getOrganizationAtoms($fetch, $session);
const actions = {
setActiveOrganization: (orgId: string | null) => {
activeOrgId.set(orgId);
},
setInvitationId: (id: string | null) => {
$activeInvitationId.set(id);
},
passkey: {
signIn: signInPasskey,
/**
* Add a new passkey
*/
register: register,
},
$atoms: {
$session,
$activeOrganization,
$listOrganizations,
$activeInvitationId,
$invitation,
},
$fetch,
type Plugins = O["authPlugins"] extends Array<AuthPlugin>
? Array<ReturnType<O["authPlugins"][number]>["plugin"]>
: undefined;
//@ts-expect-error
const { $session, $sessionSignal } = getSessionAtom<{
handler: any;
api: any;
options: {
database: any;
plugins: Plugins;
};
type HasPasskeyConfig = HasPlugin<"passkey", BAuth>;
type HasOrganizationConfig = HasPlugin<"organization", BAuth>;
type Actions = Pick<
typeof actions,
| (HasPasskeyConfig extends true ? "passkey" : never)
| (HasOrganizationConfig extends true
? "setActiveOrganization" | "setInvitationId"
: never)
| "$atoms"
| "$fetch"
}>($fetch);
let pluginsActions = {} as Record<string, any>;
type PluginActions = UnionToIntersection<
O["authPlugins"] extends Array<infer Pl>
? //@ts-expect-error
ReturnType<Pl> extends {
actions?: infer R;
}
? R
: {}
: {}
>;
const proxy = createDynamicPathProxy(actions, $fetch, [
const pluginProxySignals: AuthProxySignal[] = [];
let pluginSignals: Record<string, PreinitializedWritableAtom<boolean>> = {};
let pluginPathMethods: Record<string, "POST" | "GET"> = {};
for (const plugin of options?.authPlugins || []) {
const pl = plugin($fetch);
if (pl.authProxySignal) {
pluginProxySignals.push(...pl.authProxySignal);
}
if (pl.actions) {
pluginsActions = {
...pluginsActions,
...pl.actions,
};
}
if (pl.signals) {
pluginSignals = {
...pluginSignals,
...pl.signals,
};
}
if (pl.pathMethods) {
pluginPathMethods = {
...pluginPathMethods,
...pl.pathMethods,
};
}
}
const actions = {
$atoms: {
$session,
},
$fetch,
...(pluginsActions as object),
};
type Actions = typeof actions & PluginActions;
const proxy = createDynamicPathProxy(
actions,
$fetch,
{
...pluginPathMethods,
"/sign-out": "POST",
},
[
{
matcher: (path) => path === "/organization/create",
atom: $listOrg,
atom: "$listOrg",
},
{
matcher: (path) => path.startsWith("/organization"),
atom: $activeOrgSignal,
atom: "$activeOrgSignal",
},
{
matcher: (path) => path === "/sign-out",
atom: $sessionSignal,
atom: "$sessionSignal",
},
{
matcher: (path) =>
path === "/two-factor/enable" || path === "/two-factor/send-otp",
atom: $sessionSignal,
matcher: (path) => path.startsWith("/sign-up"),
atom: "$sessionSignal",
},
]) as unknown as InferRoutes<API> & Actions;
...pluginProxySignals,
],
{
$sessionSignal,
...pluginSignals,
},
) as unknown as InferRoutes<API> & Actions;
return proxy;
};

View File

@@ -1,16 +1,20 @@
import { createClient } from "better-call/client";
import { describe, it } from "vitest";
import { passkey } from "../plugins";
import { getTestInstance } from "../test-utils/test-instance";
import { createAuthClient } from "./react";
import { createAuthClient } from "./base";
import { createAuthClient as createReactClient } from "./react";
import { twoFactor } from "./plugins/two-factor";
import { organization } from "./plugins/organization";
import { passkey } from "./plugins/passkey";
import { twoFactorClient } from "../plugins";
describe("client path to object", async () => {
const auth = await getTestInstance({
plugins: [
passkey({
rpID: "test",
rpName: "test",
}),
// passkey({
// rpID: "test",
// rpName: "test",
// }),
],
});
@@ -20,6 +24,13 @@ describe("client path to object", async () => {
customFetchImpl: async (url, options) => {
return new Response();
},
authPlugins: [twoFactor, passkey],
});
client.$atoms.$session;
const client2 = createReactClient({
authPlugins: [organization, twoFactorClient],
});
client2.$atoms.$session;
});
});

View File

@@ -0,0 +1,44 @@
import type { BetterFetch } from "@better-fetch/fetch";
import type { Endpoint } from "better-call";
import type { AuthProxySignal } from "./proxy";
import type { Atom, PreinitializedWritableAtom } from "nanostores";
import type { BetterAuthPlugin } from "../types/plugins";
import type { AuthPlugin } from "./type";
import type { useAuthStore as reactStore } from "./react";
import type { useAuthStore as vueStore } from "./vue";
import type { useAuthStore as preactStore } from "./preact";
export const createClientPlugin = <E extends BetterAuthPlugin = never>() => {
return <
Actions extends Record<string, any>,
Integrations extends {
react?: (useStore: typeof reactStore) => Record<string, any>;
vue?: (useStore: typeof vueStore) => Record<string, any>;
preact?: (useStore: typeof preactStore) => Record<string, any>;
},
>(
$fn: ($fetch: BetterFetch) => {
id: string;
actions?: Actions;
authProxySignal?: AuthProxySignal[];
signals?: Record<string, PreinitializedWritableAtom<boolean>>;
atoms?: Record<string, Atom<any>>;
integrations?: Integrations;
pathMethods?: Record<string, "POST" | "GET">;
},
) => {
return ($fetch: BetterFetch) => {
const data = $fn($fetch);
return {
...data,
integrations: data.integrations as Integrations,
plugin: {} as E,
};
};
};
};
export interface AuthClientPlugin {
id: string;
endpoint: Record<string, Endpoint>;
}

View File

@@ -38,9 +38,11 @@ export const csrfPlugin = {
const { data, error } = await betterFetch<{
csrfToken: string;
}>("/csrf", {
body: undefined,
baseURL: options.baseURL,
...options,
plugins: [],
method: "GET",
credentials: "include",
});
if (error?.status === 404) {
throw new BetterAuthError(
@@ -55,6 +57,7 @@ export const csrfPlugin = {
csrfToken: data.csrfToken,
};
}
options.credentials = "include";
return { url, options };
},
} satisfies BetterFetchPlugin;

View File

@@ -1 +1,2 @@
export * from "./base";
export * from "./plugins";

View File

@@ -0,0 +1,3 @@
export * from "../plugins/two-factor/client";
export * from "../plugins/organization/client";
export * from "../plugins/passkey/client";

View File

@@ -1,92 +0,0 @@
import { type BetterFetch, BetterFetchError } from "@better-fetch/fetch";
import { type Atom, atom, computed, onMount, task } from "nanostores";
import type {
Invitation,
Member,
Organization,
} from "../../plugins/organization/schema";
import type { Prettify } from "../../types/helper";
export function getOrganizationAtoms($fetch: BetterFetch, $session: Atom) {
const $listOrg = atom<boolean>(false);
const activeOrgId = atom<string | null>(null);
const $activeOrgSignal = atom<boolean>(false);
const $activeOrganization = computed([activeOrgId, $activeOrgSignal], () =>
task(async () => {
if (!activeOrgId.get()) {
return null;
}
const session = $session.get();
if (!session) {
return null;
}
const organization = await $fetch<
Prettify<
Organization & {
members: Member[];
invitations: Invitation[];
}
>
>("/organization/set-active", {
method: "POST",
credentials: "include",
body: {
orgId: activeOrgId.get(),
},
});
return organization.data;
}),
);
const $listOrganizations = computed($listOrg, () =>
task(async () => {
const organizations = await $fetch<Organization[]>("/organization/list", {
method: "GET",
});
return organizations.data;
}),
);
const $activeInvitationId = atom<string | null>(null);
const $invitation = computed($activeInvitationId, () =>
task(async () => {
if (!$activeInvitationId.get()) {
return {
error: {
message: "No invitation found",
status: 400,
data: null,
},
data: null,
};
}
const res = await $fetch<
Prettify<
Invitation & {
organizationName: string;
organizationSlug: string;
inviterEmail: string;
inviterName: string;
}
>
>("/organization/get-active-invitation", {
method: "GET",
query: {
id: $activeInvitationId.get(),
},
credentials: "include",
});
return res;
}),
);
return {
$listOrganizations,
$activeOrganization,
activeOrgId,
$listOrg,
$activeOrgSignal,
$activeInvitationId,
$invitation,
};
}

View File

@@ -0,0 +1,140 @@
import { atom, computed, task } from "nanostores";
import type {
Invitation,
Member,
Organization,
} from "../../plugins/organization/schema";
import type { Prettify } from "../../types/helper";
import type { organization as org } from "../../plugins";
import { createClientPlugin } from "../create-client-plugin";
export const organization = createClientPlugin<ReturnType<typeof org>>()(
($fetch) => {
const activeOrgId = atom<string | null>(null);
const $listOrg = atom<boolean>(false);
const $activeOrgSignal = atom<boolean>(false);
const $activeOrganization = computed([activeOrgId, $activeOrgSignal], () =>
task(async () => {
if (!activeOrgId.get()) {
return null;
}
const organization = await $fetch<
Prettify<
Organization & {
members: Member[];
invitations: Invitation[];
}
>
>("/organization/set-active", {
method: "POST",
credentials: "include",
body: {
orgId: activeOrgId.get(),
},
});
return organization.data;
}),
);
const $listOrganizations = computed($listOrg, () =>
task(async () => {
const organizations = await $fetch<Organization[]>(
"/organization/list",
{
method: "GET",
},
);
return organizations.data;
}),
);
const $activeInvitationId = atom<string | null>(null);
const $invitation = computed($activeInvitationId, () =>
task(async () => {
if (!$activeInvitationId.get()) {
return {
error: {
message: "No invitation found",
status: 400,
data: null,
},
data: null,
};
}
const res = await $fetch<
Prettify<
Invitation & {
organizationName: string;
organizationSlug: string;
inviterEmail: string;
inviterName: string;
}
>
>("/organization/get-active-invitation", {
method: "GET",
query: {
id: $activeInvitationId.get(),
},
credentials: "include",
});
return res;
}),
);
return {
id: "organization",
actions: {
setActiveOrganization(orgId: string | null) {
activeOrgId.set(orgId);
},
setInvitationId: (id: string | null) => {
$activeInvitationId.set(id);
},
},
integrations: {
react(useStore) {
return {
useActiveOrganization() {
return useStore($activeOrganization);
},
useListOrganization() {
return useStore($listOrganizations);
},
useInvitation() {
return useStore($invitation);
},
};
},
vue(useStore) {
return {
useActiveOrganization() {
return useStore($activeOrganization);
},
useListOrganization() {
return useStore($listOrganizations);
},
useInvitation() {
return useStore($invitation);
},
};
},
preact(useStore) {
return {
useActiveOrganization() {
return useStore($activeOrganization);
},
useListOrganization() {
return useStore($listOrganizations);
},
useInvitation() {
return useStore($invitation);
},
};
},
},
signals: {
$listOrg,
$activeOrgSignal,
},
};
},
);

View File

@@ -10,7 +10,8 @@ import type {
} from "@simplewebauthn/types";
import type { Session } from "inspector";
import type { User } from "../../adapters/schema";
import type { Passkey } from "../../plugins";
import type { passkey as passkeyPl, Passkey } from "../../plugins";
import { createClientPlugin } from "../create-client-plugin";
export const getPasskeyActions = ($fetch: BetterFetch) => {
const signInPasskey = async (opts?: {
@@ -95,3 +96,12 @@ export const getPasskeyActions = ($fetch: BetterFetch) => {
register,
};
};
export const passkey = createClientPlugin<ReturnType<typeof passkeyPl>>()(
($fetch) => {
return {
id: "passkey",
actions: getPasskeyActions($fetch),
};
},
);

View File

@@ -0,0 +1,23 @@
import { createClientPlugin } from "../create-client-plugin";
import type { twoFactor as twoFa } from "../../plugins/two-factor";
export const twoFactor = createClientPlugin<ReturnType<typeof twoFa>>()(
($fetch) => {
return {
id: "two-factor",
authProxySignal: [
{
matcher: (path) =>
path === "/two-factor/enable" || path === "/two-factor/send-otp",
atom: "$sessionSignal",
},
],
pathMethods: {
"enable/totp": "POST",
"/two-factor/disable": "POST",
"/two-factor/enable": "POST",
"/two-factor/send-otp": "POST",
},
};
},
);

View File

@@ -6,27 +6,5 @@ import { createAuthClient as 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);
}
const useInvitation = () => {
return (
useAuthStore(client.$atoms.$invitation) || {
error: null,
data: null,
}
);
};
return Object.assign(client, {
useSession,
useActiveOrganization,
useListOrganization,
useInvitation,
});
return Object.assign(client);
};

View File

@@ -1,16 +1,13 @@
import type { BetterFetch } from "@better-fetch/fetch";
import type { PreinitializedWritableAtom } from "nanostores";
import type { ProxyRequest } from "./path-to-object";
import type { LiteralUnion } from "type-fest";
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?: ProxyRequest) {
function getMethod(
path: string,
knownPathMethods: Record<string, "POST" | "GET">,
args?: ProxyRequest,
) {
const method = knownPathMethods[path];
const { options, query, ...body } = args || {};
if (method) {
@@ -25,13 +22,17 @@ function getMethod(path: string, args?: ProxyRequest) {
return "GET";
}
export type AuthProxySignal = {
atom: LiteralUnion<string, "$sessionSignal">;
matcher: (path: string) => boolean;
};
export function createDynamicPathProxy<T extends Record<string, any>>(
routes: T,
client: BetterFetch,
$signal?: {
atom: PreinitializedWritableAtom<boolean>;
matcher: (path: string) => boolean;
}[],
knownPathMethods: Record<string, "POST" | "GET">,
$signal?: AuthProxySignal[],
$signals?: Record<string, PreinitializedWritableAtom<boolean>>,
): T {
const handler: ProxyHandler<any> = {
get(target, prop: string) {
@@ -56,7 +57,7 @@ export function createDynamicPathProxy<T extends Record<string, any>>(
.join("/");
const routePath = `/${path}`;
const arg = (args[0] || {}) as ProxyRequest;
const method = getMethod(routePath, arg);
const method = getMethod(routePath, knownPathMethods, arg);
const { query, options, ...body } = arg;
return await client(routePath, {
...options,
@@ -65,9 +66,10 @@ export function createDynamicPathProxy<T extends Record<string, any>>(
method,
onSuccess() {
const signal = $signal?.find((s) => s.matcher(routePath));
if (signal) {
signal.atom.set(!signal.atom.get());
}
if (!signal) return;
const signalAtom = $signals?.[signal.atom];
if (!signalAtom) return;
signalAtom.set(!signalAtom.get());
},
});
},

View File

@@ -1,18 +1,39 @@
import type { BetterFetchOption } from "@better-fetch/fetch";
import { useStore } from "@nanostores/react";
import type { BetterAuth } from "../auth";
import type { InferSession, InferUser } from "../types";
import { createAuthClient as createVanillaClient } from "./base";
import type { AuthPlugin, ClientOptions } from "./type";
import type { UnionToIntersection } from "../types/helper";
export const createAuthClient = <Auth extends BetterAuth>(
options?: BetterFetchOption,
export const createAuthClient = <Option extends ClientOptions>(
options?: Option,
) => {
const client = createVanillaClient<Auth>(options);
const client = createVanillaClient(options);
const hooks = options?.authPlugins?.reduce(
(acc, plugin) => {
return {
...acc,
...(plugin(client.$fetch).integrations?.react?.(useStore) || {}),
};
},
{} as Record<string, any>,
) as Option["authPlugins"] extends Array<infer Pl>
? Pl extends AuthPlugin
? UnionToIntersection<
ReturnType<Pl>["integrations"] extends
| {
react?: (useStore: any) => infer R;
}
| undefined
? R
: {
test1: ReturnType<Pl>["integrations"];
}
>
: {}
: {};
function useSession(
initialValue: {
user: InferUser<Auth>;
session: InferSession<Auth>;
} | null = null,
initialValue: typeof client.$atoms.$session.value = null,
) {
const session = useStore(client.$atoms.$session);
if (session) {
@@ -20,26 +41,10 @@ export const createAuthClient = <Auth extends BetterAuth>(
}
return initialValue;
}
function useActiveOrganization() {
return useStore(client.$atoms.$activeOrganization);
}
function useListOrganization() {
return useStore(client.$atoms.$listOrganizations);
}
function useInvitation() {
return (
useAuthStore(client.$atoms.$invitation) || {
error: null,
data: null,
}
);
}
const obj = Object.assign(client, {
useSession,
useActiveOrganization,
useListOrganization,
useInvitation,
...hooks,
});
return obj;
};

View File

@@ -1,6 +1,6 @@
import type { BetterFetch } from "@better-fetch/fetch";
import { atom, computed, task } from "nanostores";
import type { BetterAuth } from "../auth";
import type { Auth as BetterAuth } from "../auth";
import type { Prettify } from "../types/helper";
import type { InferSession, InferUser } from "../types/models";

View File

@@ -1,20 +1,47 @@
import type { BetterFetchOption } from "@better-fetch/fetch";
import type { BetterAuth } from "../auth";
import type { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
import type { Auth } from "../auth";
import type { UnionToIntersection } from "../types/helper";
import type { useAuthStore as reactStore } from "./react";
import type { useAuthStore as vueStore } from "./vue";
import type { useAuthStore as preactStore } from "./preact";
import type { AuthProxySignal } from "./proxy";
import type { Atom, PreinitializedWritableAtom } from "nanostores";
import type { BetterAuthPlugin } from "../types/plugins";
export type AuthStore = typeof reactStore | typeof vueStore;
export type AuthPlugin = ($fetch: BetterFetch) => {
id: string;
/**
* only used for type inference. don't pass the
* actual plugin
*/
plugin?: BetterAuthPlugin;
actions?: Record<string, any>;
authProxySignal?: AuthProxySignal[];
signals?: Record<string, PreinitializedWritableAtom<boolean>>;
atoms?: Record<string, Atom<any>>;
/**
* Framework integrations
*/
integrations?: {
react?: (useStore: typeof reactStore) => Record<string, any>;
vue?: (useStore: typeof vueStore) => Record<string, any>;
preact?: (useStore: typeof preactStore) => Record<string, any>;
};
pathMethods?: Record<string, "POST" | "GET">;
};
export interface ClientOptions extends BetterFetchOption {
/**
* csrf plugin is enabled by default
*/
csrfPlugin?: boolean;
authPlugins?: AuthPlugin[];
}
export type HasPlugin<
PluginId extends string,
Auth extends BetterAuth,
> = Auth["options"]["plugins"] extends Array<infer T>
A extends Auth,
> = A["options"]["plugins"] extends Array<infer T>
? UnionToIntersection<T extends { id: PluginId } ? true : false>
: false;

View File

@@ -4,29 +4,7 @@ import { createAuthClient as 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);
}
const useInvitation = () => {
return (
useAuthStore(client.$atoms.$invitation) || {
error: null,
data: null,
}
);
};
return Object.assign(client, {
useSession,
useActiveOrganization,
useListOrganization,
useInvitation,
});
return Object.assign(client);
};
export const useAuthStore = useStore;

View File

@@ -0,0 +1,66 @@
async function importKeyFromString(keyString: string): Promise<CryptoKey> {
const encoder = new TextEncoder();
const keyData = encoder.encode(keyString);
return crypto.subtle.importKey(
"raw",
keyData,
{
name: "AES-GCM",
length: 256,
},
false,
["encrypt", "decrypt"],
);
}
export async function symmetricEncrypt({
data,
key,
}: {
data: string;
key: string;
}): Promise<string> {
const _key = await importKeyFromString(key);
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12)); // Initialization vector
const encryptedContent = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
_key,
encoder.encode(data),
);
const encryptedArray = new Uint8Array(
iv.byteLength + encryptedContent.byteLength,
);
encryptedArray.set(iv);
encryptedArray.set(new Uint8Array(encryptedContent), iv.byteLength);
return Buffer.from(encryptedArray).toString("base64");
}
export async function symmetricDecrypt({
data,
key,
}: {
data: string;
key: string;
}): Promise<string> {
const _key = await importKeyFromString(key);
const encryptedArray = new Uint8Array(Buffer.from(data, "base64"));
const iv = encryptedArray.slice(0, 12);
const encryptedContent = encryptedArray.slice(12);
const decryptedContent = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
_key,
encryptedContent,
);
const decoder = new TextDecoder();
return decoder.decode(decryptedContent);
}

View File

@@ -14,6 +14,7 @@ export const init = (options: BetterAuthOptions) => {
const adapter = getAdapter(options);
const db = createKyselyAdapter(options);
const { baseURL, withPath } = getBaseURL(options.baseURL, options.basePath);
return {
options: {
...options,

View File

@@ -0,0 +1 @@

View File

@@ -8,7 +8,6 @@ import {
} from "zod";
import type { User } from "../../adapters/schema";
import { createAuthEndpoint } from "../../api/call";
import { sessionMiddleware } from "../../api/middlewares/session";
import { getSessionFromCtx } from "../../api/routes";
import type { AuthContext } from "../../init";
import type { BetterAuthPlugin } from "../../types/plugins";

View File

@@ -0,0 +1,107 @@
import type { BetterFetch } from "@better-fetch/fetch";
import {
WebAuthnError,
startAuthentication,
startRegistration,
} from "@simplewebauthn/browser";
import type {
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/types";
import type { Session } from "inspector";
import type { User } from "../../adapters/schema";
import type { passkey as passkeyPl, Passkey } from "../../plugins";
import { createClientPlugin } from "../../client/create-client-plugin";
export const getPasskeyActions = ($fetch: BetterFetch) => {
const signInPasskey = async (opts?: {
autoFill?: boolean;
email?: string;
callbackURL?: string;
}) => {
const response = await $fetch<PublicKeyCredentialRequestOptionsJSON>(
"/passkey/generate-authenticate-options",
{
method: "POST",
body: {
email: opts?.email,
},
},
);
if (!response.data) {
return response;
}
try {
const res = await startAuthentication(
response.data,
opts?.autoFill || false,
);
const verified = await $fetch<{
session: Session;
user: User;
}>("/passkey/verify-authentication", {
body: {
response: res,
type: "authenticate",
},
});
if (!verified.data) {
return verified;
}
} catch (e) {
console.log(e);
}
};
const register = async () => {
const options = await $fetch<PublicKeyCredentialCreationOptionsJSON>(
"/passkey/generate-register-options",
{
method: "GET",
},
);
if (!options.data) {
return options;
}
try {
const res = await startRegistration(options.data);
const verified = await $fetch<{
passkey: Passkey;
}>("/passkey/verify-registration", {
body: {
response: res,
type: "register",
},
});
if (!verified.data) {
return verified;
}
} catch (e) {
if (e instanceof WebAuthnError) {
if (e.code === "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED") {
return {
data: null,
error: {
message: "previously registered",
status: 400,
statusText: "BAD_REQUEST",
},
};
}
}
}
};
return {
signInPasskey,
register,
};
};
export const passkeyClient = createClientPlugin<ReturnType<typeof passkeyPl>>()(
($fetch) => {
return {
id: "passkey",
actions: getPasskeyActions($fetch),
};
},
);

View File

@@ -459,3 +459,5 @@ export const passkey = (options: PasskeyOptions) => {
},
} satisfies BetterAuthPlugin;
};
export * from "./client";

View File

@@ -49,23 +49,23 @@ export async function generateBackupCodes(
};
}
export function verifyBackupCode(
export async function verifyBackupCode(
data: {
user: UserWithTwoFactor;
code: string;
},
key: string,
) {
const codes = getBackupCodes(data.user, key);
const codes = await getBackupCodes(data.user, key);
if (!codes) {
return false;
}
return codes.includes(data.code);
}
export function getBackupCodes(user: UserWithTwoFactor, key: string) {
export async function getBackupCodes(user: UserWithTwoFactor, key: string) {
const secret = Buffer.from(
symmetricDecrypt({ key, data: user.twoFactorBackupCodes }),
await symmetricDecrypt({ key, data: user.twoFactorBackupCodes }),
).toString("utf-8");
const data = JSON.parse(secret);
const result = z.array(z.string()).safeParse(data);
@@ -81,6 +81,7 @@ export const backupCode2fa = (options?: BackupCodeOptions) => {
endpoints: {
verifyBackupCode: createAuthEndpoint(
"/two-factor/verify-backup-code",
{
method: "POST",
body: z.object({

View File

@@ -0,0 +1,23 @@
import { createClientPlugin } from "../../client/create-client-plugin";
import type { twoFactor as twoFa } from "../../plugins/two-factor";
export const twoFactorClient = createClientPlugin<ReturnType<typeof twoFa>>()(
($fetch) => {
return {
id: "two-factor",
authProxySignal: [
{
matcher: (path) =>
path === "/two-factor/enable" || path === "/two-factor/send-otp",
atom: "$sessionSignal",
},
],
pathMethods: {
"enable/totp": "POST",
"/two-factor/disable": "POST",
"/two-factor/enable": "POST",
"/two-factor/send-otp": "POST",
},
};
},
);

View File

@@ -36,7 +36,7 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
async (ctx) => {
const user = ctx.context.session.user as UserWithTwoFactor;
const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
const encryptedSecret = symmetricEncrypt({
const encryptedSecret = await symmetricEncrypt({
key: ctx.context.secret,
data: secret,
});
@@ -114,3 +114,5 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
},
} satisfies BetterAuthPlugin;
};
export * from "./client";

View File

@@ -107,7 +107,7 @@ export const totp2fa = (options: TOTPOptions) => {
}
const totp = new TOTPController(opts);
const secret = Buffer.from(
symmetricDecrypt({
await symmetricDecrypt({
key: ctx.context.secret,
data: ctx.context.session.user.twoFactorSecret,
}),

View File

@@ -1,6 +1,6 @@
import type { BetterAuthOptions } from ".";
import type { Session, User } from "../adapters/schema";
import type { BetterAuth } from "../auth";
import type { Auth } from "../auth";
import type { FieldAttribute, InferFieldOutput } from "../db";
import type { Prettify, UnionToIntersection } from "./helper";
@@ -52,22 +52,21 @@ type AdditionalUserFields<Options extends BetterAuthOptions> =
: {}
: {};
export type InferUser<O extends BetterAuthOptions | BetterAuth> =
UnionToIntersection<
export type InferUser<O extends BetterAuthOptions | Auth> = UnionToIntersection<
User &
(O extends BetterAuthOptions
? AdditionalUserFields<O>
: O extends BetterAuth
: O extends Auth
? AdditionalUserFields<O["options"]>
: {})
>;
>;
export type InferSession<O extends BetterAuthOptions | BetterAuth> =
export type InferSession<O extends BetterAuthOptions | Auth> =
UnionToIntersection<
Session &
(O extends BetterAuthOptions
? AdditionalSessionFields<O>
: O extends BetterAuth
: O extends Auth
? AdditionalSessionFields<O["options"]>
: {})
>;

View File

@@ -1,3 +1,5 @@
import { BetterAuthError } from "../error/better-auth-error";
function checkHasPath(url: string): boolean {
try {
const parsedUrl = new URL(url);
@@ -35,6 +37,7 @@ export function getBaseURL(url?: string, path?: string) {
if (fromEnv) {
return withPath(fromEnv, path);
}
console.log(process.env);
if (
!fromEnv &&
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
@@ -44,7 +47,7 @@ export function getBaseURL(url?: string, path?: string) {
withPath: "http://localhost:3000/api/auth",
};
}
throw new Error(
"Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",
throw new BetterAuthError(
"Could not infer baseURL from environment variables",
);
}

View File

@@ -7,11 +7,11 @@
"resolveJsonModule": true,
"module": "Preserve",
"noEmit": true,
"moduleResolution": "Bundler",
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
},

View File

@@ -11,6 +11,7 @@ export default defineConfig({
preact: "./src/client/preact.ts",
vue: "./src/client/vue.ts",
plugins: "./src/plugins/index.ts",
"client/plugins": "./src/client/plugins.ts",
},
splitting: false,
sourcemap: true,

144
pnpm-lock.yaml generated
View File

@@ -34,21 +34,33 @@ importers:
specifier: 5.5.0-dev.20240520
version: 5.5.0-dev.20240520
dev/hono+react/hono:
dev/bc-fe/hono:
dependencies:
'@hono/node-server':
specifier: ^1.12.2
version: 1.12.2(hono@4.5.9)
better-auth:
specifier: workspace:*
specifier: ^0.0.2-beta.8
version: link:../../../packages/better-auth
dotenv:
specifier: ^16.4.5
version: 16.4.5
hono:
specifier: ^4.5.9
version: 4.5.9
tsx:
specifier: ^4.19.0
version: 4.19.0
devDependencies:
'@types/bun':
specifier: latest
version: 1.1.8
dev/hono+react/react:
dev/bc-fe/react:
dependencies:
better-auth:
specifier: workspace:^0.0.2-beta.8
version: link:../../../packages/better-auth
react:
specifier: ^18.3.1
version: 18.3.1
@@ -135,7 +147,7 @@ importers:
specifier: ^3.12.0
version: 3.12.0(react@18.3.1)
better-auth:
specifier: workspace:*
specifier: ^0.0.2-beta.8
version: link:../../packages/better-auth
better-call:
specifier: ^0.1.0
@@ -294,6 +306,9 @@ importers:
prompts:
specifier: ^2.4.2
version: 2.4.2
ts-morph:
specifier: ^23.0.0
version: 23.0.0
zod:
specifier: ^3.22.5
version: 3.23.8
@@ -333,7 +348,7 @@ importers:
version: 2.2.5(react@18.3.1)
tsup:
specifier: ^8.2.4
version: 8.2.4(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0)
version: 8.2.4(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)(typescript@5.5.4)(yaml@2.5.0)
type-fest:
specifier: ^4.24.0
version: 4.24.0
@@ -412,11 +427,6 @@ packages:
resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.25.3':
resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.25.6':
resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==}
engines: {node: '>=6.0.0'}
@@ -442,10 +452,6 @@ packages:
resolution: {integrity: sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==}
engines: {node: '>=6.9.0'}
'@babel/types@7.25.2':
resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==}
engines: {node: '>=6.9.0'}
'@babel/types@7.25.6':
resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==}
engines: {node: '>=6.9.0'}
@@ -850,6 +856,12 @@ packages:
'@hexagon/base64@1.1.28':
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
'@hono/node-server@1.12.2':
resolution: {integrity: sha512-xjzhqhSWUE/OhN0g3KCNVzNsQMlFUAL+/8GgPUr3TKcU7cvgZVBGswFofJ8WwGEHTqobzze1lDpGJl9ZNckDhA==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: ^4
'@hookform/resolvers@3.9.0':
resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==}
peerDependencies:
@@ -1878,6 +1890,9 @@ packages:
'@tabler/icons@3.12.0':
resolution: {integrity: sha512-Im37ar/mQkqLb6XUXsU7nOc4/66VB9/3KLuZ+6tUsJKHHNLaDUkYfCTNG3pnGDI03laByxVf5+umSNK2yPTx8A==}
'@ts-morph/common@0.24.0':
resolution: {integrity: sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==}
'@tybys/wasm-util@0.8.3':
resolution: {integrity: sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==}
@@ -2262,6 +2277,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
code-block-writer@13.0.2:
resolution: {integrity: sha512-XfXzAGiStXSmCIwrkdfvc7FS5Dtj8yelCtyOf2p2skCAfvLd6zu0rGzuS9NSCO3bq1JKpFZ7tbKdKlcd5occQA==}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -2604,6 +2622,9 @@ packages:
resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
engines: {node: '>=16'}
get-tsconfig@4.8.0:
resolution: {integrity: sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==}
giget@1.2.3:
resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==}
hasBin: true
@@ -2996,6 +3017,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
mkdirp@3.0.1:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
hasBin: true
mlly@1.7.1:
resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==}
@@ -3157,6 +3183,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -3480,6 +3509,9 @@ packages:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
@@ -3773,6 +3805,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
ts-morph@23.0.0:
resolution: {integrity: sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==}
tslib@2.6.3:
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
@@ -3795,6 +3830,11 @@ packages:
typescript:
optional: true
tsx@4.19.0:
resolution: {integrity: sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==}
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -4075,10 +4115,10 @@ snapshots:
'@babel/helper-compilation-targets': 7.25.2
'@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2)
'@babel/helpers': 7.25.6
'@babel/parser': 7.25.3
'@babel/parser': 7.25.6
'@babel/template': 7.25.0
'@babel/traverse': 7.25.6
'@babel/types': 7.25.2
'@babel/types': 7.25.6
convert-source-map: 2.0.0
debug: 4.3.6
gensync: 1.0.0-beta.2
@@ -4105,7 +4145,7 @@ snapshots:
'@babel/helper-module-imports@7.24.7':
dependencies:
'@babel/traverse': 7.25.6
'@babel/types': 7.25.2
'@babel/types': 7.25.6
transitivePeerDependencies:
- supports-color
@@ -4124,7 +4164,7 @@ snapshots:
'@babel/helper-simple-access@7.24.7':
dependencies:
'@babel/traverse': 7.25.6
'@babel/types': 7.25.2
'@babel/types': 7.25.6
transitivePeerDependencies:
- supports-color
@@ -4146,10 +4186,6 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.0.1
'@babel/parser@7.25.3':
dependencies:
'@babel/types': 7.25.2
'@babel/parser@7.25.6':
dependencies:
'@babel/types': 7.25.6
@@ -4167,8 +4203,8 @@ snapshots:
'@babel/template@7.25.0':
dependencies:
'@babel/code-frame': 7.24.7
'@babel/parser': 7.25.3
'@babel/types': 7.25.2
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
'@babel/traverse@7.25.6':
dependencies:
@@ -4182,12 +4218,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/types@7.25.2':
dependencies:
'@babel/helper-string-parser': 7.24.8
'@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0
'@babel/types@7.25.6':
dependencies:
'@babel/helper-string-parser': 7.24.8
@@ -4448,6 +4478,10 @@ snapshots:
'@hexagon/base64@1.1.28': {}
'@hono/node-server@1.12.2(hono@4.5.9)':
dependencies:
hono: 4.5.9
'@hookform/resolvers@3.9.0(react-hook-form@7.52.2(react@18.3.1))':
dependencies:
react-hook-form: 7.52.2(react@18.3.1)
@@ -5317,6 +5351,13 @@ snapshots:
'@tabler/icons@3.12.0': {}
'@ts-morph/common@0.24.0':
dependencies:
fast-glob: 3.3.2
minimatch: 9.0.5
mkdirp: 3.0.1
path-browserify: 1.0.1
'@tybys/wasm-util@0.8.3':
dependencies:
tslib: 2.6.3
@@ -5324,24 +5365,24 @@ snapshots:
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.25.3
'@babel/types': 7.25.2
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.20.6
'@types/babel__generator@7.6.8':
dependencies:
'@babel/types': 7.25.2
'@babel/types': 7.25.6
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.25.3
'@babel/types': 7.25.2
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
'@types/babel__traverse@7.20.6':
dependencies:
'@babel/types': 7.25.2
'@babel/types': 7.25.6
'@types/better-sqlite3@7.6.11':
dependencies:
@@ -5812,6 +5853,8 @@ snapshots:
clsx@2.1.1: {}
code-block-writer@13.0.2: {}
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -6164,6 +6207,10 @@ snapshots:
get-stream@8.0.1: {}
get-tsconfig@4.8.0:
dependencies:
resolve-pkg-maps: 1.0.0
giget@1.2.3:
dependencies:
citty: 0.1.6
@@ -6504,6 +6551,8 @@ snapshots:
mkdirp@1.0.4: {}
mkdirp@3.0.1: {}
mlly@1.7.1:
dependencies:
acorn: 8.12.1
@@ -6680,6 +6729,8 @@ snapshots:
dependencies:
callsites: 3.1.0
path-browserify@1.0.1: {}
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -6781,12 +6832,13 @@ snapshots:
optionalDependencies:
postcss: 8.4.40
postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.41)(yaml@2.5.0):
postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)(yaml@2.5.0):
dependencies:
lilconfig: 3.1.2
optionalDependencies:
jiti: 1.21.6
postcss: 8.4.41
tsx: 4.19.0
yaml: 2.5.0
postcss-nested@6.2.0(postcss@8.4.40):
@@ -6978,6 +7030,8 @@ snapshots:
resolve-from@5.0.0: {}
resolve-pkg-maps@1.0.0: {}
resolve@1.22.8:
dependencies:
is-core-module: 2.15.0
@@ -7288,9 +7342,14 @@ snapshots:
ts-interface-checker@0.1.13: {}
ts-morph@23.0.0:
dependencies:
'@ts-morph/common': 0.24.0
code-block-writer: 13.0.2
tslib@2.6.3: {}
tsup@8.2.4(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0):
tsup@8.2.4(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)(typescript@5.5.4)(yaml@2.5.0):
dependencies:
bundle-require: 5.0.0(esbuild@0.23.0)
cac: 6.7.14
@@ -7302,7 +7361,7 @@ snapshots:
globby: 11.1.0
joycon: 3.1.1
picocolors: 1.0.1
postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.4.41)(yaml@2.5.0)
postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)(yaml@2.5.0)
resolve-from: 5.0.0
rollup: 4.19.1
source-map: 0.8.0-beta.0
@@ -7317,6 +7376,13 @@ snapshots:
- tsx
- yaml
tsx@4.19.0:
dependencies:
esbuild: 0.23.0
get-tsconfig: 4.8.0
optionalDependencies:
fsevents: 2.3.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1

View File

@@ -5,21 +5,15 @@
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"disableReferencedProjectLoad": true,
"module": "Preserve",
"noEmit": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,
"checkJs": true,
"lib": ["dom", "dom.iterable", "ES2022"],
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
"types": ["node"]
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
},
"exclude": ["**/dist/**", "**/node_modules/**", "**/examples/**"]
}