@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/dist/index.js
CHANGED
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
* and an explicit `initialize()` returns the same promise, so a second call asks the
|
|
36
36
|
* server nothing. Keep the template's explicit call (it makes the first render
|
|
37
37
|
* deterministic); an entry file that drops it still restores instead of reporting
|
|
38
|
-
* `'loading'` forever (
|
|
39
|
-
*
|
|
38
|
+
* `'loading'` forever (a `main.tsx` rewrite can drop the line, and every page
|
|
39
|
+
* then stays on its loading branch).
|
|
40
40
|
*
|
|
41
41
|
* **Never fetch viewer-dependent data before the restore settles.** A
|
|
42
42
|
* component effect that runs on mount fires while `initialize()` is still
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
* `initialize()` sets that state synchronously, before any request, and every
|
|
63
63
|
* verification settles behind that render. Never hold the first paint on
|
|
64
64
|
* `validateSession()` or a `/me` request of your own — a protected page then pays
|
|
65
|
-
* an API round trip on every navigation (
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
65
|
+
* an API round trip on every navigation (~30 full page loads in one pagination
|
|
66
|
+
* spec put four tests at 15.0–15.9 s of a 15 s budget, and two timed out). Route
|
|
67
|
+
* guards read this same `status`: wait only while it is `'loading'` — that
|
|
68
|
+
* status means "no decision yet", so an auth request in flight
|
|
69
|
+
* (`state.loading`) keeps the decided status and does not unmount a
|
|
70
|
+
* Login/Register page mid-request.
|
|
71
71
|
* `validateSession()` is the background re-check that produces the last two: call
|
|
72
72
|
* it after the first render — a 401 or 403 clears the token (`'unauthenticated'`),
|
|
73
73
|
* while a network failure, a 5xx, or a 404 from a wrong `currentUserEndpoint`
|
|
@@ -112,18 +112,18 @@
|
|
|
112
112
|
* user, `setSession({ accessToken, user })` stores both and flips the client to
|
|
113
113
|
* `authenticated` in one update, so every hook reading the shared client is signed in at once
|
|
114
114
|
* (a form's own sign-in code wrote the token and the user with `setAccessToken`/`setUser`,
|
|
115
|
-
* which change their field only — the UI
|
|
116
|
-
* out
|
|
115
|
+
* which change their field only — the UI stays signed out and the browser assertions that
|
|
116
|
+
* expect it time out). `setAccessToken()` and `setUser()` are field setters, not a
|
|
117
117
|
* sign-in: neither sets `authenticated`.
|
|
118
118
|
*
|
|
119
119
|
* **A custom user profile is typed once and used everywhere: `AuthClientConfig<MyUser>`,
|
|
120
120
|
* `createJWTAuthClient<MyUser>` and `useAuth<MyUser>()`.** The config's generic makes
|
|
121
121
|
* `mapAuthResponse` return `AuthResult<MyUser>`, so a mapper that omits a required custom
|
|
122
122
|
* field fails at the factory call instead of shipping a signed-in page whose navbar reads
|
|
123
|
-
* `undefined`
|
|
124
|
-
*
|
|
125
|
-
* wire user
|
|
126
|
-
* field, and never hide a mismatch with `as unknown as MyUser`:
|
|
123
|
+
* `undefined` — a mapper that keeps `id`/`name`/`avatar` but drops `username` leaves the
|
|
124
|
+
* signed-in controls unrendered although login succeeded. The
|
|
125
|
+
* wire user carries the base fields too, `id` included — spread it, do not rebuild it
|
|
126
|
+
* field by field, and never hide a mismatch with `as unknown as MyUser`:
|
|
127
127
|
*
|
|
128
128
|
* ```typescript
|
|
129
129
|
* import type { AuthClientConfig, UserProfile } from '@molecule/app-auth'
|
|
@@ -136,18 +136,12 @@
|
|
|
136
136
|
*
|
|
137
137
|
* const config: AuthClientConfig<MyUser> = {
|
|
138
138
|
* mapAuthResponse: (data) => {
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* // …plus the base fields the client derives its identity from.
|
|
146
|
-
* id: wire.username,
|
|
147
|
-
* name: wire.name ?? wire.username,
|
|
148
|
-
* avatar: wire.image,
|
|
149
|
-
* },
|
|
150
|
-
* }
|
|
139
|
+
* // The wire is the app's profile plus the bearer; `id` is the server's.
|
|
140
|
+
* const { user: wire } = data as { user: { token: string } & MyUser }
|
|
141
|
+
* // The bearer stays OUT of the profile: the client caches `user` in Web
|
|
142
|
+
* // Storage, where a token copy is readable by any script on the origin.
|
|
143
|
+
* const { token, ...profile } = wire
|
|
144
|
+
* return { accessToken: token, user: profile }
|
|
151
145
|
* },
|
|
152
146
|
* }
|
|
153
147
|
*
|
|
@@ -155,11 +149,55 @@
|
|
|
155
149
|
* // React: `useAuth<MyUser>()` reads the same profile.
|
|
156
150
|
* ```
|
|
157
151
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
152
|
+
* That `id` is the API's account identifier. Do not derive one from a display field
|
|
153
|
+
* (`id: wire.username`) to borrow `UserProfile` for an API that has no id: the field
|
|
154
|
+
* changes, the fabricated id changes with it, and the value then reaches storage,
|
|
155
|
+
* `getUser()` and every page as if the server had said it.
|
|
156
|
+
*
|
|
157
|
+
* A profile does not have to extend `UserProfile` and does not need an `id`: the client
|
|
158
|
+
* stores and patches the object it is given and reads no field of it. Name the API's own
|
|
159
|
+
* shape, pass it to the factory and the config, and have the mapper return it complete —
|
|
160
|
+
* every configuration adapter is typed by the same `T` (`mapAuthResponse`, the
|
|
161
|
+
* `profileEndpoint`/`profileUpdateMethod`/`profileRequestBody` write transport,
|
|
162
|
+
* `shouldRestoreUser`, and `setSession`):
|
|
163
|
+
*
|
|
164
|
+
* ```typescript
|
|
165
|
+
* import type { AuthClientConfig } from '@molecule/app-auth'
|
|
166
|
+
* import { createJWTAuthClient } from '@molecule/app-auth'
|
|
167
|
+
*
|
|
168
|
+
* interface SessionUser {
|
|
169
|
+
* username: string
|
|
170
|
+
* email: string
|
|
171
|
+
* bio: string
|
|
172
|
+
* image: string
|
|
173
|
+
* }
|
|
174
|
+
*
|
|
175
|
+
* const config: AuthClientConfig<SessionUser> = {
|
|
176
|
+
* mapAuthResponse: (data) => {
|
|
177
|
+
* const { user: wire } = data as { user: { token: string } & SessionUser }
|
|
178
|
+
* // The bearer stays OUT of the profile: the client caches `user` in Web
|
|
179
|
+
* // Storage, where a token copy is readable by any script on the origin.
|
|
180
|
+
* const { token, ...profile } = wire
|
|
181
|
+
* return { accessToken: token, user: profile }
|
|
182
|
+
* },
|
|
183
|
+
* profileEndpoint: (user) => '/people/' + encodeURIComponent(user.username),
|
|
184
|
+
* }
|
|
185
|
+
*
|
|
186
|
+
* const client = createJWTAuthClient<SessionUser>(config)
|
|
187
|
+
* // `client.getUser()` has `bio`/`image`; there is no `id` to fabricate.
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* The profile is named at the factory, never inferred from the mapper: an untyped
|
|
191
|
+
* `createJWTAuthClient({ mapAuthResponse })` stays `UserProfile`, whose required
|
|
192
|
+
* `id: string` is the default configuration's contract. An array, a function or a
|
|
193
|
+
* `Date` is not a profile the client can persist — see `AuthProfileShape`.
|
|
194
|
+
*
|
|
195
|
+
* A profile field can change during a profile save — a handle, or a username an app
|
|
196
|
+
* once derived an `id` from. It is not an immutable session identifier:
|
|
197
|
+
* `getSessionRevision()` tracks session ownership independently of profile fields,
|
|
198
|
+
* remains unchanged for profile updates and same-session refreshes, and changes when
|
|
199
|
+
* a session is replaced or ended. Compare revisions only within one client. React's
|
|
200
|
+
* `useProfileForm` uses this capability by default.
|
|
163
201
|
*
|
|
164
202
|
* **A profile-update transport that is not `PATCH` with bare JSON is a CONFIG field, not a
|
|
165
203
|
* hand-written client.** `updateProfile()` sends to `profileEndpoint` with `PATCH` and the
|
|
@@ -194,7 +232,11 @@
|
|
|
194
232
|
* coherent, and hand-written adapters have had to repair those callbacks repeatedly.
|
|
195
233
|
* A pending profile response is published only to the session that sent it:
|
|
196
234
|
* signing out or explicitly adopting/signing in another session prevents the old
|
|
197
|
-
* response from
|
|
235
|
+
* response from changing the active user's stored profile. The answer is applied onto the
|
|
236
|
+
* stored profile field by field: a field it carries replaces the stored value (`null` clears
|
|
237
|
+
* it) and one it leaves out — or that `mapAuthResponse` leaves `undefined` — is kept, so a
|
|
238
|
+
* field the API clears by leaving it out must reach the client as `null`, answered by the API
|
|
239
|
+
* or mapped by `mapAuthResponse`.
|
|
198
240
|
*
|
|
199
241
|
* **A background verification that returns an unchanged user changes nothing.** When
|
|
200
242
|
* `validateSession()` (or a reload's restore) answers with the same profile data the client
|
|
@@ -227,7 +269,8 @@
|
|
|
227
269
|
* @remarks
|
|
228
270
|
* - **An API with its own auth envelope keeps this client.** `requestBody` wraps the
|
|
229
271
|
* login/register payload (`(kind, p) => ({ user: p })`), `mapAuthResponse` reads the
|
|
230
|
-
* token and user out of the answer
|
|
272
|
+
* token and user out of the answer — keeping the wire bearer OUT of the profile,
|
|
273
|
+
* because the client caches `user` in Web Storage (see the mapper example below) —
|
|
231
274
|
* and `authScheme: 'Token'` sends `Authorization: Token <jwt>`. `mapAuthResponse` is the
|
|
232
275
|
* ONE user-shape adapter: the client applies it to every answer that carries a user (login,
|
|
233
276
|
* register, `validateSession()`, the cookie restore, a token refresh, `updateProfile()`), so
|
|
@@ -248,6 +291,16 @@
|
|
|
248
291
|
* - Client-side auth state is UX only — the server enforces authorization on
|
|
249
292
|
* every request; hiding a screen is not protection.
|
|
250
293
|
*
|
|
294
|
+
* The mapper for such an envelope, with the wire token kept out of the cached
|
|
295
|
+
* profile (`d` is the parsed answer):
|
|
296
|
+
*
|
|
297
|
+
* ```typescript
|
|
298
|
+
* (d) => {
|
|
299
|
+
* const { token, ...user } = d.user
|
|
300
|
+
* return { accessToken: token, user }
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*
|
|
251
304
|
* @example
|
|
252
305
|
* ```typescript
|
|
253
306
|
* import { createJWTAuthClient, setClient } from '@molecule/app-auth'
|
|
@@ -267,16 +320,26 @@
|
|
|
267
320
|
* the form passed — `{ user: { username, email, password } }` goes out as-is.
|
|
268
321
|
*
|
|
269
322
|
* ```typescript
|
|
270
|
-
* import type { UserProfile } from '@molecule/app-auth'
|
|
271
323
|
* import { createJWTAuthClient } from '@molecule/app-auth'
|
|
272
324
|
*
|
|
273
|
-
*
|
|
325
|
+
* // RealWorld's user has no `id`. Name the shape the wire actually sends; the
|
|
326
|
+
* // client reads no field of it, so nothing has to be invented for a base profile.
|
|
327
|
+
* interface RealWorldUser {
|
|
328
|
+
* username: string
|
|
329
|
+
* email: string
|
|
330
|
+
* bio: string
|
|
331
|
+
* image: string
|
|
332
|
+
* }
|
|
333
|
+
*
|
|
334
|
+
* const realWorldAuth = createJWTAuthClient<RealWorldUser, { username: string }>({
|
|
274
335
|
* baseURL: '/api',
|
|
275
336
|
* authScheme: 'Token',
|
|
276
337
|
* requestBody: (_kind, payload) => ({ user: payload }),
|
|
277
338
|
* mapAuthResponse: (data) => {
|
|
278
|
-
* const { user } = data as { user: { token: string
|
|
279
|
-
*
|
|
339
|
+
* const { user: wire } = data as { user: { token: string } & RealWorldUser }
|
|
340
|
+
* // Keep every wire field; the bearer stays out of the cached profile.
|
|
341
|
+
* const { token, ...profile } = wire
|
|
342
|
+
* return { accessToken: token, user: profile }
|
|
280
343
|
* },
|
|
281
344
|
* })
|
|
282
345
|
*
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2YG;AAEH,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
package/dist/provider.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
-
import type { AuthClient, RegisterExtraShape, UserProfile } from './types.js';
|
|
10
|
+
import type { AuthClient, AuthProfileConstraint, RegisterExtraShape, UserProfile } from './types.js';
|
|
11
11
|
/**
|
|
12
12
|
* Registers an auth client as the active singleton. Called by bond
|
|
13
13
|
* packages during application startup.
|
|
@@ -21,13 +21,16 @@ export declare const setClient: <T = UserProfile, TRegister extends RegisterExtr
|
|
|
21
21
|
/**
|
|
22
22
|
* Retrieves the bonded auth client, throwing if none is configured.
|
|
23
23
|
*
|
|
24
|
-
* @template T - The user profile the bonded client returns
|
|
24
|
+
* @template T - The user profile the bonded client returns: a JSON object type
|
|
25
|
+
* of named fields (see {@link AuthProfileConstraint}), including an app
|
|
26
|
+
* profile with no `id`. Name it at the call (`getClient<SessionUser>()`); it
|
|
27
|
+
* is never inferred from a config.
|
|
25
28
|
* @template TRegister - The app's own registration fields, at the top level
|
|
26
29
|
* of `register()` (see `RegisterData`).
|
|
27
30
|
* @returns The bonded auth client.
|
|
28
31
|
* @throws {Error} If no auth client has been bonded.
|
|
29
32
|
*/
|
|
30
|
-
export declare const getClient: <T extends
|
|
33
|
+
export declare const getClient: <T extends AuthProfileConstraint<T> = UserProfile, TRegister extends RegisterExtraShape<TRegister> = Record<never, never>>() => AuthClient<T, TRegister>;
|
|
31
34
|
/**
|
|
32
35
|
* Checks whether an auth client is currently bonded.
|
|
33
36
|
*
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAIpG;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,CAAC,GAAG,WAAW,EACf,SAAS,SAAS,kBAAkB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAEtE,QAAQ,UAAU,CAAC,CAAC,EAAE,SAAS,CAAC,KAC/B,IAEF,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,GACpB,CAAC,SAAS,qBAAqB,CAAC,CAAC,CAAC,GAAG,WAAW,EAChD,SAAS,SAAS,kBAAkB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,OACnE,UAAU,CAAC,CAAC,EAAE,SAAS,CAE3B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,QAAO,OAE5B,CAAA"}
|
package/dist/provider.js
CHANGED
|
@@ -24,7 +24,10 @@ export const setClient = (client) => {
|
|
|
24
24
|
/**
|
|
25
25
|
* Retrieves the bonded auth client, throwing if none is configured.
|
|
26
26
|
*
|
|
27
|
-
* @template T - The user profile the bonded client returns
|
|
27
|
+
* @template T - The user profile the bonded client returns: a JSON object type
|
|
28
|
+
* of named fields (see {@link AuthProfileConstraint}), including an app
|
|
29
|
+
* profile with no `id`. Name it at the call (`getClient<SessionUser>()`); it
|
|
30
|
+
* is never inferred from a config.
|
|
28
31
|
* @template TRegister - The app's own registration fields, at the top level
|
|
29
32
|
* of `register()` (see `RegisterData`).
|
|
30
33
|
* @returns The bonded auth client.
|
package/dist/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAI3E,MAAM,SAAS,GAAG,aAAa,CAAA;AAE/B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAIvB,MAAgC,EAC1B,EAAE;IACR,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;AACzB,CAAC,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAI3E,MAAM,SAAS,GAAG,aAAa,CAAA;AAE/B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAIvB,MAAgC,EAC1B,EAAE;IACR,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;AACzB,CAAC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,GAGK,EAAE;IAC9B,OAAO,WAAW,CAA2B,SAAS,CAAC,CAAA;AACzD,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,GAAY,EAAE;IACrC,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;AAC5B,CAAC,CAAA"}
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,117 @@ export interface UserProfile {
|
|
|
33
33
|
*/
|
|
34
34
|
metadata?: Record<string, unknown>;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The sentence every invalid profile type is rejected with.
|
|
38
|
+
*/
|
|
39
|
+
type AuthProfileRejection = 'An auth profile must be a JSON object type of named fields, not an array, a function or a Date';
|
|
40
|
+
/**
|
|
41
|
+
* The profile shapes the auth client accepts: a JSON object type of named
|
|
42
|
+
* fields, and nothing else.
|
|
43
|
+
*
|
|
44
|
+
* The client does not read a field of the profile, but it does persist it:
|
|
45
|
+
* `tokenStorage.setUser` writes it with `JSON.stringify` and `getUser` restores
|
|
46
|
+
* it with `JSON.parse`, and `updateProfile(data: Partial<T>)` patches it
|
|
47
|
+
* field-wise. An array has no named fields to read or patch, `JSON.stringify`
|
|
48
|
+
* drops a function entirely, and a `Date` comes back from `JSON.parse` as a
|
|
49
|
+
* string while still typed as a `Date` — so those three types are rejected on
|
|
50
|
+
* every entrypoint that takes a profile type (`createJWTAuthClient`,
|
|
51
|
+
* the default-auth factories, `AuthClientConfig`).
|
|
52
|
+
*
|
|
53
|
+
* The check is on the TOP-LEVEL shape only: a class instance, or a `Date` in a
|
|
54
|
+
* nested field, is the app's own persistence contract, not this validator's.
|
|
55
|
+
* A named `interface`, a type literal, an intersection and the empty object
|
|
56
|
+
* type all satisfy it — `{}` compiles because it is a JSON object that
|
|
57
|
+
* round-trips unchanged, not an invalid form.
|
|
58
|
+
*
|
|
59
|
+
* A union is checked member by member: `ExistingUser | ExistingUser[]` contains
|
|
60
|
+
* an array, so it is not an object-only profile either, and the shared
|
|
61
|
+
* rejection test (`AuthProfileIsRejected`, used by
|
|
62
|
+
* {@link AuthProfileConstraint} and {@link AuthProfileGuard}) rejects it as a
|
|
63
|
+
* whole.
|
|
64
|
+
*
|
|
65
|
+
* @template T - The candidate profile type.
|
|
66
|
+
*/
|
|
67
|
+
export type AuthProfileShape<T> = T extends readonly unknown[] | ((...args: never[]) => unknown) | Date ? AuthProfileRejection : object;
|
|
68
|
+
/**
|
|
69
|
+
* Whether `T` is, or is a union containing, a shape the client cannot persist —
|
|
70
|
+
* an array, a function or a `Date` ({@link AuthProfileShape}).
|
|
71
|
+
*
|
|
72
|
+
* This is the ONE decision {@link AuthProfileConstraint} and
|
|
73
|
+
* {@link AuthProfileGuard} share, so the entrypoints and `AuthClientConfig`
|
|
74
|
+
* cannot drift apart.
|
|
75
|
+
*
|
|
76
|
+
* Three TypeScript behaviors shape it:
|
|
77
|
+
*
|
|
78
|
+
* - **No distribution.** `AuthProfileShape<T>` already distributes, so a union
|
|
79
|
+
* `Good | Bad` yields `object | AuthProfileRejection`. Wrapping BOTH sides of
|
|
80
|
+
* the test in tuples compares those unions as a whole, so one invalid member
|
|
81
|
+
* rejects the whole union. The plain `AuthProfileShape<T> extends
|
|
82
|
+
* AuthProfileRejection` form instead asks whether the ENTIRE union is the
|
|
83
|
+
* rejection, which a valid sibling makes false — that is the union hole this
|
|
84
|
+
* closes.
|
|
85
|
+
* - **`any` is checked first.** `any` is the caller's explicit opt-out of
|
|
86
|
+
* profile typing, not an invalid member, but `AuthProfileShape<any>` resolves
|
|
87
|
+
* to `AuthProfileRejection | object`, which the tuple test reads as "contains
|
|
88
|
+
* a rejection". `0 extends 1 & T` detects `any` (and only `any` — `never`,
|
|
89
|
+
* `unknown` and `object` are all false) and takes the accepted branch.
|
|
90
|
+
* - **Deferred at the outer edge.** `[T] extends [unknown]` keeps the whole
|
|
91
|
+
* check deferred while `T` is an unresolved type parameter (it is always true
|
|
92
|
+
* for a concrete type, and does not distribute over a union). Without it,
|
|
93
|
+
* TypeScript resolves the inner `0 extends 1 & T` while checking a forwarder's
|
|
94
|
+
* constraint `T extends AuthProfileConstraint<T>` and reports TS2313 ("Type
|
|
95
|
+
* parameter 'T' has a circular constraint"); with it, the constraint checks at
|
|
96
|
+
* the call site.
|
|
97
|
+
*
|
|
98
|
+
* `never`, `unknown` and the primitives are not this helper's job: each
|
|
99
|
+
* entrypoint's `extends object` requirement rejects them, unchanged.
|
|
100
|
+
*
|
|
101
|
+
* @template T - The candidate profile type.
|
|
102
|
+
*/
|
|
103
|
+
type AuthProfileIsRejected<T> = [T] extends [unknown] ? 0 extends 1 & T ? false : [AuthProfileRejection] extends [AuthProfileShape<T>] ? true : false : never;
|
|
104
|
+
/**
|
|
105
|
+
* Requires a profile type to satisfy {@link AuthProfileShape}: an invalid
|
|
106
|
+
* profile makes {@link AuthClientConfig} impossible to populate, so the
|
|
107
|
+
* annotation itself is the error. It shares the entrypoints' rejection test,
|
|
108
|
+
* so a union with an invalid member is rejected here too.
|
|
109
|
+
*
|
|
110
|
+
* @template T - The candidate profile type.
|
|
111
|
+
*/
|
|
112
|
+
export type AuthProfileGuard<T> = AuthProfileIsRejected<T> extends true ? {
|
|
113
|
+
readonly [K in AuthProfileRejection]: never;
|
|
114
|
+
} : Record<never, never>;
|
|
115
|
+
/**
|
|
116
|
+
* The constraint every entrypoint puts on a profile type: a JSON object type of
|
|
117
|
+
* named fields ({@link AuthProfileShape}), and `object` for `any`.
|
|
118
|
+
*
|
|
119
|
+
* `any` is the caller's explicit opt-out of profile typing, not an invalid JSON
|
|
120
|
+
* shape: `createJWTAuthClient<any>()`, `getClient<any>()` and
|
|
121
|
+
* `AuthClientConfig<any>` name it deliberately. It is detected explicitly
|
|
122
|
+
* (`0 extends 1 & T`) and accepted. Every other type is measured against
|
|
123
|
+
* {@link AuthProfileShape}, member by member when it is a union: a union that
|
|
124
|
+
* holds an array, a function or a `Date` is rejected as a whole, so
|
|
125
|
+
* `createJWTAuthClient<ExistingUser | ExistingUser[]>()` is the same type error
|
|
126
|
+
* as `createJWTAuthClient<ExistingUser[]>()`.
|
|
127
|
+
*
|
|
128
|
+
* Forwarding a profile through your own generic copies this constraint, so the
|
|
129
|
+
* profile keeps being checked:
|
|
130
|
+
*
|
|
131
|
+
* ```typescript
|
|
132
|
+
* import type { AuthClientConfig, AuthProfileConstraint } from '@molecule/app-auth'
|
|
133
|
+
* import { createJWTAuthClient } from '@molecule/app-auth'
|
|
134
|
+
*
|
|
135
|
+
* export function makeAuth<T extends AuthProfileConstraint<T>>(config: AuthClientConfig<T>) {
|
|
136
|
+
* return createJWTAuthClient<T>(config)
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* `T extends object` is not enough: the factory rejects arrays, functions and
|
|
141
|
+
* `Date` at its own constraint, so a forwarder written that way does not
|
|
142
|
+
* compile. Copy this type instead.
|
|
143
|
+
*
|
|
144
|
+
* @template T - The candidate profile type.
|
|
145
|
+
*/
|
|
146
|
+
export type AuthProfileConstraint<T> = AuthProfileIsRejected<T> extends true ? never : object;
|
|
36
147
|
/**
|
|
37
148
|
* Email/password login credentials with optional "remember me" flag.
|
|
38
149
|
*/
|
|
@@ -223,15 +334,21 @@ export interface AuthState<T = UserProfile> {
|
|
|
223
334
|
}
|
|
224
335
|
/**
|
|
225
336
|
* Auth event types.
|
|
337
|
+
*
|
|
338
|
+
* A sign-in event carries the client's own profile type, so a listener on a
|
|
339
|
+
* client typed with the app's profile reads that profile — including one that
|
|
340
|
+
* does not extend {@link UserProfile}. The default is {@link UserProfile}.
|
|
341
|
+
*
|
|
342
|
+
* @template T - The user profile the API returns.
|
|
226
343
|
*/
|
|
227
|
-
export type AuthEvent = {
|
|
344
|
+
export type AuthEvent<T = UserProfile> = {
|
|
228
345
|
type: 'login';
|
|
229
|
-
user:
|
|
346
|
+
user: T;
|
|
230
347
|
} | {
|
|
231
348
|
type: 'logout';
|
|
232
349
|
} | {
|
|
233
350
|
type: 'register';
|
|
234
|
-
user:
|
|
351
|
+
user: T;
|
|
235
352
|
} | {
|
|
236
353
|
type: 'refresh';
|
|
237
354
|
} | {
|
|
@@ -240,8 +357,30 @@ export type AuthEvent = {
|
|
|
240
357
|
};
|
|
241
358
|
/**
|
|
242
359
|
* Callback invoked when an authentication event occurs (login, logout, etc.).
|
|
360
|
+
*
|
|
361
|
+
* A listener is typed by the client's profile: `AuthEventListener<MyUser>`
|
|
362
|
+
* receives `AuthEvent<MyUser>`, so a sign-in event's `user` is the app's own
|
|
363
|
+
* shape — including a profile that does not extend {@link UserProfile}. The
|
|
364
|
+
* default is {@link UserProfile}.
|
|
365
|
+
*
|
|
366
|
+
* An inline arrow is contextually typed, so its `event` parameter needs no
|
|
367
|
+
* annotation. Because listener parameters are contravariant, a client whose
|
|
368
|
+
* profile extends {@link UserProfile} also accepts a listener written against
|
|
369
|
+
* the default `AuthEventListener` (those events carry every `UserProfile`
|
|
370
|
+
* field). A client whose profile does NOT extend `UserProfile` — an API user
|
|
371
|
+
* with no `id` — accepts only `AuthEventListener<T>`: a default listener would
|
|
372
|
+
* read an `id` the runtime user does not have.
|
|
373
|
+
*
|
|
374
|
+
* **Migration to 2.0.** External providers that implement {@link AuthClient}
|
|
375
|
+
* declare their listener parameter as `AuthEventListener<T>`
|
|
376
|
+
* (`addEventListener(listener: AuthEventListener<T>): () => void`). The
|
|
377
|
+
* previous `addEventListener(listener: AuthEventListener)` declaration, which
|
|
378
|
+
* claimed every client's events carry a `UserProfile`, is no longer assignable
|
|
379
|
+
* once the client's profile is a custom one.
|
|
380
|
+
*
|
|
381
|
+
* @template T - The user profile the API returns.
|
|
243
382
|
*/
|
|
244
|
-
export type AuthEventListener = (event: AuthEvent) => void;
|
|
383
|
+
export type AuthEventListener<T = UserProfile> = (event: AuthEvent<T>) => void;
|
|
245
384
|
/**
|
|
246
385
|
* The error an auth client rejects with when the API answers a non-ok HTTP
|
|
247
386
|
* response. It is the {@link I18nError} the client throws — same `i18nKey`,
|
|
@@ -409,9 +548,12 @@ export interface AuthClient<T = UserProfile, TRegister extends RegisterExtraShap
|
|
|
409
548
|
confirmPasswordReset(data: PasswordResetConfirm): Promise<void>;
|
|
410
549
|
/**
|
|
411
550
|
* Updates the current user's profile.
|
|
412
|
-
* Returns the saved
|
|
413
|
-
*
|
|
414
|
-
*
|
|
551
|
+
* Returns the saved profile: the answer applied onto the signed-in user field
|
|
552
|
+
* by field, with or without an `id` — a field it carries replaces the value
|
|
553
|
+
* (`null` clears it), one it omits or leaves `undefined` is kept, so a field
|
|
554
|
+
* cleared by omission must arrive as `null`. A non-object answer is returned
|
|
555
|
+
* as it came. An answer for a replaced or ended session leaves the current one
|
|
556
|
+
* untouched.
|
|
415
557
|
*/
|
|
416
558
|
updateProfile(data: Partial<T>): Promise<T>;
|
|
417
559
|
/**
|
|
@@ -498,8 +640,22 @@ export interface AuthClient<T = UserProfile, TRegister extends RegisterExtraShap
|
|
|
498
640
|
getToken?(): string | null;
|
|
499
641
|
/**
|
|
500
642
|
* Adds an auth event listener.
|
|
501
|
-
|
|
502
|
-
|
|
643
|
+
*
|
|
644
|
+
* The listener is typed by this client's profile: one signature,
|
|
645
|
+
* `AuthEventListener<T>`, so an inline arrow is contextually typed and `event`
|
|
646
|
+
* is never implicitly `any`. A client whose profile extends
|
|
647
|
+
* {@link UserProfile} also accepts a default {@link AuthEventListener}: when
|
|
648
|
+
* `T` extends `UserProfile`, `AuthEvent<T>` is assignable to
|
|
649
|
+
* `AuthEvent<UserProfile>`, so a listener that takes the wider default event
|
|
650
|
+
* accepts every event this client emits and keeps compiling. (`UserProfile`
|
|
651
|
+
* is the wider type here — it has fewer required fields — and the assignable
|
|
652
|
+
* direction runs from the client's `AuthEvent<T>` to the listener's
|
|
653
|
+
* `AuthEvent<UserProfile>`.) A profile that does not extend `UserProfile`
|
|
654
|
+
* (no `id`) rejects the default listener: an `AuthEvent<UserProfile>` would
|
|
655
|
+
* promise an `id` the runtime user does not have, and the narrower-typed
|
|
656
|
+
* event is not assignable to it.
|
|
657
|
+
*/
|
|
658
|
+
addEventListener(listener: AuthEventListener<T>): () => void;
|
|
503
659
|
/**
|
|
504
660
|
* Destroys the auth client.
|
|
505
661
|
*/
|
|
@@ -520,18 +676,65 @@ export interface StorageAdapter {
|
|
|
520
676
|
*
|
|
521
677
|
* Type it with the app's user — `AuthClientConfig<MyUser>` — the same type
|
|
522
678
|
* `createJWTAuthClient<MyUser>` returns and `useAuth<MyUser>()` reads. That
|
|
523
|
-
* makes {@link mapAuthResponse} return
|
|
524
|
-
* omits a required custom field is a type
|
|
525
|
-
* reading `undefined
|
|
526
|
-
* read
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
679
|
+
* makes {@link AuthClientConfigOptions.mapAuthResponse} return
|
|
680
|
+
* `AuthResult<MyUser>`, so a mapper that omits a required custom field is a type
|
|
681
|
+
* error instead of a signed-in page reading `undefined`. Keep every wire field the pages
|
|
682
|
+
* read by spreading the
|
|
683
|
+
* wire user — the base fields (`id`, `name`, `avatar`) come from the server like
|
|
684
|
+
* any other, never re-derived from a mutable display field such as a username;
|
|
685
|
+
* mapping a field to a different name does not preserve the original, and an
|
|
686
|
+
* `as unknown as MyUser` assertion hides the loss. See the module docs for the
|
|
687
|
+
* wire-preserving adapter.
|
|
688
|
+
*
|
|
689
|
+
* `T` is the app's OWN profile shape and need not extend {@link UserProfile}:
|
|
690
|
+
* the client stores and patches the object it is given and reads no field of
|
|
691
|
+
* it, so an API whose user has no `id` — `{ username, email, bio, image }` —
|
|
692
|
+
* is typed by its own `interface` directly, with no fabricated `id`. The
|
|
693
|
+
* default, {@link UserProfile}, still requires `id: string`. A primitive,
|
|
694
|
+
* `null`, an array, a function or a `Date` — or a union containing one — is
|
|
695
|
+
* rejected; see {@link AuthProfileShape} for the persistence contract behind
|
|
696
|
+
* that list.
|
|
697
|
+
*
|
|
698
|
+
* The type argument is NEVER inferred from the config: not from
|
|
699
|
+
* `mapAuthResponse`, `profileEndpoint`, `profileRequestBody` or
|
|
700
|
+
* `shouldRestoreUser`, and not from a config variable the caller annotated as
|
|
701
|
+
* `AuthClientConfig<MyUser>`. An untyped call stays typed `UserProfile`, so the
|
|
702
|
+
* default configuration keeps requiring `id: string`. Name the profile at the
|
|
703
|
+
* call (`createJWTAuthClient<MyUser>(config)`) — which also checks this config
|
|
704
|
+
* against `MyUser`.
|
|
705
|
+
*
|
|
706
|
+
* **Callers upgrading from 1.8 — three shapes now need the profile at the call.**
|
|
707
|
+
* Each was accepted by 1.8 and is a type error here; each has a one-line fix:
|
|
708
|
+
*
|
|
709
|
+
* 1. A mapper that supplied the profile
|
|
710
|
+
* (`createJWTAuthClient({ mapAuthResponse: (d): AuthResult<MyUser> => … })`)
|
|
711
|
+
* is now typed `UserProfile`. Name it on the factory:
|
|
712
|
+
* `createJWTAuthClient<MyUser>({ mapAuthResponse })`.
|
|
713
|
+
* 2. A config variable annotated `AuthClientConfig<MyUser>` with a profile that
|
|
714
|
+
* extends `UserProfile` used to supply the type argument. Pass it:
|
|
715
|
+
* `createJWTAuthClient<MyUser>(config)`. (A no-id profile was already
|
|
716
|
+
* rejected by 1.8's `T extends UserProfile`.)
|
|
717
|
+
* 3. Generic forwarding written `function make<T extends UserProfile>(config: AuthClientConfig<T>)`
|
|
718
|
+
* no longer satisfies the factory's constraint. Copy the named constraint:
|
|
719
|
+
* `function make<T extends AuthProfileConstraint<T>>(config: AuthClientConfig<T>)`.
|
|
720
|
+
*
|
|
721
|
+
* `any` is accepted at every entrypoint (`createJWTAuthClient<any>()`,
|
|
722
|
+
* `getClient<any>()`), so a caller that deliberately opted out keeps compiling.
|
|
723
|
+
*
|
|
724
|
+
* @template T - The user profile the API returns: a JSON object type of named
|
|
725
|
+
* fields. The default, {@link UserProfile}, keeps the untyped configuration
|
|
726
|
+
* working.
|
|
727
|
+
*/
|
|
728
|
+
export type AuthClientConfig<T extends object = UserProfile> = AuthClientConfigOptions<T> & AuthProfileGuard<T>;
|
|
729
|
+
/**
|
|
730
|
+
* Every option {@link AuthClientConfig} accepts, typed by the profile.
|
|
731
|
+
*
|
|
732
|
+
* Use `AuthClientConfig<T>` rather than this type: it adds the profile-shape
|
|
733
|
+
* check. This interface exists so each member's documentation is addressable.
|
|
734
|
+
*
|
|
735
|
+
* @template T - The user profile the API returns.
|
|
533
736
|
*/
|
|
534
|
-
export interface
|
|
737
|
+
export interface AuthClientConfigOptions<T extends object = UserProfile> {
|
|
535
738
|
/**
|
|
536
739
|
* API base URL.
|
|
537
740
|
*/
|
|
@@ -556,6 +759,8 @@ export interface AuthClientConfig<T extends UserProfile = UserProfile> {
|
|
|
556
759
|
* The write route used by `updateProfile`, independently of `currentUserEndpoint`.
|
|
557
760
|
* Defaults to `/auth/profile`. A resource route with an account id can resolve
|
|
558
761
|
* from the signed-in user, e.g. `(user) => '/accounts/' + encodeURIComponent(user.id)`.
|
|
762
|
+
* A profile with no `id` resolves from a field it does have, e.g.
|
|
763
|
+
* `(user) => '/people/' + encodeURIComponent(user.username)`.
|
|
559
764
|
* The resolver sees the current user before the patch, never caller-submitted identity.
|
|
560
765
|
*/
|
|
561
766
|
profileEndpoint?: string | ((user: Readonly<T>) => string);
|
|
@@ -634,7 +839,16 @@ export interface AuthClientConfig<T extends UserProfile = UserProfile> {
|
|
|
634
839
|
/**
|
|
635
840
|
* Turn an answer that carries a user into the client's
|
|
636
841
|
* `{ accessToken, refreshToken?, user }` (default: the answer as given).
|
|
637
|
-
* RealWorld, for example
|
|
842
|
+
* RealWorld, for example — keep the wire bearer OUT of the profile, because
|
|
843
|
+
* the client caches `user` in Web Storage where a token copy is readable by
|
|
844
|
+
* any script on the origin:
|
|
845
|
+
*
|
|
846
|
+
* ```typescript
|
|
847
|
+
* (data) => {
|
|
848
|
+
* const { token, ...user } = data.user
|
|
849
|
+
* return { accessToken: token, user }
|
|
850
|
+
* }
|
|
851
|
+
* ```
|
|
638
852
|
*
|
|
639
853
|
* It is the ONE user-shape adapter, and the client applies it on EVERY path
|
|
640
854
|
* that adopts a user from an API answer — `login()`, `register()`,
|
|
@@ -642,17 +856,26 @@ export interface AuthClientConfig<T extends UserProfile = UserProfile> {
|
|
|
642
856
|
* token-refresh answer, and `updateProfile()`. Those answers carry the raw wire
|
|
643
857
|
* user (a current-user route typically answers a bare `{ user }`), so applying
|
|
644
858
|
* the same mapping is what keeps the adapted profile after a reload instead of
|
|
645
|
-
* replacing it with the unadapted one
|
|
859
|
+
* replacing it with the unadapted one. A mapper that
|
|
646
860
|
* cannot read a given answer (it throws, or returns no `user`) leaves that
|
|
647
861
|
* answer's user as it came.
|
|
648
862
|
*
|
|
649
863
|
* It is NOT applied to `setSession()` or `setUser()`: those receive a profile
|
|
650
864
|
* the caller already built in this client's shape, not a wire answer.
|
|
651
865
|
*
|
|
866
|
+
* The return type does NOT infer the profile: an untyped
|
|
867
|
+
* `createJWTAuthClient({ mapAuthResponse })` stays `UserProfile`, so a mapper
|
|
868
|
+
* that returns a no-id profile fails to compile there instead of silently
|
|
869
|
+
* replacing the default with whatever the mapper happened to return. The same
|
|
870
|
+
* rule holds for every config member below (`profileEndpoint`,
|
|
871
|
+
* `profileRequestBody`, `shouldRestoreUser`): none of them supplies the
|
|
872
|
+
* profile. Name the profile at the call
|
|
873
|
+
* (`createJWTAuthClient<MyUser>({ mapAuthResponse })`) to use a custom one.
|
|
874
|
+
*
|
|
652
875
|
* @param data - The parsed response body.
|
|
653
876
|
* @returns The auth result.
|
|
654
877
|
*/
|
|
655
|
-
mapAuthResponse?: (data: unknown) => AuthResult<T
|
|
878
|
+
mapAuthResponse?: (data: unknown) => AuthResult<NoInfer<T>>;
|
|
656
879
|
/**
|
|
657
880
|
* Where the access/refresh token and the cached user are kept — the field that
|
|
658
881
|
* decides whether a page reload keeps the user signed in.
|
|
@@ -760,4 +983,5 @@ export interface TokenStorage {
|
|
|
760
983
|
setUser<T = UserProfile>(user: T | null): void;
|
|
761
984
|
clear(): void;
|
|
762
985
|
}
|
|
986
|
+
export {};
|
|
763
987
|
//# sourceMappingURL=types.d.ts.map
|