@molecule/app-auth 1.5.2 → 1.5.4
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 +170 -97
- package/dist/browser-storage.d.ts +11 -9
- package/dist/browser-storage.d.ts.map +1 -1
- package/dist/browser-storage.js +11 -9
- package/dist/browser-storage.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +288 -48
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +42 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -11
- package/dist/index.js.map +1 -1
- package/dist/token.d.ts +2 -1
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +2 -1
- package/dist/token.js.map +1 -1
- package/dist/types.d.ts +113 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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-18T20:35:23.952Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/app-auth
|
|
@@ -55,9 +55,19 @@ mirrors the token into the bonded client.
|
|
|
55
55
|
**`getState().status` is the session state to render, in one value.**
|
|
56
56
|
`'loading'` (no decision yet) → `'authenticated'` / `'unauthenticated'`, and
|
|
57
57
|
`'unavailable'` when a stored session exists but the server could not confirm it.
|
|
58
|
+
**A stored token AND a stored user make the FIRST render `'authenticated'`**:
|
|
59
|
+
`initialize()` sets that state synchronously, before any request, and every
|
|
60
|
+
verification settles behind that render. Never hold the first paint on
|
|
61
|
+
`validateSession()` or a `/me` request of your own — a protected page then pays
|
|
62
|
+
an API round trip on every navigation (xr-molecule-flash-m177: ~30 full page
|
|
63
|
+
loads in one pagination spec; its four tests sat at 15.0–15.9 s of a 15 s
|
|
64
|
+
budget and the grader timed two out). Route guards read this same `status`:
|
|
65
|
+
wait only while it is `'loading'`.
|
|
58
66
|
`validateSession()` is the background re-check that produces the last two: call
|
|
59
|
-
it after the first render — a
|
|
60
|
-
network failure
|
|
67
|
+
it after the first render — a 401 or 403 clears the token (`'unauthenticated'`),
|
|
68
|
+
while a network failure, a 5xx, or a 404 from a wrong `currentUserEndpoint`
|
|
69
|
+
KEEPS the token and the user (`'unavailable'`, retryable; the 404 error names
|
|
70
|
+
`currentUserEndpoint`). Do not build a
|
|
61
71
|
`validating`/`unavailable` flag beside the client: a second module-level state
|
|
62
72
|
plus its own listener set is a second session store, and it drifts from the
|
|
63
73
|
client's.
|
|
@@ -71,10 +81,27 @@ therefore schedules nothing: the session stays until the API answers 401
|
|
|
71
81
|
(`validateSession()` clears it) or the user logs out. `autoRefresh: true`
|
|
72
82
|
against such an API is harmless but pointless — leave it `false`.
|
|
73
83
|
|
|
84
|
+
**Seeding a session from an exchange the app runs itself: `setSession()`.** When an OAuth
|
|
85
|
+
redirect or the app's own sign-in endpoint has already issued the token and returned the
|
|
86
|
+
user, `setSession({ accessToken, user })` stores both and flips the client to
|
|
87
|
+
`authenticated` in one update, so every hook reading the shared client is signed in at once
|
|
88
|
+
(a form's own sign-in code wrote the token and the user with `setAccessToken`/`setUser`,
|
|
89
|
+
which change their field only — the UI stayed signed out and nine browser assertions timed
|
|
90
|
+
out, xr-molecule-flash-m178). `setAccessToken()` and `setUser()` are field setters, not a
|
|
91
|
+
sign-in: neither sets `authenticated`.
|
|
92
|
+
|
|
93
|
+
**A background verification that returns an unchanged user changes nothing.** When
|
|
94
|
+
`validateSession()` (or a reload's restore) answers with the same profile data the client
|
|
95
|
+
already holds, the client keeps the SAME user object and does not notify subscribers — so an
|
|
96
|
+
effect seeded from `user` does not re-run, and a form it fills keeps what has been typed.
|
|
97
|
+
Seed such a form once (on its first load), never on every `user` identity.
|
|
98
|
+
|
|
74
99
|
**A token another tool reads at its own key is a CONFIG field, not a mirror.**
|
|
75
100
|
An e2e suite or host page that expects `localStorage.jwtToken` (the RealWorld
|
|
76
|
-
specs do) is satisfied by `tokenKey: 'jwtToken'`
|
|
77
|
-
|
|
101
|
+
specs do) is satisfied by `tokenKey: 'jwtToken'` — in a browser the client's
|
|
102
|
+
storage default is already `createBrowserStorageAdapter()`, so with an explicit
|
|
103
|
+
`'memory'` store you must also pass
|
|
104
|
+
`storage: createBrowserStorageAdapter()`. Writing the token into that key from a
|
|
78
105
|
subscription beside the client is a second store. In React, `useAuth()` from
|
|
79
106
|
`@molecule/app-react` returns the client's `state` (with `status`), `user` and
|
|
80
107
|
`isAuthenticated`; the token is `useAuthClient().getAccessToken()`.
|
|
@@ -341,6 +368,23 @@ interface AuthState<T = UserProfile> {
|
|
|
341
368
|
}
|
|
342
369
|
```
|
|
343
370
|
|
|
371
|
+
#### `AuthSessionSeed`
|
|
372
|
+
|
|
373
|
+
A complete session an exchange outside the client has already established — an OAuth
|
|
374
|
+
code→token redirect, or the app's own sign-in endpoint. {@link AuthClient.setSession} adopts
|
|
375
|
+
it in one update: the token and the user are stored and the client becomes `authenticated`.
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
interface AuthSessionSeed<T = UserProfile> {
|
|
379
|
+
/** The access token the exchange issued. */
|
|
380
|
+
accessToken: string
|
|
381
|
+
/** The profile that token belongs to, as the API returned it. */
|
|
382
|
+
user: T
|
|
383
|
+
/** A refresh token, when the exchange issued one; omitted leaves the stored one untouched. */
|
|
384
|
+
refreshToken?: string | null
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
344
388
|
#### `AuthClient`
|
|
345
389
|
|
|
346
390
|
Auth client interface that all auth bond packages must implement.
|
|
@@ -372,6 +416,11 @@ interface AuthClient<
|
|
|
372
416
|
* hitting the network. Intended for local refreshes after a per-app
|
|
373
417
|
* mutation (e.g., the user just PATCHed their own profile and the
|
|
374
418
|
* server returned the canonical row). Does NOT change tokens.
|
|
419
|
+
*
|
|
420
|
+
* This does not sign the client in: `authenticated` and `status` are
|
|
421
|
+
* unchanged, so an anonymous client stays anonymous. Use
|
|
422
|
+
* {@link login} / {@link register} for a credential exchange, or
|
|
423
|
+
* {@link setSession} when the token was issued outside the client.
|
|
375
424
|
*/
|
|
376
425
|
setUser(user: T | null): void
|
|
377
426
|
|
|
@@ -381,14 +430,34 @@ interface AuthClient<
|
|
|
381
430
|
getAccessToken(): string | null
|
|
382
431
|
|
|
383
432
|
/**
|
|
384
|
-
* Stores the access token in the configured token storage adapter (in
|
|
385
|
-
*
|
|
386
|
-
* the
|
|
387
|
-
*
|
|
388
|
-
*
|
|
433
|
+
* Stores the access token in the configured token storage adapter (in a
|
|
434
|
+
* browser, Web Storage; `'memory'` where no browser store exists). Use this
|
|
435
|
+
* to seed the token after an out-of-band exchange (e.g. the OAuth
|
|
436
|
+
* code→token redirect) instead of writing to `localStorage` directly,
|
|
437
|
+
* which would violate the storage contract and make a stored bearer token
|
|
438
|
+
* JS-readable (XSS-exfiltratable). Pass `null` to clear it.
|
|
439
|
+
*
|
|
440
|
+
* This changes token storage only; it does not sign the client in
|
|
441
|
+
* (`authenticated` and `status` are unchanged). Use {@link login} /
|
|
442
|
+
* {@link register} for a credential exchange, or call {@link setSession}
|
|
443
|
+
* with the token and the user to establish an already-issued session.
|
|
389
444
|
*/
|
|
390
445
|
setAccessToken(token: string | null): void
|
|
391
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Adopts a session an out-of-band exchange already established: stores the
|
|
449
|
+
* access token (and the refresh token when given), caches the user, and
|
|
450
|
+
* sets the COMPLETE state — `initialized`, `authenticated`, `status:
|
|
451
|
+
* 'authenticated'`, no error — in one update, with one subscriber
|
|
452
|
+
* notification. Every hook reading the shared client sees the user as
|
|
453
|
+
* signed in immediately.
|
|
454
|
+
*
|
|
455
|
+
* This is the way to seed an OAuth/custom sign-in. Calling
|
|
456
|
+
* {@link setAccessToken} and {@link setUser} instead does NOT sign the
|
|
457
|
+
* client in.
|
|
458
|
+
*/
|
|
459
|
+
setSession(session: AuthSessionSeed<T>): void
|
|
460
|
+
|
|
392
461
|
/**
|
|
393
462
|
* Gets the refresh token.
|
|
394
463
|
*/
|
|
@@ -437,6 +506,19 @@ interface AuthClient<
|
|
|
437
506
|
|
|
438
507
|
/**
|
|
439
508
|
* Initializes auth state (e.g., from stored tokens).
|
|
509
|
+
*
|
|
510
|
+
* With a stored token AND a stored user it marks the session `'authenticated'`
|
|
511
|
+
* SYNCHRONOUSLY — before its first `await` — so the first render is the
|
|
512
|
+
* signed-in UI and no page waits on a verification round trip. Any check
|
|
513
|
+
* (a refresh, the cookie-restore probe) settles behind that render, and only an
|
|
514
|
+
* explicit 401/403 ends the session; a network/5xx/404 failure keeps it and
|
|
515
|
+
* reports `'unavailable'`. With no stored user the client stays `'loading'`
|
|
516
|
+
* until the cookie restore decides.
|
|
517
|
+
*
|
|
518
|
+
* Do not put this call (or a `/me` request of your own) in front of the first
|
|
519
|
+
* render, and do not render a loading screen over a session it just restored:
|
|
520
|
+
* `whenRestored()` already resolves as soon as the restore has settled, and the
|
|
521
|
+
* HTTP gate built on it holds requests for exactly that window.
|
|
440
522
|
*/
|
|
441
523
|
initialize(): Promise<void>
|
|
442
524
|
|
|
@@ -448,11 +530,13 @@ interface AuthClient<
|
|
|
448
530
|
*
|
|
449
531
|
* - The server confirms the user: the session is refreshed and {@link AuthState.status}
|
|
450
532
|
* stays `'authenticated'`.
|
|
451
|
-
* - The server answers
|
|
452
|
-
* the status becomes `'unauthenticated'`.
|
|
453
|
-
* - The server cannot
|
|
454
|
-
*
|
|
455
|
-
* user
|
|
533
|
+
* - The server answers 401 or 403: the token is dead — the stored session is
|
|
534
|
+
* cleared and the status becomes `'unauthenticated'`.
|
|
535
|
+
* - The server cannot confirm the session — a network failure, a 5xx, or a 404
|
|
536
|
+
* from a misconfigured {@link AuthClientConfig.currentUserEndpoint}: the
|
|
537
|
+
* session (token AND user) is KEPT and the status becomes `'unavailable'`, so
|
|
538
|
+
* the app can retry instead of signing the user out over a bad connection or a
|
|
539
|
+
* route that does not exist. The 404 error names `currentUserEndpoint`.
|
|
456
540
|
*
|
|
457
541
|
* Do not hand-write this beside the client: a parallel token/user store for a
|
|
458
542
|
* "validating"/"unavailable" flag is the race this method removes.
|
|
@@ -561,11 +645,17 @@ interface AuthClientConfig {
|
|
|
561
645
|
profileEndpoint?: string
|
|
562
646
|
|
|
563
647
|
/**
|
|
564
|
-
*
|
|
565
|
-
* `initialize()`
|
|
566
|
-
* load when the token did not survive the reload itself — i.e. when
|
|
567
|
-
* `'memory'` and the API sets an httpOnly session cookie
|
|
568
|
-
*
|
|
648
|
+
* The API route that returns the signed-in user (default `/users/me`). Called
|
|
649
|
+
* with credentials by `initialize()` to re-establish the session after a full
|
|
650
|
+
* page load when the token did not survive the reload itself — i.e. when
|
|
651
|
+
* `storage` is `'memory'` and the API sets an httpOnly session cookie — and by
|
|
652
|
+
* {@link AuthClient.validateSession} on every reload to confirm a stored token
|
|
653
|
+
* is still valid. Must return the user (as `user` or `props`).
|
|
654
|
+
*
|
|
655
|
+
* Set it whenever the app serves its own current-user route: a wrong value
|
|
656
|
+
* answers 404, which `validateSession` reports as `'unavailable'` and keeps the
|
|
657
|
+
* stored token instead of signing the user out — but the session then stays
|
|
658
|
+
* unconfirmed.
|
|
569
659
|
*/
|
|
570
660
|
currentUserEndpoint?: string
|
|
571
661
|
|
|
@@ -583,6 +673,12 @@ interface AuthClientConfig {
|
|
|
583
673
|
* second library, a host page — instead of mirroring the token into
|
|
584
674
|
* `localStorage` beside the client. The refresh token and the cached user keep
|
|
585
675
|
* the prefix.
|
|
676
|
+
*
|
|
677
|
+
* In a browser the client's storage default is {@link createBrowserStorageAdapter},
|
|
678
|
+
* so `tokenKey` alone writes the token to exactly that key and a reload keeps
|
|
679
|
+
* the session. An EXPLICIT `storage: 'memory'` beside it warns at construction:
|
|
680
|
+
* the value then lives in a process-local Map nothing outside this client can
|
|
681
|
+
* read, and a reload signs the user out.
|
|
586
682
|
*/
|
|
587
683
|
tokenKey?: string
|
|
588
684
|
|
|
@@ -622,9 +718,23 @@ interface AuthClientConfig {
|
|
|
622
718
|
requestBody?: (kind: 'login' | 'register', payload: Record<string, unknown>) => unknown
|
|
623
719
|
|
|
624
720
|
/**
|
|
625
|
-
* Turn
|
|
721
|
+
* Turn an answer that carries a user into the client's
|
|
626
722
|
* `{ accessToken, refreshToken?, user }` (default: the answer as given).
|
|
627
723
|
* RealWorld, for example: `(data) => ({ accessToken: data.user.token, user: data.user })`.
|
|
724
|
+
*
|
|
725
|
+
* It is the ONE user-shape adapter, and the client applies it on EVERY path
|
|
726
|
+
* that adopts a user from an API answer — `login()`, `register()`,
|
|
727
|
+
* `validateSession()`, the cookie restore `initialize()`/`refresh()` run, the
|
|
728
|
+
* token-refresh answer, and `updateProfile()`. Those answers carry the raw wire
|
|
729
|
+
* user (a current-user route typically answers a bare `{ user }`), so applying
|
|
730
|
+
* the same mapping is what keeps the adapted profile after a reload instead of
|
|
731
|
+
* replacing it with the unadapted one (xr-molecule-flash-m184). A mapper that
|
|
732
|
+
* cannot read a given answer (it throws, or returns no `user`) leaves that
|
|
733
|
+
* answer's user as it came.
|
|
734
|
+
*
|
|
735
|
+
* It is NOT applied to `setSession()` or `setUser()`: those receive a profile
|
|
736
|
+
* the caller already built in this client's shape, not a wire answer.
|
|
737
|
+
*
|
|
628
738
|
* @param data - The parsed response body.
|
|
629
739
|
* @returns The auth result.
|
|
630
740
|
*/
|
|
@@ -634,19 +744,21 @@ interface AuthClientConfig {
|
|
|
634
744
|
* Where the access/refresh token and the cached user are kept — the field that
|
|
635
745
|
* decides whether a page reload keeps the user signed in.
|
|
636
746
|
*
|
|
637
|
-
* **In
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
747
|
+
* **In a browser the default is `createBrowserStorageAdapter()` (Web Storage)**,
|
|
748
|
+
* so a reload keeps the session with no wiring of its own; outside a browser
|
|
749
|
+
* (SSR, a Node test) the default is `'memory'`. Pass `'memory'` EXPLICITLY when
|
|
750
|
+
* the API sets an httpOnly session cookie and you do not want a script-readable
|
|
751
|
+
* copy: `'memory'` is a process-local store, and `initialize()` then restores
|
|
752
|
+
* through {@link AuthClientConfig.currentUserEndpoint} — the strongest
|
|
753
|
+
* configuration, because no script can read the cookie.
|
|
641
754
|
*
|
|
642
|
-
* **An app whose external contract requires
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
* refresh token and the cached user keep `storagePrefix`.
|
|
755
|
+
* **An app whose external contract requires the token at a specific key** — a
|
|
756
|
+
* supplied e2e suite (the RealWorld specs read `localStorage.jwtToken` in
|
|
757
|
+
* `SELECTORS.md`), a host page, a second library — sets `tokenKey: 'jwtToken'`;
|
|
758
|
+
* in a browser that is enough, because the default store is Web Storage. The
|
|
759
|
+
* session (token AND user) is then restored synchronously in `initialize()` —
|
|
760
|
+
* with no store of your own. `tokenKey` places the access token at that exact
|
|
761
|
+
* key; the refresh token and the cached user keep `storagePrefix`.
|
|
650
762
|
*
|
|
651
763
|
* A bearer token in Web Storage is readable by any script on the origin, so an
|
|
652
764
|
* XSS can steal it; see {@link createBrowserStorageAdapter}'s `@remarks` for the
|
|
@@ -664,14 +776,13 @@ interface AuthClientConfig {
|
|
|
664
776
|
* ```typescript
|
|
665
777
|
* import { createBrowserStorageAdapter } from '@molecule/app-auth'
|
|
666
778
|
*
|
|
667
|
-
* // A suite/host page reads the token at its own key
|
|
779
|
+
* // A suite/host page reads the token at its own key (the browser default
|
|
780
|
+
* // already stores it; `storage` is shown for an explicit choice):
|
|
668
781
|
* storage: createBrowserStorageAdapter()
|
|
669
782
|
* tokenKey: 'jwtToken'
|
|
670
|
-
* // A reload keeps the session at this client's own keys:
|
|
671
|
-
* storage: createBrowserStorageAdapter()
|
|
672
783
|
* // Only this tab's lifetime, at a narrower exposure than localStorage:
|
|
673
784
|
* storage: createBrowserStorageAdapter({ kind: 'session' })
|
|
674
|
-
* // An httpOnly cookie session
|
|
785
|
+
* // An httpOnly cookie session — keep the bearer out of script-reachable storage:
|
|
675
786
|
* storage: 'memory'
|
|
676
787
|
* ```
|
|
677
788
|
*/
|
|
@@ -686,50 +797,7 @@ interface AuthClientConfig {
|
|
|
686
797
|
* Use this when the server also issues sessions that must never appear
|
|
687
798
|
* signed-in client-side — e.g. an anonymous/guest session whose user row
|
|
688
799
|
* (`isAnonymous`) would otherwise hydrate as a logged-in user. Defaults to
|
|
689
|
-
|
|
690
|
-
* @param user - The user returned by the restore probe.
|
|
691
|
-
* @returns Whether to establish the client session from this user.
|
|
692
|
-
*/
|
|
693
|
-
shouldRestoreUser?: (user: UserProfile) => boolean
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Refresh the access token before it expires. A no-op unless the API issued a
|
|
697
|
-
* refresh token at login/register — with none stored, nothing is scheduled and
|
|
698
|
-
* the session is never ended by the refresh timer; an API that rejects the
|
|
699
|
-
* access token (401) or an explicit logout ends it instead. Defaults to `true`.
|
|
700
|
-
*/
|
|
701
|
-
autoRefresh?: boolean
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Refresh tokens this many seconds before expiry.
|
|
705
|
-
*/
|
|
706
|
-
refreshBuffer?: number
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Forgot password / password reset request endpoint.
|
|
710
|
-
*/
|
|
711
|
-
forgotPasswordEndpoint?: string
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Password reset confirmation endpoint.
|
|
715
|
-
*/
|
|
716
|
-
resetPasswordEndpoint?: string
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Change password endpoint.
|
|
720
|
-
*/
|
|
721
|
-
changePasswordEndpoint?: string
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Available OAuth providers (e.g., ['github', 'google', 'gitlab']).
|
|
725
|
-
*/
|
|
726
|
-
oauthProviders?: string[]
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* OAuth endpoint path (default: '/oauth').
|
|
730
|
-
*/
|
|
731
|
-
oauthEndpoint?: string
|
|
732
|
-
}
|
|
800
|
+
// … truncated — the full declaration is in node_modules/@molecule/app-auth/dist/types.d.ts
|
|
733
801
|
````
|
|
734
802
|
|
|
735
803
|
#### `TokenStorage`
|
|
@@ -857,10 +925,11 @@ token as a background request — never in front of the first render. A page tha
|
|
|
857
925
|
own `/me` call pays a round trip on EVERY navigation (xr-molecule-flash-m122: a 1–5 s restore
|
|
858
926
|
window, eight `waitForURL` timeouts).
|
|
859
927
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
928
|
+
In a browser this IS the auth client's default `AuthClientConfig.storage`, so a scaffolded
|
|
929
|
+
app keeps its session across a page reload with no extra wiring. Outside a browser (SSR, a Node
|
|
930
|
+
test) the client defaults to `'memory'` instead, because there is no Web Storage to use. Pass
|
|
931
|
+
`'memory'` explicitly to keep the bearer token out of script-reachable storage when the API sets
|
|
932
|
+
an httpOnly session cookie (see the security note below).
|
|
864
933
|
|
|
865
934
|
```ts
|
|
866
935
|
import { createJWTAuthClient, createBrowserStorageAdapter } from '@molecule/app-auth'
|
|
@@ -871,18 +940,18 @@ const authClient = createJWTAuthClient({
|
|
|
871
940
|
})
|
|
872
941
|
```
|
|
873
942
|
|
|
874
|
-
The
|
|
943
|
+
The browser client already defaults to it, so a scaffolded app survives a
|
|
875
944
|
reload out of the box; each of these replaces that default:
|
|
876
945
|
|
|
877
|
-
|
|
946
|
+
```ts
|
|
878
947
|
storage: 'memory' // memory only: pair it with an httpOnly cookie session
|
|
879
|
-
|
|
948
|
+
```
|
|
880
949
|
|
|
881
|
-
|
|
950
|
+
_(description truncated — read the full text in `node_modules/@molecule/app-auth/dist/browser-storage.d.ts`.)_
|
|
882
951
|
|
|
883
952
|
```typescript
|
|
884
953
|
function createBrowserStorageAdapter(options?: BrowserStorageAdapterOptions): StorageAdapter
|
|
885
|
-
|
|
954
|
+
```
|
|
886
955
|
|
|
887
956
|
- `options` — Which Web Storage area to use, or a store to use directly.
|
|
888
957
|
|
|
@@ -1064,7 +1133,11 @@ Peer dependencies:
|
|
|
1064
1133
|
- **An API with its own auth envelope keeps this client.** `requestBody` wraps the
|
|
1065
1134
|
login/register payload (`(kind, p) => ({ user: p })`), `mapAuthResponse` reads the
|
|
1066
1135
|
token and user out of the answer (`(d) => ({ accessToken: d.user.token, user: d.user })`),
|
|
1067
|
-
and `authScheme: 'Token'` sends `Authorization: Token <jwt
|
|
1136
|
+
and `authScheme: 'Token'` sends `Authorization: Token <jwt>`. `mapAuthResponse` is the
|
|
1137
|
+
ONE user-shape adapter: the client applies it to every answer that carries a user (login,
|
|
1138
|
+
register, `validateSession()`, the cookie restore, a token refresh, `updateProfile()`), so
|
|
1139
|
+
the adapted profile survives a reload instead of being replaced by the raw wire user.
|
|
1140
|
+
Both are fields of the
|
|
1068
1141
|
`AuthClientConfig` the client is CONSTRUCTED with
|
|
1069
1142
|
(`setClient(createJWTAuthClient({ authScheme: 'Token', … }))` in the auth bond), not
|
|
1070
1143
|
properties of a client that already exists. Do not hand-write an auth client for a spec
|
|
@@ -1109,12 +1182,12 @@ by one. A box you can't check is an integration bug to fix — not a skip:
|
|
|
1109
1182
|
- [ ] The session survives a FULL page reload: after login, hard-reload → you
|
|
1110
1183
|
stay signed in (the profile/sign-out UI a signed-in visitor sees is still
|
|
1111
1184
|
there), and you are NOT bounced to login. Which route does it is the app's
|
|
1112
|
-
`AuthClientConfig.storage`: the
|
|
1113
|
-
|
|
1114
|
-
`initialize()` through `currentUserEndpoint`
|
|
1115
|
-
|
|
1116
|
-
cookie
|
|
1117
|
-
store written beside the client.
|
|
1185
|
+
`AuthClientConfig.storage`: in a browser the client defaults to
|
|
1186
|
+
`createBrowserStorageAdapter()`, which keeps the token there; an httpOnly
|
|
1187
|
+
session cookie is restored by `initialize()` through `currentUserEndpoint`
|
|
1188
|
+
(default `/users/me`). If the app sets `storage: 'memory'` explicitly against
|
|
1189
|
+
an API that sets no cookie, this box cannot be checked, and the fix is the
|
|
1190
|
+
config, not a token store written beside the client.
|
|
1118
1191
|
- [ ] Password reset round-trips: `requestPasswordReset({ email })` then
|
|
1119
1192
|
`confirmPasswordReset({ token, password })` with the emailed token, after
|
|
1120
1193
|
which `login()` with the NEW password succeeds and the OLD password no
|
|
@@ -38,10 +38,11 @@ export interface BrowserStorageAdapterOptions {
|
|
|
38
38
|
* own `/me` call pays a round trip on EVERY navigation (xr-molecule-flash-m122: a 1–5 s restore
|
|
39
39
|
* window, eight `waitForURL` timeouts).
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* In a browser this IS the auth client's default `AuthClientConfig.storage`, so a scaffolded
|
|
42
|
+
* app keeps its session across a page reload with no extra wiring. Outside a browser (SSR, a Node
|
|
43
|
+
* test) the client defaults to `'memory'` instead, because there is no Web Storage to use. Pass
|
|
44
|
+
* `'memory'` explicitly to keep the bearer token out of script-reachable storage when the API sets
|
|
45
|
+
* an httpOnly session cookie (see the security note below).
|
|
45
46
|
*
|
|
46
47
|
* ```ts
|
|
47
48
|
* import { createJWTAuthClient, createBrowserStorageAdapter } from '@molecule/app-auth'
|
|
@@ -52,7 +53,7 @@ export interface BrowserStorageAdapterOptions {
|
|
|
52
53
|
* })
|
|
53
54
|
* ```
|
|
54
55
|
*
|
|
55
|
-
* The
|
|
56
|
+
* The browser client already defaults to it, so a scaffolded app survives a
|
|
56
57
|
* reload out of the box; each of these replaces that default:
|
|
57
58
|
*
|
|
58
59
|
* ```ts
|
|
@@ -72,10 +73,11 @@ export interface BrowserStorageAdapterOptions {
|
|
|
72
73
|
* your API supports it.** Script cannot read an httpOnly cookie, so an XSS
|
|
73
74
|
* cannot exfiltrate the credential; the token stays in memory and
|
|
74
75
|
* `initialize()` restores the session through `currentUserEndpoint`
|
|
75
|
-
* (`@molecule/api-resource-user` issues exactly this cookie —
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* the token's lifetime short and rotate
|
|
76
|
+
* (`@molecule/api-resource-user` issues exactly this cookie — pass
|
|
77
|
+
* `storage: 'memory'` when it does). `'memory'` is then the strongest
|
|
78
|
+
* configuration. If the API sets no such cookie and you cannot change that,
|
|
79
|
+
* this adapter is the alternative — keep the token's lifetime short and rotate
|
|
80
|
+
* it on refresh.
|
|
79
81
|
* - **`kind: 'session'` narrows the window** to the tab's lifetime at the cost of
|
|
80
82
|
* losing the session in a new tab; `'local'` is the default because a reload is
|
|
81
83
|
* the requirement it exists for, and a fresh tab is the same person.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-storage.d.ts","sourceRoot":"","sources":["../src/browser-storage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEpD,4EAA4E;AAC5E,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAA;IACzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAA;CACzB;AAeD
|
|
1
|
+
{"version":3,"file":"browser-storage.d.ts","sourceRoot":"","sources":["../src/browser-storage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEpD,4EAA4E;AAC5E,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAA;IACzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAA;CACzB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,eAAO,MAAM,2BAA2B,GACtC,UAAS,4BAAiC,KACzC,cA6EF,CAAA"}
|
package/dist/browser-storage.js
CHANGED
|
@@ -29,10 +29,11 @@ const warn = (message, error) => {
|
|
|
29
29
|
* own `/me` call pays a round trip on EVERY navigation (xr-molecule-flash-m122: a 1–5 s restore
|
|
30
30
|
* window, eight `waitForURL` timeouts).
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* In a browser this IS the auth client's default `AuthClientConfig.storage`, so a scaffolded
|
|
33
|
+
* app keeps its session across a page reload with no extra wiring. Outside a browser (SSR, a Node
|
|
34
|
+
* test) the client defaults to `'memory'` instead, because there is no Web Storage to use. Pass
|
|
35
|
+
* `'memory'` explicitly to keep the bearer token out of script-reachable storage when the API sets
|
|
36
|
+
* an httpOnly session cookie (see the security note below).
|
|
36
37
|
*
|
|
37
38
|
* ```ts
|
|
38
39
|
* import { createJWTAuthClient, createBrowserStorageAdapter } from '@molecule/app-auth'
|
|
@@ -43,7 +44,7 @@ const warn = (message, error) => {
|
|
|
43
44
|
* })
|
|
44
45
|
* ```
|
|
45
46
|
*
|
|
46
|
-
* The
|
|
47
|
+
* The browser client already defaults to it, so a scaffolded app survives a
|
|
47
48
|
* reload out of the box; each of these replaces that default:
|
|
48
49
|
*
|
|
49
50
|
* ```ts
|
|
@@ -63,10 +64,11 @@ const warn = (message, error) => {
|
|
|
63
64
|
* your API supports it.** Script cannot read an httpOnly cookie, so an XSS
|
|
64
65
|
* cannot exfiltrate the credential; the token stays in memory and
|
|
65
66
|
* `initialize()` restores the session through `currentUserEndpoint`
|
|
66
|
-
* (`@molecule/api-resource-user` issues exactly this cookie —
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* the token's lifetime short and rotate
|
|
67
|
+
* (`@molecule/api-resource-user` issues exactly this cookie — pass
|
|
68
|
+
* `storage: 'memory'` when it does). `'memory'` is then the strongest
|
|
69
|
+
* configuration. If the API sets no such cookie and you cannot change that,
|
|
70
|
+
* this adapter is the alternative — keep the token's lifetime short and rotate
|
|
71
|
+
* it on refresh.
|
|
70
72
|
* - **`kind: 'session'` narrows the window** to the tab's lifetime at the cost of
|
|
71
73
|
* losing the session in a new tab; `'local'` is the default because a reload is
|
|
72
74
|
* the requirement it exists for, and a fresh tab is the same person.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-storage.js","sourceRoot":"","sources":["../src/browser-storage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AA8BhD,gFAAgF;AAChF,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,KAAe,EAAQ,EAAE;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;;YACxC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACtC,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,2EAA2E;QAC3E,2EAA2E;QAC3E,wBAAwB;IAC1B,CAAC;AACH,CAAC,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"browser-storage.js","sourceRoot":"","sources":["../src/browser-storage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AA8BhD,gFAAgF;AAChF,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,KAAe,EAAQ,EAAE;IACtD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;;YACxC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACtC,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,2EAA2E;QAC3E,2EAA2E;QAC3E,wBAAwB;IAC1B,CAAC;AACH,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,UAAwC,EAAE,EAC1B,EAAE;IAClB,MAAM,EAAE,IAAI,GAAG,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAC3C,2EAA2E;IAC3E,2DAA2D;IAC3D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,yFAAyF;IACzF,IAAI,QAA2C,CAAA;IAC/C,gFAAgF;IAChF,IAAI,MAAM,GAAG,KAAK,CAAA;IAElB,MAAM,OAAO,GAAG,GAA0B,EAAE;QAC1C,IAAI,OAAO;YAAE,OAAO,OAAO,CAAA;QAC3B,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,QAAQ,CAAA;QAChE,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,UAAmE,CAAA;YACjF,MAAM,GAAG,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAA;YAC1E,IAAI,GAAG,EAAE,CAAC;gBACR,mEAAmE;gBACnE,6EAA6E;gBAC7E,MAAM,KAAK,GAAG,yBAAyB,CAAA;gBACvC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;gBACzB,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;gBACrB,QAAQ,GAAG,GAAG,CAAA;gBACd,OAAO,GAAG,CAAA;YACZ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,GAAG,IAAI,CAAA;gBACb,IAAI,CAAC,OAAO,IAAI,gEAAgE,EAAE,KAAK,CAAC,CAAA;YAC1F,CAAC;QACH,CAAC;QACD,4EAA4E;QAC5E,8CAA8C;QAC9C,OAAO,IAAI,CAAA;IACb,CAAC,CAAA;IAED,OAAO;QACL,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACf,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;YACvB,IAAI,CAAC,KAAK;gBAAE,OAAO,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAA;YAC1C,IAAI,CAAC;gBACH,sEAAsE;gBACtE,4DAA4D;gBAC5D,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAA;YACtD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,4DAA4D,EAAE,KAAK,CAAC,CAAA;gBACzE,OAAO,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAA;YAChC,CAAC;QACH,CAAC;QACD,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YACtB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;YACvB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;gBACtB,OAAM;YACR,CAAC;YACD,IAAI,CAAC;gBACH,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;gBACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACpB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,0EAA0E;gBAC1E,2EAA2E;gBAC3E,wDAAwD;gBACxD,IAAI,CAAC,kEAAkE,EAAE,KAAK,CAAC,CAAA;gBAC/E,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YACxB,CAAC;QACH,CAAC;QACD,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE;YAClB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAClB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;YACvB,IAAI,CAAC,KAAK;gBAAE,OAAM;YAClB,IAAI,CAAC;gBACH,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAA;YACpD,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC,CAAA"}
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAUhB,WAAW,EACZ,MAAM,YAAY,CAAA;AAmHnB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,CAAC,SAAS,WAAW,GAAG,WAAW,EACnC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAEhE,SAAQ,gBAAqB,KAC5B,UAAU,CAAC,CAAC,EAAE,SAAS,CA+wBzB,CAAA"}
|