@molecule/app-auth 1.0.0 → 1.0.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 +764 -0
- package/package.json +9 -8
package/README.md
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T01:50:05.358Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/app-auth
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
Client-side authentication interface for molecule.dev.
|
|
16
|
+
|
|
17
|
+
Provides a unified auth API that works across different auth strategies
|
|
18
|
+
(JWT, session, OAuth, etc.): `createJWTAuthClient` builds a fetch-based
|
|
19
|
+
client; `setClient`/`getClient` bond it; `login`/`logout`/`register`/
|
|
20
|
+
`isAuthenticated`/`getUser` delegate to the bonded client. Framework
|
|
21
|
+
bindings (e.g. `AuthProvider`/`useAuth` in `@molecule/app-react`) wrap the
|
|
22
|
+
same client.
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { createJWTAuthClient, setClient } from '@molecule/app-auth'
|
|
28
|
+
|
|
29
|
+
const client = createJWTAuthClient({ baseURL: '/api' })
|
|
30
|
+
setClient(client)
|
|
31
|
+
await client.initialize() // restore the session from the httpOnly cookie
|
|
32
|
+
|
|
33
|
+
// anywhere in the app
|
|
34
|
+
import { getUser, isAuthenticated, login, logout } from '@molecule/app-auth'
|
|
35
|
+
await login({ email, password })
|
|
36
|
+
if (isAuthenticated()) console.log(getUser()?.email)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Type
|
|
40
|
+
|
|
41
|
+
`core`
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install @molecule/app-auth @molecule/app-bond @molecule/app-i18n @molecule/app-logger
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### Interfaces
|
|
52
|
+
|
|
53
|
+
#### `AuthClient`
|
|
54
|
+
|
|
55
|
+
Auth client interface that all auth bond packages must implement.
|
|
56
|
+
Provides login/logout/register flows, token management, profile
|
|
57
|
+
updates, and auth state subscription.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
interface AuthClient<T = UserProfile> {
|
|
61
|
+
/**
|
|
62
|
+
* Returns the current authentication state snapshot.
|
|
63
|
+
*/
|
|
64
|
+
getState(): AuthState<T>
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns whether the user is currently authenticated.
|
|
68
|
+
*/
|
|
69
|
+
isAuthenticated(): boolean
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gets the current user.
|
|
73
|
+
*/
|
|
74
|
+
getUser(): T | null
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Updates the cached user object (state + persistent storage) without
|
|
78
|
+
* hitting the network. Intended for local refreshes after a per-app
|
|
79
|
+
* mutation (e.g., the user just PATCHed their own profile and the
|
|
80
|
+
* server returned the canonical row). Does NOT change tokens.
|
|
81
|
+
*/
|
|
82
|
+
setUser(user: T | null): void
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets the current access token.
|
|
86
|
+
*/
|
|
87
|
+
getAccessToken(): string | null
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stores the access token in the configured token storage adapter (in-memory
|
|
91
|
+
* by default). Use this to seed the token after an out-of-band exchange (e.g.
|
|
92
|
+
* the OAuth code→token redirect) instead of writing to `localStorage` directly,
|
|
93
|
+
* which would violate the in-memory-default storage contract and make the bearer
|
|
94
|
+
* token JS-readable (XSS-exfiltratable). Pass `null` to clear it.
|
|
95
|
+
*/
|
|
96
|
+
setAccessToken(token: string | null): void
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Gets the refresh token.
|
|
100
|
+
*/
|
|
101
|
+
getRefreshToken(): string | null
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Logs in with credentials.
|
|
105
|
+
*/
|
|
106
|
+
login(credentials: LoginCredentials): Promise<AuthResult<T>>
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Logs out the current user.
|
|
110
|
+
*/
|
|
111
|
+
logout(): Promise<void>
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Registers a new user.
|
|
115
|
+
*/
|
|
116
|
+
register(data: RegisterData): Promise<AuthResult<T>>
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Refreshes the access token.
|
|
120
|
+
*/
|
|
121
|
+
refresh(): Promise<AuthResult<T>>
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Requests a password reset.
|
|
125
|
+
*/
|
|
126
|
+
requestPasswordReset(data: PasswordResetRequest): Promise<void>
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Confirms a password reset.
|
|
130
|
+
*/
|
|
131
|
+
confirmPasswordReset(data: PasswordResetConfirm): Promise<void>
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Updates the current user's profile.
|
|
135
|
+
*/
|
|
136
|
+
updateProfile(data: Partial<T>): Promise<T>
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Changes the current user's password.
|
|
140
|
+
*/
|
|
141
|
+
changePassword(oldPassword: string, newPassword: string): Promise<void>
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Initializes auth state (e.g., from stored tokens).
|
|
145
|
+
*/
|
|
146
|
+
initialize(): Promise<void>
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Subscribes to auth state changes.
|
|
150
|
+
*/
|
|
151
|
+
subscribe(callback: (state: AuthState<T>) => void): () => void
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Subscribes to auth state changes (alias for subscribe).
|
|
155
|
+
*/
|
|
156
|
+
onAuthChange(callback: (state: AuthState<T>) => void): () => void
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets the current access token (alias for getAccessToken).
|
|
160
|
+
*/
|
|
161
|
+
getToken?(): string | null
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Adds an auth event listener.
|
|
165
|
+
*/
|
|
166
|
+
addEventListener(listener: AuthEventListener): () => void
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Destroys the auth client.
|
|
170
|
+
*/
|
|
171
|
+
destroy(): void
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### `AuthClientConfig`
|
|
176
|
+
|
|
177
|
+
Configuration options for creating an auth client, including API
|
|
178
|
+
endpoints, storage strategy, token refresh, and OAuth providers.
|
|
179
|
+
|
|
180
|
+
````typescript
|
|
181
|
+
interface AuthClientConfig {
|
|
182
|
+
/**
|
|
183
|
+
* API base URL.
|
|
184
|
+
*/
|
|
185
|
+
baseURL?: string
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Login endpoint.
|
|
189
|
+
*/
|
|
190
|
+
loginEndpoint?: string
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Logout endpoint.
|
|
194
|
+
*/
|
|
195
|
+
logoutEndpoint?: string
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Register endpoint.
|
|
199
|
+
*/
|
|
200
|
+
registerEndpoint?: string
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Refresh endpoint.
|
|
204
|
+
*/
|
|
205
|
+
refreshEndpoint?: string
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* User profile endpoint.
|
|
209
|
+
*/
|
|
210
|
+
profileEndpoint?: string
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Current-user endpoint for session restore (default `/users/me`). Called by
|
|
214
|
+
* `initialize()` with credentials to re-establish the session from the
|
|
215
|
+
* httpOnly cookie after a full page load — the only way to stay logged in when
|
|
216
|
+
* the bearer token is held in memory (the secure default; a localStorage copy
|
|
217
|
+
* is XSS-exfiltratable). Must return the user (as `user` or `props`).
|
|
218
|
+
*/
|
|
219
|
+
currentUserEndpoint?: string
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Storage key prefix.
|
|
223
|
+
*/
|
|
224
|
+
storagePrefix?: string
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Token storage type or custom storage adapter.
|
|
228
|
+
* - 'memory': In-memory storage (lost on page refresh)
|
|
229
|
+
* - StorageAdapter: Custom storage implementation (e.g., from `@molecule/app-storage`)
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* // Use in-memory storage
|
|
234
|
+
* storage: 'memory'
|
|
235
|
+
*
|
|
236
|
+
* // Use custom storage from `@molecule/app-storage`
|
|
237
|
+
* import { getProvider } from '@molecule/app-storage'
|
|
238
|
+
* const storageProvider = getProvider()
|
|
239
|
+
* storage: {
|
|
240
|
+
* getItem: (key) => storageProvider.get(key),
|
|
241
|
+
* setItem: (key, value) => storageProvider.set(key, value),
|
|
242
|
+
* removeItem: (key) => storageProvider.remove(key),
|
|
243
|
+
* }
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
storage?: 'memory' | StorageAdapter
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Auto refresh tokens before expiry.
|
|
250
|
+
*/
|
|
251
|
+
autoRefresh?: boolean
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Refresh tokens this many seconds before expiry.
|
|
255
|
+
*/
|
|
256
|
+
refreshBuffer?: number
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Forgot password / password reset request endpoint.
|
|
260
|
+
*/
|
|
261
|
+
forgotPasswordEndpoint?: string
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Password reset confirmation endpoint.
|
|
265
|
+
*/
|
|
266
|
+
resetPasswordEndpoint?: string
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Change password endpoint.
|
|
270
|
+
*/
|
|
271
|
+
changePasswordEndpoint?: string
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Available OAuth providers (e.g., ['github', 'google', 'gitlab']).
|
|
275
|
+
*/
|
|
276
|
+
oauthProviders?: string[]
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* OAuth endpoint path (default: '/oauth').
|
|
280
|
+
*/
|
|
281
|
+
oauthEndpoint?: string
|
|
282
|
+
}
|
|
283
|
+
````
|
|
284
|
+
|
|
285
|
+
#### `AuthResult`
|
|
286
|
+
|
|
287
|
+
Successful authentication result containing the user, access/refresh tokens, and expiration.
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
interface AuthResult<T = UserProfile> {
|
|
291
|
+
/**
|
|
292
|
+
* Authenticated user.
|
|
293
|
+
*/
|
|
294
|
+
user: T
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Access token (for token-based auth).
|
|
298
|
+
*/
|
|
299
|
+
accessToken?: string
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Refresh token (for token-based auth).
|
|
303
|
+
*/
|
|
304
|
+
refreshToken?: string
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Token expiration time (Unix timestamp).
|
|
308
|
+
*/
|
|
309
|
+
expiresAt?: number
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Set (with HTTP 206) when the account has two-factor auth enabled and the
|
|
313
|
+
* login did not include a valid `twoFactorToken`. No tokens or user are
|
|
314
|
+
* issued until login is retried with the TOTP — callers must check this
|
|
315
|
+
* flag BEFORE treating the result as a successful authentication.
|
|
316
|
+
*/
|
|
317
|
+
twoFactorRequired?: boolean
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### `AuthState`
|
|
322
|
+
|
|
323
|
+
Reactive authentication state snapshot (initialized, authenticated, user, loading, and error).
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
interface AuthState<T = UserProfile> {
|
|
327
|
+
/**
|
|
328
|
+
* Whether auth state has been initialized.
|
|
329
|
+
*/
|
|
330
|
+
initialized: boolean
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Whether the user is authenticated.
|
|
334
|
+
*/
|
|
335
|
+
authenticated: boolean
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Current user (if authenticated).
|
|
339
|
+
*/
|
|
340
|
+
user: T | null
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Whether an auth operation is in progress.
|
|
344
|
+
*/
|
|
345
|
+
loading: boolean
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Last auth error (if any).
|
|
349
|
+
*/
|
|
350
|
+
error: string | null
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### `LoginCredentials`
|
|
355
|
+
|
|
356
|
+
Email/password login credentials with optional "remember me" flag.
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
interface LoginCredentials {
|
|
360
|
+
/**
|
|
361
|
+
* Email or username.
|
|
362
|
+
*/
|
|
363
|
+
email: string
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Password.
|
|
367
|
+
*/
|
|
368
|
+
password: string
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Whether to remember the session.
|
|
372
|
+
*/
|
|
373
|
+
remember?: boolean
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Time-based one-time password (TOTP) from the user's authenticator app.
|
|
377
|
+
* Required when the account has two-factor auth enabled — the server
|
|
378
|
+
* answers `{ twoFactorRequired: true }` (HTTP 206) until a valid token
|
|
379
|
+
* is supplied.
|
|
380
|
+
*/
|
|
381
|
+
twoFactorToken?: string
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### `PasswordResetConfirm`
|
|
386
|
+
|
|
387
|
+
Password reset confirmation data.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
interface PasswordResetConfirm {
|
|
391
|
+
/**
|
|
392
|
+
* Reset token.
|
|
393
|
+
*/
|
|
394
|
+
token: string
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* New password.
|
|
398
|
+
*/
|
|
399
|
+
password: string
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### `PasswordResetRequest`
|
|
404
|
+
|
|
405
|
+
Password reset request data.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
interface PasswordResetRequest {
|
|
409
|
+
/**
|
|
410
|
+
* Email address.
|
|
411
|
+
*/
|
|
412
|
+
email: string
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
#### `RegisterData`
|
|
417
|
+
|
|
418
|
+
New user registration payload (email, password, optional name and metadata).
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
interface RegisterData {
|
|
422
|
+
/**
|
|
423
|
+
* Email address.
|
|
424
|
+
*/
|
|
425
|
+
email: string
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Password.
|
|
429
|
+
*/
|
|
430
|
+
password: string
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Display name.
|
|
434
|
+
*/
|
|
435
|
+
name?: string
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Additional registration fields.
|
|
439
|
+
*/
|
|
440
|
+
metadata?: Record<string, unknown>
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### `StorageAdapter`
|
|
445
|
+
|
|
446
|
+
Minimal key-value storage adapter used by the auth client for
|
|
447
|
+
persisting tokens and user data.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
interface StorageAdapter {
|
|
451
|
+
getItem(key: string): string | null
|
|
452
|
+
setItem(key: string, value: string): void
|
|
453
|
+
removeItem(key: string): void
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### `TokenStorage`
|
|
458
|
+
|
|
459
|
+
Internal token storage interface for reading and writing access/refresh
|
|
460
|
+
tokens and user profile data.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
interface TokenStorage {
|
|
464
|
+
getAccessToken(): string | null
|
|
465
|
+
setAccessToken(token: string | null): void
|
|
466
|
+
getRefreshToken(): string | null
|
|
467
|
+
setRefreshToken(token: string | null): void
|
|
468
|
+
getUser<T = UserProfile>(): T | null
|
|
469
|
+
setUser<T = UserProfile>(user: T | null): void
|
|
470
|
+
clear(): void
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### `UserProfile`
|
|
475
|
+
|
|
476
|
+
User profile information.
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
interface UserProfile {
|
|
480
|
+
/**
|
|
481
|
+
* Unique user identifier.
|
|
482
|
+
*/
|
|
483
|
+
id: string
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* User's email address.
|
|
487
|
+
*/
|
|
488
|
+
email?: string
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* User's display name.
|
|
492
|
+
*/
|
|
493
|
+
name?: string
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* User's avatar/profile image URL.
|
|
497
|
+
*/
|
|
498
|
+
avatar?: string
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* User's roles or permissions.
|
|
502
|
+
*/
|
|
503
|
+
roles?: string[]
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Additional metadata.
|
|
507
|
+
*/
|
|
508
|
+
metadata?: Record<string, unknown>
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Types
|
|
513
|
+
|
|
514
|
+
#### `AuthEvent`
|
|
515
|
+
|
|
516
|
+
Auth event types.
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
type AuthEvent =
|
|
520
|
+
| { type: 'login'; user: UserProfile }
|
|
521
|
+
| { type: 'logout' }
|
|
522
|
+
| { type: 'register'; user: UserProfile }
|
|
523
|
+
| { type: 'refresh' }
|
|
524
|
+
| { type: 'error'; error: string }
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### `AuthEventListener`
|
|
528
|
+
|
|
529
|
+
Callback invoked when an authentication event occurs (login, logout, etc.).
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
type AuthEventListener = (event: AuthEvent) => void
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Functions
|
|
536
|
+
|
|
537
|
+
#### `createJWTAuthClient(config)`
|
|
538
|
+
|
|
539
|
+
Creates a simple JWT-based auth client.
|
|
540
|
+
|
|
541
|
+
This is a basic implementation that can be extended or replaced
|
|
542
|
+
with more sophisticated auth providers.
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
function createJWTAuthClient(config?: AuthClientConfig): AuthClient<T>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
- `config` — Auth client configuration including endpoints, storage, and refresh settings.
|
|
549
|
+
|
|
550
|
+
**Returns:** A fully configured `AuthClient` instance.
|
|
551
|
+
|
|
552
|
+
#### `createTokenStorage(storage, prefix)`
|
|
553
|
+
|
|
554
|
+
Creates a token storage implementation backed by either in-memory
|
|
555
|
+
storage or a custom `StorageAdapter`.
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
function createTokenStorage(storage?: 'memory' | StorageAdapter, prefix?: string): TokenStorage
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
- `storage` — `'memory'` for in-memory storage (lost on refresh), or a `StorageAdapter` for persistent storage.
|
|
562
|
+
- `prefix` — Key prefix for storage items (default: `'molecule:auth:'`).
|
|
563
|
+
|
|
564
|
+
**Returns:** A `TokenStorage` instance.
|
|
565
|
+
|
|
566
|
+
#### `getClient()`
|
|
567
|
+
|
|
568
|
+
Retrieves the bonded auth client, throwing if none is configured.
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
function getClient(): AuthClient<T>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Returns:** The bonded auth client.
|
|
575
|
+
|
|
576
|
+
#### `getTokenExpiration(token)`
|
|
577
|
+
|
|
578
|
+
Returns the expiration timestamp of a JWT token in milliseconds.
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
function getTokenExpiration(token: string): number | null
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
- `token` — The raw JWT string.
|
|
585
|
+
|
|
586
|
+
**Returns:** The expiration time in ms since epoch, or `null` if the token has no `exp` claim.
|
|
587
|
+
|
|
588
|
+
#### `getUser()`
|
|
589
|
+
|
|
590
|
+
Returns the current user profile, or `null` if not authenticated.
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
function getUser(): T | null
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Returns:** The user profile, or `null` if not authenticated.
|
|
597
|
+
|
|
598
|
+
#### `hasClient()`
|
|
599
|
+
|
|
600
|
+
Checks whether an auth client is currently bonded.
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
function hasClient(): boolean
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Returns:** `true` if an auth client is bonded.
|
|
607
|
+
|
|
608
|
+
#### `isAuthenticated()`
|
|
609
|
+
|
|
610
|
+
Checks if the current user is authenticated.
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
function isAuthenticated(): boolean
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Returns:** `true` if the user has an active session.
|
|
617
|
+
|
|
618
|
+
#### `isTokenExpired(token, bufferSeconds)`
|
|
619
|
+
|
|
620
|
+
Checks if a JWT token is expired (or will expire within the buffer window).
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
function isTokenExpired(token: string, bufferSeconds?: number): boolean
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
- `token` — The raw JWT string.
|
|
627
|
+
- `bufferSeconds` — Seconds before actual expiry to consider the token expired (default: 0).
|
|
628
|
+
|
|
629
|
+
**Returns:** `true` if the token is expired or lacks an `exp` claim.
|
|
630
|
+
|
|
631
|
+
#### `login(credentials)`
|
|
632
|
+
|
|
633
|
+
Logs in with the given credentials (email/username + password).
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
function login(credentials: LoginCredentials): Promise<AuthResult<UserProfile>>
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
- `credentials` — Email/username and password.
|
|
640
|
+
|
|
641
|
+
**Returns:** The auth result containing tokens and user profile.
|
|
642
|
+
|
|
643
|
+
#### `logout()`
|
|
644
|
+
|
|
645
|
+
Logs out the current user, clearing tokens and session state.
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
function logout(): Promise<void>
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
**Returns:** A promise that resolves when logout completes.
|
|
652
|
+
|
|
653
|
+
#### `parseJWT(token)`
|
|
654
|
+
|
|
655
|
+
Parses a JWT token payload without verification (base64-decodes
|
|
656
|
+
the payload segment).
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
function parseJWT(token: string): T | null
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
- `token` — The raw JWT string (header.payload.signature).
|
|
663
|
+
|
|
664
|
+
**Returns:** The decoded payload object, or `null` if parsing fails.
|
|
665
|
+
|
|
666
|
+
#### `register(data)`
|
|
667
|
+
|
|
668
|
+
Registers a new user account.
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
function register(data: RegisterData): Promise<AuthResult<UserProfile>>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
- `data` — Registration data (email, password, name, etc.).
|
|
675
|
+
|
|
676
|
+
**Returns:** The auth result containing tokens and user profile.
|
|
677
|
+
|
|
678
|
+
#### `setClient(client)`
|
|
679
|
+
|
|
680
|
+
Registers an auth client as the active singleton. Called by bond
|
|
681
|
+
packages during application startup.
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
function setClient(client: AuthClient<UserProfile>): void
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
- `client` — The auth client implementation to bond.
|
|
688
|
+
|
|
689
|
+
## Injection Notes
|
|
690
|
+
|
|
691
|
+
### Requirements
|
|
692
|
+
|
|
693
|
+
Peer dependencies:
|
|
694
|
+
|
|
695
|
+
- `@molecule/app-bond` ^1.0.1
|
|
696
|
+
- `@molecule/app-i18n` ^1.0.1
|
|
697
|
+
- `@molecule/app-logger` ^1.0.1
|
|
698
|
+
|
|
699
|
+
### Runtime Dependencies
|
|
700
|
+
|
|
701
|
+
- `@molecule/app-bond`
|
|
702
|
+
- `@molecule/app-i18n`
|
|
703
|
+
- `@molecule/app-logger`
|
|
704
|
+
|
|
705
|
+
- **A page reload "logging the user out" is the config default, not a bug.**
|
|
706
|
+
Token storage defaults to `'memory'` (the secure default — a localStorage
|
|
707
|
+
bearer token is XSS-exfiltratable). Staying signed in across reloads works
|
|
708
|
+
by calling `client.initialize()` once at startup: it re-fetches
|
|
709
|
+
`currentUserEndpoint` (default `/users/me`) with the httpOnly cookie the
|
|
710
|
+
API set at login. Wire `initialize()`; do NOT "fix" reload-logout by
|
|
711
|
+
copying tokens into localStorage.
|
|
712
|
+
- **Endpoint defaults must match your API's real routes** (`/auth/login`,
|
|
713
|
+
`/auth/register`, `/auth/refresh`, `/users/logout`, `/users/me`, …).
|
|
714
|
+
Align them via `createJWTAuthClient({ baseURL, ...endpoints })` — never by
|
|
715
|
+
hand-editing fetch calls in components.
|
|
716
|
+
- Client-side auth state is UX only — the server enforces authorization on
|
|
717
|
+
every request; hiding a screen is not protection.
|
|
718
|
+
|
|
719
|
+
## E2E Tests
|
|
720
|
+
|
|
721
|
+
Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
722
|
+
each item to this app's actual screens/flows, and check every box off one
|
|
723
|
+
by one. A box you can't check is an integration bug to fix — not a skip:
|
|
724
|
+
|
|
725
|
+
- [ ] Logging in with VALID credentials on the real login screen resolves
|
|
726
|
+
`login()` and flips AuthState to `authenticated: true` with `getUser()`
|
|
727
|
+
returning the actual UserProfile — the UI shows that user's own `email` /
|
|
728
|
+
`name` / `avatar`, never a placeholder or a different account.
|
|
729
|
+
- [ ] A WRONG password shows a visible error (AuthState `error` is set and
|
|
730
|
+
the promise rejects) and establishes NO session: `isAuthenticated()` stays
|
|
731
|
+
false and no protected screen becomes reachable.
|
|
732
|
+
- [ ] Registering through the sign-up form calls `register()`, creates the
|
|
733
|
+
account, and lands authenticated (`authenticated: true`, `user` populated)
|
|
734
|
+
on the app's post-signup screen — not back on the login form. If the flow
|
|
735
|
+
shows an email-verification / "check your inbox" step, it must be genuinely
|
|
736
|
+
backed (this client's `register()` returns an authenticated session
|
|
737
|
+
directly and exposes no verify method) — never a dead screen the auth
|
|
738
|
+
contract cannot advance past.
|
|
739
|
+
- [ ] Logout calls `logout()` and clears the session (`authenticated: false`,
|
|
740
|
+
`user: null`, tokens cleared); afterward every protected screen redirects
|
|
741
|
+
back to login.
|
|
742
|
+
- [ ] Visiting a protected route while logged OUT redirects to login (the
|
|
743
|
+
guard reads `isAuthenticated()`), and after authenticating you land back on
|
|
744
|
+
the originally-requested screen rather than a generic home.
|
|
745
|
+
- [ ] The session survives a FULL page reload: after login, hard-reload →
|
|
746
|
+
`initialize()` restores it from the httpOnly cookie (via
|
|
747
|
+
`currentUserEndpoint`, default `/users/me`) and you stay signed in — you are
|
|
748
|
+
NOT bounced to login. (The in-memory bearer token is dropped by design; the
|
|
749
|
+
cookie restore is what keeps you in — do not "fix" this by writing the token
|
|
750
|
+
to localStorage.)
|
|
751
|
+
- [ ] Password reset round-trips: `requestPasswordReset({ email })` then
|
|
752
|
+
`confirmPasswordReset({ token, password })` with the emailed token, after
|
|
753
|
+
which `login()` with the NEW password succeeds and the OLD password no
|
|
754
|
+
longer works.
|
|
755
|
+
- [ ] Authorization holds: a signed-in user only ever reads/edits their OWN
|
|
756
|
+
record (`getUser()` is the caller's profile, never another user's); the
|
|
757
|
+
bearer token lives in memory only — it is never written to
|
|
758
|
+
localStorage / sessionStorage where another script could read it; and an
|
|
759
|
+
expired/invalid token fails closed — the app re-authenticates (`refresh()`
|
|
760
|
+
or `logout()`), never silently serving another user's data.
|
|
761
|
+
|
|
762
|
+
## Translations
|
|
763
|
+
|
|
764
|
+
Translation strings are provided by `@molecule/app-locales-auth`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/app-auth",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Client-side authentication interface for molecule.dev",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -27,14 +28,14 @@
|
|
|
27
28
|
],
|
|
28
29
|
"license": "Apache-2.0",
|
|
29
30
|
"peerDependencies": {
|
|
30
|
-
"@molecule/app-bond": "^1.0.
|
|
31
|
-
"@molecule/app-i18n": "^1.0.
|
|
32
|
-
"@molecule/app-logger": "^1.0.
|
|
31
|
+
"@molecule/app-bond": "^1.0.1",
|
|
32
|
+
"@molecule/app-i18n": "^1.0.1",
|
|
33
|
+
"@molecule/app-logger": "^1.0.1"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@molecule/app-bond": "1.0.
|
|
36
|
-
"@molecule/app-i18n": "1.0.
|
|
37
|
-
"@molecule/app-logger": "1.0.
|
|
36
|
+
"@molecule/app-bond": "1.0.1",
|
|
37
|
+
"@molecule/app-i18n": "1.0.1",
|
|
38
|
+
"@molecule/app-logger": "1.0.1",
|
|
38
39
|
"@types/node": "26.1.2",
|
|
39
40
|
"typescript": "6.0.3",
|
|
40
41
|
"vitest": "4.1.10"
|