@puzzle-ui/fullstack 0.0.0-stage → 0.5.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 +558 -2
- package/dist/LICENSE +21 -0
- package/dist/auth/client/auth.css +206 -0
- package/dist/auth/client/experience.cjs +340 -0
- package/dist/auth/client/experience.cjs.map +1 -0
- package/dist/auth/client/experience.d.cts +67 -0
- package/dist/auth/client/experience.d.ts +67 -0
- package/dist/auth/client/experience.js +340 -0
- package/dist/auth/client/experience.js.map +1 -0
- package/dist/auth/client/index.cjs +234 -0
- package/dist/auth/client/index.cjs.map +1 -0
- package/dist/auth/client/index.d.cts +48 -0
- package/dist/auth/client/index.d.ts +48 -0
- package/dist/auth/client/index.js +216 -0
- package/dist/auth/client/index.js.map +1 -0
- package/dist/auth/client/messages.cjs +80 -0
- package/dist/auth/client/messages.cjs.map +1 -0
- package/dist/auth/client/messages.d.cts +92 -0
- package/dist/auth/client/messages.d.ts +92 -0
- package/dist/auth/client/messages.js +79 -0
- package/dist/auth/client/messages.js.map +1 -0
- package/dist/auth/client/passkeys.cjs +43 -0
- package/dist/auth/client/passkeys.cjs.map +1 -0
- package/dist/auth/client/passkeys.d.cts +23 -0
- package/dist/auth/client/passkeys.d.ts +23 -0
- package/dist/auth/client/passkeys.js +40 -0
- package/dist/auth/client/passkeys.js.map +1 -0
- package/dist/auth/client/react.cjs +859 -0
- package/dist/auth/client/react.cjs.map +1 -0
- package/dist/auth/client/react.d.cts +72 -0
- package/dist/auth/client/react.d.ts +72 -0
- package/dist/auth/client/react.js +850 -0
- package/dist/auth/client/react.js.map +1 -0
- package/dist/auth/next/index.cjs +40 -0
- package/dist/auth/next/index.cjs.map +1 -0
- package/dist/auth/next/index.d.cts +12 -0
- package/dist/auth/next/index.d.ts +12 -0
- package/dist/auth/next/index.js +38 -0
- package/dist/auth/next/index.js.map +1 -0
- package/dist/auth/server/health.cjs +51 -0
- package/dist/auth/server/health.cjs.map +1 -0
- package/dist/auth/server/health.d.cts +15 -0
- package/dist/auth/server/health.d.ts +15 -0
- package/dist/auth/server/health.js +51 -0
- package/dist/auth/server/health.js.map +1 -0
- package/dist/auth/server/index.cjs +108 -0
- package/dist/auth/server/index.cjs.map +1 -0
- package/dist/auth/server/index.d.cts +22 -0
- package/dist/auth/server/index.d.ts +22 -0
- package/dist/auth/server/index.js +88 -0
- package/dist/auth/server/index.js.map +1 -0
- package/dist/auth/server/lifecycle.cjs +126 -0
- package/dist/auth/server/lifecycle.cjs.map +1 -0
- package/dist/auth/server/lifecycle.d.cts +20 -0
- package/dist/auth/server/lifecycle.d.ts +20 -0
- package/dist/auth/server/lifecycle.js +126 -0
- package/dist/auth/server/lifecycle.js.map +1 -0
- package/dist/auth/server/mfa.cjs +682 -0
- package/dist/auth/server/mfa.cjs.map +1 -0
- package/dist/auth/server/mfa.d.cts +90 -0
- package/dist/auth/server/mfa.d.ts +90 -0
- package/dist/auth/server/mfa.js +673 -0
- package/dist/auth/server/mfa.js.map +1 -0
- package/dist/auth/server/session.cjs +58 -0
- package/dist/auth/server/session.cjs.map +1 -0
- package/dist/auth/server/session.d.cts +14 -0
- package/dist/auth/server/session.d.ts +14 -0
- package/dist/auth/server/session.js +53 -0
- package/dist/auth/server/session.js.map +1 -0
- package/dist/auth/shared/index.cjs +0 -0
- package/dist/auth/shared/index.d.cts +64 -0
- package/dist/auth/shared/index.d.ts +64 -0
- package/dist/auth/shared/index.js +1 -0
- package/dist/auth/tanstack/index.cjs +34 -0
- package/dist/auth/tanstack/index.cjs.map +1 -0
- package/dist/auth/tanstack/index.d.cts +7 -0
- package/dist/auth/tanstack/index.d.ts +7 -0
- package/dist/auth/tanstack/index.js +32 -0
- package/dist/auth/tanstack/index.js.map +1 -0
- package/package.json +108 -4
package/README.md
CHANGED
|
@@ -1,3 +1,559 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @puzzle-ui/fullstack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
First-party full-stack integrations for Puzzle.
|
|
4
|
+
|
|
5
|
+
The package provides browser-safe authentication functionality, server-side authentication handlers, reusable authentication screens and framework adapters.
|
|
6
|
+
|
|
7
|
+
Authentication policies, persistence and security-sensitive operations remain in the framework-independent `@puzzle-ui/auth` and `@puzzle-ui/session` packages.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @puzzle-ui/fullstack @puzzle-ui/auth @puzzle-ui/session @puzzle-ui/ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Install React 19 and the appropriate framework peer dependencies in your application.
|
|
16
|
+
|
|
17
|
+
## Package architecture
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
@puzzle-ui/fullstack
|
|
21
|
+
└── auth/
|
|
22
|
+
├── client/
|
|
23
|
+
│ ├── index.ts
|
|
24
|
+
│ ├── react.tsx
|
|
25
|
+
│ ├── messages.ts
|
|
26
|
+
│ ├── auth.css
|
|
27
|
+
│ └── passkeys.ts
|
|
28
|
+
├── server/
|
|
29
|
+
│ ├── index.ts
|
|
30
|
+
│ ├── mfa.ts
|
|
31
|
+
│ ├── lifecycle.ts
|
|
32
|
+
│ └── session.ts
|
|
33
|
+
├── shared/
|
|
34
|
+
│ └── index.ts
|
|
35
|
+
├── next/
|
|
36
|
+
│ └── index.ts
|
|
37
|
+
└── tanstack/
|
|
38
|
+
└── index.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Public exports
|
|
42
|
+
|
|
43
|
+
| Import | Responsibility |
|
|
44
|
+
| ------------------------------------ | ----------------------------------------------------------- |
|
|
45
|
+
| `@puzzle-ui/fullstack/auth/client` | React provider, hooks, authentication UI and browser client |
|
|
46
|
+
| `@puzzle-ui/fullstack/auth/server` | Server handlers, sessions and authentication lifecycle |
|
|
47
|
+
| `@puzzle-ui/fullstack/auth/shared` | Serializable public contracts |
|
|
48
|
+
| `@puzzle-ui/fullstack/auth/next` | Next.js App Router adapter |
|
|
49
|
+
| `@puzzle-ui/fullstack/auth/tanstack` | TanStack Start adapter |
|
|
50
|
+
|
|
51
|
+
## Client
|
|
52
|
+
|
|
53
|
+
### AuthProvider
|
|
54
|
+
|
|
55
|
+
Wrap the application in `AuthProvider`.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { AuthProvider } from "@puzzle-ui/fullstack/auth/client";
|
|
59
|
+
|
|
60
|
+
export const Providers = ({ children }: { children: React.ReactNode }) => (
|
|
61
|
+
<AuthProvider
|
|
62
|
+
options={{
|
|
63
|
+
basePath: "/api/auth",
|
|
64
|
+
}}
|
|
65
|
+
locale="en"
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</AuthProvider>
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The provider creates and shares one authentication client.
|
|
73
|
+
|
|
74
|
+
Authentication state is retrieved from the server. Session credentials are not stored in browser storage.
|
|
75
|
+
|
|
76
|
+
### Hooks
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useAuth, useSession, useUser } from "@puzzle-ui/fullstack/auth/client";
|
|
80
|
+
|
|
81
|
+
const Profile = () => {
|
|
82
|
+
const user = useUser();
|
|
83
|
+
const session = useSession();
|
|
84
|
+
const auth = useAuth();
|
|
85
|
+
|
|
86
|
+
if (session.isLoading) return null;
|
|
87
|
+
|
|
88
|
+
if (!user) return null;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<span>{user.email}</span>
|
|
93
|
+
<button onClick={() => void auth.signOut()}>Sign out</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Authentication components
|
|
100
|
+
|
|
101
|
+
Available components:
|
|
102
|
+
|
|
103
|
+
- `SignInForm`
|
|
104
|
+
- `SignUpForm`
|
|
105
|
+
- `MfaChallengeForm`
|
|
106
|
+
- `RequiredMfaEnrollment`
|
|
107
|
+
- `PasswordResetRequestForm`
|
|
108
|
+
- `PasswordResetForm`
|
|
109
|
+
- `EmailVerificationForm`
|
|
110
|
+
- `EmailVerificationNotice`
|
|
111
|
+
- `ChangePasswordForm`
|
|
112
|
+
- `MfaSettings`
|
|
113
|
+
- `AuthScreen`
|
|
114
|
+
|
|
115
|
+
All components support className customization.
|
|
116
|
+
|
|
117
|
+
Redirects are opt-in through `redirectTo` and restricted to same-origin paths.
|
|
118
|
+
|
|
119
|
+
## Configurable authentication experience
|
|
120
|
+
|
|
121
|
+
`@puzzle-ui/fullstack` provides an optional `AuthExperience` component for building branded sign-in and sign-up pages without having to implement the standard authentication flow yourself.
|
|
122
|
+
|
|
123
|
+
The experience supports:
|
|
124
|
+
|
|
125
|
+
- Custom branding, including a logo, title, description and footer.
|
|
126
|
+
- Layout configuration, including content width and alignment.
|
|
127
|
+
- Configurable redirects after successful authentication.
|
|
128
|
+
- Custom registration fields with validation.
|
|
129
|
+
- Custom submit callbacks for integrating application-specific registration logic.
|
|
130
|
+
- Existing Puzzle authentication messages and form behavior.
|
|
131
|
+
|
|
132
|
+
### Basic usage
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { AuthExperience } from "@puzzle-ui/fullstack/auth/client";
|
|
136
|
+
|
|
137
|
+
export const SignUpPage = () => (
|
|
138
|
+
<AuthExperience
|
|
139
|
+
mode="sign-up"
|
|
140
|
+
config={{
|
|
141
|
+
branding: {
|
|
142
|
+
title: "Create your account",
|
|
143
|
+
description: "Get started with your new account.",
|
|
144
|
+
},
|
|
145
|
+
layout: {
|
|
146
|
+
width: "md",
|
|
147
|
+
align: "center",
|
|
148
|
+
},
|
|
149
|
+
redirects: {
|
|
150
|
+
afterSignUp: "/dashboard",
|
|
151
|
+
},
|
|
152
|
+
fields: [
|
|
153
|
+
{
|
|
154
|
+
name: "company",
|
|
155
|
+
label: "Company",
|
|
156
|
+
type: "text",
|
|
157
|
+
required: true,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "role",
|
|
161
|
+
label: "Your role",
|
|
162
|
+
type: "select",
|
|
163
|
+
options: [
|
|
164
|
+
{ label: "Developer", value: "developer" },
|
|
165
|
+
{ label: "Designer", value: "designer" },
|
|
166
|
+
{ label: "Other", value: "other" },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Custom registration handling
|
|
176
|
+
|
|
177
|
+
Custom fields are UI-only extensions. They are not automatically added to the authentication user model or persisted by the auth engine.
|
|
178
|
+
|
|
179
|
+
The experience uses Puzzle UI form fields, text inputs, text areas, checkboxes,
|
|
180
|
+
selects, buttons and typography. URL and number fields retain native inputs to
|
|
181
|
+
preserve their input types and string values, which `TextInput` does not support.
|
|
182
|
+
|
|
183
|
+
Use `onSubmit` to receive the standard registration input and custom field values. The callback can call `signUp` to execute the normal registration flow.
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { AuthExperience } from "@puzzle-ui/fullstack/auth/client";
|
|
187
|
+
|
|
188
|
+
export const SignUpPage = () => (
|
|
189
|
+
<AuthExperience
|
|
190
|
+
mode="sign-up"
|
|
191
|
+
config={{
|
|
192
|
+
fields: [
|
|
193
|
+
{
|
|
194
|
+
name: "company",
|
|
195
|
+
label: "Company",
|
|
196
|
+
type: "text",
|
|
197
|
+
required: true,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
}}
|
|
201
|
+
onSubmit={async ({ registration, fields, signUp }) => {
|
|
202
|
+
await signUp(registration);
|
|
203
|
+
|
|
204
|
+
// Handle application-specific values separately.
|
|
205
|
+
await saveCompanyInformation({
|
|
206
|
+
email: registration.email,
|
|
207
|
+
company: fields.company,
|
|
208
|
+
});
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Browser tests
|
|
215
|
+
|
|
216
|
+
Run the Fullstack authentication experience browser tests with:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
bun run --filter @puzzle-ui/fullstack test:browser
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### AuthScreen
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
import { AuthScreen } from "@puzzle-ui/fullstack/auth/client";
|
|
226
|
+
|
|
227
|
+
export default function LoginPage() {
|
|
228
|
+
return <AuthScreen mode="sign-in" redirectTo="/dashboard" />;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Registration
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import { AuthScreen } from "@puzzle-ui/fullstack/auth/client";
|
|
236
|
+
|
|
237
|
+
export default function RegisterPage() {
|
|
238
|
+
return <AuthScreen mode="sign-up" />;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### MFA management
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { MfaSettings } from "@puzzle-ui/fullstack/auth/client";
|
|
246
|
+
|
|
247
|
+
export default function SecurityPage() {
|
|
248
|
+
return <MfaSettings />;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Localization
|
|
253
|
+
|
|
254
|
+
Puzzle Fullstack includes English messages as its default locale and fallback.
|
|
255
|
+
|
|
256
|
+
Applications can define their own locale identifiers by extending the `AuthLocale` type. Every custom locale must provide a complete `AuthMessages` object. English messages are optional because the built-in English messages are used whenever no application-provided English messages are available.
|
|
257
|
+
|
|
258
|
+
For example, an application can add Dutch and German:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
import { AuthProvider } from "@puzzle-ui/fullstack/auth/client";
|
|
262
|
+
|
|
263
|
+
import {
|
|
264
|
+
getAuthLocale,
|
|
265
|
+
resolveAuthMessages,
|
|
266
|
+
type AuthLocale,
|
|
267
|
+
type AuthLocaleMessages,
|
|
268
|
+
} from "@puzzle-ui/fullstack/auth/client";
|
|
269
|
+
|
|
270
|
+
type AppLocale = AuthLocale<"nl" | "de">;
|
|
271
|
+
|
|
272
|
+
const appMessages: AuthLocaleMessages<"nl" | "de"> = {
|
|
273
|
+
nl: {
|
|
274
|
+
// Supply every AuthMessages field in Dutch.
|
|
275
|
+
// For example:
|
|
276
|
+
signIn: "Inloggen",
|
|
277
|
+
signUp: "Account aanmaken",
|
|
278
|
+
email: "E-mailadres",
|
|
279
|
+
password: "Wachtwoord",
|
|
280
|
+
name: "Naam",
|
|
281
|
+
confirmPassword: "Bevestig wachtwoord",
|
|
282
|
+
submit: "Doorgaan",
|
|
283
|
+
signingIn: "Inloggen...",
|
|
284
|
+
signingUp: "Account aanmaken...",
|
|
285
|
+
forgotPassword: "Wachtwoord vergeten?",
|
|
286
|
+
createAccount: "Account aanmaken",
|
|
287
|
+
existingAccount: "Heb je al een account?",
|
|
288
|
+
noAccount: "Nog geen account?",
|
|
289
|
+
mfaTitle: "Verifieer je identiteit",
|
|
290
|
+
mfaDescription: "Voer je authenticatiecode in om door te gaan.",
|
|
291
|
+
verificationCode: "Authenticatiecode",
|
|
292
|
+
recoveryCode: "Herstelcode",
|
|
293
|
+
verify: "Verifiëren",
|
|
294
|
+
verifying: "Verifiëren...",
|
|
295
|
+
useRecoveryCode: "Gebruik een herstelcode",
|
|
296
|
+
useAuthenticator: "Gebruik authenticator-app",
|
|
297
|
+
usePasskey: "Doorgaan met passkey",
|
|
298
|
+
passkeyUnavailable: "Passkeys zijn niet beschikbaar in deze browser.",
|
|
299
|
+
loading: "Laden...",
|
|
300
|
+
signOut: "Uitloggen",
|
|
301
|
+
unexpectedError: "Er is iets misgegaan. Probeer het opnieuw.",
|
|
302
|
+
invalidCredentials: "Het e-mailadres of wachtwoord is onjuist.",
|
|
303
|
+
passwordMismatch: "De wachtwoorden komen niet overeen.",
|
|
304
|
+
passwordRequired: "Een wachtwoord is verplicht.",
|
|
305
|
+
requestPasswordReset: "Wachtwoord herstellen aanvragen",
|
|
306
|
+
resetPassword: "Wachtwoord herstellen",
|
|
307
|
+
newPassword: "Nieuw wachtwoord",
|
|
308
|
+
sendResetLink: "Verstuur herstellink",
|
|
309
|
+
passwordResetSent: "De herstellink is verstuurd",
|
|
310
|
+
verificationSent: "Verificatiemail is verstuurd",
|
|
311
|
+
verifyEmail: "E-mailadres verifiëren",
|
|
312
|
+
changePassword: "Wachtwoord wijzigen",
|
|
313
|
+
passwordChanged: "Wachtwoord gewijzigd",
|
|
314
|
+
emailRequired: "Een e-mailadres is verplicht.",
|
|
315
|
+
nameRequired: "Een naam is verplicht.",
|
|
316
|
+
authenticated: "Je bent ingelogd.",
|
|
317
|
+
mfaSettings: "Beveiligingsinstellingen",
|
|
318
|
+
addTotp: "Authenticator toevoegen",
|
|
319
|
+
addPasskey: "Passkey toevoegen",
|
|
320
|
+
removeFactor: "Verwijderen",
|
|
321
|
+
recoveryCodes: "Herstelcodes",
|
|
322
|
+
saveRecoveryCodes: "Bewaar deze codes op een veilige plek.",
|
|
323
|
+
currentPassword: "Huidig wachtwoord",
|
|
324
|
+
totpSecret: "Authenticator-geheim",
|
|
325
|
+
verifyTotp: "Authenticator verifiëren",
|
|
326
|
+
cancel: "Annuleren",
|
|
327
|
+
},
|
|
328
|
+
// Optionally (partially)override messages for English:
|
|
329
|
+
en: {
|
|
330
|
+
signUp: "Create your account",
|
|
331
|
+
}
|
|
332
|
+
de: {
|
|
333
|
+
// Supply every AuthMessages field in German.
|
|
334
|
+
// The same complete set of fields as the Dutch example is required.
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// The locale type can be used throughout the application.
|
|
339
|
+
const requestedLocale: string = "nl";
|
|
340
|
+
|
|
341
|
+
const locale: AppLocale = getAuthLocale(requestedLocale, appMessages);
|
|
342
|
+
|
|
343
|
+
// Resolve the complete message set before passing it to the provider.
|
|
344
|
+
// Built-in English messages are used for any missing English fallback.
|
|
345
|
+
const messages = resolveAuthMessages(locale, appMessages);
|
|
346
|
+
|
|
347
|
+
export const Providers = ({ children }: { children: React.ReactNode }) => (
|
|
348
|
+
<AuthProvider
|
|
349
|
+
locale={locale}
|
|
350
|
+
messages={messages}
|
|
351
|
+
options={{
|
|
352
|
+
basePath: "/api/auth",
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
{children}
|
|
356
|
+
</AuthProvider>
|
|
357
|
+
);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The `AuthLocale` type accepts custom locale identifiers:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
type AppLocale = AuthLocale<"nl" | "de" | "es">;
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The corresponding message map requires a complete message set for every declared custom locale:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
const messages: AuthLocaleMessages<"nl" | "de"> = {
|
|
370
|
+
nl: dutchMessages,
|
|
371
|
+
de: germanMessages,
|
|
372
|
+
|
|
373
|
+
// Optional: English overrides.
|
|
374
|
+
// Without this property, Puzzle's built-in English messages are used.
|
|
375
|
+
// en: englishMessages,
|
|
376
|
+
};
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The locale resolver only selects a custom locale when its messages are available:
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const locale = getAuthLocale("nl-NL", messages);
|
|
383
|
+
// "en", because "nl-NL" is not a configured locale.
|
|
384
|
+
|
|
385
|
+
const dutchLocale = getAuthLocale("nl", messages);
|
|
386
|
+
// "nl"
|
|
387
|
+
|
|
388
|
+
const fallbackLocale = getAuthLocale("fr", messages);
|
|
389
|
+
// "en"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Locale identifiers are exact matches. If an application wants to support regional identifiers such as `nl-NL` or `en-GB`, it should declare and provide those locales explicitly.
|
|
393
|
+
|
|
394
|
+
Messages are resolved in this order:
|
|
395
|
+
|
|
396
|
+
1. Built-in English messages.
|
|
397
|
+
2. Optional application-provided English messages.
|
|
398
|
+
3. Messages for the selected custom locale.
|
|
399
|
+
4. Optional per-message overrides.
|
|
400
|
+
|
|
401
|
+
This means a custom locale always has a complete message set, while English can remain entirely implicit.
|
|
402
|
+
|
|
403
|
+
## Server
|
|
404
|
+
|
|
405
|
+
### Creating the server
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import { createAuthServer } from "@puzzle-ui/fullstack/auth/server";
|
|
409
|
+
|
|
410
|
+
import {
|
|
411
|
+
authManager,
|
|
412
|
+
mfaManager,
|
|
413
|
+
stepUpManager,
|
|
414
|
+
sessions,
|
|
415
|
+
cookie,
|
|
416
|
+
csrf,
|
|
417
|
+
} from "./auth-managers";
|
|
418
|
+
|
|
419
|
+
export const authServer = createAuthServer({
|
|
420
|
+
auth: authManager,
|
|
421
|
+
mfa: mfaManager,
|
|
422
|
+
stepUp: stepUpManager,
|
|
423
|
+
sessions,
|
|
424
|
+
cookie,
|
|
425
|
+
csrf,
|
|
426
|
+
resolveClientIp,
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
The supplied managers and providers must be initialized using their respective package contracts.
|
|
431
|
+
|
|
432
|
+
### Route handlers
|
|
433
|
+
|
|
434
|
+
`createAuthRouteHandlers` returns a map of paths and HTTP methods to Web Request handlers.
|
|
435
|
+
|
|
436
|
+
The default base path is `/api/auth`.
|
|
437
|
+
|
|
438
|
+
A custom base path can be configured.
|
|
439
|
+
|
|
440
|
+
### Lifecycle integration
|
|
441
|
+
|
|
442
|
+
The optional lifecycle integration provides:
|
|
443
|
+
|
|
444
|
+
- Email verification requests.
|
|
445
|
+
- Email verification.
|
|
446
|
+
- Password reset requests.
|
|
447
|
+
- Password reset completion.
|
|
448
|
+
- Authenticated password changes.
|
|
449
|
+
|
|
450
|
+
Password reset requests use a generic public response to prevent account enumeration.
|
|
451
|
+
|
|
452
|
+
### Session security
|
|
453
|
+
|
|
454
|
+
- Session tokens remain in HttpOnly cookies.
|
|
455
|
+
- State-changing requests require CSRF protection.
|
|
456
|
+
- Authentication responses use no-store caching.
|
|
457
|
+
- Password changes invalidate existing sessions.
|
|
458
|
+
- MFA challenge tokens remain server-managed.
|
|
459
|
+
- Client-facing responses contain only public user data.
|
|
460
|
+
|
|
461
|
+
## Next.js App Router
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// app/api/auth/[...auth]/route.ts
|
|
465
|
+
|
|
466
|
+
import { auth } from "@/lib/auth";
|
|
467
|
+
|
|
468
|
+
import { createNextAuthHandler } from "@puzzle-ui/fullstack/auth/next";
|
|
469
|
+
|
|
470
|
+
const handlers = createNextAuthHandler({
|
|
471
|
+
auth,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
export const GET = handlers.GET;
|
|
475
|
+
export const POST = handlers.POST;
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## TanStack Start
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import { createTanStackAuthHandler } from "@puzzle-ui/fullstack/auth/tanstack";
|
|
482
|
+
|
|
483
|
+
import { auth } from "./auth";
|
|
484
|
+
|
|
485
|
+
export const authHandler = createTanStackAuthHandler({
|
|
486
|
+
auth,
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Register the handler using the application's TanStack server route configuration.
|
|
491
|
+
|
|
492
|
+
## Runtime support
|
|
493
|
+
|
|
494
|
+
The package uses Web APIs for HTTP request and response handling.
|
|
495
|
+
|
|
496
|
+
Supported targets:
|
|
497
|
+
|
|
498
|
+
- Node.js
|
|
499
|
+
- Cloudflare Workers
|
|
500
|
+
- Bun development environments
|
|
501
|
+
- Browser authentication interfaces
|
|
502
|
+
|
|
503
|
+
Persistent sessions, tokens and authentication records must use durable storage.
|
|
504
|
+
|
|
505
|
+
## Build
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
bun run --filter @puzzle-ui/fullstack build
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Typecheck
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
bun run --filter @puzzle-ui/fullstack typecheck
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## Tests
|
|
518
|
+
|
|
519
|
+
Tests run against the built package with Node's test runner. The test setup
|
|
520
|
+
ignores CSS imports so client components can be rendered without a browser.
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
bun run --filter @puzzle-ui/fullstack build
|
|
524
|
+
bun run --filter @puzzle-ui/fullstack test
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
The required TOTP and passkey enrollment end-to-end tests live in this package
|
|
528
|
+
because they exercise the HTTP handlers together with the auth managers:
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
node --test packages/fullstack/tests/auth-mfa.e2e.test.mjs
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## Security boundaries
|
|
535
|
+
|
|
536
|
+
The package maintains separate public entry points for:
|
|
537
|
+
|
|
538
|
+
- Client functionality.
|
|
539
|
+
- Server functionality.
|
|
540
|
+
- Shared contracts.
|
|
541
|
+
- Framework integrations.
|
|
542
|
+
|
|
543
|
+
Client code must never import server implementations, database adapters, secrets or password hashing implementations.
|
|
544
|
+
|
|
545
|
+
Server functionality must never be re-exported from browser-facing entry points.
|
|
546
|
+
|
|
547
|
+
Framework adapters must delegate security policies and authentication operations to the underlying authentication packages.
|
|
548
|
+
|
|
549
|
+
## Scope
|
|
550
|
+
|
|
551
|
+
This package is designed to support additional full-stack Puzzle modules in the future.
|
|
552
|
+
|
|
553
|
+
Authentication is the first module. Other modules should follow the same principles:
|
|
554
|
+
|
|
555
|
+
- Explicit client/server boundaries.
|
|
556
|
+
- Framework-independent domain logic.
|
|
557
|
+
- Serializable shared contracts.
|
|
558
|
+
- Thin framework adapters.
|
|
559
|
+
- Explicit public exports.
|
package/dist/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sjoerd van Den Berg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|