@cfast/auth 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,12 +138,12 @@ function Header() {
138
138
 
139
139
  ### Login Page
140
140
 
141
- The consumer creates their own login route and renders `<LoginPage>`. The component accepts an `authClient` prop and a `components` prop for UI slot overrides. Default slots render plain HTML — use `@cfast/ui/joy` for Joy UI styling.
141
+ The consumer creates their own login route and renders `<LoginPage>`. The component accepts an `authClient` prop and a `components` prop for UI slot overrides. Default slots render plain HTML — use `@cfast/joy` for Joy UI styling.
142
142
 
143
143
  ```typescript
144
144
  // routes/login.tsx
145
145
  import { LoginPage } from "@cfast/auth/client";
146
- import { joyLoginComponents } from "@cfast/ui/joy";
146
+ import { joyLoginComponents } from "@cfast/joy";
147
147
  import { authClient } from "~/auth.client";
148
148
 
149
149
  export default function Login() {
@@ -196,7 +196,7 @@ export default function Login() {
196
196
  }
197
197
  ```
198
198
 
199
- For Joy UI, use the pre-built `joyLoginComponents` from `@cfast/ui/joy` instead of writing custom slots.
199
+ For Joy UI, use the pre-built `joyLoginComponents` from `@cfast/joy` instead of writing custom slots.
200
200
 
201
201
  ### Redirect Flow
202
202
 
package/dist/client.d.ts CHANGED
@@ -72,9 +72,15 @@ type LoginComponents = {
72
72
  ErrorMessage?: ComponentType<{
73
73
  error: string;
74
74
  }>;
75
+ PasskeySignUpButton?: ComponentType<{
76
+ onClick: () => void;
77
+ loading: boolean;
78
+ }>;
75
79
  };
76
80
  type LoginPageProps = {
77
- authClient: {
81
+ /** Pass `undefined` during SSR (e.g. from a `.client.ts` module). The
82
+ * component renders a static shell and hydrates with full interactivity. */
83
+ authClient?: {
78
84
  signIn: {
79
85
  magicLink: (opts: {
80
86
  email: string;
@@ -89,6 +95,24 @@ type LoginPageProps = {
89
95
  } | null;
90
96
  } | undefined>;
91
97
  };
98
+ signUp?: {
99
+ email: (opts: {
100
+ email: string;
101
+ password: string;
102
+ name: string;
103
+ }) => Promise<{
104
+ error?: {
105
+ message?: string;
106
+ } | null;
107
+ }>;
108
+ };
109
+ passkey?: {
110
+ addPasskey: () => Promise<{
111
+ error?: {
112
+ message?: string;
113
+ } | null;
114
+ } | undefined>;
115
+ };
92
116
  };
93
117
  components?: LoginComponents;
94
118
  title?: string;
package/dist/client.js CHANGED
@@ -133,6 +133,21 @@ function DefaultPasskeyButton({
133
133
  }
134
134
  );
135
135
  }
136
+ function DefaultPasskeySignUpButton({
137
+ onClick,
138
+ loading
139
+ }) {
140
+ return /* @__PURE__ */ jsx4(
141
+ "button",
142
+ {
143
+ type: "button",
144
+ onClick,
145
+ disabled: loading,
146
+ style: { width: "100%", padding: 8 },
147
+ children: loading ? "Creating account..." : "Sign up with Passkey"
148
+ }
149
+ );
150
+ }
136
151
  function DefaultSuccessMessage({ email }) {
137
152
  return /* @__PURE__ */ jsxs("div", { role: "status", children: [
138
153
  "Check your email (",
@@ -154,6 +169,7 @@ function LoginPage({
154
169
  const EmailInput = components.EmailInput ?? DefaultEmailInput;
155
170
  const MagicLinkBtn = components.MagicLinkButton ?? DefaultMagicLinkButton;
156
171
  const PasskeyBtn = components.PasskeyButton ?? DefaultPasskeyButton;
172
+ const PasskeySignUpBtn = components.PasskeySignUpButton ?? DefaultPasskeySignUpButton;
157
173
  const SuccessMsg = components.SuccessMessage ?? DefaultSuccessMessage;
158
174
  const ErrorMsg = components.ErrorMessage ?? DefaultErrorMessage;
159
175
  const [email, setEmail] = useState("");
@@ -161,11 +177,14 @@ function LoginPage({
161
177
  const [loading, setLoading] = useState(false);
162
178
  const [error, setError] = useState(null);
163
179
  const [passkeyLoading, setPasskeyLoading] = useState(false);
180
+ const [passkeySignUpLoading, setPasskeySignUpLoading] = useState(false);
181
+ const canPasskeySignUp = !!(authClient?.signUp?.email && authClient?.passkey?.addPasskey);
164
182
  async function handleMagicLink() {
165
183
  if (!email.trim()) {
166
184
  setError("Please enter your email address.");
167
185
  return;
168
186
  }
187
+ if (!authClient) return;
169
188
  setLoading(true);
170
189
  setError(null);
171
190
  try {
@@ -183,6 +202,7 @@ function LoginPage({
183
202
  }
184
203
  }
185
204
  async function handlePasskey() {
205
+ if (!authClient) return;
186
206
  setPasskeyLoading(true);
187
207
  setError(null);
188
208
  try {
@@ -198,6 +218,36 @@ function LoginPage({
198
218
  setPasskeyLoading(false);
199
219
  }
200
220
  }
221
+ async function handlePasskeySignUp() {
222
+ if (!email.trim()) {
223
+ setError("Please enter your email address.");
224
+ return;
225
+ }
226
+ if (!authClient) return;
227
+ setPasskeySignUpLoading(true);
228
+ setError(null);
229
+ try {
230
+ const signUpResult = await authClient.signUp.email({
231
+ email,
232
+ password: crypto.randomUUID(),
233
+ name: ""
234
+ });
235
+ if (signUpResult.error) {
236
+ setError(signUpResult.error.message ?? "Sign-up failed.");
237
+ return;
238
+ }
239
+ const passkeyResult = await authClient.passkey.addPasskey();
240
+ if (passkeyResult?.error) {
241
+ setError(passkeyResult.error.message ?? "Passkey registration failed.");
242
+ return;
243
+ }
244
+ onSuccess?.();
245
+ } catch {
246
+ setError("Passkey sign-up failed. Please try again.");
247
+ } finally {
248
+ setPasskeySignUpLoading(false);
249
+ }
250
+ }
201
251
  return /* @__PURE__ */ jsxs(Layout, { children: [
202
252
  /* @__PURE__ */ jsx4("h2", { children: title }),
203
253
  subtitle && /* @__PURE__ */ jsx4("p", { children: subtitle }),
@@ -205,7 +255,14 @@ function LoginPage({
205
255
  sent ? /* @__PURE__ */ jsx4(SuccessMsg, { email }) : /* @__PURE__ */ jsxs("div", { children: [
206
256
  /* @__PURE__ */ jsx4(EmailInput, { value: email, onChange: setEmail }),
207
257
  /* @__PURE__ */ jsx4("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx4(MagicLinkBtn, { onClick: handleMagicLink, loading }) }),
208
- authClient?.signIn?.passkey && /* @__PURE__ */ jsx4("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx4(PasskeyBtn, { onClick: handlePasskey, loading: passkeyLoading }) })
258
+ authClient?.signIn?.passkey && /* @__PURE__ */ jsx4("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx4(PasskeyBtn, { onClick: handlePasskey, loading: passkeyLoading }) }),
259
+ canPasskeySignUp && /* @__PURE__ */ jsx4("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx4(
260
+ PasskeySignUpBtn,
261
+ {
262
+ onClick: handlePasskeySignUp,
263
+ loading: passkeySignUpLoading
264
+ }
265
+ ) })
209
266
  ] })
210
267
  ] });
211
268
  }
@@ -213,6 +270,7 @@ function LoginPage({
213
270
  // src/client/create-auth-client.ts
214
271
  import { createAuthClient } from "better-auth/react";
215
272
  import { magicLinkClient } from "better-auth/client/plugins";
273
+ import { passkeyClient } from "@better-auth/passkey/client";
216
274
  export {
217
275
  AuthClientProvider,
218
276
  AuthGuard,
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { betterAuth } from "better-auth";
3
3
  import { drizzleAdapter } from "better-auth/adapters/drizzle";
4
4
  import { magicLink } from "better-auth/plugins/magic-link";
5
+ import { passkey } from "@better-auth/passkey";
5
6
  import { drizzle } from "drizzle-orm/d1";
6
7
  import { resolveGrants } from "@cfast/permissions";
7
8
 
@@ -127,6 +128,15 @@ function createAuth(config) {
127
128
  magicLink({ sendMagicLink: config.magicLink.sendMagicLink })
128
129
  );
129
130
  }
131
+ if (config.passkeys) {
132
+ plugins.push(
133
+ passkey({
134
+ rpName: config.passkeys.rpName,
135
+ rpID: config.passkeys.rpId,
136
+ origin: env.appUrl
137
+ })
138
+ );
139
+ }
130
140
  const expiresIn = config.session?.expiresIn ? parseExpiresIn(config.session.expiresIn) : void 0;
131
141
  const auth = betterAuth({
132
142
  baseURL: env.appUrl,
package/llms.txt CHANGED
@@ -110,17 +110,45 @@ useAuth(): UseAuthReturn // { signOut, registerPasskey, de
110
110
  // Login
111
111
  <LoginPage authClient={authClient} components? title? subtitle? />
112
112
  createAuthClient(): AuthClientInstance
113
+ magicLinkClient(): plugin // re-exported from better-auth
114
+ passkeyClient(): plugin // re-exported from @better-auth/passkey
113
115
  ```
114
116
 
115
117
  ### LoginComponents slots
116
118
 
117
119
  ```typescript
118
120
  type LoginComponents = {
119
- Layout?, EmailInput?, PasskeyButton?, MagicLinkButton?,
120
- SuccessMessage?, ErrorMessage?,
121
+ Layout?, EmailInput?, PasskeyButton?, PasskeySignUpButton?,
122
+ MagicLinkButton?, SuccessMessage?, ErrorMessage?,
121
123
  };
122
124
  ```
123
125
 
126
+ ### Passkey sign-up
127
+
128
+ When the `authClient` passed to `<LoginPage>` has both `signUp.email` and `passkey.addPasskey` (i.e., the client was created with `passkeyClient()`), a "Sign up with Passkey" button appears automatically. The flow:
129
+
130
+ 1. User enters email, clicks "Sign up with Passkey"
131
+ 2. Account is created via `signUp.email` with a random password
132
+ 3. Browser's WebAuthn registration prompt fires immediately via `addPasskey()`
133
+ 4. User is signed up with a passkey — no magic link email needed
134
+
135
+ To enable passkey sign-up, configure both server and client:
136
+
137
+ ```typescript
138
+ // Server: createAuth config
139
+ const initAuth = createAuth({
140
+ permissions,
141
+ passkeys: { rpName: "My App", rpId: "myapp.com" },
142
+ magicLink: { sendMagicLink: ... },
143
+ });
144
+
145
+ // Client: auth client
146
+ import { createAuthClient, magicLinkClient, passkeyClient } from "@cfast/auth/client";
147
+ const authClient = createAuthClient({
148
+ plugins: [magicLinkClient(), passkeyClient()],
149
+ });
150
+ ```
151
+
124
152
  ### Route plugin (`@cfast/auth/plugin`)
125
153
 
126
154
  ```typescript
@@ -178,7 +206,7 @@ await auth.removeRole(userId, "editor");
178
206
 
179
207
  - **@cfast/permissions** -- `createAuth({ permissions })` takes the permissions config. Roles defined in permissions are the same roles assigned to users. `auth.requireUser()` returns `grants` resolved from the user's roles.
180
208
  - **@cfast/db** -- Pass `{ user, grants }` from `auth.requireUser()` directly to `createDb()`. Role changes via `auth.setRole()` take effect on the next request.
181
- - **@cfast/ui/joy** -- Provides `joyLoginComponents` for Joy UI styled login page slots.
209
+ - **@cfast/joy** -- Provides `joyLoginComponents` for Joy UI styled login page slots.
182
210
 
183
211
  ## Common Mistakes
184
212
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfast/auth",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Authentication for Cloudflare Workers: magic email, passkeys, roles, and impersonation",
5
5
  "keywords": [
6
6
  "cfast",
@@ -52,6 +52,7 @@
52
52
  "lint": "eslint src/"
53
53
  },
54
54
  "peerDependencies": {
55
+ "@better-auth/passkey": ">=1",
55
56
  "better-auth": ">=1",
56
57
  "drizzle-orm": ">=0.35",
57
58
  "react": ">=18"