@cfast/auth 0.1.0 → 0.2.1
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 +3 -3
- package/dist/client.d.ts +25 -1
- package/dist/client.js +59 -1
- package/dist/index.js +10 -0
- package/dist/schema.d.ts +22 -3
- package/dist/schema.js +4 -3
- package/llms.txt +31 -3
- package/package.json +2 -1
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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/dist/schema.d.ts
CHANGED
|
@@ -732,7 +732,7 @@ declare const passkeys: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
732
732
|
}, {}, {
|
|
733
733
|
length: number | undefined;
|
|
734
734
|
}>;
|
|
735
|
-
|
|
735
|
+
credentialID: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
736
736
|
name: "credential_id";
|
|
737
737
|
tableName: "passkeys";
|
|
738
738
|
dataType: "string";
|
|
@@ -775,7 +775,7 @@ declare const passkeys: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
775
775
|
columnType: "SQLiteText";
|
|
776
776
|
data: string;
|
|
777
777
|
driverParam: string;
|
|
778
|
-
notNull:
|
|
778
|
+
notNull: true;
|
|
779
779
|
hasDefault: false;
|
|
780
780
|
isPrimaryKey: false;
|
|
781
781
|
isAutoincrement: false;
|
|
@@ -794,7 +794,7 @@ declare const passkeys: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
794
794
|
columnType: "SQLiteBoolean";
|
|
795
795
|
data: boolean;
|
|
796
796
|
driverParam: number;
|
|
797
|
-
notNull:
|
|
797
|
+
notNull: true;
|
|
798
798
|
hasDefault: true;
|
|
799
799
|
isPrimaryKey: false;
|
|
800
800
|
isAutoincrement: false;
|
|
@@ -823,6 +823,25 @@ declare const passkeys: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
823
823
|
}, {}, {
|
|
824
824
|
length: number | undefined;
|
|
825
825
|
}>;
|
|
826
|
+
aaguid: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
827
|
+
name: "aaguid";
|
|
828
|
+
tableName: "passkeys";
|
|
829
|
+
dataType: "string";
|
|
830
|
+
columnType: "SQLiteText";
|
|
831
|
+
data: string;
|
|
832
|
+
driverParam: string;
|
|
833
|
+
notNull: false;
|
|
834
|
+
hasDefault: false;
|
|
835
|
+
isPrimaryKey: false;
|
|
836
|
+
isAutoincrement: false;
|
|
837
|
+
hasRuntimeDefault: false;
|
|
838
|
+
enumValues: [string, ...string[]];
|
|
839
|
+
baseColumn: never;
|
|
840
|
+
identity: undefined;
|
|
841
|
+
generated: undefined;
|
|
842
|
+
}, {}, {
|
|
843
|
+
length: number | undefined;
|
|
844
|
+
}>;
|
|
826
845
|
createdAt: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
827
846
|
name: "created_at";
|
|
828
847
|
tableName: "passkeys";
|
package/dist/schema.js
CHANGED
|
@@ -51,11 +51,12 @@ var passkeys = sqliteTable("passkeys", {
|
|
|
51
51
|
name: text("name"),
|
|
52
52
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
53
53
|
publicKey: text("public_key").notNull(),
|
|
54
|
-
|
|
54
|
+
credentialID: text("credential_id").notNull().unique(),
|
|
55
55
|
counter: integer("counter").notNull().default(0),
|
|
56
|
-
deviceType: text("device_type"),
|
|
57
|
-
backedUp: integer("backed_up", { mode: "boolean" }).default(false),
|
|
56
|
+
deviceType: text("device_type").notNull(),
|
|
57
|
+
backedUp: integer("backed_up", { mode: "boolean" }).notNull().default(false),
|
|
58
58
|
transports: text("transports"),
|
|
59
|
+
aaguid: text("aaguid"),
|
|
59
60
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
60
61
|
() => /* @__PURE__ */ new Date()
|
|
61
62
|
)
|
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?,
|
|
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/
|
|
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
|
|
3
|
+
"version": "0.2.1",
|
|
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"
|