@molecule/app-auth 1.8.2 → 3.0.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 +304 -88
- package/dist/browser-storage.d.ts +2 -2
- package/dist/browser-storage.js +2 -2
- package/dist/client.d.ts +22 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +70 -11
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +100 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -37
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +6 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +4 -1
- package/dist/provider.js.map +1 -1
- package/dist/types.d.ts +247 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/utilities.d.ts +26 -4
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +24 -2
- package/dist/utilities.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-25T20:03:32.020Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/app-auth
|
|
@@ -44,8 +44,8 @@ The first read of the client (`getState()`, `subscribe()`/`onAuthChange()`, a
|
|
|
44
44
|
and an explicit `initialize()` returns the same promise, so a second call asks the
|
|
45
45
|
server nothing. Keep the template's explicit call (it makes the first render
|
|
46
46
|
deterministic); an entry file that drops it still restores instead of reporting
|
|
47
|
-
`'loading'` forever (
|
|
48
|
-
|
|
47
|
+
`'loading'` forever (a `main.tsx` rewrite can drop the line, and every page
|
|
48
|
+
then stays on its loading branch).
|
|
49
49
|
|
|
50
50
|
**Never fetch viewer-dependent data before the restore settles.** A
|
|
51
51
|
component effect that runs on mount fires while `initialize()` is still
|
|
@@ -71,12 +71,12 @@ mirrors the token into the bonded client.
|
|
|
71
71
|
`initialize()` sets that state synchronously, before any request, and every
|
|
72
72
|
verification settles behind that render. Never hold the first paint on
|
|
73
73
|
`validateSession()` or a `/me` request of your own — a protected page then pays
|
|
74
|
-
an API round trip on every navigation (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
an API round trip on every navigation (~30 full page loads in one pagination
|
|
75
|
+
spec put four tests at 15.0–15.9 s of a 15 s budget, and two timed out). Route
|
|
76
|
+
guards read this same `status`: wait only while it is `'loading'` — that
|
|
77
|
+
status means "no decision yet", so an auth request in flight
|
|
78
|
+
(`state.loading`) keeps the decided status and does not unmount a
|
|
79
|
+
Login/Register page mid-request.
|
|
80
80
|
`validateSession()` is the background re-check that produces the last two: call
|
|
81
81
|
it after the first render — a 401 or 403 clears the token (`'unauthenticated'`),
|
|
82
82
|
while a network failure, a 5xx, or a 404 from a wrong `currentUserEndpoint`
|
|
@@ -121,18 +121,18 @@ redirect or the app's own sign-in endpoint has already issued the token and retu
|
|
|
121
121
|
user, `setSession({ accessToken, user })` stores both and flips the client to
|
|
122
122
|
`authenticated` in one update, so every hook reading the shared client is signed in at once
|
|
123
123
|
(a form's own sign-in code wrote the token and the user with `setAccessToken`/`setUser`,
|
|
124
|
-
which change their field only — the UI
|
|
125
|
-
out
|
|
124
|
+
which change their field only — the UI stays signed out and the browser assertions that
|
|
125
|
+
expect it time out). `setAccessToken()` and `setUser()` are field setters, not a
|
|
126
126
|
sign-in: neither sets `authenticated`.
|
|
127
127
|
|
|
128
128
|
**A custom user profile is typed once and used everywhere: `AuthClientConfig<MyUser>`,
|
|
129
129
|
`createJWTAuthClient<MyUser>` and `useAuth<MyUser>()`.** The config's generic makes
|
|
130
130
|
`mapAuthResponse` return `AuthResult<MyUser>`, so a mapper that omits a required custom
|
|
131
131
|
field fails at the factory call instead of shipping a signed-in page whose navbar reads
|
|
132
|
-
`undefined`
|
|
133
|
-
|
|
134
|
-
wire user
|
|
135
|
-
field, and never hide a mismatch with `as unknown as MyUser`:
|
|
132
|
+
`undefined` — a mapper that keeps `id`/`name`/`avatar` but drops `username` leaves the
|
|
133
|
+
signed-in controls unrendered although login succeeded. The
|
|
134
|
+
wire user carries the base fields too, `id` included — spread it, do not rebuild it
|
|
135
|
+
field by field, and never hide a mismatch with `as unknown as MyUser`:
|
|
136
136
|
|
|
137
137
|
```typescript
|
|
138
138
|
import type { AuthClientConfig, UserProfile } from '@molecule/app-auth'
|
|
@@ -145,18 +145,12 @@ interface MyUser extends UserProfile {
|
|
|
145
145
|
|
|
146
146
|
const config: AuthClientConfig<MyUser> = {
|
|
147
147
|
mapAuthResponse: (data) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// …plus the base fields the client derives its identity from.
|
|
155
|
-
id: wire.username,
|
|
156
|
-
name: wire.name ?? wire.username,
|
|
157
|
-
avatar: wire.image,
|
|
158
|
-
},
|
|
159
|
-
}
|
|
148
|
+
// The wire is the app's profile plus the bearer; `id` is the server's.
|
|
149
|
+
const { user: wire } = data as { user: { token: string } & MyUser }
|
|
150
|
+
// The bearer stays OUT of the profile: the client caches `user` in Web
|
|
151
|
+
// Storage, where a token copy is readable by any script on the origin.
|
|
152
|
+
const { token, ...profile } = wire
|
|
153
|
+
return { accessToken: token, user: profile }
|
|
160
154
|
},
|
|
161
155
|
}
|
|
162
156
|
|
|
@@ -164,11 +158,55 @@ const client = createJWTAuthClient<MyUser>(config)
|
|
|
164
158
|
// React: `useAuth<MyUser>()` reads the same profile.
|
|
165
159
|
```
|
|
166
160
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
161
|
+
That `id` is the API's account identifier. Do not derive one from a display field
|
|
162
|
+
(`id: wire.username`) to borrow `UserProfile` for an API that has no id: the field
|
|
163
|
+
changes, the fabricated id changes with it, and the value then reaches storage,
|
|
164
|
+
`getUser()` and every page as if the server had said it.
|
|
165
|
+
|
|
166
|
+
A profile does not have to extend `UserProfile` and does not need an `id`: the client
|
|
167
|
+
stores and patches the object it is given and reads no field of it. Name the API's own
|
|
168
|
+
shape, pass it to the factory and the config, and have the mapper return it complete —
|
|
169
|
+
every configuration adapter is typed by the same `T` (`mapAuthResponse`, the
|
|
170
|
+
`profileEndpoint`/`profileUpdateMethod`/`profileRequestBody` write transport,
|
|
171
|
+
`shouldRestoreUser`, and `setSession`):
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import type { AuthClientConfig } from '@molecule/app-auth'
|
|
175
|
+
import { createJWTAuthClient } from '@molecule/app-auth'
|
|
176
|
+
|
|
177
|
+
interface SessionUser {
|
|
178
|
+
username: string
|
|
179
|
+
email: string
|
|
180
|
+
bio: string
|
|
181
|
+
image: string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const config: AuthClientConfig<SessionUser> = {
|
|
185
|
+
mapAuthResponse: (data) => {
|
|
186
|
+
const { user: wire } = data as { user: { token: string } & SessionUser }
|
|
187
|
+
// The bearer stays OUT of the profile: the client caches `user` in Web
|
|
188
|
+
// Storage, where a token copy is readable by any script on the origin.
|
|
189
|
+
const { token, ...profile } = wire
|
|
190
|
+
return { accessToken: token, user: profile }
|
|
191
|
+
},
|
|
192
|
+
profileEndpoint: (user) => '/people/' + encodeURIComponent(user.username),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const client = createJWTAuthClient<SessionUser>(config)
|
|
196
|
+
// `client.getUser()` has `bio`/`image`; there is no `id` to fabricate.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The profile is named at the factory, never inferred from the mapper: an untyped
|
|
200
|
+
`createJWTAuthClient({ mapAuthResponse })` stays `UserProfile`, whose required
|
|
201
|
+
`id: string` is the default configuration's contract. An array, a function or a
|
|
202
|
+
`Date` is not a profile the client can persist — see `AuthProfileShape`.
|
|
203
|
+
|
|
204
|
+
A profile field can change during a profile save — a handle, or a username an app
|
|
205
|
+
once derived an `id` from. It is not an immutable session identifier:
|
|
206
|
+
`getSessionRevision()` tracks session ownership independently of profile fields,
|
|
207
|
+
remains unchanged for profile updates and same-session refreshes, and changes when
|
|
208
|
+
a session is replaced or ended. Compare revisions only within one client. React's
|
|
209
|
+
`useProfileForm` uses this capability by default.
|
|
172
210
|
|
|
173
211
|
**A profile-update transport that is not `PATCH` with bare JSON is a CONFIG field, not a
|
|
174
212
|
hand-written client.** `updateProfile()` sends to `profileEndpoint` with `PATCH` and the
|
|
@@ -203,7 +241,11 @@ the client to change a method or an envelope also replaces the lifecycle that ke
|
|
|
203
241
|
coherent, and hand-written adapters have had to repair those callbacks repeatedly.
|
|
204
242
|
A pending profile response is published only to the session that sent it:
|
|
205
243
|
signing out or explicitly adopting/signing in another session prevents the old
|
|
206
|
-
response from
|
|
244
|
+
response from changing the active user's stored profile. The answer is applied onto the
|
|
245
|
+
stored profile field by field: a field it carries replaces the stored value (`null` clears
|
|
246
|
+
it) and one it leaves out — or that `mapAuthResponse` leaves `undefined` — is kept, so a
|
|
247
|
+
field the API clears by leaving it out must reach the client as `null`, answered by the API
|
|
248
|
+
or mapped by `mapAuthResponse`.
|
|
207
249
|
|
|
208
250
|
**A background verification that returns an unchanged user changes nothing.** When
|
|
209
251
|
`validateSession()` (or a reload's restore) answers with the same profile data the client
|
|
@@ -257,16 +299,26 @@ fields with the factory's second generic, and `requestBody` receives exactly wha
|
|
|
257
299
|
the form passed — `{ user: { username, email, password } }` goes out as-is.
|
|
258
300
|
|
|
259
301
|
```typescript
|
|
260
|
-
import type { UserProfile } from '@molecule/app-auth'
|
|
261
302
|
import { createJWTAuthClient } from '@molecule/app-auth'
|
|
262
303
|
|
|
263
|
-
|
|
304
|
+
// RealWorld's user has no `id`. Name the shape the wire actually sends; the
|
|
305
|
+
// client reads no field of it, so nothing has to be invented for a base profile.
|
|
306
|
+
interface RealWorldUser {
|
|
307
|
+
username: string
|
|
308
|
+
email: string
|
|
309
|
+
bio: string
|
|
310
|
+
image: string
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const realWorldAuth = createJWTAuthClient<RealWorldUser, { username: string }>({
|
|
264
314
|
baseURL: '/api',
|
|
265
315
|
authScheme: 'Token',
|
|
266
316
|
requestBody: (_kind, payload) => ({ user: payload }),
|
|
267
317
|
mapAuthResponse: (data) => {
|
|
268
|
-
const { user } = data as { user: { token: string
|
|
269
|
-
|
|
318
|
+
const { user: wire } = data as { user: { token: string } & RealWorldUser }
|
|
319
|
+
// Keep every wire field; the bearer stays out of the cached profile.
|
|
320
|
+
const { token, ...profile } = wire
|
|
321
|
+
return { accessToken: token, user: profile }
|
|
270
322
|
},
|
|
271
323
|
})
|
|
272
324
|
|
|
@@ -676,9 +728,12 @@ interface AuthClient<
|
|
|
676
728
|
|
|
677
729
|
/**
|
|
678
730
|
* Updates the current user's profile.
|
|
679
|
-
* Returns the saved
|
|
680
|
-
*
|
|
681
|
-
*
|
|
731
|
+
* Returns the saved profile: the answer applied onto the signed-in user field
|
|
732
|
+
* by field, with or without an `id` — a field it carries replaces the value
|
|
733
|
+
* (`null` clears it), one it omits or leaves `undefined` is kept, so a field
|
|
734
|
+
* cleared by omission must arrive as `null`. A non-object answer is returned
|
|
735
|
+
* as it came. An answer for a replaced or ended session leaves the current one
|
|
736
|
+
* untouched.
|
|
682
737
|
*/
|
|
683
738
|
updateProfile(data: Partial<T>): Promise<T>
|
|
684
739
|
|
|
@@ -742,9 +797,6 @@ interface AuthClient<
|
|
|
742
797
|
* called, and after it has finished. A restore that failed resolves too (it settled the
|
|
743
798
|
* question), so an HTTP client can await this before every request without risking one
|
|
744
799
|
* held forever. That is what makes the fetch-before-restore race impossible: a page that
|
|
745
|
-
* fetches viewer-dependent data on mount waits here instead of going out anonymously
|
|
746
|
-
* (see `setAuthGate` in `@molecule/app-http`).
|
|
747
|
-
*
|
|
748
800
|
// … truncated — the full declaration is in node_modules/@molecule/app-auth/dist/types.d.ts
|
|
749
801
|
```
|
|
750
802
|
|
|
@@ -761,23 +813,15 @@ interface StorageAdapter {
|
|
|
761
813
|
}
|
|
762
814
|
```
|
|
763
815
|
|
|
764
|
-
#### `
|
|
816
|
+
#### `AuthClientConfigOptions`
|
|
765
817
|
|
|
766
|
-
|
|
767
|
-
endpoints, storage strategy, token refresh, and OAuth providers.
|
|
818
|
+
Every option {@link AuthClientConfig} accepts, typed by the profile.
|
|
768
819
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
makes {@link mapAuthResponse} return `AuthResult<MyUser>`, so a mapper that
|
|
772
|
-
omits a required custom field is a type error instead of a signed-in page
|
|
773
|
-
reading `undefined` (xr-molecule-flash-m255). Keep every wire field the pages
|
|
774
|
-
read (spread the wire user and add `id`/`name`/`avatar`); mapping a field to a
|
|
775
|
-
different name does not preserve the original, and a `as unknown as MyUser`
|
|
776
|
-
assertion hides the loss. See the module docs for the wire-preserving
|
|
777
|
-
adapter.
|
|
820
|
+
Use `AuthClientConfig<T>` rather than this type: it adds the profile-shape
|
|
821
|
+
check. This interface exists so each member's documentation is addressable.
|
|
778
822
|
|
|
779
823
|
````typescript
|
|
780
|
-
interface
|
|
824
|
+
interface AuthClientConfigOptions<T extends object = UserProfile> {
|
|
781
825
|
/**
|
|
782
826
|
* API base URL.
|
|
783
827
|
*/
|
|
@@ -807,6 +851,8 @@ interface AuthClientConfig<T extends UserProfile = UserProfile> {
|
|
|
807
851
|
* The write route used by `updateProfile`, independently of `currentUserEndpoint`.
|
|
808
852
|
* Defaults to `/auth/profile`. A resource route with an account id can resolve
|
|
809
853
|
* from the signed-in user, e.g. `(user) => '/accounts/' + encodeURIComponent(user.id)`.
|
|
854
|
+
* A profile with no `id` resolves from a field it does have, e.g.
|
|
855
|
+
* `(user) => '/people/' + encodeURIComponent(user.username)`.
|
|
810
856
|
* The resolver sees the current user before the patch, never caller-submitted identity.
|
|
811
857
|
*/
|
|
812
858
|
profileEndpoint?: string | ((user: Readonly<T>) => string)
|
|
@@ -893,7 +939,16 @@ interface AuthClientConfig<T extends UserProfile = UserProfile> {
|
|
|
893
939
|
/**
|
|
894
940
|
* Turn an answer that carries a user into the client's
|
|
895
941
|
* `{ accessToken, refreshToken?, user }` (default: the answer as given).
|
|
896
|
-
* RealWorld, for example
|
|
942
|
+
* RealWorld, for example — keep the wire bearer OUT of the profile, because
|
|
943
|
+
* the client caches `user` in Web Storage where a token copy is readable by
|
|
944
|
+
* any script on the origin:
|
|
945
|
+
*
|
|
946
|
+
* ```typescript
|
|
947
|
+
* (data) => {
|
|
948
|
+
* const { token, ...user } = data.user
|
|
949
|
+
* return { accessToken: token, user }
|
|
950
|
+
* }
|
|
951
|
+
* ```
|
|
897
952
|
*
|
|
898
953
|
* It is the ONE user-shape adapter, and the client applies it on EVERY path
|
|
899
954
|
* that adopts a user from an API answer — `login()`, `register()`,
|
|
@@ -901,17 +956,26 @@ interface AuthClientConfig<T extends UserProfile = UserProfile> {
|
|
|
901
956
|
* token-refresh answer, and `updateProfile()`. Those answers carry the raw wire
|
|
902
957
|
* user (a current-user route typically answers a bare `{ user }`), so applying
|
|
903
958
|
* the same mapping is what keeps the adapted profile after a reload instead of
|
|
904
|
-
* replacing it with the unadapted one
|
|
959
|
+
* replacing it with the unadapted one. A mapper that
|
|
905
960
|
* cannot read a given answer (it throws, or returns no `user`) leaves that
|
|
906
961
|
* answer's user as it came.
|
|
907
962
|
*
|
|
908
963
|
* It is NOT applied to `setSession()` or `setUser()`: those receive a profile
|
|
909
964
|
* the caller already built in this client's shape, not a wire answer.
|
|
910
965
|
*
|
|
966
|
+
* The return type does NOT infer the profile: an untyped
|
|
967
|
+
* `createJWTAuthClient({ mapAuthResponse })` stays `UserProfile`, so a mapper
|
|
968
|
+
* that returns a no-id profile fails to compile there instead of silently
|
|
969
|
+
* replacing the default with whatever the mapper happened to return. The same
|
|
970
|
+
* rule holds for every config member below (`profileEndpoint`,
|
|
971
|
+
* `profileRequestBody`, `shouldRestoreUser`): none of them supplies the
|
|
972
|
+
* profile. Name the profile at the call
|
|
973
|
+
* (`createJWTAuthClient<MyUser>({ mapAuthResponse })`) to use a custom one.
|
|
974
|
+
*
|
|
911
975
|
* @param data - The parsed response body.
|
|
912
976
|
* @returns The auth result.
|
|
913
977
|
*/
|
|
914
|
-
mapAuthResponse?: (data: unknown) => AuthResult<T
|
|
978
|
+
mapAuthResponse?: (data: unknown) => AuthResult<NoInfer<T>>
|
|
915
979
|
|
|
916
980
|
/**
|
|
917
981
|
* Where the access/refresh token and the cached user are kept — the field that
|
|
@@ -936,25 +1000,6 @@ interface AuthClientConfig<T extends UserProfile = UserProfile> {
|
|
|
936
1000
|
* A bearer token in Web Storage is readable by any script on the origin, so an
|
|
937
1001
|
* XSS can steal it; see {@link createBrowserStorageAdapter}'s `@remarks` for the
|
|
938
1002
|
* trade and for the cookie alternative. Never answer a suite's storage key with a
|
|
939
|
-
* second store or a restore script beside this client — that races the client's
|
|
940
|
-
* own restore, and either side's 401 signs the user out for good.
|
|
941
|
-
*
|
|
942
|
-
* Any adapter with `getItem`/`setItem`/`removeItem` works — the methods are
|
|
943
|
-
* SYNCHRONOUS, so the async `@molecule/app-storage` provider
|
|
944
|
-
* (`getProvider()`, whose `get()` returns a Promise) is NOT one: wrap the
|
|
945
|
-
* `Storage` object it was built on (`createBrowserStorageAdapter({ storage })`),
|
|
946
|
-
* or store a different way.
|
|
947
|
-
*
|
|
948
|
-
* @example
|
|
949
|
-
* ```typescript
|
|
950
|
-
* import { createBrowserStorageAdapter } from '@molecule/app-auth'
|
|
951
|
-
*
|
|
952
|
-
* // A suite/host page reads the token at its own key (the browser default
|
|
953
|
-
* // already stores it; `storage` is shown for an explicit choice):
|
|
954
|
-
* storage: createBrowserStorageAdapter()
|
|
955
|
-
* tokenKey: 'jwtToken'
|
|
956
|
-
* // Only this tab's lifetime, at a narrower exposure than localStorage:
|
|
957
|
-
* storage: createBrowserStorageAdapter({ kind: 'session' })
|
|
958
1003
|
// … truncated — the full declaration is in node_modules/@molecule/app-auth/dist/types.d.ts
|
|
959
1004
|
````
|
|
960
1005
|
|
|
@@ -987,6 +1032,86 @@ Which Web Storage area {@link createBrowserStorageAdapter} persists to:
|
|
|
987
1032
|
type BrowserStorageKind = 'local' | 'session'
|
|
988
1033
|
```
|
|
989
1034
|
|
|
1035
|
+
#### `AuthProfileShape`
|
|
1036
|
+
|
|
1037
|
+
The profile shapes the auth client accepts: a JSON object type of named
|
|
1038
|
+
fields, and nothing else.
|
|
1039
|
+
|
|
1040
|
+
The client does not read a field of the profile, but it does persist it:
|
|
1041
|
+
`tokenStorage.setUser` writes it with `JSON.stringify` and `getUser` restores
|
|
1042
|
+
it with `JSON.parse`, and `updateProfile(data: Partial<T>)` patches it
|
|
1043
|
+
field-wise. An array has no named fields to read or patch, `JSON.stringify`
|
|
1044
|
+
drops a function entirely, and a `Date` comes back from `JSON.parse` as a
|
|
1045
|
+
string while still typed as a `Date` — so those three types are rejected on
|
|
1046
|
+
every entrypoint that takes a profile type (`createJWTAuthClient`,
|
|
1047
|
+
the default-auth factories, `AuthClientConfig`).
|
|
1048
|
+
|
|
1049
|
+
The check is on the TOP-LEVEL shape only: a class instance, or a `Date` in a
|
|
1050
|
+
nested field, is the app's own persistence contract, not this validator's.
|
|
1051
|
+
A named `interface`, a type literal, an intersection and the empty object
|
|
1052
|
+
type all satisfy it — `{}` compiles because it is a JSON object that
|
|
1053
|
+
round-trips unchanged, not an invalid form.
|
|
1054
|
+
|
|
1055
|
+
A union is checked member by member: `ExistingUser | ExistingUser[]` contains
|
|
1056
|
+
an array, so it is not an object-only profile either, and the shared
|
|
1057
|
+
rejection test (`AuthProfileIsRejected`, used by
|
|
1058
|
+
{@link AuthProfileConstraint} and {@link AuthProfileGuard}) rejects it as a
|
|
1059
|
+
whole.
|
|
1060
|
+
|
|
1061
|
+
```typescript
|
|
1062
|
+
type AuthProfileShape<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) | Date
|
|
1063
|
+
? AuthProfileRejection
|
|
1064
|
+
: object
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
#### `AuthProfileGuard`
|
|
1068
|
+
|
|
1069
|
+
Requires a profile type to satisfy {@link AuthProfileShape}: an invalid
|
|
1070
|
+
profile makes {@link AuthClientConfig} impossible to populate, so the
|
|
1071
|
+
annotation itself is the error. It shares the entrypoints' rejection test,
|
|
1072
|
+
so a union with an invalid member is rejected here too.
|
|
1073
|
+
|
|
1074
|
+
```typescript
|
|
1075
|
+
type AuthProfileGuard<T> =
|
|
1076
|
+
AuthProfileIsRejected<T> extends true
|
|
1077
|
+
? { readonly [K in AuthProfileRejection]: never }
|
|
1078
|
+
: Record<never, never>
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
#### `AuthProfileConstraint`
|
|
1082
|
+
|
|
1083
|
+
The constraint every entrypoint puts on a profile type: a JSON object type of
|
|
1084
|
+
named fields ({@link AuthProfileShape}), and `object` for `any`.
|
|
1085
|
+
|
|
1086
|
+
`any` is the caller's explicit opt-out of profile typing, not an invalid JSON
|
|
1087
|
+
shape: `createJWTAuthClient<any>()`, `getClient<any>()` and
|
|
1088
|
+
`AuthClientConfig<any>` name it deliberately. It is detected explicitly
|
|
1089
|
+
(`0 extends 1 & T`) and accepted. Every other type is measured against
|
|
1090
|
+
{@link AuthProfileShape}, member by member when it is a union: a union that
|
|
1091
|
+
holds an array, a function or a `Date` is rejected as a whole, so
|
|
1092
|
+
`createJWTAuthClient<ExistingUser | ExistingUser[]>()` is the same type error
|
|
1093
|
+
as `createJWTAuthClient<ExistingUser[]>()`.
|
|
1094
|
+
|
|
1095
|
+
Forwarding a profile through your own generic copies this constraint, so the
|
|
1096
|
+
profile keeps being checked:
|
|
1097
|
+
|
|
1098
|
+
```typescript
|
|
1099
|
+
import type { AuthClientConfig, AuthProfileConstraint } from '@molecule/app-auth'
|
|
1100
|
+
import { createJWTAuthClient } from '@molecule/app-auth'
|
|
1101
|
+
|
|
1102
|
+
export function makeAuth<T extends AuthProfileConstraint<T>>(config: AuthClientConfig<T>) {
|
|
1103
|
+
return createJWTAuthClient<T>(config)
|
|
1104
|
+
}
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
`T extends object` is not enough: the factory rejects arrays, functions and
|
|
1108
|
+
`Date` at its own constraint, so a forwarder written that way does not
|
|
1109
|
+
compile. Copy this type instead.
|
|
1110
|
+
|
|
1111
|
+
```typescript
|
|
1112
|
+
type AuthProfileConstraint<T> = AuthProfileIsRejected<T> extends true ? never : object
|
|
1113
|
+
```
|
|
1114
|
+
|
|
990
1115
|
#### `RegisterExtraShape`
|
|
991
1116
|
|
|
992
1117
|
The shape an app's own registration fields (`TRegister`) must satisfy.
|
|
@@ -1091,11 +1216,15 @@ type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'unavailable
|
|
|
1091
1216
|
|
|
1092
1217
|
Auth event types.
|
|
1093
1218
|
|
|
1219
|
+
A sign-in event carries the client's own profile type, so a listener on a
|
|
1220
|
+
client typed with the app's profile reads that profile — including one that
|
|
1221
|
+
does not extend {@link UserProfile}. The default is {@link UserProfile}.
|
|
1222
|
+
|
|
1094
1223
|
```typescript
|
|
1095
|
-
type AuthEvent =
|
|
1096
|
-
| { type: 'login'; user:
|
|
1224
|
+
type AuthEvent<T = UserProfile> =
|
|
1225
|
+
| { type: 'login'; user: T }
|
|
1097
1226
|
| { type: 'logout' }
|
|
1098
|
-
| { type: 'register'; user:
|
|
1227
|
+
| { type: 'register'; user: T }
|
|
1099
1228
|
| { type: 'refresh' }
|
|
1100
1229
|
| { type: 'error'; error: string }
|
|
1101
1230
|
```
|
|
@@ -1104,8 +1233,63 @@ type AuthEvent =
|
|
|
1104
1233
|
|
|
1105
1234
|
Callback invoked when an authentication event occurs (login, logout, etc.).
|
|
1106
1235
|
|
|
1236
|
+
A listener is typed by the client's profile: `AuthEventListener<MyUser>`
|
|
1237
|
+
receives `AuthEvent<MyUser>`, so a sign-in event's `user` is the app's own
|
|
1238
|
+
shape — including a profile that does not extend {@link UserProfile}. The
|
|
1239
|
+
default is {@link UserProfile}.
|
|
1240
|
+
|
|
1241
|
+
An inline arrow is contextually typed, so its `event` parameter needs no
|
|
1242
|
+
annotation. Because listener parameters are contravariant, a client whose
|
|
1243
|
+
profile extends {@link UserProfile} also accepts a listener written against
|
|
1244
|
+
the default `AuthEventListener` (those events carry every `UserProfile`
|
|
1245
|
+
field). A client whose profile does NOT extend `UserProfile` — an API user
|
|
1246
|
+
with no `id` — accepts only `AuthEventListener<T>`: a default listener would
|
|
1247
|
+
read an `id` the runtime user does not have.
|
|
1248
|
+
|
|
1249
|
+
**Migration to 2.0.** External providers that implement {@link AuthClient}
|
|
1250
|
+
declare their listener parameter as `AuthEventListener<T>`
|
|
1251
|
+
(`addEventListener(listener: AuthEventListener<T>): () => void`). The
|
|
1252
|
+
previous `addEventListener(listener: AuthEventListener)` declaration, which
|
|
1253
|
+
claimed every client's events carry a `UserProfile`, is no longer assignable
|
|
1254
|
+
once the client's profile is a custom one.
|
|
1255
|
+
|
|
1256
|
+
```typescript
|
|
1257
|
+
type AuthEventListener<T = UserProfile> = (event: AuthEvent<T>) => void
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
#### `AuthClientConfig`
|
|
1261
|
+
|
|
1262
|
+
Configuration options for creating an auth client, including API
|
|
1263
|
+
endpoints, storage strategy, token refresh, and OAuth providers.
|
|
1264
|
+
|
|
1265
|
+
Type it with the app's user — `AuthClientConfig<MyUser>` — the same type
|
|
1266
|
+
`createJWTAuthClient<MyUser>` returns and `useAuth<MyUser>()` reads. That
|
|
1267
|
+
makes {@link AuthClientConfigOptions.mapAuthResponse} return
|
|
1268
|
+
`AuthResult<MyUser>`, so a mapper that omits a required custom field is a type
|
|
1269
|
+
error instead of a signed-in page reading `undefined`. Keep every wire field the pages
|
|
1270
|
+
read by spreading the
|
|
1271
|
+
wire user — the base fields (`id`, `name`, `avatar`) come from the server like
|
|
1272
|
+
any other, never re-derived from a mutable display field such as a username;
|
|
1273
|
+
mapping a field to a different name does not preserve the original, and an
|
|
1274
|
+
`as unknown as MyUser` assertion hides the loss. See the module docs for the
|
|
1275
|
+
wire-preserving adapter.
|
|
1276
|
+
|
|
1277
|
+
`T` is the app's OWN profile shape and need not extend {@link UserProfile}:
|
|
1278
|
+
the client stores and patches the object it is given and reads no field of
|
|
1279
|
+
it, so an API whose user has no `id` — `{ username, email, bio, image }` —
|
|
1280
|
+
is typed by its own `interface` directly, with no fabricated `id`. The
|
|
1281
|
+
default, {@link UserProfile}, still requires `id: string`. A primitive,
|
|
1282
|
+
`null`, an array, a function or a `Date` — or a union containing one — is
|
|
1283
|
+
rejected; see {@link AuthProfileShape} for the persistence contract behind
|
|
1284
|
+
that list.
|
|
1285
|
+
|
|
1286
|
+
The type argument is NEVER inferred from the config: not from
|
|
1287
|
+
|
|
1288
|
+
_(description truncated — read the full text in `node_modules/@molecule/app-auth/dist/types.d.ts`.)_
|
|
1289
|
+
|
|
1107
1290
|
```typescript
|
|
1108
|
-
type
|
|
1291
|
+
type AuthClientConfig<T extends object = UserProfile> = AuthClientConfigOptions<T> &
|
|
1292
|
+
AuthProfileGuard<T>
|
|
1109
1293
|
```
|
|
1110
1294
|
|
|
1111
1295
|
### Functions
|
|
@@ -1118,8 +1302,8 @@ The auth client's `StorageAdapter` over the browser's Web Storage.
|
|
|
1118
1302
|
token AND the user in the store, `initialize()` restores the authenticated state synchronously
|
|
1119
1303
|
(`whenRestored()` is already resolved on the first render); render from it, and validate the
|
|
1120
1304
|
token as a background request — never in front of the first render. A page that waits on its
|
|
1121
|
-
own `/me` call pays a round trip on EVERY navigation
|
|
1122
|
-
|
|
1305
|
+
own `/me` call pays a round trip on EVERY navigation: a 1–5 s restore window is enough to
|
|
1306
|
+
time out a navigation that expects the signed-in UI.
|
|
1123
1307
|
|
|
1124
1308
|
In a browser this IS the auth client's default `AuthClientConfig.storage`, so a scaffolded
|
|
1125
1309
|
app keeps its session across a page reload with no extra wiring. Outside a browser (SSR, a Node
|
|
@@ -1160,8 +1344,25 @@ Creates a simple JWT-based auth client.
|
|
|
1160
1344
|
This is a basic implementation that can be extended or replaced
|
|
1161
1345
|
with more sophisticated auth providers.
|
|
1162
1346
|
|
|
1347
|
+
`T` is the profile the API returns. The default, `UserProfile`, requires
|
|
1348
|
+
`id: string`, and an untyped call — `createJWTAuthClient({ mapAuthResponse })`
|
|
1349
|
+
— KEEPS that default: the profile is never inferred from the config (no
|
|
1350
|
+
member of it, and not from an annotated config variable), so a mapper that
|
|
1351
|
+
returns a user without `id` fails to compile instead of quietly becoming the
|
|
1352
|
+
client's profile. To use the app's own profile, name it here,
|
|
1353
|
+
`createJWTAuthClient<SessionUser>(config)`; a profile with no `id`
|
|
1354
|
+
(`{ username, email, bio, image }`) is then used as-is. Pass the same type to
|
|
1355
|
+
the config (`AuthClientConfig<SessionUser>`), so `mapAuthResponse` and the
|
|
1356
|
+
write transport are checked against the profile the client and the React
|
|
1357
|
+
hooks hand to your pages.
|
|
1358
|
+
|
|
1359
|
+
`any` is accepted (`createJWTAuthClient<any>()`) as the caller's explicit
|
|
1360
|
+
opt-out; array, function and `Date` profiles are rejected. A wrapper that
|
|
1361
|
+
forwards a profile type copies `AuthProfileConstraint` (see
|
|
1362
|
+
{@link AuthClientConfig}'s migration note).
|
|
1363
|
+
|
|
1163
1364
|
```typescript
|
|
1164
|
-
function createJWTAuthClient(config?:
|
|
1365
|
+
function createJWTAuthClient(config?: NoInfer<AuthClientConfigOptions<T>>): AuthClient<T, TRegister>
|
|
1165
1366
|
```
|
|
1166
1367
|
|
|
1167
1368
|
- `config` — Auth client configuration including endpoints, storage, and refresh settings.
|
|
@@ -1281,7 +1482,7 @@ function getUser(): T | null
|
|
|
1281
1482
|
Logs in with the given credentials (email/username + password).
|
|
1282
1483
|
|
|
1283
1484
|
```typescript
|
|
1284
|
-
function login(credentials: LoginCredentials): Promise<AuthResult<
|
|
1485
|
+
function login(credentials: LoginCredentials): Promise<AuthResult<T>>
|
|
1285
1486
|
```
|
|
1286
1487
|
|
|
1287
1488
|
- `credentials` — Email/username and password.
|
|
@@ -1302,8 +1503,12 @@ function logout(): Promise<void>
|
|
|
1302
1503
|
|
|
1303
1504
|
Registers a new user account.
|
|
1304
1505
|
|
|
1506
|
+
The registration extra fields stay the FIRST type argument, exactly as before
|
|
1507
|
+
(`register<{ username: string }>(data)` keeps compiling); the profile is the
|
|
1508
|
+
second one (`register<{ username: string }, SessionUser>(data)`).
|
|
1509
|
+
|
|
1305
1510
|
```typescript
|
|
1306
|
-
function register(data: RegisterData<TExtra>): Promise<AuthResult<
|
|
1511
|
+
function register(data: RegisterData<TExtra>): Promise<AuthResult<T>>
|
|
1307
1512
|
```
|
|
1308
1513
|
|
|
1309
1514
|
- `data` — Registration data (email, password, and any `TExtra` fields).
|
|
@@ -1328,7 +1533,8 @@ Peer dependencies:
|
|
|
1328
1533
|
|
|
1329
1534
|
- **An API with its own auth envelope keeps this client.** `requestBody` wraps the
|
|
1330
1535
|
login/register payload (`(kind, p) => ({ user: p })`), `mapAuthResponse` reads the
|
|
1331
|
-
token and user out of the answer
|
|
1536
|
+
token and user out of the answer — keeping the wire bearer OUT of the profile,
|
|
1537
|
+
because the client caches `user` in Web Storage (see the mapper example below) —
|
|
1332
1538
|
and `authScheme: 'Token'` sends `Authorization: Token <jwt>`. `mapAuthResponse` is the
|
|
1333
1539
|
ONE user-shape adapter: the client applies it to every answer that carries a user (login,
|
|
1334
1540
|
register, `validateSession()`, the cookie restore, a token refresh, `updateProfile()`), so
|
|
@@ -1349,6 +1555,16 @@ Peer dependencies:
|
|
|
1349
1555
|
- Client-side auth state is UX only — the server enforces authorization on
|
|
1350
1556
|
every request; hiding a screen is not protection.
|
|
1351
1557
|
|
|
1558
|
+
The mapper for such an envelope, with the wire token kept out of the cached
|
|
1559
|
+
profile (`d` is the parsed answer):
|
|
1560
|
+
|
|
1561
|
+
```typescript
|
|
1562
|
+
;(d) => {
|
|
1563
|
+
const { token, ...user } = d.user
|
|
1564
|
+
return { accessToken: token, user }
|
|
1565
|
+
}
|
|
1566
|
+
```
|
|
1567
|
+
|
|
1352
1568
|
## E2E Tests
|
|
1353
1569
|
|
|
1354
1570
|
Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
@@ -35,8 +35,8 @@ export interface BrowserStorageAdapterOptions {
|
|
|
35
35
|
* token AND the user in the store, `initialize()` restores the authenticated state synchronously
|
|
36
36
|
* (`whenRestored()` is already resolved on the first render); render from it, and validate the
|
|
37
37
|
* token as a background request — never in front of the first render. A page that waits on its
|
|
38
|
-
* own `/me` call pays a round trip on EVERY navigation
|
|
39
|
-
*
|
|
38
|
+
* own `/me` call pays a round trip on EVERY navigation: a 1–5 s restore window is enough to
|
|
39
|
+
* time out a navigation that expects the signed-in UI.
|
|
40
40
|
*
|
|
41
41
|
* In a browser this IS the auth client's default `AuthClientConfig.storage`, so a scaffolded
|
|
42
42
|
* app keeps its session across a page reload with no extra wiring. Outside a browser (SSR, a Node
|
package/dist/browser-storage.js
CHANGED
|
@@ -26,8 +26,8 @@ const warn = (message, error) => {
|
|
|
26
26
|
* token AND the user in the store, `initialize()` restores the authenticated state synchronously
|
|
27
27
|
* (`whenRestored()` is already resolved on the first render); render from it, and validate the
|
|
28
28
|
* token as a background request — never in front of the first render. A page that waits on its
|
|
29
|
-
* own `/me` call pays a round trip on EVERY navigation
|
|
30
|
-
*
|
|
29
|
+
* own `/me` call pays a round trip on EVERY navigation: a 1–5 s restore window is enough to
|
|
30
|
+
* time out a navigation that expects the signed-in UI.
|
|
31
31
|
*
|
|
32
32
|
* In a browser this IS the auth client's default `AuthClientConfig.storage`, so a scaffolded
|
|
33
33
|
* app keeps its session across a page reload with no extra wiring. Outside a browser (SSR, a Node
|
package/dist/client.d.ts
CHANGED
|
@@ -5,21 +5,38 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module
|
|
7
7
|
*/
|
|
8
|
-
import type { AuthClient,
|
|
8
|
+
import type { AuthClient, AuthClientConfigOptions, AuthProfileConstraint, RegisterExtraShape, UserProfile } from './types.js';
|
|
9
9
|
/**
|
|
10
10
|
* Creates a simple JWT-based auth client.
|
|
11
11
|
*
|
|
12
12
|
* This is a basic implementation that can be extended or replaced
|
|
13
13
|
* with more sophisticated auth providers.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* `T` is the profile the API returns. The default, `UserProfile`, requires
|
|
16
|
+
* `id: string`, and an untyped call — `createJWTAuthClient({ mapAuthResponse })`
|
|
17
|
+
* — KEEPS that default: the profile is never inferred from the config (no
|
|
18
|
+
* member of it, and not from an annotated config variable), so a mapper that
|
|
19
|
+
* returns a user without `id` fails to compile instead of quietly becoming the
|
|
20
|
+
* client's profile. To use the app's own profile, name it here,
|
|
21
|
+
* `createJWTAuthClient<SessionUser>(config)`; a profile with no `id`
|
|
22
|
+
* (`{ username, email, bio, image }`) is then used as-is. Pass the same type to
|
|
23
|
+
* the config (`AuthClientConfig<SessionUser>`), so `mapAuthResponse` and the
|
|
24
|
+
* write transport are checked against the profile the client and the React
|
|
25
|
+
* hooks hand to your pages.
|
|
26
|
+
*
|
|
27
|
+
* `any` is accepted (`createJWTAuthClient<any>()`) as the caller's explicit
|
|
28
|
+
* opt-out; array, function and `Date` profiles are rejected. A wrapper that
|
|
29
|
+
* forwards a profile type copies `AuthProfileConstraint` (see
|
|
30
|
+
* {@link AuthClientConfig}'s migration note).
|
|
31
|
+
*
|
|
32
|
+
* @template T - The user profile the API returns: a JSON object type of named
|
|
33
|
+
* fields (see {@link AuthProfileConstraint}), including a profile with no `id`.
|
|
34
|
+
* The default, `UserProfile`, requires `id: string`.
|
|
18
35
|
* @template TRegister - The app's own registration fields, accepted at the top
|
|
19
36
|
* level of `register()` (e.g. `{ username: string }` for a RealWorld-shaped API).
|
|
20
37
|
* An interface and a type alias are equally valid.
|
|
21
38
|
* @param config - Auth client configuration including endpoints, storage, and refresh settings.
|
|
22
39
|
* @returns A fully configured `AuthClient` instance.
|
|
23
40
|
*/
|
|
24
|
-
export declare const createJWTAuthClient: <T extends
|
|
41
|
+
export declare const createJWTAuthClient: <T extends AuthProfileConstraint<T> = UserProfile, TRegister extends RegisterExtraShape<TRegister> = Record<never, never>>(config?: NoInfer<AuthClientConfigOptions<T>>) => AuthClient<T, TRegister>;
|
|
25
42
|
//# sourceMappingURL=client.d.ts.map
|