@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/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 (xr-molecule-flash-m192: a `main.tsx` rewrite dropped the line
39
- * and 18 tests timed out on pages that never left their loading branch).
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 (xr-molecule-flash-m177: ~30 full page
66
- * loads in one pagination spec; its four tests sat at 15.0–15.9 s of a 15 s
67
- * budget and the grader timed two out). Route guards read this same `status`:
68
- * wait only while it is `'loading'` — that status means "no decision yet", so an
69
- * auth request in flight (`state.loading`) keeps the decided status and does not
70
- * unmount a Login/Register page mid-request (xr-molecule-flash-m380).
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 stayed signed out and nine browser assertions timed
116
- * out, xr-molecule-flash-m178). `setAccessToken()` and `setUser()` are field setters, not a
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` (xr-molecule-flash-m255: the mapper kept `id`/`name`/`avatar` but dropped
124
- * `username`, and the signed-in controls never rendered although login succeeded). Keep the
125
- * wire user's fields and ADD the base identity fields; do not rebuild the profile field by
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
- * const { user: wire } = data as { user: { token: string } & Omit<MyUser, 'id'> }
140
- * return {
141
- * accessToken: wire.token,
142
- * user: {
143
- * // Every wire field the pages read (`username`, `image`, …) survives…
144
- * ...wire,
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
- * A username-derived `id` can change during a profile save. It is not an immutable
159
- * session identifier: `getSessionRevision()` tracks session ownership independently
160
- * of profile fields, remains unchanged for profile updates and same-session
161
- * refreshes, and changes when a session is replaced or ended. Compare revisions
162
- * only within one client. React's `useProfileForm` uses this capability by default.
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 replacing the active user's stored profile.
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 (`(d) => ({ accessToken: d.user.token, user: d.user })`),
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
- * const realWorldAuth = createJWTAuthClient<UserProfile, { username: string }>({
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; username: string; email: string } }
279
- * return { accessToken: user.token, user: { id: user.username, email: user.email } }
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4UG;AAEH,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
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"}
@@ -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 UserProfile = UserProfile, TRegister extends RegisterExtraShape<TRegister> = Record<never, never>>() => AuthClient<T, TRegister>;
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
  *
@@ -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;AAI7E;;;;;;;;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;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,CAAC,SAAS,WAAW,GAAG,WAAW,EACnC,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"}
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.
@@ -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;;;;;;;;GAQG;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"}
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: UserProfile;
346
+ user: T;
230
347
  } | {
231
348
  type: 'logout';
232
349
  } | {
233
350
  type: 'register';
234
- user: UserProfile;
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 response. If the session is explicitly replaced or ended
413
- * while the request is pending, that response does not replace the current
414
- * session's user or persisted cache.
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
- addEventListener(listener: AuthEventListener): () => void;
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 `AuthResult<MyUser>`, so a mapper that
524
- * omits a required custom field is a type error instead of a signed-in page
525
- * reading `undefined` (xr-molecule-flash-m255). Keep every wire field the pages
526
- * read (spread the wire user and add `id`/`name`/`avatar`); mapping a field to a
527
- * different name does not preserve the original, and a `as unknown as MyUser`
528
- * assertion hides the loss. See the module docs for the wire-preserving
529
- * adapter.
530
- *
531
- * @template T - The user profile the API returns. The default,
532
- * {@link UserProfile}, keeps the untyped configuration working.
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 AuthClientConfig<T extends UserProfile = UserProfile> {
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: `(data) => ({ accessToken: data.user.token, user: data.user })`.
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 (xr-molecule-flash-m184). A mapper that
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